Cześć. W tym artykule skupimy się na zapisywaniu Value Object’ów w bazie danych. Jeżeli nie wiesz czym jest Obiekt Wartości, to zapraszam Cię do mojego poprzedniego artykułu: Value Object – Podstawowy element Domain Driven Design. Tymczasem przejdźmy do meritum.

Na początek należy uzupełnić poprzedni artykuł, iż Value Object należy do Encji i nigdy nie jest zapisywany w bazie danych samemu sobie. Zazwyczaj zapisujemy je poprzez Agregat, o którym napiszę w kolejnym artykule. Istnieje kilka sposobów na zapis obiektu wartości w bazie danych.

W świecie PHP istnieje wiele narzędzi ORM, które pomagają nam w codziennej pracy z bazami danych. Nie mniej jednak, w tym artykule skupię się jedynie na Doctrine, ponieważ znam go najlepiej oraz własnej implementacji ORM na podstawie Doctrine DBAL.

Obiekt wartości we własnym ORM

Jeżeli implementujemy własne narzędzie ORM, musimy sami zadbać o wszystkie szczegóły, które pozwolą nam współpracować z bazami danych. Na przykładzie relacyjnej bazy danych MySQL, musimy utworzyć schemat tabeli Encji, którą będziemy zapisywać. Nasza przykładowa Encja to Person zawierająca Imię i Nazwisko jako Value Object, datę urodzenia oraz PESEL jako identyfikator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php declare(strict_types=1);

final class Person
{
    private string $id;
    private Name $name;
    private DateTimeImmutable $birthDate;

    public function __construct(string $id, Name $name, DateTimeImmutable $birthDate)
    {
        $this->id = $id;
        $this->name = $name;
        $this->birthDate = $birthDate;
    }
}

final class Name
{
    private string $first;
    private string $last;

    public function __construct(string $first, string $last)
    {
        $this->first = $first;
        $this->last = $last;
    }

    public function first(): string
    {
        return $this->firstName;
    }

    public function last(): string
    {
        return $this->lastName;
    }
}

Dobrą praktyką jest utrzymywanie identyfikatora również jako Value Object, ale dla uproszczenia pominiemy ten aspekt. Obsługę Value Object’u jako identyfikator Encji omówimy w innym artykule.

Na początek musimy utworzyć odpowiedni schemat:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
create table person
(
    id char(11) not null,
    name_first varchar(255) not null,
    name_last varchar(255) not null,
    date_birth date not null
);

create unique index person_id_uindex
    on person (id);

Analizując powyższą kwerendę SQL, możesz zauważyć, że Encja posiada 3 pola, a tabela w bazie danych 4. Wynika to z faktu, że nasz Value Object posiada w sobie 2 pola, które zostały oznaczone odpowiednio prefiksem name_ w tabeli bazy danych. Aby zapisać utworzoną encję w bazie, utwórzmy przykładowe repozytorium, które wykona zapytanie INSERT do bazy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php declare(strict_types=1);

final class PersonDbalRepository extends DbalRepository implements PersonRepository
{
    public function save(Person $person): void
    {
        $sql = 'INSERT INTO person (id, name_first, name_last, birth_date) VALUES (?, ?, ?, ?);';
        $stmt = $this->connection()->prepare($sql);
        $stmt->bindValue(1, $person->getId());
        $stmt->bindValue(2, $person->getName()->first());
        $stmt->bindValue(3, $person->getName()->last());
        $stmt->bindValue(4, $person->getBirthDate()->format('Y-m-d'));
        $stmt->execute();
    }
}

To wszystko, nasza encja została zapisana w bazie danych. Teraz tylko pozostało nam odczytać wiersz z bazy danych i zmaapować go na obiekt Encji oraz Value Objectu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php declare(strict_types=1);

final class PersonDbalRepository extends DbalRepository implements PersonRepository
{
    // ...

    public function find(string $id): Person
    {
        $sql = 'SELECT * FROM person WHERE id = ?';
        $stmt = $this->connection()->prepare($sql);
        $stmt->bindValue(1, $id);
        $result = $stmt->execute();
        
        // ...

        return new Person(
            $row['id'],
            new Name($row['name_first'], $row['name_last']),
            DateTimeImmutable::createFromFormat('Y-m-d', $row['birth_date'])
        );
    }
}

Proste, prawda? Niestety niesie to za sobą kilka konsekwencji. Przy każdym odczycie danych z bazy, wywołany będzie konstruktor obiektów, który bardzo często będzie zawierał logikę walidacji. Spowoduje to wydłużenie czasu tworzenia obiektów. To jednak nie jest jeszcze takie złe. Wyobraźmy sobie, że w konstruktorze Encji którą tworzymy, wysyłamy zdarzenie domenowe: Osoba została utworzona (Tak wiem, dziwnie to brzmi :D). Za każdym raziem, gdy będziemy odczytywać obiekt z bazy danych, zdarzenie zostanie wysłane i nie jest to poprawne zachowanie. Dlatego warto korzystać z gotowych i sprawdzonych narzędzi takich jak Doctrine, który tworzy nowy obiekt bezwykorzystania konstruktora.

