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;
|
||||
|
||||
use Drupal\Core\Hook\Order\OrderInterface;
|
||||
|
||||
/**
|
||||
* This class will not have an effect until Drupal 11.1.0.
|
||||
*
|
||||
|
@ -12,13 +14,29 @@ namespace Drupal\Core\Hook\Attribute;
|
|||
* #LegacyHook attributes.
|
||||
*/
|
||||
#[\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.
|
||||
*
|
||||
* @param string $hook
|
||||
* 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
|
||||
* (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
|
||||
|
@ -30,16 +48,26 @@ class Hook {
|
|||
* are executed first. If omitted, the module order is used to order the
|
||||
* hook implementations.
|
||||
* @param string|null $module
|
||||
* (optional) The module this implementation is for. This allows one module to
|
||||
* implement a hook on behalf of another module. Defaults to the module the
|
||||
* implementation is in.
|
||||
* (optional) The module this implementation is for. This allows one module
|
||||
* to implement a hook on behalf of another module. Defaults to the module
|
||||
* 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 string $hook,
|
||||
public string $hook = '',
|
||||
public string $method = '',
|
||||
public ?int $priority = 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.
|
||||
|
|
|
@ -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