xtompie / typed
Typed is a library that provides a set of classes that allow you to define types for your variables.
Installs: 1 943
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
Requires
- xtompie/result: ^1.5
Requires (Dev)
- phpunit/phpunit: ^10.5
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
.