markhuot / data
Data objects
Requires
- doctrine/annotations: ^1.13
- illuminate/collections: ^8|^9.36
- pestphp/pest: ^1.22
- phpdocumentor/reflection-docblock: ^5.3
- symfony/cache: ^6.0
- symfony/string: ^5|^6.1
- symfony/validator: ^5|^6.1
Requires (Dev)
- phpstan/phpstan: ^1.8
README
Data (or value) objects can be used to represent data that may not otherwise have a strict class in your application. For example, POST data coming from a web request or JSON data coming from an API call. In these situations you may want a strongly typed version of the data but not want to write all the boilerplate to cast the data from the source to your data object.
This package handles all that boilerplate for you and aims to cover 80% of the most common use cases with the ability to extend the package should you need more specific transformations.
At it's most basic level the package is a transformer from your source representation (usually loosely typed) in to the target representation (more strongly typed). It looks like this (seriously).
class Repository { public int $id; public string $name; public string $fullName; public array $topics; public bool $private; }
The goal of this package is to be able to layer this package on top of your existing data objects without needing much (if any) customization. It works off attribute labels and tries to infer as much through convention as possible.
To convert source data in to your data object you'll call ->fill($data)
on the data builder.
(new Data(new MyAwesomeObject))->fill($data)->get();
When you call ->fill($data)
the package will use introspection to determine how the source $data
maps to the properties in the Repository
object. By default there is no mapping and it will blindly look for source fields like id
and push them in to the destination property of ->id
.
When the mapping is not 1:1 and you need to more control over the field naming you can use the MapFrom
attribute to help the transformer along.
use markhuot\data\attributes\MapFrom; class Repository { public int $id; public string $name; #[MapFrom('full_name')] public string $fullName; }
That will pull the full_name
field from the source and drop it in to the ->fullName
property in the destination. If your conversion is snake case to camel case there is a convent helper to do just that,
use markhuot\data\attributes\MapFrom; class Repository { public int $id; public string $name; #[MapFrom(MapFrom::SNAKE)] public string $fullName; }
If you find yourself using identical MapFrom
annotations several times over you can also apply the attribute to the class to have all property names converted in the same way,
use markhuot\data\attributes\MapFrom; #[MapFrom(MapFrom::SNAKE)] class Repository { public int $id; public string $name; public string $fullName; }
In example all fields in the Repository
class will be converted during transformation. id
will remain ->id
, name
will remain ->name
but full_name
will automatically map to ->fullName
.
Nested maps
By default nested properties will be mapped based on the type hint. The typehit can either come from PHP or the docblock depending on your needs. For single object nesting you can use native PHP type hints like this,
class Repository { public int $id; public string $name; public string $fullName; public Owner $owner; public array $topics; public bool $private; } class Owner { public int $id; public string $login; public string $avatarUrl; }
This will map the source data ['owner' => ['id' => 1, 'login' => 'foo']]
over to a Repository
with an ->owner
property that is correctly set to an Owner
instance with ->id
and ->login
set on the Owner
instance.
For more advanced mappings you can use a docblock to define properties that PHP doesn't yet understand. For example,
class Release { public int $id; public string $tagName; /** @var Asset[] */ public array $assets; } class Asset { public int $id; public string $name; public string $url; }
This will correctly read that ->assets
expects to be an array of assets and will try to transform the source in to an array of objects. It would work with the following source data,
{ "id": "1", "tag_name": "1.0.0", "assets": [ { "id": "1", "name": "1.0.0.zip", "url" "..." }, { "id": "2", "name": "1.0.1.zip", "url" "..." } ] }
Validation
Because many times strict typing isn't enough to ensure the data is correct you can also use the Symfony validation component to validate the data after the ->fill()
process.
use Symfony\Component\Validator\Constraints as Assert; class CreateBlogPostData { #[Assert\NotNull] public int $authorId; #[Assert\NotBlank] public string $title; #[Assert\Regex('/[a-z0-9_-]+/')] public ?string $slug; } $data = (new Data(new CreateBlogPostData))->fill($_POST)->validate()->get();