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 actual enum column in MySQL/MariaDB. We still default to Types::STRING or TYPES::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 by getEnumClass.

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:

  • Modernising Legacy Applications
  • Migration from any version of Zend Framework to Laminas
  • Migration from legacy Laminas API Tools (formerly Apigility) to Dotkernel API
  • Mezzio and Laminas Consulting and Technical Audit
  • Leave a Reply

    Your email address will not be published. Required fields are marked *

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>