xtompie/typed

Typed is a library that provides a set of classes that allow you to define types for your variables.

4.0.0 2024-04-02 19:31 UTC

This package is auto-updated.

Last update: 2024-05-02 19:53:02 UTC


README

Primitives as Typed Objects. Library that maps pritmitve types into typed objects. Can be used to maps request/input into objects of defined classes. Gives ErrorCollection on fail.

Requiments

PHP >= 8.0

Installation

Using composer

composer require xtompie/typed

Docs

Basic

<?php

use Xtompie\Typed\Max;
use Xtompie\Typed\Min;
use Xtompie\Typed\NotBlank;
use Xtompie\Typed\Typed;

Class PetPayload
{
    public function __construct(
        #[NotBlank]
        protected string $name,

        #[NotBlank]
        #[Min(0)]
        #[Max(30)]
        protected int $age,
    ) {}

    public function name(): string
    {
        return $this->name;
    }

    public function age(): int
    {
        return $this->age;
    }
}

$pet = Typed::typed(PetPayload::class, $_POST);

Maping is done throught class constructor.

When the conditions are met then $pet will be an instance of PetPayload e.g.

object(PetPayload)#5 (2) {
  ["name":protected] => string(5) "Cicik"
  ["age":protected] => int(3)
}

Else $pet will be an instance of ErrorCollection e.g.

object(Xtompie\Result\ErrorCollection)#8 (1) {
  ["collection":protected]=> array(2) {
    [0] => object(Xtompie\Result\Error)#10 (3) {
      ["message":protected] => string(24) "Value must not be blank"
      ["key":protected] => string(9) "not_blank"
      ["space":protected] => string(4) "name"
    }
    [1] => object(Xtompie\Result\Error)#12 (3) {
      ["message":protected] => string(37) "Value should be less than or equal 30"
      ["key":protected] => string(3) "max"
      ["space":protected] => string(3) "age"
    }
  }
}

Advantages of use typed objects:

  • Better static code analysis e.g. phpstan.
  • Request payload in one place.

For maping objects Typed::object() have more precise type definition:

<?php

Class Typed
{
    /**
     * @template T of object
     * @param class-string<T> $type
     * @param mixed $input
     * @return T|ErrorCollection
     */
    public static function object(string $type, mixed $input): object
    {
        // ...
    }
    // ...
}

It is better for phpstan.

Class

<?php

use Xtompie\Typed\NotBlank;
use Xtompie\Typed\Typed;

class Author
{
    public function __construct(
        #[NotBlank]
        protected string $name,
    ) {
    }
}

class Article
{
    public function __construct(
        protected Author $author,
    ) {
    }
}

$article = Typed::typed(Article::class, ['author' => ['name' => 'John']]);
var_dump($article);
/* Output:
object(Article)#4 (1) {
    ["author":protected] => object(Author)#9 (1) {
         ["name":protected] => string(4) "John"
    }
}
*/

ArrayOf

<?php

use Xtompie\Typed\ArrayOf;
use Xtompie\Typed\NotBlank;
use Xtompie\Typed\Typed;

class Comment
{
    public function __construct(
        #[NotBlank]
        protected string $text,
    ) {
    }
}

class Article
{
    public function __construct(
        #[ArrayOf(Comment::class)]
        protected array $comments,
    ) {
    }
}

$article = Typed::typed(Article::class, ['comments' => [['text' => 'A'], ['text' => 'B']]]);
var_dump($article);
/* Output:
object(Article)#6 (1) {
    ["comments":protected] => array(2) {
        [0] => object(Comment)#12 (1) {
            ["text":protected] => string(1) "A"
        }
        [1] => object(Comment)#13 (1) {
            ["text":protected] => string(1) "B"
        }
    }
}
*/

Source

Primitve field name can have characters that can't be used in method property name. To solve this Source can be used.

<?php

use Xtompie\Typed\Source;
use Xtompie\Typed\Typed;

class ArticleQuery
{
    public function __construct(
        #[Source('id:qt')]
        protected int $idGt,
    ) {
    }
}

$query = Typed::typed(ArticleQuery::class, ['id:qt' => 1234]);
var_dump($query);
/* Output:
object(ArticleQuery)#4 (1) {
    ["idGt":protected] => int(1234)
}
*/

