Skip to content

Instantly share code, notes, and snippets.

@yaroslavche
Last active July 27, 2020 22:28
Show Gist options
  • Save yaroslavche/385cdccdb82bfb2c74beeeeda9961901 to your computer and use it in GitHub Desktop.
Save yaroslavche/385cdccdb82bfb2c74beeeeda9961901 to your computer and use it in GitHub Desktop.
Symfony 4 Doctrine Custom Mapping Type

Symfony 4 Doctrine Custom Mapping Type

TL;DR; I planed write this article as reading about how to create a custom doctrine mapping type. But I give you one example for user roles based on bitmask values used as doctrine type that uses my library on github. And you can decide is that right solution or not. Anyway this is example of using custom Doctrine types in Symfony 4.

Doctrine Custom Mapping Types

Official Doctrine documentation for custom mapping type: Custom Mapping Types

Prepare clean project

If you already have project where you want implement custom type - just skip reading this section. Here we go step by step from scratch.

$ composer create-project symfony/skeleton bitmasktypetest
$ cd bitmasktypetest

Here I must say that I always on this step initialize git and install dev dependencies that make my life easier (and this is one from reasons why I really love Symfony with their ecosystem).

$ git init
$ git add .
$ git commit -m "Init commit"
$ composer require --dev maker profiler symfony/web-server-bundle

You can skip this, if you want =) But later I will use some commands from this packages. And you can do this manually =)

So, in our newly created project we need annotations for controller and orm for working with doctrine:

$ composer require annotations orm

And create User entity and DefaultController:

$ bin/console make:controller DefaultController
$ bin/console make:entity User

Start webserver and check on http://localhost:8000/default and we done with preparing:

$ bin/console server:run # or server:start

Just one more beautiful thing: you can type commands shorter: make:entity - m:e, make:controller - m:cont, s:r and so on.

Create custom type

First we need to create a class. In this doc we can see that custom type has namespace App\DBAL. On SO or other articles sometimes use App\Types, sometimes App\Type, AppBundle\Doctrine\Type. In Doctrine doc - My\Project\Types -> App\Types. But I think App\DBAL is good. Anyway if you want - you can use any FQCN in config/doctrine.yaml.

<?php
# src/DBAL/BitMaskType.php

namespace App\DBAL;

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

/**
 * Bitmask datatype.
 */
class BitMaskType extends Type
{
    const BITMASK = Type::BINARY;

    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return $platform->getBinaryTypeDeclarationSQL($fieldDeclaration);
    }

    public function convertToPHPValue($value, AbstractPlatform $platform): BitMaskInterface
    {
        $phpValue = new BitMask($value);
        return $phpValue;
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value instanceof BitMaskInterface) {
            return $value->get();
        } elseif (is_int($value)) {
            return $value;
        } else {
            return null;
        }
    }

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

As I say above - it's don't matter what FQCN will be. For custom mapping type we need extends Doctrine\DBAL\Types\Type and implement/override the methods. There is a good article that's clearly described all methods purposes: Advanced field value conversion using custom mapping types.

And finally important thing - to say doctrine, that we have custom type. For this add type definition in docrine config and Symfony will do all work for us:

# config/doctrine.yaml

doctrine:
    dbal:
        types:
            bitmask: App\DBAL\BitMaskType

Now we can use custom type bitmask in entities:

<?php
# src/Entity/User.php

class User
{
    /**
     * @ORM\Column(type="bitmask")
     */
    private $username;

Also you can select this type in maker, when create entity fields:

image

And that's all. You already read about main topic how to create custom mapping type =) With Doctrine it's easy and Symfony do it much easier.

Using custom mapping type

When we created roles bitmask field - need update database. I assumed that database not created yet. Configure in .env.local file and create:

$ bin/console d:d:c # doctrine:database:create
$ bin/console d:s:c # doctrine:schema:create

If you need remove database - bin/console d:d:d --force. But always use migrations =)

Then in User entity need define constants represents roles:

<?php
# src/Entity/User.php

class User
{
    const ROLE_ADMIN = 1 << 0;
    const ROLE_EDITOR = 1 << 1;
    const ROLE_MANAGER = 1 << 2;
    const ROLE_CUSTOMER = 1 << 3;
    const ROLE_ANONYMOUS = 1 << 4;

Also would be great correct generated stubs with type-hinting:

<?php
# src/Entity/User.php

    public function getRoles(): BitMaskInterface
    {
        return $this->roles;
    }

    public function setRoles(BitMaskInterface $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

For this example lets create just two "important" getters in entity:

<?php
# src/Entity/User.php

    public function isAdmin(): bool
    {
        return $this->roles->isSetBit(static::ROLE_ADMIN);
    }

    public function isCustomer(): bool
    {
        return $this->roles->isSetBit(static::ROLE_CUSTOMER);
    }

And now it's time for controllers. First - need view each user id and roles. I think we don't need twig for this =)

<?php
# src/Controller/DefaultController.php

    /**
     * @Route("/default", name="default")
     */
    public function index()
    {
        $userRepository = $this->getDoctrine()->getRepository(User::class);
        $users = $userRepository->findAll();
        $html = '<table><tr><th>id</th><th>admin</th><th>editor</th><th>manager</th><th>customer</th><th>anonymous</th></tr>';
        foreach ($users as $index => $user) {
            $html .= sprintf('<tr style="text-align: center"><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>',
                $user->getId(),
                $user->isAdmin() ? '&times;' : '',
                $user->getRoles()->isSetBit(User::ROLE_EDITOR) ? '&times;' : '',
                $user->getRoles()->isSetBit(User::ROLE_MANAGER) ? '&times;' : '',
                $user->isCustomer() ? '&times;' : '',
                $user->getRoles()->isSetBit(User::ROLE_ANONYMOUS) ? '&times;' : ''
            );
        }
        $html .= '</table>';
        return new Response($html);
    }

Second thing - need generate some users. We can use doctrine/doctrine-fixtures-bundle package, but also not need - just simple data array without truncating database table on requesting generate route (this is just demo example, I know this is not good solution):

<?php
# src/Controller/DefaultController.php

    /**
     * @Route("/generate", name="generate")
     */
    public function generate()
    {
        $objectManager = $this->getDoctrine()->getManager();

        $usersRoles = [
            User::ROLE_ADMIN,
            User::ROLE_EDITOR,
            User::ROLE_MANAGER,
            User::ROLE_CUSTOMER,
            User::ROLE_ANONYMOUS,
            User::ROLE_ADMIN | User::ROLE_MANAGER,
            User::ROLE_CUSTOMER | User::ROLE_EDITOR,
            User::ROLE_EDITOR | User::ROLE_MANAGER | User::ROLE_CUSTOMER,
            User::ROLE_ADMIN | User::ROLE_CUSTOMER
        ];

        foreach ($usersRoles as $rolesBitmask) {
            $user = new User();
            $bitmaskRoles = new BitMask($rolesBitmask);
            $user->setRoles($bitmaskRoles);
            $objectManager->persist($user);
        }

        $objectManager->flush();
        return $this->json(['status' => 'success']);
    }

Also you can combine roles and check with isSet method. Difference - isSetBit expects only single bit. isSet - check if mask is set:

$isManagerAndAdmin = $user->getRoles()->isSet(User::ROLE_MANAGER | User::ROLE_ADMIN);

Check http://localhost:8000/default. But before generate users by hitting route http://localhost:8000/generate

image2

As you can see - it stores correct roles. Reduced data size, and can think, for example, about bitwise operations on database side for searching all admins.

Thanks for reading. Correct me please, if I'm wrong somewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment