Issue #3512835 by nicxvan: [11.1.x] Add BC stubs for Hook ordering
(cherry picked from commit 64bfef04fa
)
11.0.x
parent
902e7a8c57
commit
55dfb3cd96
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Drupal\Core\Hook\Attribute;
|
namespace Drupal\Core\Hook\Attribute;
|
||||||
|
|
||||||
|
use Drupal\Core\Hook\Order\OrderInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will not have an effect until Drupal 11.1.0.
|
* This class will not have an effect until Drupal 11.1.0.
|
||||||
*
|
*
|
||||||
|
@ -12,13 +14,29 @@ namespace Drupal\Core\Hook\Attribute;
|
||||||
* #LegacyHook attributes.
|
* #LegacyHook attributes.
|
||||||
*/
|
*/
|
||||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||||
class Hook {
|
class Hook implements HookAttributeInterface {
|
||||||
|
/**
|
||||||
|
* The hook prefix such as `form`.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const string PREFIX = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hook suffix such as `alter`.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const string SUFFIX = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a Hook attribute object.
|
* Constructs a Hook attribute object.
|
||||||
*
|
*
|
||||||
* @param string $hook
|
* @param string $hook
|
||||||
* The short hook name, without the 'hook_' prefix.
|
* The short hook name, without the 'hook_' prefix.
|
||||||
|
* $hook is only optional when Hook is extended and a PREFIX or SUFFIX is
|
||||||
|
* defined. When using the [#Hook] attribute directly $hook is required.
|
||||||
|
* See Drupal\Core\Hook\Attribute\Preprocess.
|
||||||
* @param string $method
|
* @param string $method
|
||||||
* (optional) The method name. If this attribute is on a method, this
|
* (optional) The method name. If this attribute is on a method, this
|
||||||
* parameter is not required. If this attribute is on a class and this
|
* parameter is not required. If this attribute is on a class and this
|
||||||
|
@ -30,16 +48,26 @@ class Hook {
|
||||||
* are executed first. If omitted, the module order is used to order the
|
* are executed first. If omitted, the module order is used to order the
|
||||||
* hook implementations.
|
* hook implementations.
|
||||||
* @param string|null $module
|
* @param string|null $module
|
||||||
* (optional) The module this implementation is for. This allows one module to
|
* (optional) The module this implementation is for. This allows one module
|
||||||
* implement a hook on behalf of another module. Defaults to the module the
|
* to implement a hook on behalf of another module. Defaults to the module
|
||||||
* implementation is in.
|
* the implementation is in.
|
||||||
|
* @param \Drupal\Core\Hook\Order\OrderInterface|null $order
|
||||||
|
* (optional) Set the order of the implementation. This parameter is
|
||||||
|
* supported in Drupal 11.2 and greater. It will have no affect in Drupal
|
||||||
|
* 11.1.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $hook,
|
public string $hook = '',
|
||||||
public string $method = '',
|
public string $method = '',
|
||||||
public ?int $priority = NULL,
|
public ?int $priority = NULL,
|
||||||
public ?string $module = NULL,
|
public ?string $module = NULL,
|
||||||
) {}
|
public OrderInterface|null $order = NULL,
|
||||||
|
) {
|
||||||
|
$this->hook = implode('_', array_filter([static::PREFIX, $hook, static::SUFFIX]));
|
||||||
|
if ($this->hook === '') {
|
||||||
|
throw new \LogicException('The Hook attribute or an attribute extending the Hook attribute must provide the $hook parameter, a PREFIX or a SUFFIX.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the method the hook should apply to.
|
* Set the method the hook should apply to.
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common interface for attributes used for hook discovery.
|
||||||
|
*
|
||||||
|
* This does not imply any shared behavior, it is only used to collect all
|
||||||
|
* hook-related attributes in the same call.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface HookAttributeInterface {}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents procedural hook_module_implements_alter from executing.
|
||||||
|
*
|
||||||
|
* This allows the use of the legacy hook_module_implements_alter alongside
|
||||||
|
* attribute-based ordering. Providing support for versions of Drupal older
|
||||||
|
* than 11.2.0.
|
||||||
|
*
|
||||||
|
* Marking hook_module_implements_alter as #LegacyModuleImplementsAlter will
|
||||||
|
* prevent hook_module_implements_alter from running when attribute-based
|
||||||
|
* ordering is available.
|
||||||
|
*
|
||||||
|
* On older versions of Drupal which are not aware of attribute-based ordering,
|
||||||
|
* only the legacy hook implementation is executed.
|
||||||
|
*/
|
||||||
|
#[\Attribute(\Attribute::TARGET_FUNCTION)]
|
||||||
|
class LegacyModuleImplementsAlter {}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an already existing implementation.
|
||||||
|
*
|
||||||
|
* The effect of this attribute is independent from the specific class or method
|
||||||
|
* on which it is placed.
|
||||||
|
*
|
||||||
|
* This attribute is supported in Drupal 11.2 and greater.
|
||||||
|
*/
|
||||||
|
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||||
|
class RemoveHook implements HookAttributeInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a RemoveHook object.
|
||||||
|
*
|
||||||
|
* @param string $hook
|
||||||
|
* The hook name from which to remove the target implementation.
|
||||||
|
* @param class-string $class
|
||||||
|
* The class name of the target hook implementation.
|
||||||
|
* @param string $method
|
||||||
|
* The method name of the target hook implementation.
|
||||||
|
* If the class instance itself is the listener, this should be '__invoke'.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $hook,
|
||||||
|
public readonly string $class,
|
||||||
|
public readonly string $method,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Attribute;
|
||||||
|
|
||||||
|
use Drupal\Core\Hook\Order\OrderInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the order of an already existing implementation.
|
||||||
|
*
|
||||||
|
* The effect of this attribute is independent from the specific class or method
|
||||||
|
* on which it is placed.
|
||||||
|
*
|
||||||
|
* This attribute is supported in Drupal 11.2 and greater.
|
||||||
|
*/
|
||||||
|
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||||
|
class ReorderHook implements HookAttributeInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a ReorderHook object.
|
||||||
|
*
|
||||||
|
* @param string $hook
|
||||||
|
* The hook for which to reorder an implementation.
|
||||||
|
* @param class-string $class
|
||||||
|
* The class of the targeted hook implementation.
|
||||||
|
* @param string $method
|
||||||
|
* The method name of the targeted hook implementation.
|
||||||
|
* If the #[Hook] attribute is on the class itself, this should be
|
||||||
|
* '__invoke'.
|
||||||
|
* @param \Drupal\Core\Hook\Order\OrderInterface $order
|
||||||
|
* Specifies a new position for the targeted hook implementation relative to
|
||||||
|
* other implementations.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public string $hook,
|
||||||
|
public string $class,
|
||||||
|
public string $method,
|
||||||
|
public OrderInterface $order,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Order;
|
||||||
|
|
||||||
|
use Drupal\Core\Hook\OrderOperation\FirstOrLast;
|
||||||
|
use Drupal\Core\Hook\OrderOperation\OrderOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this implementation to be first or last.
|
||||||
|
*/
|
||||||
|
enum Order: int implements OrderInterface {
|
||||||
|
|
||||||
|
// This implementation should execute first.
|
||||||
|
case First = 1;
|
||||||
|
|
||||||
|
// This implementation should execute last.
|
||||||
|
case Last = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getOperation(string $identifier): OrderOperation {
|
||||||
|
return new FirstOrLast($identifier, $this === self::Last);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this implementation to be after others.
|
||||||
|
*/
|
||||||
|
readonly class OrderAfter extends RelativeOrderBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function isAfter(): bool {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Order;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set this implementation to be before others.
|
||||||
|
*/
|
||||||
|
readonly class OrderBefore extends RelativeOrderBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function isAfter(): bool {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Order;
|
||||||
|
|
||||||
|
use Drupal\Core\Hook\OrderOperation\OrderOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for order specifiers used in hook attributes.
|
||||||
|
*
|
||||||
|
* Objects implementing this interface allow for relative ordering of hooks.
|
||||||
|
* These objects are passed as an order parameter to a Hook or ReorderHook
|
||||||
|
* attribute.
|
||||||
|
* Order::First and Order::Last are simple order operations that move the hook
|
||||||
|
* implementation to the first or last position of hooks at the time the order
|
||||||
|
* directive is executed.
|
||||||
|
* @code
|
||||||
|
* #[Hook('custom_hook', order: Order::First)]
|
||||||
|
* @endcode
|
||||||
|
* OrderBefore and OrderAfter take additional parameters
|
||||||
|
* for ordering. See Drupal\Core\Hook\Order\RelativeOrderBase.
|
||||||
|
* @code
|
||||||
|
* #[Hook('custom_hook', order: new OrderBefore(['other_module']))]
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
interface OrderInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets order operations specified by this object.
|
||||||
|
*
|
||||||
|
* @param string $identifier
|
||||||
|
* Identifier of the implementation to move to a new position. The format
|
||||||
|
* is the class followed by "::" then the method name. For example,
|
||||||
|
* "Drupal\my_module\Hook\MyModuleHooks::methodName".
|
||||||
|
*
|
||||||
|
* @return \Drupal\Core\Hook\OrderOperation\OrderOperation
|
||||||
|
* Order operation to apply to a hook implementation list.
|
||||||
|
*/
|
||||||
|
public function getOperation(string $identifier): OrderOperation;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\Order;
|
||||||
|
|
||||||
|
use Drupal\Core\Hook\OrderOperation\BeforeOrAfter;
|
||||||
|
use Drupal\Core\Hook\OrderOperation\OrderOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orders an implementation relative to other implementations.
|
||||||
|
*/
|
||||||
|
abstract readonly class RelativeOrderBase implements OrderInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param list<string> $modules
|
||||||
|
* A list of modules the implementations should order against.
|
||||||
|
* @param list<array{class-string, string}> $classesAndMethods
|
||||||
|
* A list of implementations to order against, as [$class, $method].
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public array $modules = [],
|
||||||
|
public array $classesAndMethods = [],
|
||||||
|
) {
|
||||||
|
if (!$this->modules && !$this->classesAndMethods) {
|
||||||
|
throw new \LogicException('Order must provide either modules or class-method pairs to order against.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the ordering direction.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* TRUE, if the ordered implementation should be inserted after the
|
||||||
|
* implementations specified in the constructor.
|
||||||
|
*/
|
||||||
|
abstract protected function isAfter(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getOperation(string $identifier): OrderOperation {
|
||||||
|
return new BeforeOrAfter(
|
||||||
|
$identifier,
|
||||||
|
$this->modules,
|
||||||
|
array_map(
|
||||||
|
static fn(array $class_and_method) => implode('::', $class_and_method),
|
||||||
|
$this->classesAndMethods,
|
||||||
|
),
|
||||||
|
$this->isAfter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\OrderOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves one listener to be called before or after other listeners.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class BeforeOrAfter extends OrderOperation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param string $identifier
|
||||||
|
* Identifier of the implementation to move to a new position. The format
|
||||||
|
* is the class followed by "::" then the method name. For example,
|
||||||
|
* "Drupal\my_module\Hook\MyModuleHooks::methodName".
|
||||||
|
* @param list<string> $modulesToOrderAgainst
|
||||||
|
* Module names of listeners to order against.
|
||||||
|
* @param list<string> $identifiersToOrderAgainst
|
||||||
|
* Identifiers of listeners to order against.
|
||||||
|
* The format is "$class::$method".
|
||||||
|
* @param bool $isAfter
|
||||||
|
* TRUE, if the listener to move should be moved after the listener to order
|
||||||
|
* against, FALSE if it should be moved before.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected readonly string $identifier,
|
||||||
|
protected readonly array $modulesToOrderAgainst,
|
||||||
|
protected readonly array $identifiersToOrderAgainst,
|
||||||
|
protected readonly bool $isAfter,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply(array &$identifiers, array $module_finder): void {
|
||||||
|
assert(array_is_list($identifiers));
|
||||||
|
$index = array_search($this->identifier, $identifiers);
|
||||||
|
if ($index === FALSE) {
|
||||||
|
// Nothing to reorder.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$identifiers_to_order_against = $this->identifiersToOrderAgainst;
|
||||||
|
if ($this->modulesToOrderAgainst) {
|
||||||
|
$identifiers_to_order_against = [
|
||||||
|
...$identifiers_to_order_against,
|
||||||
|
...array_keys(array_intersect($module_finder, $this->modulesToOrderAgainst)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$indices_to_order_against = array_keys(array_intersect($identifiers, $identifiers_to_order_against));
|
||||||
|
if ($indices_to_order_against === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($this->isAfter) {
|
||||||
|
$max_index_to_order_against = max($indices_to_order_against);
|
||||||
|
if ($index >= $max_index_to_order_against) {
|
||||||
|
// The element is already after the other elements.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
array_splice($identifiers, $max_index_to_order_against + 1, 0, $this->identifier);
|
||||||
|
// Remove the element after splicing.
|
||||||
|
unset($identifiers[$index]);
|
||||||
|
$identifiers = array_values($identifiers);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$min_index_to_order_against = min($indices_to_order_against);
|
||||||
|
if ($index <= $min_index_to_order_against) {
|
||||||
|
// The element is already before the other elements.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Remove the element before splicing.
|
||||||
|
unset($identifiers[$index]);
|
||||||
|
array_splice($identifiers, $min_index_to_order_against, 0, $this->identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\OrderOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves one listener to the start or end of the list.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class FirstOrLast extends OrderOperation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param string $identifier
|
||||||
|
* Identifier of the implementation to move to a new position. The format
|
||||||
|
* is the class followed by "::" then the method name. For example,
|
||||||
|
* "Drupal\my_module\Hook\MyModuleHooks::methodName".
|
||||||
|
* @param bool $isLast
|
||||||
|
* TRUE to move to the end, FALSE to move to the start.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
protected readonly string $identifier,
|
||||||
|
protected readonly bool $isLast,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function apply(array &$identifiers, array $module_finder): void {
|
||||||
|
$index = array_search($this->identifier, $identifiers);
|
||||||
|
if ($index === FALSE) {
|
||||||
|
// The element does not exist.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unset($identifiers[$index]);
|
||||||
|
if ($this->isLast) {
|
||||||
|
$identifiers[] = $this->identifier;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$identifiers = [$this->identifier, ...$identifiers];
|
||||||
|
}
|
||||||
|
$identifiers = array_values($identifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types = 1);
|
||||||
|
|
||||||
|
namespace Drupal\Core\Hook\OrderOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for order operations.
|
||||||
|
*/
|
||||||
|
abstract class OrderOperation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the order of a list of hook implementations.
|
||||||
|
*
|
||||||
|
* @param list<string> $identifiers
|
||||||
|
* Hook implementation identifiers, as "$class::$method", to be changed by
|
||||||
|
* reference.
|
||||||
|
* The order operation must make sure that the array remains a list, and
|
||||||
|
* that the values are the same as before.
|
||||||
|
* @param array<string, string> $module_finder
|
||||||
|
* Lookup map to find a module name for each implementation.
|
||||||
|
* This may contain more entries than $identifiers.
|
||||||
|
*/
|
||||||
|
abstract public function apply(array &$identifiers, array $module_finder): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the operation to a structure that can be stored in the container.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* Packed operation.
|
||||||
|
*/
|
||||||
|
final public function pack(): array {
|
||||||
|
$is_before_or_after = match(get_class($this)) {
|
||||||
|
BeforeOrAfter::class => TRUE,
|
||||||
|
FirstOrLast::class => FALSE,
|
||||||
|
};
|
||||||
|
return [$is_before_or_after, get_object_vars($this)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the stored operation to objects that can apply ordering rules.
|
||||||
|
*
|
||||||
|
* @param array $packed_operation
|
||||||
|
* Packed operation.
|
||||||
|
*
|
||||||
|
* @return self
|
||||||
|
* Unpacked operation.
|
||||||
|
*/
|
||||||
|
final public static function unpack(array $packed_operation): self {
|
||||||
|
[$is_before_or_after, $args] = $packed_operation;
|
||||||
|
$class = $is_before_or_after ? BeforeOrAfter::class : FirstOrLast::class;
|
||||||
|
return new $class(...$args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue