CreateIT
CreateIT
BLOG

Value Object in Symfony – an update

Symfony

Value Object in Symfony – an update

SHARE

A problem with null

This entry is a follow-up to an earlier one about Value Object – “All about value object in Symfony”.

In the previous article on the use of value object in Symfony I showed how to use VOs along with the Embeddable mechanism in Doctrine. However, this method has a one, clear problem. The mapping value as Embedd cannot assume the null value. This time, I will try to show two different ways that can be used when we need our VO to be able to assume the null value as an entity field.

Setter/getter mapping

The first way is to resign from specialized mapping mechanisms with Doctrine. Instead, we personally define the columns required to store our VO, and we perform the appropriate transformation using the right methods:

/**
 * @ORM\Entity(repositoryClass=Product1Repository::class)
 */
class Product1
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\Column(type="integer")
     */
    private $priceValue;

    /**
     * @ORM\Column(type="string")
     */
    private $priceCurrency;

    public function __construct(string $name, Money $price)
    {
        $this->name = $name;
        $this->priceValue = $price->value();
        $this->priceCurrency = $price->currency();
    }

    ...


    public function setPrice(Money $price)
    {
        $this->priceValue = $price->value();
        $this->priceCurrency = $price->currency();
    }

    public function getPrice()
    {
        return new Money($this->priceValue, $this->priceCurrency);
    }

}

As you can see, $priceValue and $priceCurrency are defined, they will serve to store the value and currency from VO Money. The constructor and setter assume the Money object as an argument, and then they extract the necessary information from it and save it in the $priceValue and $priceCurrency fields. The getter constructs the Money object on the basis of the information from the database and returns it.

Mapping using Custom Mapping Type

The second method uses the Custom Mapping Type from Doctrine. It allows to define our own mapping types for Doctrtine. They work as standard column types so they can assume the “nullable” parameter. The downside is that we must map our object to a single column in the database.

First, we define our mapping:

namespace App\Type;

use App\ValueObject\Money;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;

class MoneyType extends Type
{
    const MONEY = 'money';

    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return 'varchar(100)';
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        [$value, $currency] = explode(' ', $value);
        return new Money($value, $currency);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value === null) {
            return null;
        }
        return $value->value().' '.$value->currency();
    }

    public function getName()
    {
        return self::MONEY;
    }
}

getSQLDeclaration indicates the type of column we will use to store our value in the database.

convertToPHPValue transforms the value from the database into our VO. In this case, we store information as a string type , where the value and currency are separated with the space sign, so we divide it into a table using explode and transfer the appropriate elements to the Money constructor.

convertToDatabaseValue is used to transform our VO to a format that we will be able to store in the database.

The last function, getName, defines the name of our mapping, which we will be using in Doctrine annotations.

Next, we register our mapping in Symfony:

# doctrine.yaml

doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'

        # IMPORTANT: You MUST configure your server version,
        # either here or in the DATABASE_URL env var (see .env file)
        #server_version: '13'

        types:
          money: App\Type\MoneyType

        mapping_types:
          money: string

We can now use our mapping to determine the type of column in the entity:

/**
 * @ORM\Entity(repositoryClass=ProductCustomMappingRepository::class)
 */
class ProductCustomMapping
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $name;

    /**
     * @ORM\Column(type="money", nullable=true)
     */
    private $price;

    public function __construct(string $name, ?Money $price)
    {
        $this->name = $name;
        $this->price = $price;
    }

    ...

    /**
     * @return null|Money
     */
    public function getPrice(): ?Money
    {
        return $this->price;
    }

    /**
     * @param  ?Money  $price
     */
    public function setPrice(?Money $price): void
    {
        $this->price = $price;
    }

}

Links

https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/cookbook/custom-mapping-types.html

https://www.doctrine-project.org/projects/doctrine-orm/en/2.9/cookbook/advanced-field-value-conversion-using-custom-mapping-types.html

About the author:

Adam Zając is a Backend Developer with many years of experience, but he is also skilled in Frontend. Among his many interests, there are architecture, clean code and broadly understood quality in programming.

References:

Polish version: http://adamzajac.info/2021/09/30/value-object-w-symfony-update/

Need help?

  • Looking for support from experienced programmers?

  • Need to fix a bug in the code?

  • Want to customize your webste/application?

ADD COMMENT

Your email address will not be published. Required fields are marked *

createIT Contact