Symfony 4 Doctrine Custom Mapping Type

TL;DR I planed write this article 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, orm for working with doctrine and the package I mentioned:

$ composer require annotations orm yaroslavche/bitmask

Create User entity and DefaultController:

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

Start webserver and check on http://localhost:8000/default :

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

We done with preparing.

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/packages/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 said above — it doesn’t matter what the FQCN will be. For a custom mapping type, we need to extend Doctrine\DBAL\Types\Type and implement/override methods. There is a good article that clearly describes all the objectives of the methods: 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/packages/doctrine.yaml
doctrine:
dbal:
types:
bitmask: App\DBAL\BitMaskType

Now we can use custom type bitmask in our entities:

<?php
# src/Entity/User.php
class User
{
/**
* @ORM\Column(type="bitmask")
*/
private $roles;

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

And it’s all. You have already read the main topic on how to create a custom mapping type =) With Doctrine this is easy, with Symfony it’s even easier.

Using custom mapping type

When we created roles bitmask field - need to 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 let’s create just two “important/usual” 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 display each user id and his 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 to generate some users. We can use doctrine/doctrine-fixtures-bundle package, but also not need in this case - 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.

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

Check out http://localhost:8000/default. But before that, do not forget to generate users by visiting the route http://localhost:8000/generate

As you can see, the right roles are stored. The size of the data has been reduced and you might think, for example, of bitwise operations on the database side to search for all administrators.

Thanks for reading. I hope this could be interesting. And maybe I made a lot of mistakes. Please correct me if I am mistaken somewhere =)