Only

To not allow undefined fields Only can be used.

<?php

use Xtompie\Typed\Only;
use Xtompie\Typed\Typed;

#[Only]
class Article
{
    public function __construct(
        protected string $title,
        protected string $body,
    ) {
    }
}

$article = Typed::typed(Article::class, ['title' => 'T', 'body' => 'B', 'desc' => 'D']);
var_dump($article);
/* Output:
object(Xtompie\Result\ErrorCollection)#9 (1) {
    ["collection":protected] => array(1) {
        [0]=>object(Xtompie\Result\Error)#8 (3) {
            ["message":protected] => string(17) "Invalid key: desc"
            ["key":protected] => string(4) "only"
            ["space":protected] => NULL
        }
    }
}
*/

Callback

<?php

use Xtompie\Result\ErrorCollection;
use Xtompie\Typed\Callback;
use Xtompie\Typed\NotBlank;
use Xtompie\Typed\Typed;

#[Callback('typed')]
class Password
{
    public function __construct(
        #[NotBlank]
        protected string $new_password,
        protected string $new_password_confirm,
    ) {
    }

    protected function passwordIdentical(): bool
    {
        return $this->new_password === $this->new_password_confirm;
    }

    public function typed(): static|ErrorCollection
    {
        if (!$this->passwordIdentical()) {
            return ErrorCollection::ofErrorMsg('Passwords must be indentical', 'identical', 'new_password_confirm');
        }
        return $this;
    }
}

$password = Typed::typed(Password::class, ['new_password' => '1234', 'new_password_confirm' => '123']);
var_dump($password);
/* Output:
object(Xtompie\Result\ErrorCollection)#7 (1) {
    ["collection":protected] => array(1) {
        [0] => object(Xtompie\Result\Error)#4 (3) {
            ["message":protected] => string(28) "Passwords must be indentical"
            ["key":protected] => string(9) "identical"
            ["space":protected] => string(20) "new_password_confirm"
        }
    }
}
*/

Factory

By default objects are build through __constructor and their propertires. Alternative object can be build throught static factory method provided by Factory class attribute.

<?php

use Xtompie\Result\ErrorCollection;
use Xtompie\Typed\Factory;
use Xtompie\Typed\Typed;

#[Factory(class: Time::class, method: 'typed')]
class Time
{
    public static function typed(mixed $input): static|ErrorCollection
    {
        $input = (int)$input;
        if ($input < 0) {
            return ErrorCollection::ofErrorMsg('Time must be positive', 'time');
        }
        return new Time($input);
    }

    public  function __construct(
        protected int $time,
    ) {
    }
}

class Article
{
    public function __construct(
        protected Time $time,
    ) {
    }
}

$article = Typed::typed(Article::class, ['time' => time()]);

In above example the Factory attribute can be even: #[Factory]. If class is null then the context class is used. method parameter is be default typed. Method must be static. Must have one argument of type mixed. Must return the object or ErrorCollection.

Creating assert

<?php

use Attribute;
use Xtompie\Result\ErrorCollection;
use Xtompie\Typed\Assert;

#[Attribute(Attribute::TARGET_PARAMETER)]
class Positive implements Assert
{
    public function assert(mixed $input, string $type): mixed
    {
        $input = (int)$input;
        if ($input < 0) {
            return ErrorCollection::ofErrorMsg(
                message: 'Value must be positive',
                key: 'positive',
            );
        }

        return $input;
    }
}

Then add created assert attriute into property.

<?php
class Pet
{
    public function __construct(
        #[Positive]
        protected int $age,
    ) {
    }
}

Others

Alnum, Alpha, ArrayKeyRegex, ArrayKeyString, ArrayLengthMax, ArrayLengthMin, ArrayValueLengthMax, ArrayValueLengthMin, ArrayValueString, Choice, Date, Digit, Email, LengthMax, LengthMin, Max, Min, NotBlank, Regex, Replace, ToBool, ToInt, ToString, Trim, TrimLeft, TrimRight,

Limitations

Object properties must have a specified type. The type cannot be a union or intersection. If incoming primitive data can have multiple types, use a mixed property. In such cases, you can us a To* assert or a Callback.