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.
Official Doctrine documentation for custom mapping type: Custom Mapping Types
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.
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:
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.
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() ? '×' : '',
$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 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
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.