You are browsing a version that has not yet been released. |
Working with Embeddables
Doctrine provides so-called embeddables as a layer of abstraction that
allows reusing partial objects across entities. For example, one might
have an Address which is not only used for a Person, but
also for an Organization. This page shows how embeddables are
hydrated and extracted.
Embeddables as Objects
First, we have an Address class, which will be our embeddable:
namespace Application\Entity;use Doctrine\ORM\Mapping as ORM;#[ORM\Embeddable]class Address{ #[ORM\Column(type: 'string', nullable: true)] private ?string $postalCode = null; #[ORM\Column(type: 'string', nullable: true)] private ?string $city = null; public function getPostalCode(): ?string { return $this->postalCode; } public function setPostalCode(?string $postalCode): void { $this->postalCode = $postalCode; } public function getCity(): ?string { return $this->city; } public function setCity(?string $city): void { $this->city = $city; }}
Then we have a corresponding Person entity, where the above
embeddable is used:
namespace Application\Entity;use Doctrine\ORM\Mapping as ORM;#[ORM\Entity]class Person{ #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] private ?int $id = null; #[ORM\Column(type: 'string', nullable: true)] private ?string $name = null; #[ORM\Embedded(class: Address::class)] private Address $address; /** * Similar to collections you should initialize embeddables in the constructor! */ public function __construct() { $this->address = new Address(); } public function getId(): ?int { return $this->id; } public function getName(): ?string { return $this->name; } public function setName(?string $name): void { $this->name = $name; } public function getAddress(): Address { return $this->address; } public function setAddress(Address $address): void { $this->address = $address; }}
By default, the hydrator handles an embeddable as a single field holding the embeddable object. To hydrate it, provide an instance of it:
use Doctrine\Laminas\Hydrator\DoctrineObject as DoctrineHydrator;$hydrator = new DoctrineHydrator($entityManager);$person = new Person();$address = new Address();$address->setPostalCode('48149');$address->setCity('Münster');$data = [ 'name' => 'Mr. Example', 'address' => $address,];$person = $hydrator->hydrate($data, $person);echo $person->getAddress()->getPostalCode(); // prints "48149"echo $person->getAddress()->getCity(); // prints "Münster"
Likewise, extracting a Person returns the Address object under
the address key.
Embeddables as Arrays
To work with plain data instead of embeddable objects, e.g. for a JSON API or a form, you can change how embeddables are represented. The setting applies to both extracting and hydrating:
use Doctrine\Laminas\Hydrator\EmbeddableExtraction;$hydrator->setEmbeddableExtraction(EmbeddableExtraction::Array);
The following modes are available:
| Mode | Embeddables in the data |
|---|---|
EmbeddableExtraction::Object |
Embeddable objects (default). |
EmbeddableExtraction::Array |
Nested arrays of the fields of the embeddables. |
EmbeddableExtraction::Flattened |
Fields of the embeddables in the data of the entity. |
With EmbeddableExtraction::Array, the Person above is extracted
and hydrated as follows:
[ 'id' => 1, 'name' => 'Mr. Example', 'address' => [ 'postalCode' => '48149', 'city' => 'Münster', ],]
With EmbeddableExtraction::Flattened, the name of the embeddable and
the name of the field are joined by a separator, which is __ by default:
[ 'id' => 1, 'name' => 'Mr. Example', 'address__postalCode' => '48149', 'address__city' => 'Münster',]
The separator can be passed as second argument, e.g.
$hydrator->setEmbeddableExtraction(EmbeddableExtraction::Flattened, '.')
for address.postalCode. Note that a dot does not work for HTML forms,
as PHP converts dots in the names of submitted fields to underscores, and
that a single underscore easily collides with the names of other fields
when using the UnderscoreNamingStrategy. If a flattened key equals the
key of another field, a LogicException is thrown.
When hydrating:
- Nested arrays are hydrated with
EmbeddableExtraction::Arrayonly, flattened keys withEmbeddableExtraction::Flattenedonly. Embeddable objects are hydrated in all modes. - Only the fields contained in the data are written. If the entity has no embeddable yet, a new one is created, using the instantiator if one is configured (see Creating New Associated Entities).
- The fields of embeddables are read and written like the fields of
entities: by value using getters and setters, by reference using their
properties. Embeddables without setters can therefore only be hydrated
by reference,
readonlyproperties cannot be hydrated.
The fields of embeddables are converted by their own Doctrine types, see Converting Custom DBAL Types. Strategies can be added for them by their full name, as Doctrine lists them in the metadata:
$hydrator->addStrategy('address.postalCode', new PostalCodeStrategy());
Filters for the embeddable itself, e.g. for address, apply to the
embeddable as a whole. A strategy for the embeddable itself is applied
when extracting, before the embeddable is converted; if the strategy
converts it into something else than an object, the result is kept as it
is. Nested arrays and flattened keys are hydrated without that strategy.
An entity without an embeddable is extracted with null under the name
of the embeddable. The naming strategy is applied to each part of the
name, e.g. address__postal_code with the UnderscoreNamingStrategy.
Embeddables within embeddables are supported as well, e.g. as
address__geo__lat. Like the extraction of associations, the setting of
the hydrator hydrate() or extract() is called on also applies to the
associated entities. Embeddables are recognized by the field names of the
Doctrine ORM metadata, e.g. address.postalCode.
Embeddables in Forms
With Laminas forms, the fields of an embeddable can either be placed in a
fieldset bound to the embeddable object, or directly in the fieldset of the
entity using EmbeddableExtraction::Flattened, see
Example Usage with Laminas Form.
