When you have more than one entity having common properties such as address or geolocation coordinates for example, rather than duplicate those properties on each entity they can be split out into reusable value objects and embedded into those entities for better separation of concerns.

Embeddables are classes which are not entities themselves, but are embedded in entities and can also be queried in DQL. You’ll mostly want to use them to reduce duplication or separating concerns. Value objects such as date range or address are the primary use case for this feature.

Example entity

<?php

declare(strict_types=1);

namespace App\Entity;

use App\ValueObject\GeoLocation;
use Symfony\Component\Uid\Ulid;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
final class Property
{
    #[ORM\Id]
    #[ORM\Column(type: UlidType::NAME)]
    private Ulid $id;

    // N.B. annotation marking this as embedded
    #[ORM\Embedded(class: GeoLocation::class, columnPrefix: false)]
    private GeoLocatable $geoLocation;
}

Embeddable

A reusable value object.

<?php

declare(strict_types=1);

namespace App\ValueObject;

use App\Interface\GeoLocatable;
use Doctrine\ORM\Mapping as ORM;

// N.B. annotation marking this as an embeddable
#[ORM\Embeddable]
final class GeoLocation implements GeoLocatable
{
    #[ORM\Column(type: "float")]
    private float $latitude;

    #[ORM\Column(type: "float")]
    private float $longitude;

    public function __construct(float $latitude, float $longitude)
    {
        $this->latitude = $latitude;
        $this->longitude = $longitude;
    }

    public function latitude(): float
    {
        return $this->latitude;
    }

    public function longitude(): float
    {
        return $this->longitude;
    }
}

At the database level the value object properties will be in-lined as columns, prefixed with the value object name, in the property table. The prefix can be removed by setting the columnPrefix attribute to false, as above.

Don’t forget to update the config/packages/doctrine.yaml config to define the mappings for the embeddable value objects.

orm:
  mappings:
    App:
        type: attribute
        is_bundle: false
        dir: '%kernel.project_dir%/src/Entity'
        prefix: 'App\Entity'
        alias: App
    App\ValueObject:
        type: attribute
        is_bundle: false
        dir: '%kernel.project_dir%/src/ValueObject'
        prefix: 'App\ValueObject'
        alias: ValueObject

Their values can be accessed via the entity as follows:

/** @var \App\Entity\Property
$property->geoLocation()->latitude();