Value Object w Doctrine

Zapisywanie Obiektu wartości za pomocą Doctrine można wykonać na 3 sposoby. Pierwszym z nich i chyba najłatwiejszym jest ręczne tworzenie Obiektu Wartości w metodzie get lub odpowiednie przypisanie do pól encji podczas wykonania metody set czy w konstruktorze.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php declare(strict_types=1);

final class Name
{
    private string $first;
    private string $last;

    public function __construct(string $first, string $last)
    {
        $this->first = $first;
        $this->last = $last;
    }

    public function first(): string
    {
        return $this->first;
    }

    public function last(): string
    {
        return $this->last;
    }
}

/** @Entity */
final class Person
{
    /**
     * @Id
     * @Column(name="id", type="string", length=11)
     * @GeneratedValue(strategy="NONE")
     */
    private string $id;
    /** @ORM\Column(type="string") */
    private string $nameFirst;
    /** @ORM\Column(type="string") */
    private string $nameLast;
    /** @ORM\Column(type="string") */
    private DateTimeImmutable $birthDate;

    public function __construct(string $id, Name $name, DateTimeImmutable $birthDate)
    {
        $this->id = $id;
        $this->name_first = $name->first();
        $this->name_last = $name->last();
        $this->birthDate = $birthDate;
    }

    public function getName(): Name
    {
        return new Name($this->name_first, $this->name_last);
    }
}

Proste i szybkie rozwiązanie, jednak mamy odrobinę więcej kodu do utrzymania.

Embeddables

Drugim sposobem jest wykorzystanie mechanizmu Embedded. Na początek wyrzućmy pola $name_first oraz $name_last na rzecz jednego pola $name oraz dodajmy mapping encji. Dla uproszczenia jako adnotacje:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php declare(strict_types=1);

/** @Embeddable */
final class Name
{
    /** @ORM\Column(type="string") */
    private string $first;
    /** @ORM\Column(type="string") */
    private string $last;

    public function __construct(string $first, string $last)
    {
        $this->first = $first;
        $this->last = $last;
    }

    public function first(): string
    {
        return $this->first;
    }

    public function last(): string
    {
        return $this->last;
    }
}

/** @Entity */
final class Person
{
    /**
     * @Id
     * @Column(name="id", type="string", length=11)
     * @GeneratedValue(strategy="NONE")
     */
    private string $id;
    /** @Embedded(class="Name") */
    private Name $name;
    /** @ORM\Column(type="string") */
    private DateTimeImmutable $birthDate;

    public function __construct(string $id, Name $name, DateTimeImmutable $birthDate)
    {
        $this->id = $id;
        $this->name = $name;
        $this->birthDate = $birthDate;
    }

    public function getName(): Name
    {
        return $this->name;
    }
}

Spójrz proszę na linię 3 oraz 37. W linii 3 oznaczyliśmy naszą klasę jako Embeddable, dzięki czemu Doctrine wie, że jest to Value Object, który będzie wykorzystywany w Encji. Tak właśnie zrobiliśmy, w linii 37 oznaczyliśmy pole $name adnotację Embedded, która mówi, że to pole jest Value Object’em o podanej klasie. Doctrine z tak skonfigurowaną Encją podczas odczytywania, przy wykorzystaniu swojej implementacji EntityRepository, utworzy nam poprawny obiekt Person z polem $name jako Value Object.

Proste, prawda? Niestety to rozwiązanie ma swoje minusy. W wersji Doctrine/ORM 2.x pole Embedded nie może być null’em. Prawdopodobnie Embbeded będzie mogło być nullem w wersja 3.x, która na dzień pisania postu jest jeszcze w fazie developmentu.

Jak poradzić sobie z tym problemem? Albo wybieramy sposób pierwszy opisany wyżej, albo wybieramy sposób trzeci. Spoiler ALERT! Jednak już na początku muszę Cię uprzedzić, że i to rozwiązanie ma swój minus. Niestety w naszym opisywanym przypadku, niestety nie pozwala on na implementację obiektu Name jako Value Object w naszej Encji.

Ten sposób to Własny Typ (ang. Custom Mapping Type). Sama implementacja jest również dość prosta i co ważne, pozwala na użycie opcji nullable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php declare(strict_types=1);

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;

class MyType extends Type
{
    const MYTYPE = 'mytype'; // modify to match your type name

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        // return the SQL used to create your column type. To create a portable column type, use the $platform.
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        // This is executed when the value is read from the database. Make your conversions here, optionally using the $platform.
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        // This is executed when the value is written to the database. Make your conversions here, optionally using the $platform.
    }

    public function getName()
    {
        return self::MYTYPE; // modify to match your constant name
    }
}

Tak wygląda szablon implementacji własnego typu w dokumentacji Doctrine.

Jak poprawnie zaimplementować custom mapping type? Tym zajmiemy się w kolejnym artykule, w którym opiszemy jak użyć Value Object jako identyfikator. Z tego miejsca zapraszam Cię do śledzenia mojego bloga.