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.phpnamespace 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.yamldoctrine:
dbal:
types:
bitmask: App\DBAL\BitMaskType
Now we can use custom type bitmask
in our entities:
<?php
# src/Entity/User.phpclass 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.phpclass 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() ? '×' : '',
$user->getRoles()->isSetBit(User::ROLE_EDITOR) ? '×' : '',
$user->getRoles()->isSetBit(User::ROLE_MANAGER) ? '×' : '',
$user->isCustomer() ? '×' : '',
$user->getRoles()->isSetBit(User::ROLE_ANONYMOUS) ? '×' : ''
);
}
$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 =)