Working with value objects for ids in Symfony

Installs: 23 932

Dependents: 1

Suggesters: 0

Security: 0

Stars: 4

Watchers: 0

Forks: 6

Open Issues: 0

Type:symfony-bundle

v0.14.0 2024-04-17 06:30 UTC

README

A Symfony bundle to work with id and id list value objects in Symfony. It includes Symfony normalizers for automatic normalization and denormalization and Doctrine types to store the ids and id lists directly in the database.

As it's a central part of an application, it's tested thoroughly (including mutation testing).

Latest Stable Version PHP Version Require codecov Total Downloads License

Installation and configuration

Install package through composer:

composer require digital-craftsman/ids

It's recommended that you install the uuid PHP extension for better performance of id creation and validation. symfony/polyfill-uuid is used as a fallback. You can prevent installing the polyfill when you've installed the PHP extension.

⚠️ This bundle can be used (and is being used) in production, but hasn't reached version 1.0 yet. Therefore, there will be breaking changes between minor versions. I'd recommend that you require the bundle only with the current minor version like composer require digital-craftsman/ids:0.14.*. Breaking changes are described in the releases and the changelog. Updates are described in the upgrade guide.

Working with ids

Creating a new id

The bulk of the logic is in the Id class. Creating a new id is as simple as creating a new class and extending from it like the following:

<?php

declare(strict_types=1);

namespace App\ValueObject;

use DigitalCraftsman\Ids\ValueObject\Id;

final readonly class UserId extends Id
{
}

Now you're already able to use it in your code like this:

$userId = UserId::generateRandom();
if ($userId->isEqualTo($command->userId)) {
    ...
}
$requestingUser->userId->mustNotBeEqualTo($command->targetUserId);

Symfony serializer

If you're injecting the SerializerInterface directly, there is nothing to do. The normalizer for the id is automatically registered.

namespace App\DTO;

final readonly class UserPayload
{
    public function __construct(
        public UserId $userId,
        public string $firstName,
        public string $lastName,
    ) {
    }
}
public function __construct(
    private SerializerInterface $serializer,
) {
}

public function handle(UserPayload $userPayload): string
{
    return $this->serializer->serialize($userPayload, JsonEncoder::FORMAT);
}
{
  "userId": "15d6208b-7cf2-49e5-a193-301d594d98a7",
  "firstName": "Tomas",
  "lastName": "Bauer"
}

This can be combined with the CQRS bundle to have serialized ids there.

Doctrine types

To use an id in your entities, you just need to register a new type for the id. Create a new class for the new id like the following:

<?php

declare(strict_types=1);

namespace App\Doctrine;

use App\ValueObject\UserId;
use DigitalCraftsman\Ids\Doctrine\IdType;

final class UserIdType extends IdType
{
    public static function getTypeName(): string
    {
        return 'user_id';
    }

    public static function getClass(): string
    {
        return UserId::class;
    }
}

Then register the new type in your config/packages/doctrine.yaml file:

doctrine:
  dbal:
    types:
      user_id: App\Doctrine\UserIdType

Alternatively you can also add a compiler pass to register the types automatically.

Then you're already able to add it into your entity like this:

<?php

declare(strict_types=1);

namespace App\Entity;

use App\ValueObject\UserId;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    #[ORM\Id]
    #[ORM\Column(name: 'id', type: 'user_id')]
    public UserId $id;
    
    ...
}

Working with id lists

Id lists are wrapper for an array of ids. They contain a few utility functions and improved type safety.

The IdList is immutable. Therefore, the mutation methods (like add, remove, ...) always return a new instance of the list.

Creating a new id list

The bulk of the logic is in the IdList class. Creating a new id list is as simple as creating a new class and extending from it like the following:

<?php

declare(strict_types=1);

namespace App\ValueObject;

use DigitalCraftsman\Ids\ValueObject\IdList;

/** @extends IdList<UserId> */
final readonly class UserIdList extends IdLIst
{
    public static function handlesIdClass(): string
    {
        return UserId::class;
    }
}

Now you're already able to use it in your code like this:

$userIdList = new UserIdList($userIds);
if ($idsOfEnabledUsers->contains($command->userId)) {
    ...
}
$idsOfEnabledUsers->mustContain($command->targetUserId);

Symfony serializer

If you're injecting the SerializerInterface directly, there is nothing to do. The normalizer for the id list is automatically registered.

Doctrine types

To use an id list in your entities, you just need to register a new type for the id list. Create a new class for the new id list like the following:

<?php

declare(strict_types=1);

namespace App\Doctrine;

use App\ValueObject\UserId;
use App\ValueObject\UserIdList;
use DigitalCraftsman\Ids\Doctrine\IdListType;

final class UserIdListType extends IdListType
{
    protected function getTypeName(): string
    {
        return 'user_id_list';
    }

    protected function getIdListClass(): string
    {
        return UserIdList::class;
    }
    
    protected function getIdClass(): string
    {
        return UserId::class;
    }
}

Then register the new type in your config/packages/doctrine.yaml file:

doctrine:
  dbal:
    types:
      user_id_list: App\Doctrine\UserIdListType

Then you're already able to add it into your entity like this:

<?php

declare(strict_types=1);

namespace App\Entity;

use App\ValueObject\UserIdList;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: InvestorRepository::class)]
#[ORM\Table(name: 'investor')]
class Investor
{
    #[ORM\Column(name: 'ids_of_users_with_access', type: 'user_id_list')]
    public UserIdList $idsOfUsersWithAccess;
    
    ...
}

Additional documentation