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.
PHP >= 8.0
Using composer
composer require xtompie/typed
<?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.
<?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"
}
}
*/
<?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"
}
}
}
*/
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)
}
*/
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
}
}
}
*/
<?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"
}
}
}
*/
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.
<?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,
) {
}
}
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,
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
.