Add initial config and pipeline source files

This commit is contained in:
Tim
2023-03-19 12:05:45 +01:00
commit f2a18d2928
16 changed files with 1146 additions and 0 deletions

View File

@ -0,0 +1,15 @@
<?php
namespace Ardent\PipelineBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
class ArdentPipelineBundle extends AbstractBundle
{
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
$container->import('../config/services.yaml');
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Service\Attribute\Required;
abstract class AbstractOptionsStage implements OptionsStageInterface
{
protected Options $options;
protected LoggerInterface $logger;
public function setOptions(Options $options): void
{
$this->options = $options;
}
#[Required]
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function before(ItemBag $items): void
{
$this->logger->notice(sprintf('Begin %s with %d item(s)', static::class, $items->count()));
}
public function after(ItemBag $items): void
{
$this->logger->notice(sprintf('End %s with %d item(s)', static::class, $items->count()));
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
class Item
{
private string|int $id;
private bool $delete = false;
private ?ItemBag $children = null;
private ?Item $parent = null;
private array $data = [];
public function __construct(
string|int|null $id = null
)
{
$id ??= sha1(microtime());
$this->id = $id;
}
public function getId(): string
{
return $this->id;
}
public function isDelete(): bool
{
return $this->delete;
}
public function setDelete(): void
{
$this->delete = true;
}
// Data manipulation
public function get(string $key, mixed $default = null): mixed
{
return $this->has($key) ? $this->data[$key] : $default;
}
public function set(string $key, mixed $value): self
{
$this->data[$key] = $value;
return $this;
}
public function has(string $key): bool
{
return array_key_exists($key, $this->data);
}
public function getChildren(): ?ItemBag
{
return $this->children;
}
public function setChildren(ItemBag $children): void
{
$this->children = $children;
}
public function addChild(Item $item): self
{
if (!$this->children) {
$this->children = new ItemBag();
}
$item->setParent($this);
$this->children->add($item);
return $this;
}
public function getParent(): ?Item
{
return $this->parent;
}
public function setParent(?Item $parent): Item
{
$this->parent = $parent;
return $this;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
class ItemBag
{
/**
* @var Item[]
*/
private array $items = [];
public function count(): int
{
return count($this->items);
}
public function add(Item $item): self
{
$this->items[$item->getId()] = $item;
return $this;
}
public function remove(Item $item): self
{
if ($this->has($item)) {
unset($this->items[$item->getId()]);
}
return $this;
}
public function has(Item $item): bool
{
return array_key_exists($item->getId(), $this->items);
}
/**
* @return Item[]
*/
public function all(): array
{
return $this->items;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
use Psr\Log\LoggerInterface;
class ItemBagPipeline
{
/**
* @var OptionsStageInterface[]
*/
private array $stages;
public function __construct(
private readonly OptionsProcessor $processor,
OptionsStageInterface ...$stages)
{
$this->stages = $stages;
}
public function setLogger(LoggerInterface $logger): void
{
foreach ($this->stages as $stage) {
$stage->setLogger($logger);
}
}
/**
* @param Item[]|ItemBag $items
*/
public function process(array|ItemBag $items): ItemBag
{
if (is_array($items)) {
$itemBag = new ItemBag();
foreach ($items as $item) {
$itemBag->add($item);
}
} else {
$itemBag = $items;
}
return $this->processor->process($itemBag, ...$this->stages);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
class Options
{
public function __construct(private readonly array $options)
{
}
public function get(string $key, $default = null): mixed
{
if (array_key_exists($key, $this->options) && $this->options[$key] !== null) {
return $this->options[$key];
} else {
return $default;
}
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
class OptionsProcessor
{
private Options $options;
public function __construct(
?Options $options = null,
)
{
$this->options = $options ?? new Options([]);
}
public function process(ItemBag $items, OptionsStageInterface ...$stages): ItemBag
{
foreach ($stages as $stage) {
$stage->setOptions($this->options);
$stage->before($items);
foreach ($items->all() as $item) {
$newItems = $stage($item);
if ($item->isDelete()) {
$items->remove($item);
}
foreach ($newItems as $newItem) {
$items->add($newItem);
}
}
$stage->after($items);
}
return $items;
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
use Exception;
use Psr\Container\ContainerInterface;
class OptionsStageBag implements ContainerInterface
{
/**
* @var OptionsStageInterface[]
*/
private array $stages = [];
public function __construct(
iterable $optionProcessors
)
{
foreach ($optionProcessors as $optionProcessor) {
/** @var OptionsStageInterface $optionProcessor */
$this->stages[get_class($optionProcessor)] = $optionProcessor;
}
}
/**
* @template T of OptionsStageInterface
* @param class-string<T> $id
* @return T
*/
public function get(string $id): OptionsStageInterface
{
if (!$this->has($id)) {
throw new Exception(sprintf('Class "%s" in not an OptionsStageInterface, it is not in the OptionsStageBag', $id));
}
return $this->stages[$id];
}
public function has(string $id): bool
{
return array_key_exists($id, $this->stages);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
class OptionsStageBuilder
{
/**
* @var OptionsStageInterface[]
*/
private array $stages = [];
public function __construct(
private readonly OptionsStageBag $bag,
private readonly ?Options $options = null,
)
{
}
/**
* @param string|OptionsStageInterface $stageClass Can either be the classFQN of an OptionsStageInterface or one directly
*/
public function add(string|OptionsStageInterface $stageClass): self
{
if (is_string($stageClass)) {
$this->stages[] = $this->bag->get($stageClass);
} else {
$this->stages[] = $stageClass;
}
return $this;
}
public function each(ItemBagPipeline $pipeline): self
{
$this->stages[] = $this->bag->get(SubItemStage::class)->setSubPipeline($pipeline);
return $this;
}
public function build(): ItemBagPipeline
{
return new ItemBagPipeline(new OptionsProcessor($this->options), ...$this->stages);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
use Psr\Log\LoggerInterface;
interface OptionsStageInterface
{
/**
* @return Item[] New items
*/
public function __invoke(Item $item): array;
public function setLogger(LoggerInterface $logger): void;
public function before(ItemBag $items): void;
public function after(ItemBag $items): void;
public function setOptions(Options $options): void;
}

View File

@ -0,0 +1,32 @@
<?php
namespace Ardent\PipelineBundle\Pipeline\OptionsProcessor;
use Exception;
class SubItemStage extends AbstractOptionsStage
{
private ?ItemBagPipeline $subPipeline = null;
public function getSubPipeline(): ItemBagPipeline
{
if (!$this->subPipeline) {
throw new Exception('subPipeline not set');
}
return $this->subPipeline;
}
public function setSubPipeline(ItemBagPipeline $subPipeline): self
{
$clone = clone $this;
$clone->subPipeline = $subPipeline;
return $clone;
}
public function __invoke(Item $item): array
{
$this->getSubPipeline()->process($item->getChildren());
return [];
}
}