Download the PHP package slepic/value-object without Composer
On this page you can find all versions of the php package slepic/value-object. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package value-object
php-value-object
PHP Value Objects
Requirements
- PHP >=7.4
Installation
Introduction
This library aims to provide unification of commonalities of all value objects. Additionally, it provides some features that take advantage of the unified environment:
- unified language to describe violations can be integrated with validation systems
- common base value objects for enums, collections and scalar types with restrictions
- interfaces to unify conversions from and to primitive types, or even between objects
- automatic construction of composite value objects using their own class definition
What is a value object?
Value objects are guards of validity. Once a value object is constructed, it contains valid data. And as long as that object lives, we don't have to validate it again.
Note: For simplicity, I am exposing everything in the examples as public properties which allows modifications. They should really be private with public getters.
What commonalities do value objects have a how we unify them?
- They are (or at least should be) immutable
- we cannot fully enforce that the value objects you create are immutable
- we can only support you in creating immutable objects
- we provide base classes and traits that prevent implementation of some mutable magic methods and mutable methods of \ArrayAccess
- They cannot be constructed into invalid state
- we cannot fully enforce this either
- we can support you by providing base value objects that obey the rule
- Attempt to construct them with invalid data leads to exception
- we can unify what kind of exception is thrown
- They can be constructed from primitive data types
- we can unify how value objects expose this ability
- They can be converted to primitive data types
- we can unify how value objects expose this ability
How can we take advantage of the unifications?
Unified errors
Since value objects need valid state, they have to check it. And this inherently means that they are doing validations and they have to do it themselves. If we just let value objects throw \InvalidArgumentException and instead we do validations (with client error reporting) beforehand, we are basically doing the validation twice. Once to feed the client with reasonable explanation where he screwed up and once in the value object to make sure it's not constructed from bullocks. And if we are lazy, we omit one or the other (or both in worst case).
Omitting validation outside value objects means that our value objects may throw and we end up with 500 Internal Server Error. Omitting validation inside value objects means that we will never be truly sure that they are valid. Omitting them both is just disaster. Having both is redundant and may lead to de-synchronization between the two.
If our value objects do the validations (which they should anyway) and offer a unified way to describe their expectations and eventual violations of said expectations, they effectively force you to validate your data:
- once
- in whichever point in code you find appropriate
- using the logic of your value objects
Unification of how value objects represent violations allows applications to incorporate value objects errors into their input validation process.
However, the way this incorporation is done for specific application is outside the scope of the library.
This library attempts to unify the error exception as ViolationExceptionInterface
with a getViolations(): array<ViolationInterface>
method.
A default implementation ViolationException
is also provided by the library.
ViolationInterface
is a marker interface for violations and each violation class name represents an error code.
With possibility to carry additional information exposed as properties/getters.
This also allows for error code inheritance and avoids error code conflicts between vendors.
Having an array of violations also gives is the option to report multiple violations at the same time.
A set of implementations is provided by this library, including some violations that describe nested errors of collections.
Unified conversions
Often value objects provide named constructors that allow to construct them from a primitive. And it isn't also uncommon that value objects can convert themselves to a primitive type.
This library provides a set of interfaces that define how conversion from and to primitive types should look like.
In the example below, they are the FromStringConstructableInterface
and ToStringConvertibleInterface
,
defining the public static function fromString(string $value): self
and public function __toString(): string
methods respectively.
This gives as unified environment.
It is sure easier to work with if all value objects that can be constructed from a single string value
have the same named constructor for this.
And, well, in case of conversion to string, that is already covered by PHP's magic method __toString()
,
but we also offer interfaces for int, float, etc. that go about the same names like ToIntConvertibleInterface
and so on...
And it allows to automatically construct composite value objects using their own class definition. See collections section.
Common value objects
Now, wait a minute! Both first name and surname check the same thing. Let's get rid of that duplication.
We have moved the responsibility for checking the value emptiness to the NonEmptyString
class.
However we have lost the ability to be specific about which property it is that is empty.
That is however responsibility of the caller of the constructor, because he is now creating those NonEmptyString value objects.
Let's see such a caller in the form of a factory that creates the object from primitive strings. It now manages the error codes and messages and overrides the defaults, while using the same codes (violation classes).
But we have also added a new type that is now a guarantee of non empty string.
And you know, if you don't care that much for all the violations, you do just this:
Anyway, we can now avoid some checks on other places.
This effectively forces the caller to validate the input at some point, while leaving the function to care only about its logic.
This library provides a set of base value objects that encapsulate some common restrictions we have on our primitive data types.
Note: ViolationInterface implementations should only describe the error, what it is that was violated. If consumers want to know everything that could have been violated, they have to reach for the value object type itself.
Immutable Traits
This package offers two traits that support immutability of value objects.
ImmutableObjectTrait
- disables implementation of magic __set
and __unset
methods.
ImmutableArrayAccessTrait
- disables implementation of ArrayAccess::offsetSet
and ArrayAccess::offsetUnset
.
These traits are used by all the base value objects in this package. And you are encouraged to use them on your value objects as well.
Nevertheless you are still free to create public properties. There's actually one exception in this package that relies on public properties - DataTransferObject class. But other than that, we discourage you from using them, although it is often a bit less writing if you do.
You are also always free to modify your objects using reflection, etc. So to be truly immutable is basically impossible in PHP. But we try as much as we can :)
Base Value Objects
Often we need to wrap primitive values and enforce some kind of limitation, like a limit on a string length, allow only subset of all characters in a string, or limit a maximum value of a number.
Simple implementations of scalar objects with these common restrictions can be found in the package.
Strings
Slepic\ValueObject\Strings\StringValue
- a string value object without restrictions
- violation:
StringViolation
Slepic\ValueObject\Strings\MaxRawLengthString
- a string value object with max length (using strlen)
- children need to implement
protected static function maxLength(): int
- violation:
StringTooLong
Slepic\ValueObject\Strings\MinRawLengthString
- a string value object with min length (using strlen)
- children need to implement
protected static function minLength(): int
- violation:
StringTooShort
Slepic\ValueObject\Strings\BoundedRawLengthString
- string value object with both min and max length (using strlen)
- children need to implement both
protected static function minLength(): int
andprotected static function maxLength(): int
- violation:
StringLengthOutOfBounds
Slepic\ValueObject\Strings\MaxMbLengthString
- a string value object with max length (using mb_strlen)
- children need to implement
protected static function maxLength(): int
- violation:
StringTooLong
Slepic\ValueObject\Strings\MinMbLengthString
- a string value object with min length (using mb_strlen)
- children need to implement
protected static function minLength(): int
- violation:
StringTooShort
Slepic\ValueObject\Strings\BoundedMbLengthString
- string value object with both min and max length (using mb_strlen)
- children need to implement both
protected static function minLength(): int
andprotected static function maxLength(): int
- violation:
StringLengthOutOfBounds
Slepic\ValueObject\Strings\RegexTemplateString
- a string value object which checks the value to match a regex pattern
- children need to implement
protected static function pattern(): string
- violation:
StringPatternViolation
Integers
- see Slepic\ValueObject\Integers namespace
Floats
- see Slepic\ValueObject\Floats namespace
Enums
- see Slepic\ValueObject\Enums namespace
We consider several axis for enums
- strong vs. weak
- strong enums
- strong enums exist as singleton instances
- strict comparision using
===
and!==
is possible
- weak enums
- new instances can be created to represent the same value
- must compare the underlying value to tell if the instances are the same
- strong enums
- value types
- all allowed values of an enum must be of same type
- we distinguish between string enums and int enums
- float enums can be supported in future, but we dont have a use case for it now
- the way the set of allowed values is defined
- class's constants values
- class's constants keys
- class's named constructors
- custom way, driven by the enum class.
Each aspect has cons and pros. Currently only strong string enums are implemented.
Collections
The package supports 3 main types of collections
- see Slepic\ValueObject\Collections namespace
DataTransferObject
- expects keys of the array to match its public property name and the values must match the corresponding property type
- the property types are denoted by their typehints.
- violations:
- InvalidPropertyValue - if a known property has errors
- MissingRequiredProperty - if a property without default value is not provided
- UnknownProperty - if input contains property that does not exist on the DTO
- this can be turned off in the children by overriding the class's protected constant IGNORE_UNKNOWN_PROPERTIES
Note: If you sometimes need to construct the object not from array, but directly from separate variables,
consider not extending DataTransferObject, implement your object, with own constructor and
use FromArrayConstructor
helper class to simplify the creation from array.
ArrayList
- expects iterable with zero based index keys and all values matching the same type
- the value type is denoted by the return type of the
current()
method. - violations:
- InvalidListItem - when an item violates the expected type
- TypeViolation - when indexes are not zero based
ArrayMap
- expects associative array with string keys and values matching the same type
- the value type is denoted by the return tpe of the
current()
method. - violations:
- InvalidPropertyValue - if a property value is invalid.
FromArrayConstructor
This is not a value object, it is a static helper class which simplifies construction of objects from associative array of named parameters for their class constructor.
This helper also supports upcasting and downcasting. See upcasting/downcasting sections.
Constructor parameters must have a default value to become optional in the input array. The parameters also must have a typehint.
By default the method reports any unexpected properties of the input through UnknownProperty
violation.
This can be turned off by passing true as the 3rd parameter $ignoreExtraProperties
.
Basically, this method throws the same violations as DataTransferObject.
The combineWithArray
method expects that non-instance properties exists with the same name as each constructor parameter,
that is not passed as the $data
argument to the combineWithArray
method.
These properties must have a compatible type with the respective constructor parameter.
Calling the combineWithArray
method on objects that don't obey this rule, will result in a LogicException
and no violations will be reported.
The extractConstructorArguments
method expects non-instance properties exist with the same name as each constructor parameter,
These properties must have a compatible type with the respective constructor parameter.
Calling the extractConstructorArguments
method on objects that don't obey this rule, will result in a LogicException
and no violations will be reported.
Note: writing these value objects will become even easier with PHP8's constructor promotion. If you only need your object constructed from array and the constructor itself seems like a burden, you should consider DataTransferObject instead.
DataStructure
This is basically a wrapper for FromArrayConstructor capabilities, which also provides from and to array upcasting/downcasting capabilities. This is the most solid base for an immutable data structure, if you are willing to write the constructor with all its properties. If you are not willing, use DataTransferObject. However with PHP8's constructor promotion feature, this will become equally simple and the DataStructure class will become the choice #1.
Standards
Additionaly we provide a set of standard value objects for common things, like email, etc. But this sections is currently not ready and in future this may probably be in a separate package.
- see Slepic\ValueObject\Standard namespace
Upcasting
Whenever a collection expects a value object type and it receives a primitive type, it will look for the appropriate upcasting interface on the target value object class. If it exists, it will automatically construct the value object using the interface.
Existing upcasting interfaces are:
- FromIntConstructableInterface
- FromFloatConstructableInterface
- FromStringConstructableInterface
- FromArrayConstructableInterface
- FromObjectConstructableInterface
- FromBoolConstructableInterface
Downcasting
Whenever a collection expects a primitive type and it receives an object, it will look for the appropriate downcasting interface on the value. If it exists it will be used to obtain the primitive value.
Existing downcasting interface are:
- ToIntConvertibleInterface
- ToFloatConvertibleInterface
- ToStringConvertibleInterface
- ToArrayConvertibleInterface
- ToBoolConvertibleInterface
FAQ
Is this only usable for validation
No. That's only a side effect that it allows to enhance and integrate with your validation system.
What other use cases are there.
This is basically for any use case where you would use a value object. It just helps you write them in unified way and simplifies some of its concerns.
How about contextual validation? Some field must be an email if another field has a specific value?
That is up to the constructor of the value object. Such a rule cannot be attributed to either of the fields alone. It's absolutely fine to define your own violation class for that and eventually reuse it in multiple value objects.
How about nested validation rules? A client (user/api consumer etc) is sending data to your app and it gets back some errors? How do I communicate back "hey, addresses[5].state is not a valid state"?
The communication of invalid state to the client is under control of your application. You can take advantage on unified violations environment, but you still have to be aware of what kinds of violations your value objects throw and represent each of them accordingly. Although violations are represented as classes, they really are just simple error codes with additional features leveraging the PHP class system. The violations from Collections namespace should make it easy enough to create nested violations for nested value objects. See the code of the collections to see how they are used. And you can also check their tests to see how we check for what happened in the nested violations tree.
How about custom validation messages?
Validation messages provided by ViolationInterface::getMessage() are not meant to be communicated to the client directly. They represent an immediately readable explanation of the violation for a developer. But if the violations are to be communicated to a general user, the messages should be generated within your application's validation system, based on the error codes and eventually other violation properties.
You can use the default messages probably only on API where devs are the only ones who is going to read them.
Chances are though, that in future the ViolationInterface::getMessage() method will be removed entirely.
Some component that simplify this task of integrating the violations into validation systems may be created in future. At this point this is out of the scope of the library, and it would probably become a separate package anyway.
How about validation that requires dependencies (eg: a database connection)?
It is of course possible to throw the unified ViolationExceptionInterface from any factory you want. But this cannot happen within the automatic upcasting, since it happens through static named constructors. But there is no problem passing an already created value object to an automatically constructed value object from this package. And that makes it irrelevant whether the passed-in value object needed database to be created or not.
Similar projects and inspiration
- spatie/data-transfer-object
- This was a great inspiration to begin with
- The class DataTransferObject got its name because of this package
- immutablephp/immutable
- Rather simpler implementation of immutable value objects
- This inspired the creation of the immutable traits in this package
Thank you, guys!
All versions of value-object with dependencies
ext-json Version *