, * options?: array, * priority?: int, * }>, * callbacks?: list * } * @extends AbstractFilter * @implements IteratorAggregate */ class FilterChain extends AbstractFilter implements Countable, IteratorAggregate { /** * Default priority at which filters are added */ public const DEFAULT_PRIORITY = 1000; /** @var FilterPluginManager|null */ protected $plugins; /** * Filter chain * * @var PriorityQueue */ protected $filters; /** * Initialize filter chain * * @param FilterChainConfiguration|Traversable|null $options */ public function __construct($options = null) { $this->filters = new PriorityQueue(); if (null !== $options) { $this->setOptions($options); } } /** * @param FilterChainConfiguration|Traversable $options * @return $this * @throws Exception\InvalidArgumentException */ public function setOptions($options) { if (! is_array($options) && ! $options instanceof Traversable) { throw new Exception\InvalidArgumentException(sprintf( 'Expected array or Traversable; received "%s"', get_debug_type($options) )); } foreach ($options as $key => $value) { switch (strtolower($key)) { case 'callbacks': foreach ($value as $spec) { $callback = $spec['callback'] ?? false; $priority = $spec['priority'] ?? static::DEFAULT_PRIORITY; if (is_callable($callback) || $callback instanceof FilterInterface) { $this->attach($callback, $priority); } } break; case 'filters': foreach ($value as $spec) { $name = $spec['name'] ?? false; $options = $spec['options'] ?? []; $priority = $spec['priority'] ?? static::DEFAULT_PRIORITY; if (is_string($name) && $name !== '') { $this->attachByName($name, $options, $priority); } } break; default: // ignore other options break; } } return $this; } /** * Return the count of attached filters * * @return int */ #[ReturnTypeWillChange] public function count() { return count($this->filters); } /** * Get plugin manager instance * * @return FilterPluginManager */ public function getPluginManager() { $plugins = $this->plugins; if (! $plugins instanceof FilterPluginManager) { $plugins = new FilterPluginManager(new ServiceManager()); $this->setPluginManager($plugins); } return $plugins; } /** * Set plugin manager instance * * @return self */ public function setPluginManager(FilterPluginManager $plugins) { $this->plugins = $plugins; return $this; } /** * Retrieve a filter plugin by name * * @param string $name * @return FilterInterface|callable(mixed): mixed */ public function plugin($name, array $options = []) { $plugins = $this->getPluginManager(); return $plugins->get($name, $options); } /** * Attach a filter to the chain * * @param callable(mixed): mixed|FilterInterface $callback A Filter implementation or valid PHP callback * @param int $priority Priority at which to enqueue filter; defaults to 1000 (higher executes earlier) * @throws Exception\InvalidArgumentException * @return self */ public function attach($callback, $priority = self::DEFAULT_PRIORITY) { if (! is_callable($callback)) { if (! $callback instanceof FilterInterface) { throw new Exception\InvalidArgumentException(sprintf( 'Expected a valid PHP callback; received "%s"', get_debug_type($callback) )); } $callback = [$callback, 'filter']; } $this->filters->insert($callback, $priority); return $this; } /** * Attach a filter to the chain using a short name * * Retrieves the filter from the attached plugin manager, and then calls attach() * with the retrieved instance. * * @param string $name * @param int $priority Priority at which to enqueue filter; defaults to 1000 (higher executes earlier) * @return self */ public function attachByName($name, mixed $options = [], $priority = self::DEFAULT_PRIORITY) { if (! is_array($options)) { $options = (array) $options; } elseif (empty($options)) { $options = null; } $filter = $this->getPluginManager()->get($name, $options); return $this->attach($filter, $priority); } /** * Merge the filter chain with the one given in parameter * * @return self */ public function merge(FilterChain $filterChain) { foreach ($filterChain->filters->toArray(PriorityQueue::EXTR_BOTH) as $item) { $this->attach($item['data'], $item['priority']); } return $this; } /** * Get all the filters * * @return PriorityQueue */ public function getFilters() { return $this->filters; } /** * Returns $value filtered through each filter in the chain * * Filters are run in the order in which they were added to the chain (FIFO) * * @param mixed $value * @return mixed * @psalm-suppress MixedAssignment values are always mixed */ public function filter($value) { $valueFiltered = $value; foreach ($this as $filter) { if ($filter instanceof FilterInterface) { $valueFiltered = $filter->filter($valueFiltered); continue; } $valueFiltered = call_user_func($filter, $valueFiltered); } return $valueFiltered; } /** * Clone filters */ public function __clone() { $this->filters = clone $this->filters; } /** * Prepare filter chain for serialization * * Plugin manager (property 'plugins') cannot * be serialized. On wakeup the property remains unset * and next invocation to getPluginManager() sets * the default plugin manager instance (FilterPluginManager). */ public function __sleep() { return ['filters']; } /** @return Traversable */ public function getIterator(): Traversable { return clone $this->filters; } }