Giter VIP home page Giter VIP logo

ids's Introduction

Id handling with value objects in Symfony

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.

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)) {
    ...
}

Guard against invalid usages:

$requestingUser->userId->mustNotBeEqualTo($command->targetUserId);

Or with a custom exception:

$requestingUser->userId->mustNotBeEqualTo(
    $command->targetUserId,
    static fn () => new Exception\UserCanNotTargetItself(),
);

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)) {
    ...
}

Guard against invalid usages:

$idsOfEnabledUsers->mustContainId($command->targetUserId);

Or with custom exception:

$idsOfEnabledUsers->mustContainId(
    $command->targetUserId,
    static fn () => new Exception\UserIsNotEnabled(),
);

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

ids's People

Contributors

christian-kolb avatar dependabot[bot] avatar ghry5 avatar jofrly avatar scuben avatar tschmidtdev avatar

Stargazers

 avatar  avatar  avatar  avatar

ids's Issues

Add IdList::fromMap constructor

Something like

/**
 * @psalm-param callable(mixed):UserId $mapFunction
 */
public static function fromMap(
    iterable $map,
    callable $mapFunction,
): self {
    $ids = [];
    foreach ($map as $key => $value) {
        $ids[] = $mapFunction($key, $value);
    }

    return new self($ids);
}

PHPStan error for `IdList::handlesIdClass`

I am not sure why, but phpstan reports this:
Method App\Domain\ValueObject\IdList\CustomerIdList::handlesIdClass() should return class-string<TT of App\Domain\ValueObject\Id\CustomerId> but returns string.

This is how my CustomerIdList object looks like:

<?php

namespace App\Domain\ValueObject\IdList;

use App\Domain\ValueObject\Id\CustomerId;
use DigitalCraftsman\Ids\ValueObject\IdList;

/** @extends IdList<CustomerId> */
final class CustomerIdList extends IdList
{
    public static function handlesIdClass(): string
    {
        return CustomerId::class;
    }
}

Add addIds and addIdsWhenNotInList methods

Currently we can only loop though a list of ids and add them one after the other. It would be great if we could add them all at once and also optimise the performance by that.

Rename parameter $exception to $otherwiseThrow

Instead of

$requestingUser->userId->mustNotBeEqualTo(
    $command->targetUserId,
    exception: static fn () => new Exception\UserCanNotTargetItself(),
);

it's better to read:

$requestingUser->userId->mustNotBeEqualTo(
    $command->targetUserId,
    otherwiseThrow: static fn () => new Exception\UserCanNotTargetItself(),
);

Add support for throwing custom exceptions in the guards

/**
 * @template E of \Throwable
 *
 * @param T $id
 * @param ?callable(): E $exception
 *
 * @throws E
 * @throws Exception\IdListDoesNotContainId
 */
public function mustContainId(
    Id $id,
    ?callable $exception = null,
): void {
    if ($this->notContainsId($id)) {
        throw $exception !== null
            ? $exception()
            : new Exception\IdListDoesNotContainId($id);
    }
}

Usage

$this->idsOfUsersWithAccess->mustContainId(
    $command->userId,
    static fn () => new Exception\UserDoesNotHaveAccess($command->userId),
);

Use template variable for id lists

At the moment the $ids in the IdList must be overwritten to define the type of ids in it. This should be also possible with a psalm template syntax.

Using with binary(16)

I am currently stuck with a problem I do not yet see a solution: I have database entries with binary(16) which are for now Symfony Uuid primary keys. The problem with that is, that I can't always define the "uuid" type for a doctrine query parameter to make it work. Otherwise, it will just use the string representation to query the database and won't find anything. I need to either specify the parameter type or use the binary representation of the uuid directly as the parameter value.

So i installed this library and defined the IdType and overrid this methods:

    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        $column['length'] = 16;
        $column['fixed'] = true;

        return $platform->getBinaryTypeDeclarationSQL($column);
    }

    public function getBindingType()
    {
        return ParameterType::BINARY;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
    {
        if (null === $value) {
            return null;
        }

        if (is_string($value)) {
            return Uuid::fromString($value)->toBinary();
        }

        if ($value instanceof Id) {
            return Uuid::fromString((string) $value)->toBinary();
        }
    }

    public function convertToPHPValue($value, AbstractPlatform $platform): ?Id
    {
        if (null === $value) {
            return null;
        }

        return $this->getIdClass()::fromBinary($value);
    }

Not pretty, but does the job. Now when I use the query builder like this:

    $result = $this->createQueryBuilder('a')
            ->andWhere('a.relation = :relation')
            ->setParameter('relation', $relationEntityObject)
            ->getQuery()
            ->getResult()
        ;

I still do not get the result as the generated query still using the string representation and not the binary (as I was expecting with the convertToDatabaseValue method.

Does anyone have an idea what I am missing?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.