apie / composite-value-objects
Composer package of the apie library: composite value objects
Installs: 1 085
Dependents: 9
Suggesters: 0
Security: 0
Stars: 0
Watchers: 2
Forks: 0
Open Issues: 0
Requires
- php: >=8.1
- apie/core: dev-main
Requires (Dev)
- apie/fixtures: dev-main
- phpspec/prophecy-phpunit: ^2.0
- phpunit/phpunit: ^9.5
This package is auto-updated.
Last update: 2024-10-17 17:15:43 UTC
README
composite-value-objects
This package is part of the Apie library. The code is maintained in a monorepo, so PR's need to be sent to the monorepo
Documentation
Composite value objects is mainly a trait that can be used inside value objects for value objects that are a composite of value objects or primitives. For example a range value object is often best used as a value object itself because the start and the end of the range should be in a restricted range..
Usage
All you need to do is make an object with the ValueObjectInterface and the CompositeValueObject trait. Then all you need are properties and fromNative and toNative will change accordingly.
For example:
<?php use Apie\CommonValueObjects\Texts\DatabaseText; use Apie\CompositeValueObjects\CompositeValueObject; use Stringable; final class StreetAddress implements ValueObjectInterface, Stringable { use CompositeValueObject; public function __construct(private DatabaseText $street, private DatabaseText $streetNumber) {} public function __toString(): string { return $this->street . ' ' . $this->streetNumber; } } // creates a StreetAddress value object from an array. $address = StreetAddress::fromNative([ 'street' => 'Example Street', 'streetNumber' => 42 ]); // $addressDisplay = 'Example Street 42'; $addressDisplay = (string) $address; // return array again $address->toNative(); // throws error for missing street number $address = StreetAddress::fromNative([ 'street' => 'Example Street' ]);
Remember that the example has a constructor, but this is not required, but if you do forget to add one someone could misuser your value object incorrectly by just calling new ValueObject() without constructor arguments. You could also make a private constructor to force people to use fromNative() to create your object.
Optional fields
By default all non-static fields are required and will throw an error if missing. To make a field optional you have 2 options. You either add the Optional attribute to a property, or you give the property a default value.
Both examples below have the same result:
<?php use Apie\CommonValueObjects\Texts\DatabaseText; use Apie\CompositeValueObjects\CompositeValueObject; final class StreetAddress implements ValueObjectInterface { use CompositeValueObject; private function __construct() { // this enforces other programmers to use fromNative } private DatabaseText $street; private DatabaseText $streetNumber; private ?DatabaseText $streetNumberSuffix = null; }
<?php use Apie\CommonValueObjects\Texts\DatabaseText; use Apie\CompositeValueObjects\CompositeValueObject; use Apie\Core\Attributes\Optional; final class StreetAddress implements ValueObjectInterface { use CompositeValueObject; private function __construct() { // this enforces other programmers to use fromNative } private DatabaseText $street; private DatabaseText $streetNumber; #[Optional] private DatabaseText $streetNumberSuffix; }
Remember that in PHP you will get errors if you try to read typehinted properties if they are not set. toNative() will not return a value if they are not set.
Validation
To add validation you can add a method validateState(). If the current state is invalid this method should throw an error. It is being called by fromNative() if the method exists and should also be called with any custom constructor.
A good example is time ranges where the start time needs to be created before the end time.
Here we give an example of a combination of first and last name and the total length of both fields should not extend 255 characters.
<?php use Apie\CompositeValueObjects\CompositeValueObject; final class FirstNameAndLastName implements ValueObjectInterface, Stringable { use CompositeValueObject; private string $firstName; private string $lastName; public function __construct(private string $firstName, private string $lastName) { $this->validateState(); } public function __toString(): string { return $this->firstName . ' ' . $this->lastName; } private function validateState(): void { if (strlen((string) $this) > 255) { throw new RuntimeException('Length of first name and last name should not exceed 255 characters'); } } } ### Union typehints The composite value object trait supports union typehints. To avoid accidental casting and that the reflection API of PHP will always return typehints in the same order(you don't have control over this) we check specific types first. If the input is a string and string is a typehint it will pick string. Otherwise the order of doing typecast is: - objects+other types - float - int - string ```php <?php use Apie\CommonValueObjects\Texts\DatabaseText; use Apie\CompositeValueObjects\CompositeValueObject; use Apie\Core\Attributes\Optional; final class Example implements ValueObjectInterface { use CompositeValueObject; private function __construct() { // this enforces other programmers to use fromNative } private string|int $value; public function getValue(): string|int { return $this->value; } } // getValue() returns '12' and is not casting to integer. Example::fromNative(['value' => '12'])->getValue(); // getValue() returns 12 and is not casting to string. Example::fromNative(['value' => 12])->getValue();