The update of doctrine/orm
to version 3.2.0 saw the introduction of EnumType columns. The enum type was introduced in PHP 8.1. This new data type is now implemented in Dotkernel, on both the PHP side and the database side.
Below we will discuss some technical aspects behind this update. You can review the full update in this Dotkernel API Pull Request.
Doctrine’s approach
The update introduces the detection of enumType
and options.values
from a property with type: Types::ENUM
. This PR discusses the update and links to several older relevant issues.
Old setup
#[Entity] class Card { #[Id] #[GeneratedValue] #[Column] public int $id; #[Column( type: Types::ENUM, enumType: Suit::class, options: ['values' => ['H', 'D', 'C', 'S']], )] public Suit $suit; }
New setup
#[Entity] class Card { #[Id] #[GeneratedValue] #[Column] public int $id; #[Column(type: Types::ENUM)] public Suit $suit; }
Note that the type
Types::ENUM
part is still required if we want to have an actualenum
column in MySQL/MariaDB. We still default toTypes::STRING
orTYPES::INTEGER
for columns types with a PHP enum as this is the more portable solution and the safer default.
Dotkernel’s approach
Old setup
Dotkernel uses flags for columns like User->Status
, but we resorted to the simpler string
type. The obvious disadvantage is that you can’t definitively enforce a set of values for a given column. Sure, the PHP can be set up to only use the agrred upon set of values, but the database is independent from it. If you edit a value manually in the database, any string is accepted.
The issue is the same on the side of the PHP code. If the developer adds a value with a typo, it’s supported, but will not work as intended.
The only advantage this setup has is the ability to easily add more values in the value set. This may be seen as a feature, but it invites bugs in the execution.
Our old implementation defined the values like below, for the User
entity.
public const STATUS_PENDING = 'pending'; public const STATUS_ACTIVE = 'active'; public const STATUSES = [ self::STATUS_PENDING, self::STATUS_ACTIVE, ];
The column for the ORM was defined like this, as a simple string, with pending
as its default value:
#[ORM\Column(name: "status", type: "string", length: 20)] protected string $status = self::STATUS_PENDING;
Obviously, the getStatus
and setStatus
also work with strings:
public function getStatus(): string { return $this->status; } public function setStatus(string $status): self { $this->status = $status; }
New setup
Thanks to the update of doctrine/orm
to version 3.2.0, Dotkernel can now have a proper link between the PHP code and database values. Now the link between the PHP code and the database is explicit and enforced.
Any update to the value set must be on both the PHP code and the database.
Let’s review how the update affects the User
entity.
In the next example, we show how to implement a value set using a custom enum.
First, we define our custom value set in src/User/src/Enum/UserStatusEnum.php
.
namespace Api\User\Enum; enum UserStatusEnum: string { case Active = 'active'; case Pending = 'pending'; }
We need to create src/User/src/DBAL/Types/UserStatusEnumType.php
to process the new values for the status
column.
AbstractEnumType
must be extended by any future custom enum type.
namespace Api\User\DBAL\Types; use Api\App\DBAL\Types\AbstractEnumType; use Api\User\Enum\UserStatusEnum; class UserStatusEnumType extends AbstractEnumType { public const NAME = 'user_status_enum'; protected function getEnumClass(): string { return UserStatusEnum::class; } public function getName(): string { return self::NAME; } }
If you create your own enum types, make sure to update the
NAME
constant and the value returned bygetEnumClass
.
Let’s register the custom type in config/autoload/doctrine.global.php
under the types
key:
'types' => [ [...] UserStatusEnumType::NAME => UserStatusEnumType::class, [...] ],
The filtering is updated in src/User/src/InputFilter/Input/StatusInput.php
:
$this->getFilterChain() ->attachByName(StringTrim::class) ->attachByName(StripTags::class) ->attach(fn($value) => $value === null ? UserStatusEnum::Active : UserStatusEnum::from($value)); $this->getValidatorChain() ->attachByName(InArray::class, [ 'haystack' => UserStatusEnum::cases(), 'message' => sprintf(Message::INVALID_VALUE, 'status'), ], true);
The above ensures that the new UserStatusEnum
class is used for the status
column updates.
The User
entity uses the new UserStatusEnum
class.
#[ORM\Column(type: 'user_status_enum', options: ['default' => UserStatusEnum::Pending])] protected UserStatusEnum $status = UserStatusEnum::Pending;
The status
getter and setter are also updated:
public function getStatus(): UserStatusEnum { return $this->status; } public function setStatus(UserStatusEnum $status): self { $this->status = $status; }
Dotkernel checks the user status during login in src/User/src/Repository/UserRepository.php
. If the user is not activated, the login is rejected.
if ($clientEntity->getName() === 'frontend' && $result['status'] !== UserStatusEnum::Active) { throw new OAuthServerException(Message::USER_NOT_ACTIVATED, 6, 'inactive_user', 401); }
A new user is created using the enum
type and pending
as the default.
$user = (new User()) ->setDetail($detail) ->setIdentity($data['identity']) ->usePassword($data['password']) ->setStatus($data['status'] ?? UserStatusEnum::Pending);
Note the status
column in the migration query which now looks like this:
$this->addSql(' CREATE TABLE user ( uuid BINARY(16) NOT NULL, identity VARCHAR(191) NOT NULL, password VARCHAR(191) NOT NULL, status ENUM(\'active\', \'pending\') DEFAULT \'pending\' NOT NULL, isDeleted TINYINT(1) NOT NULL, hash VARCHAR(64) NOT NULL, created DATETIME NOT NULL, updated DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_8D93D6496A95E9C4 (identity), UNIQUE INDEX UNIQ_8D93D649D1B862B8 (hash), PRIMARY KEY(uuid)) DEFAULT CHARACTER SET utf8mb4');
The difference for the migration query is for the status
column, highlighted below:
old setup: status VARCHAR(20) NOT NULL new setup: status ENUM(\'active\', \'pending\') DEFAULT \'pending\' NOT NULL
Conclusions
The old setup used in the Dotkernel applications worked fine, but the limitations were clear as day. There was:
- No enforcement of the value set.
- No link between the PHP code and the database.
The new setup solves both issues, ensuring more consistent flag management for your classes.
Relevant links
Looking for PHP, Laminas or Mezzio Support?
As part of the Laminas Commercial Vendor Program, Apidemia offers expert technical support and services for:
Leave a Reply