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
$nameand$ageare 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
DatabaseContactRepositorymight return aContactsobject.
Example: Database integration
PHPStan rules
To enforce the rules above automatically via PHPStan, you can install the wwwision/types-phpstan package
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 valueexamples- to provide valid example values (since version 1.7)extensions– to attach custom key/value pairs to the schema (see Schema extensions)
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)examples- to provide valid example values (since version 1.7)extensions– to attach custom key/value pairs to the schema (see Schema extensions)
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)examples- to provide valid example values (since version 1.7)extensions– to attach custom key/value pairs to the schema (see Schema extensions)
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 mostextensions– to attach custom key/value pairs to the schema (see Schema extensions)
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)Schema extensions
Starting with version 1.10, the ListBased attributes accept an optional extensions argument: an associative array of arbitrary key/value pairs that are merged into the schema's JSON representation.
This is useful when translating the schema for downstream tooling (for example, JSON Schema or OpenAPI specification extensions, which use the x- prefix for vendor-specific keys).
The values are also accessible on the corresponding Schema instance via the $extensions property.
Example: Custom editor hint via an OpenAPI-style extension
Composite types
The examples above demonstrate how to create very specific Value Objects with strict validation and introspection. Those Value Objects can be composed into composite types (aka shape).
Example: Complex composite object
Ignore unrecognized keys
Just like single-value objects, composite objects can be instantiated from unstructured input:
This will fail, if the input contains keys that do not map to a property of the target class:
Sometimes it can be useful to ignore those unknown properties instead, e.g. when consuming 3rd party APIs. Starting with version 1.8, the ignoreUnrecognizedKeys option can be specified to achieve that:
Interface properties
When generating a schema for an interface, its readable members are considered properties of that schema. A member becomes a property if it is either
- a property hook with a
getaccessor, or - a public, non-static method without parameters whose return type can be mapped to a schema
Note If an interface declares a property hook and a method of the same name, the property hook takes precedence.
All other methods are considered behavior rather than data and are silently skipped.
This includes methods that have parameters, return void, are static, or have a return type that cannot be mapped to a schema.
This makes it possible to generate schemas for interfaces that mix data accessors and behavior – including 3rd party interfaces you cannot change:
To exclude a member that would qualify as a property, add the #[Ignore] attribute (supported on methods since version 1.9 and on property hooks since version 1.11:
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
Discriminatorattribute 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
- types/openapi – to declare and serve OpenAPI compatible HTTP APIs
Dependencies
This package currently relies on the following 3rd party libraries:
- webmozart/assert – to simplify type and value assertions
- ramsey/uuid – for the
StringTypeFormat::uuidcheck
...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