Download the PHP package wwwision/types without Composer
On this page you can find all versions of the php package wwwision/types. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download wwwision/types
More information about wwwision/types
Files in wwwision/types
Package types
Short Description Tools to create PHP types that adhere to JSON schema like rules
License MIT
Informations about the package types
Library to narrow the scope of your PHP types with JSON Schema inspired attributes allowing for validating and mapping unknown data.
Why this package might be for you | Why this package might NOT be for you |
---|---|
Extends the PHP type system | Uses reflection at runtime (see performance considerations) |
Great integrations | Partly unconventional best practices |
Simple Generics | Static class, i.e. global (namespaced) instantiate() method |
No need to implement interfaces or extend base classes | Very young project – I certainly would be skeptical if I hadn't written this myself ;) |
Small footprint (just one public function/class and a couple of 3rd party dependencies) | You just don't like me.. pff.. whateva |
Usage
This package can be installed via composer:
Afterward, three steps are required to profit from the type safety of this package.
Given, you have the following Contact
entity:
This class has a couple of issues:
- The values are mutable, so every part of the system can just change them without control (
$contact->name = 'changed';
) - The values of
$name
and$age
are unbound – this makes the type very fragile. For example, you could specify a name with thousands of characters or a negative number for the age, possibly breaking at the integration level - There is no human readably type information – is the $name supposed to be a full name, just the given name or a family name, ...?
0. Create classes for your Value Objects
Note This list is 0-based because that part is slightly out of scope, it is merely a general recommendation
1. Add attributes
By adding one of the provided attributes, schema information and documentation can be added to type classes:
Note In most cases it makes sense to specify an upper bound for your types because that allows you to re-use that at "the edges" (e.g. for frontend validation and database schemas)
2. Make constructor private and classes immutable
By making constructors private, validation can be enforced providing confidence that the objects don't violate their allowed range. See best practices for more details.
3. Use instantiate()
to create instances
With private constructors in place, the instantiate()
function should be used to create new instances of the affected
classes:
Note In practice you'll realize that you hardly need to create new Entity/Value Object instances within your application logic but mostly in the infrastructure layer. E.g. a
DatabaseContactRepository
might return aContacts
object.
Example: Database integration
Best practices
In order to gain the most with this package, a couple of rules should be considered:
All state fields in the constructor
This package uses reflection to parse the constructors of involved classes. Therefore the constructor should contain every variable that makes up the internal state (IMO that's a good practice anyways).
In general you should only allow state changes through the constructor and it's a good idea to mark DTO classes as readonly
Private constructors
In order to allow data to be validated everywhere, there must be no way to instantiate
an ListBased class other than with the
provided instantiate()
method.
Therefore, constructors of Value Objects should be private:
Note For Shapes (i.e. composite) objects that rule doesn't apply, because all of their properties are valid if the above rule is followed:
Final classes
In my opinion, classes in PHP should be final by default. For the core domain types this is especially true because inheritance could lead to invalid schemas and failing validation. Instead, composition should be used where it applies.
Immutability
In order to guarantee the correctness of the types, there should be no way to change a value without re-applying validation. The easiest way to achieve this, is to make those types immutable – and this comes with some other benefits as well.
The readonly
keyword can be used on properties (with PHP 8.2+ even on the class itself) to ensure immutability on the
PHP type level.
If types should be updatable from the outside, ...
- a new instance should be returned
- and it should not call the private constructor but use
instantiate()
in order to apply validation
Attributes
Description
The Description
attribute allows you to add some domain specific documentation to classes and parameters.
Example: Class with description
IntegerBased
With the IntegerBased
attribute you can create Value Objects that represent an integer.
It has the optional arguments
minimum
– to specify the allowed minimum valuemaximum
– to specify the allowed maximum value
Example
FloatBased
Starting with version 1.2
With the FloatBased
attribute you can create Value Objects that represent a floating point number (aka double).
It has the optional arguments
minimum
– to specify the allowed minimum value (as integer or float)maximum
– to specify the allowed maximum value (as integer or float)
Example
StringBased
With the StringBased
attribute you can create Value Objects that represent a string.
It has the optional arguments
minLength
– to specify the allowed minimum length of the stringmaxLength
– to specify the allowed maximum length of the stringpattern
– to specify a regular expression that the string has to matchformat
– one of the predefined formats the string has to satisfy (this is a subset of the JSON Schema string format)
Example: String Value Object with min and max length constraints
Example: String Value Object with format and pattern constraints
Just like with JSON Schema, `format` and `pattern` can be _combined_ to further narrow the type:ListBased
With the ListBased
attribute you can create generic lists (i.e. collections, arrays, sets, ...) of the
specified itemClassName
.
It has the optional arguments
minCount
– to specify how many items the list has to contain at leastmaxCount
– to specify how many items the list has to contain at most
Example: Simple generic array
Example: More verbose generic array with type hints and min and max count constraints
The following example shows a more realistic implementation of a List, with: * An `@implements` annotation that allows IDEs and static type analyzers to improve the DX * A [Description](#description) attribute * `minCount` and `maxCount` validation * `Countable` and `JsonSerializable` implementation (just as an example, this is not required for the validation to work)Composite types
The examples above demonstrate how to create very specific Value Objects with strict validation and introspection.
Example: Complex composite object
Generics
Generics won't make it into PHP most likely (see this video from Brent that explains why that is the case).
The ListBased attribute allows for relatively easily creation of type-safe collections of a specific item type.
Currently you still have to create a custom class for that, but I don't think that this is a big problem because mostly a common collection class won't fit all the specific requirements.
For example: PostResults
could provide different functions and implementations than a Posts
set (the former might be unbound, the latter might have a minCount
constraint etc).
Further thoughts
I'm thinking about adding a more generic (no pun intended) way to allow for common classes without having to specify the itemClassName
in the attribute but at instantiation time, maybe something along the lines of
But it adds some more oddities and I currently don't really need it becaused of the reasons mentioned above.
Interfaces
Starting with version 1.1, this package allows to refer to interface types.
In order to instantiate an object via its interface, the instance class name has to be specified via the __type
key (with version 1.4+ the name of this key can be configured, see Discriminator)
All remaining array items will be used as usual. For simple objects, that only expect a single scalar value, the __value
key can be specified additionally:
Especially when working with generic lists, it can be useful to allow for polymorphism, i.e. allow the list to contain any instance of an interface:
Example: Generic list of interfaces
Union types
Starting with version 1.4, this package allows to refer to union types (aka "oneOf").
Like with interfaces, to instantiate object-based union types, the concrete type has to be specified via the __type
key:
For simple union types, the type discrimination is not required of course:
Discriminator
By default, in order to instantiate an instance of an interface or union type, the target class has to be specified via the __type
discriminator (see example above).
Starting with version 1.4, the name of this discriminator key can be changed with the Discriminator
attribute.
Additionally, the mapping from the type value to the fully qualified class name can be specified (optional).
This can be done on the interface level:
...and on interface or union type parameters:
Note If a
Discriminator
attribute exists on the parameter as well as on the respective interface within a Shape object, the parameter attribute overrules the one on the interface
Error handling
Errors that occur during the instantiation of objects lead to an InvalidArgumentException
to be thrown.
That exception contains a human-readable error message that can be helpful to debug any errors, for example:
Failed to instantiate FullNames: At key "0": At property "givenName": Value "a" does not have the required minimum length of 3 characters
Starting with version 1.2, the more specific CoerceException
is thrown with an improved exception message that collects all failures:
Failed to cast value of type array to FullNames: At "0.givenName": too_small (String must contain at least 3 character(s)). At "1.familyName": invalid_type (Required)
In addition, the exception contains a property issues
that allows for programmatic parsing and/or rewriting of the error messages.
The exception itself is JSON-serializable and the above example would be equivalent to:
Note If the syntax is familiar to you, that's no surpise. It is inspired (and in fact almost completely compatible) with the issue format of the fantastic Zod library
Serialization
This package promotes the heavy usage of dedicated value objects for a greater type-safety. When it comes to serializing those objects (e.g. to transmit them to a database or API) this comes at a cost: The default behavior of PHPs built-in json_encode function will, by default, just include all properties of a class.
For the simple type-based objects this is not feasible as it turns the simple value into an associative array of ['value' => <the-actual-value>]
instead of the desired simple representation of <the-actual-value>
.
Example:
Also, Discriminator details will be lost.
Starting with version 1.4, this package provides a dedicated Normalizer
that can be used to encode types:
Example: Complex type with type discrimination
Integrations
The declarative approach of this library allows for some interesting integrations. So far, the following two exist – Feel free to create another one and I will gladly add it to this list:
- types/graphql – to create GraphQL schemas from PHP types
- types/glossary – to create Markdown glossaries for all relevant PHP types
Dependencies
This package currently relies on the following 3rd party libraries:
- webmozart/assert – to simplify type and value assertions
- ramsey/uuid – for the
StringTypeFormat::uuid
check
...and has the following DEV-requirements:
- roave/security-advisories – to detect vulnerabilities in dependant packages
- phpstan/phpstan – for static code analysis
- squizlabs/php_codesniffer – for code style analysis
- phpunit/phpunit – for unit and integration tests
- phpbench/phpbench – for performance benchmarks
Performance
This package uses Reflection in order to introspect types. So it comes with a performance hit. Fortunately the performance of Reflection in PHP is not as bad as its reputation and while you can certainly measure a difference, I doubt that it will have a notable effect in practice – unless you are dealing with extremely time critical applications like realtime trading in which case you should not be using PHP in the first place... And you should probably reconsider your life choices in general :)
Nevertheless, this package contains a runtime cache for all reflected classes. So if you return a huge list of the same type, the performance impact should be minimal. I am measuring performance of the API via PHPBench to avoid regressions, and I might add further caches if performance turns out to become an issue.
Contribution
Contributions in the form of issues, pull requests or discussions are highly appreciated
License
See LICENSE