Ian Rodrigues

Engenheiro de DevOps na Oowlish, trabalhando para a Petco. Eu gosto de aprender coisas novas e, compartilhar conhecimento.

| 5 min leitura

Utilizando value objects com PHP

#php #domain-driven-design #value-objects #doctrine

Se você já ouviu falar de DDD (Domain-Driven Design), você provavelmente deve ter ouvido falar de Value Objects. Ele é um dos elementos apresentados por Eric Evans no livro conhecido como “the blue book”.

Um value object é um tipo customizado e é distinto de outro apenas por suas propriedades. Diferente de uma Entidade, ele não tem um identificador único. Então, dois value objects com as mesmas propriedades são considerados iguais.

Bons exemplos de candidatos a value objects são:

  • telefone
  • endereço
  • preço
  • commit hash
  • ID de uma entidade
  • entre outros.

Ao projetar um value object, você deve se ter atenção nas suas três principais caracteristicas: imutabilidade, equidade estrutural, e auto-validação.

Aqui temos um exemplo:

<?php declare(strict_types=1);

final class Price
{
    const USD = 'USD';
    const CAD = 'CAD';

    /** @var float */
    private $amount;

    /** @var string */
    private $currency;

    public function __construct(float $amount, string $currency = 'USD')
    {
        if ($amount < 0) {
            throw new \InvalidArgumentException("Amount should be a positive value: {$amount}.");
        }

        if (!in_array($currency, $this->getAvailableCurrencies())) {
            throw new \InvalidArgumentException("Currency should be a valid one: {$currency}.");
        }

        $this->amount = $amount;
        $this->currency = $currency;
    }

    private function getAvailableCurrencies(): array
    {
        return [self::USD, self::CAD];
    }

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

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

Imutabilidade

Uma vez instanciado, um value object deve ser o mesmo durante todo o ciclo de vida da aplicação. Se você precisar modificar o seu valor, isso deve ser feito substituindo o objeto por completo.

Usar value objects mutáveis até pode ser usado desde que você os use inteiramente dentro de um escopo local, com apenas uma refência a ele. Caso contrário, você certamente terá problemas.

Utilizando o exemplo anterior, veja como você pode atualizar o valor de um objeto Price:

<?php declare(strict_types=1);

final class Price
{
    // ...

    private function hasSameCurrency(Price $price): bool
    {
        return $this->currency === $price->currency;
    }

    public function sum(Price $price): self
    {
        if (!$this->hasSameCurrency($price)) {
            throw \InvalidArgumentException(
                "You can only sum values with the same currency: {$this->currency} !== {$price->currency}."
            );
        }

        return new self($this->amount + $price->amount, $this->currency);
    }
}

Equidade Estrutural

Value objects não tem um identificador. Isto é, se dois ou mais value objects tem os mesmos valores internamente, eles devem ser considerado iguais. E como no PHP não existe a possibilidade de modificar o comparador de equidade, você deve implementar isso na mão.

Você pode tanto criar um método customizado que faz isso:

<?php declare(strict_types=1);

final class Price
{
    // ...

    public function isEqualsTo(Price $price): bool
    {
        return $this->amount === $price->amount && $this->currency === $price->currency;
    }
}

Ou criar um hash baseado nas suas propriedades, e compará-los:

<?php declare(strict_types=1);

final class Price
{
    // ...

    private function hash(): string
    {
        return md5("{$this->amount}{$this->currency}");
    }

    public function isEqualsTo(Price $price): bool
    {
        return $this->hash() === $price->hash();
    }
}

Auto-Validação

A validação das propriedades de um value object deve acontecer na sua criação. Se qualquer das propriedades for inválida, uma exceção deve ser lançada. Juntando isso com a imutabilidade, uma vez criado, você pode ter a certeza que o value object será sempre válido.

Pegando o exemplo do Price novamente, não faz sentido ter um valor de preço negativo no domínio da minha aplicação:

<?php declare(strict_types=1);

final class Price
{
    // ...

    public function __construct(float $amount, string $currency = 'USD')
    {
        if ($amount < 0) {
            throw new \InvalidArgumentException("Amount should be a positive value: {$amount}.");
        }

        if (!in_array($currency, $this->getAvailableCurrencies())) {
            throw new \InvalidArgumentException("Currency should be a valid one: {$currency}.");
        }

        $this->amount = $amount;
        $this->currency = $currency;
    }
}

Usando com Doctrine

Armazenar e recuperar value objects do banco de dados usando Doctrine se torna fácil usando Embeddables. De acordo com a documentação do Doctrine, Embeddables não são entidades. Mas, eles são incorporados nas entidades, o que os torna perfeito para serem tratados como value objects.

Digamos que você tem uma classe Product, e você quer armazenar o preço nessa classe. Você terá implementado algo parecido com isso:

<?php declare(strict_types=1);

/** @Embeddable */
final class Price
{
    /** @Column(type="float") */
    private $amount;

    /** @Column(type="string") */
    private $currency;

    public function __construct(float $amount, string $currency = 'USD')
    {
        // ...

        $this->amount = $amount;
        $this->currency = $currency;
    }

    // ...
}

/** @Entity */
class Product
{
    /** @Embedded(class="Price") */
    private $price;

    public function __construct(Price $price)
    {
        $this->price = $price;
    }
}

O Doctrine cria automaticamente colunas baseadas nas propriedades da classe Price na tabela da Product. Por padrão, ele prefixa as colunas com o nome da class Embeddable, no caso: price_amount e price_currency.

Conclusão

Value objects ajudam a escrever um código limpo. No lugar de escrever:

public function addPhoneNumber(string $phone): void {}

Você escreve:

public function addPhoneNumber(PhoneNumber $phone): void {}

O que faz com que o código se torne fácil de ler e interpretar, além disso, você não precisa tentar advinhar que formato de telefone passar como parâmetro.

Já que os atributos de um value object os define, você pode usar ele com diferente entidades, e cachea-los para sempre.

Eles ajudam a reduzir a duplicidade de código. No lugar de ter diversos campos amount e currency em diferente entidades, você pode usar apenas a classe Price.

Claro, como tudo na vida, você não deve abusar dos value objects. Imagine só, converter toneladas de objetos para tipos primitivos para armazenar no banco de dados, ou fazer o caminho inverso convertendo tipos primitivos em diversos objetos. Certamente, você pode ter problema de performance. Além disso, ter value objects muito granulares pode levar sua base de código a ficar muito inchada.

Utilizando value objects, você pode você pode reduzir o “primitive obsession. Use-os para representar um campo, ou conjunto de campos do seu domínio, que precisam de uma validação customizada e que certamente poderia causar um problema de ambiguidade caso fossem usados tipos primitivos.

Obrigado pela leitura. Até mais!

Leitura Adicional

Obrigado por ler e, se gostou, fique a vontade para compartilhar.

Post Anterior

Olá, mundo!
comments powered by Disqus