Skip to content

Use PHP 8.1 Enums

Use PHP 8.1 Enums

INFO

This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository. You can find the original version here

Context

As of Shopware 6.5 the minimum version of PHP is 8.1. We would like to promote the usage of PHP Enums.

Enums are useful where we have a predefined list of constant values. It's now not necessary to provide values as constants, and it's not necessary to create arrays of the constants to check validity.

Decision

All new code which needs to represent a collection of constant values should now use Enums.

A few examples might be:

  • Product Types (Parent, Variant)
  • Product Status (Enabled, Disabled)
  • Backup Type (Full, Incremental)

Where possible, we should migrate existing constant lists to use Enums. See the following Migration Strategy:

Backwards Compatibility / Migration Strategy

To migrate a list of constant values, where an API accepts a "type" parameter which should exist in the list of constant values we can use the Expand & Contract pattern to migrate in a backwards compatible manner:

Consider the following example:

php
class Indexer
{
    public const PARTIAL = 'partial';
    public const FULL = 'full';

    public function product(int $id, string $method): void
    {
        if (!in_array($method, [self::PARTIAL, self::FULL], true)) {
            throw new \InvalidArgumentException();
        }
    
        match ($method) {
            self::PARTIAL => $this->partial($id),
            self::FULL => $this->full($id)
        };
    }
}

Step 1: Create the ENUM & Accept in API as well as string. For this step it is necessary to maintain the allowed values in both the constants and the ENUM:

Note: In PHP 8.1 we cannot assign directly an ENUM to a constant. For the future, it is worth noting that this is supported in PHP 8.2 with backed ENUMS: public const PARTIAL = IndexMethod::PARTIAL->value;

php
enum IndexMethod
{
    case PARTIAL;
    case FULL;
}

class Indexer
{ 
    public function product(int $id, IndexMethod|string $method): void
    {
       ...
    }
}

Step 2: Create ENUM from primitive type if string value passed:

Note: If your ENUM is backed with a value you can use BackedEnum::from to perform automatic casting and validation. Otherwise you will need to map the values manually.

php
class Indexer
{
    public const PARTIAL = 'partial';
    public const FULL = 'full';

    public function product(int $id, IndexMethod|string $method): void
    {
        if (is_string($method)) {
            $method = match ($method) {
                'partial' => IndexMethod::PARTIAL,  
                'full' => IndexMethod::FULL,
                default => throw new \InvalidArgumentException()
            };
        }

        match ($method) {
            IndexMethod::PARTIAL => $this->partial($id),
            IndexMethod::FULL => $this->full($id)
        };
    }
}

Step 3: Deprecate the constants and passing primitive values in the method:

php
class Indexer
{
    // @deprecated tag:v6.6.0 - Constant will be removed, use enum IndexMethod::PARTIAL
    public const PARTIAL = 'partial';
    // @deprecated tag:v6.6.0 - Constant will be removed, use enum IndexMethod::FULL
    public const FULL = 'full';

    /**
     * @deprecated tag:v6.6.0 - Parameter $method will not accept a primitive in v6.6.0
     */
    public function product(int $id, IndexMethod|string $method): void
    {
        if (is_string($method)) {
            $method = match ($method) {
                'partial' => IndexMethod::PARTIAL,  
                'full' => IndexMethod::FULL,
                default => throw new \InvalidArgumentException()
            };
        }

        match ($method) {
            IndexMethod::PARTIAL => $this->partial($id),
            IndexMethod::FULL => $this->full($id)
        };
    }
}

Step 4: Remove deprecations in next major.

Which leaves us with the following, succinct code:

php
enum IndexMethod
{
    case PARTIAL;
    case FULL;
}

class Indexer
{
    public function product(int $id, IndexMethod $method): void
    {
        match ($method) {
            IndexMethod::PARTIAL => $this->partial($id),
            IndexMethod::FULL => $this->full($id)
        };
    }
}

(new Indexer())->product(1, IndexMethod::PARTIAL);