From c9caece71052de8460cb1cac8f8e6b9612b772d3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 15 Nov 2025 10:27:36 +0100 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/.init.php | 43 +++++++ examples/00-SimpleCases/00-CsvRead.php | 25 ++++ examples/00-SimpleCases/01-CsvTransform.php | 51 +++++++++ examples/00-SimpleCases/data/customers.csv | 6 + .../Component/PhpEtl/ChainBuilderV2.php | 39 +++++++ .../Component/PhpEtl/ChainConfig.php | 37 ++++++ .../ConfigurableChainOperationInterface.php | 6 + .../Extract/CsvExtractOperation.php | 35 +++--- .../Loader/FileWriterOperation.php | 35 +++--- .../CallbackTransformerOperation.php | 28 +---- .../Transformer/RuleTransformOperation.php | 33 ++---- .../Component/PhpEtl/ChainProcessor.php | 2 +- .../Exception/ChainBuilderException.php | 8 ++ .../Exception/ChainOperationException.php | 2 +- .../Component/PhpEtl/GenericChainFactory.php | 108 ++++++++++++++++++ .../AbstractOperationConfig.php | 31 +++++ .../Extract/CsvExtractConfig.php | 25 ++++ .../Loader/CsvFileWriterConfig.php | 35 ++++++ .../Loader/FileWriterConfigInterface.php | 13 +++ .../OperationConfigInterface.php | 8 ++ .../Transformer/CallBackTransformerConfig.php | 26 +++++ .../Transformer/RuleTransformConfig.php | 17 +++ 22 files changed, 523 insertions(+), 90 deletions(-) create mode 100644 examples/.init.php create mode 100644 examples/00-SimpleCases/00-CsvRead.php create mode 100644 examples/00-SimpleCases/01-CsvTransform.php create mode 100644 examples/00-SimpleCases/data/customers.csv create mode 100644 src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php create mode 100644 src/Oliverde8/Component/PhpEtl/ChainConfig.php create mode 100644 src/Oliverde8/Component/PhpEtl/ChainOperation/ConfigurableChainOperationInterface.php create mode 100644 src/Oliverde8/Component/PhpEtl/Exception/ChainBuilderException.php create mode 100644 src/Oliverde8/Component/PhpEtl/GenericChainFactory.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/FileWriterConfigInterface.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/OperationConfigInterface.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/CallBackTransformerConfig.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php diff --git a/examples/.init.php b/examples/.init.php new file mode 100644 index 0000000..45bc9d7 --- /dev/null +++ b/examples/.init.php @@ -0,0 +1,43 @@ + $ruleApplier]), + new GenericChainFactory(FileWriterOperation::class, CsvFileWriterConfig::class), + ], +); \ No newline at end of file diff --git a/examples/00-SimpleCases/00-CsvRead.php b/examples/00-SimpleCases/00-CsvRead.php new file mode 100644 index 0000000..884d089 --- /dev/null +++ b/examples/00-SimpleCases/00-CsvRead.php @@ -0,0 +1,25 @@ +addLink(New CsvExtractConfig()); +$chainConfig->addLink(new CallBackTransformerConfig(function (DataItem $dataItem) { + var_dump($dataItem->getData()); + return $dataItem; +})); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new DataItem([ + 'file' => 'data/customers.csv', + ]), + [] +); \ No newline at end of file diff --git a/examples/00-SimpleCases/01-CsvTransform.php b/examples/00-SimpleCases/01-CsvTransform.php new file mode 100644 index 0000000..15b22ae --- /dev/null +++ b/examples/00-SimpleCases/01-CsvTransform.php @@ -0,0 +1,51 @@ +addLink(New CsvExtractConfig()) + ->addLink(new RuleTransformConfig( + rules: [ + 'Name' => [ + 'rules' => [ + ['implode' => [ + 'values' => [ + [[ 'get' => [ 'field' => 'FirstName' ]]], + [[ 'get' => [ 'field' => 'LastName' ]]], + ], + 'with' => ' ', + ]], + ], + ], + 'SubscriptionStatus' => [ + 'rules' => [ + ['get' => [ 'field' => 'IsSubscribed' ]] + ], + ], + ], + add: false, + flavor: 'default' + )) + ->addLink(new CsvFileWriterConfig('customers-transformed.csv')) + ->addLink(new CallBackTransformerConfig(function (DataItem $dataItem) { + var_dump($dataItem->getData()); + return $dataItem; + })) +; + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new DataItem([ + 'file' => 'data/customers.csv', + ]), + [] +); diff --git a/examples/00-SimpleCases/data/customers.csv b/examples/00-SimpleCases/data/customers.csv new file mode 100644 index 0000000..e5e2f7b --- /dev/null +++ b/examples/00-SimpleCases/data/customers.csv @@ -0,0 +1,6 @@ +ID;FirstName;LastName;IsSubscribed +1;"Fahima";"Mathews";1 +2;"Stephan";"Patel";0 +3;"Leena";"Rennie";0 +4;"Christine";"Findlay";1 +5;"Jeffrey";"Oneal";1 \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php b/src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php new file mode 100644 index 0000000..7416b31 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php @@ -0,0 +1,39 @@ + $factories + */ + public function __construct( + private readonly ExecutionContextFactoryInterface $contextFactory, + private iterable $factories = [], + ) {} + + public function createChain(ChainConfig $chainConfig): ChainProcessorInterface + { + $operations = []; + foreach ($chainConfig->getConfigs() as $linkConfig) { + $operations[] = $this->getOperationFromConfig($linkConfig); + } + return new ChainProcessor($operations, $this->contextFactory, $chainConfig->maxAsynchronousItems); + } + + private function getOperationFromConfig(OperationConfigInterface $linkConfig): ConfigurableChainOperationInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($linkConfig)) { + return $factory->build($linkConfig); + } + } + + throw new \RuntimeException('No factory found for link config of type ' . get_class($linkConfig)); + } +} \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/ChainConfig.php b/src/Oliverde8/Component/PhpEtl/ChainConfig.php new file mode 100644 index 0000000..8bf37d7 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/ChainConfig.php @@ -0,0 +1,37 @@ + */ + private array $configs; + + /** + * @param OperationConfigInterface[] $configs + */ + public function __construct(public readonly int $maxAsynchronousItems = 1) + {} + + + public function addLink(OperationConfigInterface $linkConfig): self { + $this->configs[] = $linkConfig; + return $this; + } + + /** + * @return array + */ + public function getConfigs(): array + { + return $this->configs; + } + + public function getFlavor(): string + { + // Main chain does not have a flavor. + return ""; + } +} diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/ConfigurableChainOperationInterface.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/ConfigurableChainOperationInterface.php new file mode 100644 index 0000000..d95bb0f --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/ConfigurableChainOperationInterface.php @@ -0,0 +1,6 @@ +delimiter = $delimiter; - $this->enclosure = $enclosure; - $this->escape = $escape; - $this->fileKey = $fileKey; - $this->scoped = $scoped; - } + public function __construct(protected readonly CsvExtractConfig $config) + {} public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface { $filename = $item->getData(); if (is_array($filename)) { - $filename = AssociativeArray::getFromKey($filename, $this->fileKey); + $filename = AssociativeArray::getFromKey($filename, $this->config->fileKey); } - $fileIterator = new Csv($context->getFileSystem()->readStream($filename), $this->delimiter, $this->enclosure, $this->escape); + $fileIterator = new Csv($context->getFileSystem()->readStream($filename), $this->config->delimiter, $this->config->enclosure, $this->config->escape); return new MixItem([new GroupedItem($fileIterator), new FileExtractedItem($filename)]); } -} \ No newline at end of file + + public function getConfigurationClass(): string + { + return CsvExtractConfig::class; + } +} diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Loader/FileWriterOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Loader/FileWriterOperation.php index e4a463a..40af0bc 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Loader/FileWriterOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Loader/FileWriterOperation.php @@ -5,6 +5,7 @@ namespace Oliverde8\Component\PhpEtl\ChainOperation\Loader; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\FileLoadedItem; @@ -13,32 +14,24 @@ use Oliverde8\Component\PhpEtl\Item\StopItem; use Oliverde8\Component\PhpEtl\Load\File\FileWriterInterface; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Loader\FileWriterConfigInterface; -/** - * Class FileWriter - * - * @author de Cramer Oliver - * @copyright 2022 Oliverde8 - * @package Oliverde8\Component\PhpEtl\ChainOperation\Loader - */ -class FileWriterOperation extends AbstractChainOperation implements DataChainOperationInterface +class FileWriterOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { - /** @var FileWriterInterface */ - protected $writer; + private ?FileWriterInterface $writer = null; - protected string $fileName; - - public function __construct(FileWriterInterface $writer, string $fileName) - { - $this->writer = $writer; - $this->fileName = $fileName; - } + public function __construct(private readonly FileWriterConfigInterface $config) + {} /** * @inheritdoc */ public function processData(DataItemInterface $item, ExecutionContext $context): DataItemInterface { + if ($this->writer === null) { + $this->writer = $this->config->getFile(); + } + $this->writer->write($item->getData()); return $item; @@ -46,18 +39,22 @@ public function processData(DataItemInterface $item, ExecutionContext $context): public function processStop(StopItem $stopItem, ExecutionContext $context): ItemInterface { + if ($this->writer === null) { + $this->writer = $this->config->getFile(); + } + $resource = $this->writer->getResource(); if(is_resource($resource) && $stopItem->isFinal) { $meta_data = stream_get_meta_data($resource); $filename = $meta_data["uri"]; - $context->getFileSystem()->writeStream($this->fileName, fopen($filename, 'r')); + $context->getFileSystem()->writeStream($this->config->getFileName(), fopen($filename, 'r')); fclose($resource); unlink($filename); } - return new MixItem([new FileLoadedItem($this->fileName), $stopItem]); + return new MixItem([new FileLoadedItem($this->config->getFileName(),), $stopItem]); } } diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/CallbackTransformerOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/CallbackTransformerOperation.php index b188ad1..b55c54f 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/CallbackTransformerOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/CallbackTransformerOperation.php @@ -5,38 +5,22 @@ namespace Oliverde8\Component\PhpEtl\ChainOperation\Transformer; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\ItemInterface; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\CallBackTransformerConfig; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; -/** - * Class CallbackTransformerOperation - * - * @author de Cramer Oliver - * @copyright 2018 Oliverde8 - * @package Oliverde8\Component\PhpEtl\ChainOperation\Transformer - */ -class CallbackTransformerOperation extends AbstractChainOperation implements DataChainOperationInterface +class CallbackTransformerOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { - protected $callback; - - /** - * CallbackTransformerOperation constructor. - * - * @param $callback - */ - public function __construct($callback) + public function __construct(private CallBackTransformerConfig $config) { - $this->callback = $callback; } - /** - * @inheritdoc - */ public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface { - $method = $this->callback; + $method = $this->config->getCallable(); return $method($item, $context); } -} \ No newline at end of file +} diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php index f74915e..69a760f 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php @@ -6,42 +6,25 @@ use oliverde8\AssociativeArraySimplified\AssociativeArray; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\DataItem; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\RuleTransformConfig; use Oliverde8\Component\RuleEngine\RuleApplier; -/** - * Class RuleTransformOperation - * - * @author de Cramer Oliver - * @copyright 2018 Oliverde8 - * @package Oliverde8\Component\PhpEtl\ChainOperation\Transformer - */ -class RuleTransformOperation extends AbstractChainOperation implements DataChainOperationInterface + +class RuleTransformOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { const VARIABLE_MATCH_REGEX = '/{(?[^{}]+)}/'; /** @var string[] */ protected array $parsedColumns = []; - /** @var RuleApplier */ - protected RuleApplier $ruleApplier; - - /** @var array */ - protected array $rules; - - /** @var boolean */ - protected bool $add; - - public function __construct(RuleApplier $ruleApplier, array $rules, bool $add) - { - $this->ruleApplier = $ruleApplier; - $this->rules = $rules; - $this->add = $add; - } + public function __construct(private readonly RuleApplier $ruleApplier, private readonly RuleTransformConfig $config) + {} public function processData(DataItemInterface $item, ExecutionContext $context): DataItemInterface { @@ -49,11 +32,11 @@ public function processData(DataItemInterface $item, ExecutionContext $context): $newData = []; // We add data and don't send new data. - if ($this->add) { + if ($this->config->add) { $newData = $data; } - foreach ($this->rules as $column => $rule) { + foreach ($this->config->rules as $column => $rule) { // Add context to the data. $data['@context'] = array_merge($context->getParameters(), $rule['context'] ?? []); diff --git a/src/Oliverde8/Component/PhpEtl/ChainProcessor.php b/src/Oliverde8/Component/PhpEtl/ChainProcessor.php index 17c305e..b069534 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainProcessor.php +++ b/src/Oliverde8/Component/PhpEtl/ChainProcessor.php @@ -181,7 +181,7 @@ protected function getItemsFromGroupItem(GroupedItem $item): \Generator } } - protected function handleAsyncItems(int $maxItems = null): \Generator + protected function handleAsyncItems(?int $maxItems = null): \Generator { if ($maxItems === null) { $maxItems = $this->maxAsynchronousItems; diff --git a/src/Oliverde8/Component/PhpEtl/Exception/ChainBuilderException.php b/src/Oliverde8/Component/PhpEtl/Exception/ChainBuilderException.php new file mode 100644 index 0000000..70e69e3 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/Exception/ChainBuilderException.php @@ -0,0 +1,8 @@ +chainOperationName = $chainOperationName; diff --git a/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php b/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php new file mode 100644 index 0000000..e1ab3a2 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php @@ -0,0 +1,108 @@ +isOfType($this->operationClassName, ConfigurableChainOperationInterface::class)) { + throw new ChainBuilderException("Operation class '{$this->operationClassName}' must implement ConfigurableChainOperationInterface"); + } + } + + public function build(OperationConfigInterface $linkConfig): ConfigurableChainOperationInterface + { + $refClass = new \ReflectionClass($this->operationClassName); + $constructor = $refClass->getConstructor(); + + var_dump("GOGOGOGO"); + if ($constructor) { + $params = $constructor->getParameters(); + + // Build arguments in the correct order + $args = []; + foreach ($params as $param) { + $name = $param->getName(); + if ($param->getType() !== null && $this->reflectionIsOfType($param->getType(), OperationConfigInterface::class)) { + $args[] = $linkConfig; + } elseif ($param->getType() !== null && $param->getType()->getName() === 'string' && $name === 'flavor') { + $args[] = $this->flavor; + } else + if (array_key_exists($name, $this->injections)) { + $args[] = $this->injections[$name]; + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + throw new \InvalidArgumentException("Missing parameter '$name' while creating instance of '{$this->operationClassName}' with flavor '{$this->flavor}'"); + } + } + + return $refClass->newInstanceArgs($args); + } else { + return $refClass->newInstance(); + } + } + + public function supports(OperationConfigInterface $linkConfig): bool + { + if (!$this->isOfType(get_class($linkConfig), $this->configClassName)) { + return false; + } + if ($linkConfig->getFlavor() !== $this->flavor) { + return false; + } + return true; + } + + private function reflectionIsOfType(ReflectionType $type, string $expectedClassName): bool + { + foreach ($this->flattenTypes($type) as $reflectionType) { + if ($this->isOfType($reflectionType->getName(), $expectedClassName)) { + return true; + } + } + return false; + } + + private function isOfType(string $className, string $expectedClassName): bool + { + if (!class_exists($className) && !interface_exists($className)) { + return false; + } + + if (!class_exists($expectedClassName) && !interface_exists($expectedClassName)) { + return false; + } + + if (!is_a($className, $expectedClassName, true)) { + return false; + } + + return true; + } + + + private function flattenTypes(ReflectionType $type): array + { + if ($type instanceof ReflectionUnionType) { + return array_values(array_filter($type->getTypes(), fn($t) => $t instanceof ReflectionNamedType)); + } + if ($type instanceof ReflectionNamedType) { + return [$type]; + } + return []; + } +} \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php new file mode 100644 index 0000000..96623c2 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php @@ -0,0 +1,31 @@ +validated = true; + $this->validate(); + } + + /** + * @throws ChainBuilderValidationException + */ + abstract protected function validate(): void; + + public function getFlavor(): string + { + if (!$this->validated) { + throw new ChainBuilderException("Impossible to get flavor are you sure the config calls it's parent constructor?"); + } + return $this->flavor; + } +} \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php new file mode 100644 index 0000000..b0138d7 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php @@ -0,0 +1,25 @@ +enclosure, ["'", '"'], true)) { + throw new \InvalidArgumentException("Enclosure must be a single or double quote"); + } + } +} \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php new file mode 100644 index 0000000..14dd229 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php @@ -0,0 +1,35 @@ +fileName; + } + + public function getFile(): Csv + { + $tmp = tempnam(sys_get_temp_dir(), 'etl'); + return new Csv($tmp, $this->hasHeader, $this->delimiter, $this->enclosure, $this->escape); + } + + protected function validate(): void + {} +} \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/FileWriterConfigInterface.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/FileWriterConfigInterface.php new file mode 100644 index 0000000..657548a --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/FileWriterConfigInterface.php @@ -0,0 +1,13 @@ +callable = $callable; + parent::__construct($flavor); + } + + public function getCallable(): callable + { + return $this->callable; + } + + function validate(): void + { + // All callables are valid. Maybe add check on signature in the future. + } +} \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php new file mode 100644 index 0000000..1ad28b3 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php @@ -0,0 +1,17 @@ + Date: Sat, 15 Nov 2025 11:45:00 +0100 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.lock | 3718 +++++++++++++++++ examples/.init.php | 3 + examples/00-SimpleCases/01-CsvTransform.php | 34 +- examples/00-SimpleCases/02-CsvMerge.php | 41 + .../03-CsvMergeGrouoIntoJson.php | 25 + .../00-SimpleCases/customers-transformed.csv | 11 + examples/00-SimpleCases/data/customers2.csv | 6 + .../Grouping/SimpleGroupingOperation.php | 41 +- .../Transformer/RuleTransformOperation.php | 2 +- .../Grouping/SimpleGroupingConfig.php | 34 + .../Transformer/RuleTransformConfig.php | 15 +- 11 files changed, 3873 insertions(+), 57 deletions(-) create mode 100644 composer.lock create mode 100644 examples/00-SimpleCases/02-CsvMerge.php create mode 100644 examples/00-SimpleCases/03-CsvMergeGrouoIntoJson.php create mode 100644 examples/00-SimpleCases/customers-transformed.csv create mode 100644 examples/00-SimpleCases/data/customers2.csv create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..b87729d --- /dev/null +++ b/composer.lock @@ -0,0 +1,3718 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e262f0efb1cdea6b1d8d820e30bb9841", + "packages": [ + { + "name": "oliverde8/associative-array-simplified", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/oliverde8/AssociativeArraySimplified.git", + "reference": "aa1af7c404d62879140a76e8e9404584c7776b5f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/oliverde8/AssociativeArraySimplified/zipball/aa1af7c404d62879140a76e8e9404584c7776b5f", + "reference": "aa1af7c404d62879140a76e8e9404584c7776b5f", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "oliverde8\\AssociativeArraySimplified\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver de Cramer", + "homepage": "http://oliver-decramer.com" + } + ], + "description": "Allow simplified manupulation of recursive associative arrays.", + "support": { + "issues": "https://github.com/oliverde8/AssociativeArraySimplified/issues", + "source": "https://github.com/oliverde8/AssociativeArraySimplified/tree/master" + }, + "time": "2017-02-17T10:14:26+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/cache", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", + "reference": "1277a1ec61c8d93ea61b2a59738f1deb9bfb6701", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^3.6", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.4|^7.0" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/dependency-injection": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/var-dumper": "<6.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-30T13:22:58+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/expression-language", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/expression-language.git", + "reference": "32d2d19c62e58767e6552166c32fb259975d2b23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/expression-language/zipball/32d2d19c62e58767e6552166c32fb259975d2b23", + "reference": "32d2d19c62e58767e6552166c32fb259975d2b23", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/cache": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ExpressionLanguage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an engine that can compile and evaluate expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/expression-language/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:29:33+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T09:52:27+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/validator", + "version": "v7.3.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "8290a095497c3fe5046db21888d1f75b54ddf39d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/8290a095497c3fe5046db21888d1f75b54ddf39d", + "reference": "8290a095497c3fe5046db21888d1f75b54ddf39d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<7.0", + "symfony/expression-language": "<6.4", + "symfony/http-kernel": "<6.4", + "symfony/intl": "<6.4", + "symfony/property-info": "<6.4", + "symfony/translation": "<6.4.3|>=7.0,<7.0.3", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/translation": "^6.4.3|^7.0.3", + "symfony/type-info": "^7.1.8", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/bin/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v7.3.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-08T16:29:29+00:00" + }, + { + "name": "symfony/var-exporter", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-exporter.git", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "reference": "0f020b544a30a7fe8ba972e53ee48a74c0bc87f4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-27T09:00:46+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + }, + "time": "2025-10-21T19:32:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.32", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-09-30T10:16:31+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.29", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.9", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:29:11+00:00" + }, + { + "name": "rector/rector", + "version": "1.2.10", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "40f9cf38c05296bd32f444121336a521a293fa61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/40f9cf38c05296bd32f444121336a521a293fa61", + "reference": "40f9cf38c05296bd32f444121336a521a293fa61", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "phpstan/phpstan": "^1.12.5" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/1.2.10" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2024-11-08T13:59:10+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:51:50+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-04T01:21:42+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", + "reference": "3c0a55a2c8e21e30a37022801c11c7ab5a6cb2de", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v7.3.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-05T17:41:46+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-29T11:18:49+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f96476035142921000338bad71e5247fbc138872" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T14:36:48+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-13T13:44:09+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/examples/.init.php b/examples/.init.php index 45bc9d7..3a70681 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -2,12 +2,14 @@ use Oliverde8\Component\PhpEtl\ChainBuilderV2; use Oliverde8\Component\PhpEtl\ChainOperation\Extract\CsvExtractOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\Grouping\SimpleGroupingOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Loader\FileWriterOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\CallbackTransformerOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\RuleTransformOperation; use Oliverde8\Component\PhpEtl\ExecutionContextFactory; use Oliverde8\Component\PhpEtl\GenericChainFactory; use Oliverde8\Component\PhpEtl\OperationConfig\Extract\CsvExtractConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\Grouping\SimpleGroupingConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Loader\CsvFileWriterConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\CallBackTransformerConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\RuleTransformConfig; @@ -39,5 +41,6 @@ new GenericChainFactory(CallbackTransformerOperation::class, CallBackTransformerConfig::class), new GenericChainFactory(RuleTransformOperation::class, RuleTransformConfig::class, injections: ['ruleApplier' => $ruleApplier]), new GenericChainFactory(FileWriterOperation::class, CsvFileWriterConfig::class), + new GenericChainFactory(SimpleGroupingOperation::class, SimpleGroupingConfig::class), ], ); \ No newline at end of file diff --git a/examples/00-SimpleCases/01-CsvTransform.php b/examples/00-SimpleCases/01-CsvTransform.php index 15b22ae..cf2e1d0 100644 --- a/examples/00-SimpleCases/01-CsvTransform.php +++ b/examples/00-SimpleCases/01-CsvTransform.php @@ -13,28 +13,20 @@ $chainConfig = new ChainConfig(); $chainConfig->addLink(New CsvExtractConfig()) - ->addLink(new RuleTransformConfig( - rules: [ - 'Name' => [ - 'rules' => [ - ['implode' => [ - 'values' => [ - [[ 'get' => [ 'field' => 'FirstName' ]]], - [[ 'get' => [ 'field' => 'LastName' ]]], - ], - 'with' => ' ', - ]], + ->addLink((new RuleTransformConfig(add: false, flavor: 'default')) + ->addColumn('Name', [ + ['implode' => [ + 'values' => [ + [[ 'get' => [ 'field' => 'FirstName' ]]], + [[ 'get' => [ 'field' => 'LastName' ]]], ], - ], - 'SubscriptionStatus' => [ - 'rules' => [ - ['get' => [ 'field' => 'IsSubscribed' ]] - ], - ], - ], - add: false, - flavor: 'default' - )) + 'with' => ' ', + ]], + ]) + ->addColumn('SubscriptionStatus', [ + ['get' => [ 'field' => 'IsSubscribed' ]] + ]) + ) ->addLink(new CsvFileWriterConfig('customers-transformed.csv')) ->addLink(new CallBackTransformerConfig(function (DataItem $dataItem) { var_dump($dataItem->getData()); diff --git a/examples/00-SimpleCases/02-CsvMerge.php b/examples/00-SimpleCases/02-CsvMerge.php new file mode 100644 index 0000000..c819822 --- /dev/null +++ b/examples/00-SimpleCases/02-CsvMerge.php @@ -0,0 +1,41 @@ +addLink(New CsvExtractConfig()) + ->addLink((new RuleTransformConfig(add: false, flavor: 'default')) + ->addColumn('Name', [ + ['implode' => [ + 'values' => [ + [[ 'get' => [ 'field' => 'FirstName' ]]], + [[ 'get' => [ 'field' => 'LastName' ]]], + ], + 'with' => ' ', + ]], + ]) + ->addColumn('SubscriptionStatus', [ + ['get' => [ 'field' => 'IsSubscribed' ]] + ]) + ) + ->addLink(new CsvFileWriterConfig('customers-transformed.csv')) + ->addLink(new CallBackTransformerConfig(function (DataItem $dataItem) { + var_dump($dataItem->getData()); + return $dataItem; + })) +; + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem(['file' => 'data/customers.csv',]), new DataItem(['file' => 'data/customers2.csv',])]), + [] +); diff --git a/examples/00-SimpleCases/03-CsvMergeGrouoIntoJson.php b/examples/00-SimpleCases/03-CsvMergeGrouoIntoJson.php new file mode 100644 index 0000000..70a29a9 --- /dev/null +++ b/examples/00-SimpleCases/03-CsvMergeGrouoIntoJson.php @@ -0,0 +1,25 @@ +addLink(new CsvExtractConfig()) + ->addLink(new SimpleGroupingConfig(['IsSubscribed'])) + ->addLink(new CallBackTransformerConfig(function (DataItem $dataItem) { + var_dump($dataItem->getData()); + return $dataItem; + })); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem(['file' => 'data/customers.csv',]), new DataItem(['file' => 'data/customers2.csv',])]), + [] +); diff --git a/examples/00-SimpleCases/customers-transformed.csv b/examples/00-SimpleCases/customers-transformed.csv new file mode 100644 index 0000000..f81b02e --- /dev/null +++ b/examples/00-SimpleCases/customers-transformed.csv @@ -0,0 +1,11 @@ +Name;SubscriptionStatus +"Fahima Mathews";1 +"Stephan Patel";0 +"Leena Rennie";0 +"Christine Findlay";1 +"Jeffrey Oneal";1 +"Nikhil Amin";0 +"Yannis Bonner";1 +"Jess Doherty";1 +"Tiya Harris";0 +"Amira Merritt";0 diff --git a/examples/00-SimpleCases/data/customers2.csv b/examples/00-SimpleCases/data/customers2.csv new file mode 100644 index 0000000..ab24b13 --- /dev/null +++ b/examples/00-SimpleCases/data/customers2.csv @@ -0,0 +1,6 @@ +ID;FirstName;LastName;IsSubscribed +10;"Nikhil";"Amin";0 +20;"Yannis";"Bonner";1 +30;"Jess";"Doherty";1 +40;"Tiya";"Harris";0 +50;"Amira";"Merritt";0 \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Grouping/SimpleGroupingOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Grouping/SimpleGroupingOperation.php index 2fab9e7..7715588 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Grouping/SimpleGroupingOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Grouping/SimpleGroupingOperation.php @@ -6,6 +6,7 @@ use oliverde8\AssociativeArraySimplified\AssociativeArray; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\ChainBreakItem; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; @@ -13,40 +14,12 @@ use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Item\StopItem; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Grouping\SimpleGroupingConfig; -/** - * Class SimpleGrouping - * - * @author de Cramer Oliver - * @copyright 2018 Oliverde8 - * @package Oliverde8\Component\PhpEtl\ChainOperation\Grouping - */ -class SimpleGroupingOperation extends AbstractChainOperation implements DataChainOperationInterface +class SimpleGroupingOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { - /** @var string[] Key to use for grouping, if array it will be used to read recursively inside the array. */ - protected $groupKey = []; - - /** @var string[] Key to identify each individual data inside the group. */ - protected $groupIdentifierKey = []; - - /** - * Grouped data kept in memory. - * - * @var array - */ - protected $data = []; - - /** - * SimpleGroupingOperation constructor. - * - * @param string[] $groupKey Key to use for grouping, if array it will be used to read recursively inside the array. - * @param string[] $groupIdentifierKey key to identify each individual data inside the group. - */ - public function __construct(array $groupKey, array $groupIdentifierKey = []) - { - $this->groupKey = $groupKey; - $this->groupIdentifierKey = $groupIdentifierKey; - } + public function __construct(private readonly SimpleGroupingConfig $config) + {} /** @@ -54,10 +27,10 @@ public function __construct(array $groupKey, array $groupIdentifierKey = []) */ public function processData(DataItemInterface $item, ExecutionContext $context): ChainBreakItem { - $groupingValue = AssociativeArray::getFromKey($item->getData(), $this->groupKey); + $groupingValue = AssociativeArray::getFromKey($item->getData(), $this->config->groupKey); if (!empty($this->groupIdentifierKey)) { - $groupIdValue = AssociativeArray::getFromKey($item->getData(), $this->groupIdentifierKey); + $groupIdValue = AssociativeArray::getFromKey($item->getData(), $this->config->groupIdentifierKey); $this->data[$groupingValue][$groupIdValue] = $item->getData(); } else { $this->data[$groupingValue][] = $item->getData(); diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php index 69a760f..c78d0a1 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/RuleTransformOperation.php @@ -36,7 +36,7 @@ public function processData(DataItemInterface $item, ExecutionContext $context): $newData = $data; } - foreach ($this->config->rules as $column => $rule) { + foreach ($this->config->getRules() as $column => $rule) { // Add context to the data. $data['@context'] = array_merge($context->getParameters(), $rule['context'] ?? []); diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php new file mode 100644 index 0000000..2f838c1 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php @@ -0,0 +1,34 @@ +groupKey)) { + throw new \InvalidArgumentException('Group key cannot be empty'); + } + foreach ($this->groupKey as $groupKey) { + if (!is_string($groupKey)) { + throw new \InvalidArgumentException('Group key must be an array of strings'); + } + } + foreach ($this->groupIdentifierKey as $groupIdentifierKey) { + if (!is_string($groupIdentifierKey)) { + throw new \InvalidArgumentException('Group identifier key must be an array of strings'); + } + } + } +} \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php index 1ad28b3..ac18efd 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php @@ -6,11 +6,24 @@ class RuleTransformConfig extends AbstractOperationConfig { - public function __construct(public readonly array $rules, public readonly bool $add, string $flavor) + protected array $rules = []; + + public function __construct(public readonly bool $add, string $flavor) { parent::__construct($flavor); } + public function addColumn(string $columnName, array $rules): self + { + $this->rules[$columnName]['rules'] = $rules; + return $this; + } + + public function getRules(): array + { + return $this->rules; + } + protected function validate(): void { } From 45d1dc58cea134dde96050b264f6931c8bcc3a79 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 15 Nov 2025 12:07:29 +0100 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/.init.php | 3 +++ examples/00-SimpleCases/04-CsvFileFilter.php | 27 +++++++++++++++++++ .../Transformer/FilterDataOperation.php | 23 +++++++--------- .../Component/PhpEtl/GenericChainFactory.php | 1 - .../Transformer/FilterDataConfig.php | 20 ++++++++++++++ 5 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 examples/00-SimpleCases/04-CsvFileFilter.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php diff --git a/examples/.init.php b/examples/.init.php index 3a70681..5a36f84 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -5,6 +5,7 @@ use Oliverde8\Component\PhpEtl\ChainOperation\Grouping\SimpleGroupingOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Loader\FileWriterOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\CallbackTransformerOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\FilterDataOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\RuleTransformOperation; use Oliverde8\Component\PhpEtl\ExecutionContextFactory; use Oliverde8\Component\PhpEtl\GenericChainFactory; @@ -12,6 +13,7 @@ use Oliverde8\Component\PhpEtl\OperationConfig\Grouping\SimpleGroupingConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Loader\CsvFileWriterConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\CallBackTransformerConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\FilterDataConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\RuleTransformConfig; use Oliverde8\Component\RuleEngine\RuleApplier; use Oliverde8\Component\RuleEngine\Rules\ExpressionLanguage; @@ -42,5 +44,6 @@ new GenericChainFactory(RuleTransformOperation::class, RuleTransformConfig::class, injections: ['ruleApplier' => $ruleApplier]), new GenericChainFactory(FileWriterOperation::class, CsvFileWriterConfig::class), new GenericChainFactory(SimpleGroupingOperation::class, SimpleGroupingConfig::class), + new GenericChainFactory(FilterDataOperation::class, FilterDataConfig::class, injections: ['ruleApplier' => $ruleApplier]), ], ); \ No newline at end of file diff --git a/examples/00-SimpleCases/04-CsvFileFilter.php b/examples/00-SimpleCases/04-CsvFileFilter.php new file mode 100644 index 0000000..fc58f05 --- /dev/null +++ b/examples/00-SimpleCases/04-CsvFileFilter.php @@ -0,0 +1,27 @@ +addLink(new CsvExtractConfig()) + ->addLink(new FilterDataConfig([["get" => ["field" => 'IsSubscribed']]])) + ->addLink(new CallBackTransformerConfig(function (DataItem $dataItem) { + var_dump($dataItem->getData()); + return $dataItem; + })); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem(['file' => 'data/customers.csv',]), new DataItem(['file' => 'data/customers2.csv',])]), + [] +); + diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/FilterDataOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/FilterDataOperation.php index 850494c..aa69a61 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/FilterDataOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/FilterDataOperation.php @@ -3,27 +3,22 @@ namespace Oliverde8\Component\PhpEtl\ChainOperation\Transformer; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\ChainBreakItem; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\FilterDataConfig; use Oliverde8\Component\RuleEngine\RuleApplier; -class FilterDataOperation extends AbstractChainOperation implements DataChainOperationInterface +class FilterDataOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { - protected RuleApplier $ruleApplier; - protected array $rule; - - protected bool $negate; - - public function __construct(RuleApplier $ruleApplier, array $rule, bool $negate) - { - $this->ruleApplier = $ruleApplier; - $this->rule = $rule; - $this->negate = $negate; - } + public function __construct( + private readonly RuleApplier $ruleApplier, + private readonly FilterDataConfig $config + ) {} public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface @@ -31,9 +26,9 @@ public function processData(DataItemInterface $item, ExecutionContext $context): $data = $item->getData(); $resultData = []; - $result = $this->ruleApplier->apply($data, $resultData, $this->rule); + $result = $this->ruleApplier->apply($data, $resultData, $this->config->rules); - if (($this->negate && $result == false) || (!$this->negate && $result == true)) { + if (($this->config->negate && $result == false) || (!$this->config->negate && $result == true)) { return $item; } diff --git a/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php b/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php index e1ab3a2..ebdc11a 100644 --- a/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php +++ b/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php @@ -28,7 +28,6 @@ public function build(OperationConfigInterface $linkConfig): ConfigurableChainOp $refClass = new \ReflectionClass($this->operationClassName); $constructor = $refClass->getConstructor(); - var_dump("GOGOGOGO"); if ($constructor) { $params = $constructor->getParameters(); diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php new file mode 100644 index 0000000..b8cd4f8 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php @@ -0,0 +1,20 @@ + Date: Sun, 16 Nov 2025 11:35:08 +0100 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain=20-=20Updated=20ChainSplit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + examples/.init.php | 6 +++ examples/00-SimpleCases/04-CsvFileFilter.php | 1 - .../00-SimpleCases/05-SplitDataIn3Files.php | 35 ++++++++++++++++ .../Component/PhpEtl/ChainBuilderV2.php | 2 +- .../ChainOperation/ChainSplitOperation.php | 20 ++++----- .../Component/PhpEtl/GenericChainFactory.php | 7 ++-- .../AbstractOperationConfig.php | 12 +++--- .../OperationConfig/ChainSplitConfig.php | 42 +++++++++++++++++++ .../Extract/CsvExtractConfig.php | 2 +- .../Grouping/SimpleGroupingConfig.php | 2 +- .../Loader/CsvFileWriterConfig.php | 2 +- .../Transformer/CallBackTransformerConfig.php | 2 +- .../Transformer/FilterDataConfig.php | 2 +- .../Transformer/RuleTransformConfig.php | 2 +- 15 files changed, 111 insertions(+), 27 deletions(-) create mode 100644 examples/00-SimpleCases/05-SplitDataIn3Files.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/ChainSplitConfig.php diff --git a/.gitignore b/.gitignore index fdbb5a5..6968377 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build ./composer.lock var /docs/_site/ +/examples/**.csv diff --git a/examples/.init.php b/examples/.init.php index 5a36f84..bbe370d 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -1,6 +1,8 @@ $ruleApplier]), + new GenericChainFactory(ChainSplitOperation::class, ChainSplitConfig::class), ], ); \ No newline at end of file diff --git a/examples/00-SimpleCases/04-CsvFileFilter.php b/examples/00-SimpleCases/04-CsvFileFilter.php index fc58f05..25a0901 100644 --- a/examples/00-SimpleCases/04-CsvFileFilter.php +++ b/examples/00-SimpleCases/04-CsvFileFilter.php @@ -24,4 +24,3 @@ new ArrayIterator([new DataItem(['file' => 'data/customers.csv',]), new DataItem(['file' => 'data/customers2.csv',])]), [] ); - diff --git a/examples/00-SimpleCases/05-SplitDataIn3Files.php b/examples/00-SimpleCases/05-SplitDataIn3Files.php new file mode 100644 index 0000000..95f87b4 --- /dev/null +++ b/examples/00-SimpleCases/05-SplitDataIn3Files.php @@ -0,0 +1,35 @@ +addLink(new CsvExtractConfig()) + ->addLink( + (new ChainSplitConfig()) + ->addSplit( + (new ChainConfig()) + ->addLink(new FilterDataConfig([["get" => ["field" => 'IsSubscribed']]])) + ->addLink(new CsvFileWriterConfig('customers-subscribed.csv')) + ) + ->addSplit( + (new ChainConfig()) + ->addLink(new FilterDataConfig([["get" => ["field" => 'IsSubscribed']]], true)) + ->addLink(new CsvFileWriterConfig('customers-not-subscribed.csv')) + ) + ) + ->addLink(new CsvFileWriterConfig('customers-all.csv')); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem(['file' => 'data/customers.csv',]), new DataItem(['file' => 'data/customers2.csv',])]), + [] +); diff --git a/src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php b/src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php index 7416b31..52b0a63 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php +++ b/src/Oliverde8/Component/PhpEtl/ChainBuilderV2.php @@ -30,7 +30,7 @@ private function getOperationFromConfig(OperationConfigInterface $linkConfig): C { foreach ($this->factories as $factory) { if ($factory->supports($linkConfig)) { - return $factory->build($linkConfig); + return $factory->build($linkConfig, $this); } } diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainSplitOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainSplitOperation.php index 5e519ae..17fef8f 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainSplitOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainSplitOperation.php @@ -4,12 +4,14 @@ namespace Oliverde8\Component\PhpEtl\ChainOperation; +use Oliverde8\Component\PhpEtl\ChainBuilderV2; use Oliverde8\Component\PhpEtl\ChainProcessor; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Item\StopItem; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; use Oliverde8\Component\PhpEtl\Model\State\OperationState; +use Oliverde8\Component\PhpEtl\OperationConfig\ChainSplitConfig; /** * Class ChainSplitOperation @@ -18,24 +20,22 @@ * @copyright 2018 Oliverde8 * @package Oliverde8\Component\PhpEtl\ChainOperation */ -class ChainSplitOperation extends AbstractChainOperation implements DataChainOperationInterface, DetailedObservableOperation +class ChainSplitOperation extends AbstractChainOperation implements DataChainOperationInterface, DetailedObservableOperation, ConfigurableChainOperationInterface { use SplittedChainOperationTrait; /** * @var ChainProcessor[] */ - private array $chainProcessors; + private array $chainProcessors = []; - /** - * ChainSplitOperation constructor. - * - * @param ChainProcessor[] $chainProcessors - */ - public function __construct(array $chainProcessors) + + public function __construct(ChainBuilderV2 $chainProcessors, ChainSplitConfig $config) { - $this->chainProcessors = $chainProcessors; - $this->onSplittedChainOperationConstruct($chainProcessors); + foreach ($config->getChainConfigs() as $chainConfig) { + $this->chainProcessors[] = $chainProcessors->createChain($chainConfig); + } + $this->onSplittedChainOperationConstruct($this->chainProcessors); } public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface diff --git a/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php b/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php index ebdc11a..510ff5e 100644 --- a/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php +++ b/src/Oliverde8/Component/PhpEtl/GenericChainFactory.php @@ -23,7 +23,7 @@ public function __construct( } } - public function build(OperationConfigInterface $linkConfig): ConfigurableChainOperationInterface + public function build(OperationConfigInterface $linkConfig, ChainBuilderV2 $chainBuilder): ConfigurableChainOperationInterface { $refClass = new \ReflectionClass($this->operationClassName); $constructor = $refClass->getConstructor(); @@ -39,8 +39,9 @@ public function build(OperationConfigInterface $linkConfig): ConfigurableChainOp $args[] = $linkConfig; } elseif ($param->getType() !== null && $param->getType()->getName() === 'string' && $name === 'flavor') { $args[] = $this->flavor; - } else - if (array_key_exists($name, $this->injections)) { + } elseif ($param->getType() !== null && $this->reflectionIsOfType($param->getType(), ChainBuilderV2::class)) { + $args[] = $chainBuilder; + } elseif (array_key_exists($name, $this->injections)) { $args[] = $this->injections[$name]; } elseif ($param->isDefaultValueAvailable()) { $args[] = $param->getDefaultValue(); diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php index 96623c2..e528cb5 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/AbstractOperationConfig.php @@ -8,22 +8,22 @@ abstract class AbstractOperationConfig implements OperationConfigInterface { - private bool $validated = false; + private bool $constructed = false; - public function __construct(protected readonly string $flavor) + public function __construct(protected readonly string $flavor = 'default') { - $this->validated = true; - $this->validate(); + $this->constructed = true; + $this->validate(true); } /** * @throws ChainBuilderValidationException */ - abstract protected function validate(): void; + abstract protected function validate(bool $constructOnly): void; public function getFlavor(): string { - if (!$this->validated) { + if (!$this->constructed) { throw new ChainBuilderException("Impossible to get flavor are you sure the config calls it's parent constructor?"); } return $this->flavor; diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainSplitConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainSplitConfig.php new file mode 100644 index 0000000..ef0317d --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainSplitConfig.php @@ -0,0 +1,42 @@ +chainConfigs; + } + + public function addSplit(ChainConfig $chainConfig): self + { + $this->chainConfigs[] = $chainConfig; + return $this; + } + + protected function validate(bool $constructOnly): void + { + if ($constructOnly) { + return; + } + + if (empty($this->chainConfigs)) { + throw new ChainBuilderException("At least one chain config must be provided for ChainSplitConfig"); + } + foreach ($this->chainConfigs as $chainConfig) { + if (!$chainConfig instanceof ChainConfig) { + throw new ChainBuilderException("All chain configs must be instances of ChainConfig"); + } + } + } +} diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php index b0138d7..525d560 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/CsvExtractConfig.php @@ -16,7 +16,7 @@ public function __construct( parent::__construct($flavor); } - function validate(): void + function validate(bool $constructOnly): void { if (!in_array($this->enclosure, ["'", '"'], true)) { throw new \InvalidArgumentException("Enclosure must be a single or double quote"); diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php index 2f838c1..d04ee8a 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Grouping/SimpleGroupingConfig.php @@ -15,7 +15,7 @@ public function __construct( } - protected function validate(): void + protected function validate(bool $constructOnly): void { if (empty($this->groupKey)) { throw new \InvalidArgumentException('Group key cannot be empty'); diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php index 14dd229..5880772 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Loader/CsvFileWriterConfig.php @@ -30,6 +30,6 @@ public function getFile(): Csv return new Csv($tmp, $this->hasHeader, $this->delimiter, $this->enclosure, $this->escape); } - protected function validate(): void + protected function validate(bool $constructOnly): void {} } \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/CallBackTransformerConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/CallBackTransformerConfig.php index 2b42f51..b65bbd9 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/CallBackTransformerConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/CallBackTransformerConfig.php @@ -19,7 +19,7 @@ public function getCallable(): callable return $this->callable; } - function validate(): void + function validate(bool $constructOnly): void { // All callables are valid. Maybe add check on signature in the future. } diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php index b8cd4f8..9bacdd4 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/FilterDataConfig.php @@ -15,6 +15,6 @@ public function __construct( parent::__construct($flavor); } - protected function validate(): void + protected function validate(bool $constructOnly): void {} } \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php index ac18efd..eb55c07 100644 --- a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/RuleTransformConfig.php @@ -24,7 +24,7 @@ public function getRules(): array return $this->rules; } - protected function validate(): void + protected function validate(bool $constructOnly): void { } } From 24ff97866d84e3df18a47384a637553efc2cef4b Mon Sep 17 00:00:00 2001 From: oliverde8 Date: Sun, 16 Nov 2025 14:58:02 +0100 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain=20-=20Updated=20JsonExtract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + examples/.init.php | 3 +++ examples/00-SimpleCases/07-JsonToCsv.php | 26 +++++++++++++++++++ examples/00-SimpleCases/data/products.json | 26 +++++++++++++++++++ .../Extract/JsonExtractOperation.php | 23 +++++----------- .../Extract/JsonExtractConfig.php | 16 ++++++++++++ .../Transformer/RuleTransformConfig.php | 2 +- 7 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 examples/00-SimpleCases/07-JsonToCsv.php create mode 100644 examples/00-SimpleCases/data/products.json create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/JsonExtractConfig.php diff --git a/.gitignore b/.gitignore index 6968377..68e5859 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build var /docs/_site/ /examples/**.csv +/examples/00-SimpleCases/*.csv diff --git a/examples/.init.php b/examples/.init.php index bbe370d..d63c263 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -4,6 +4,7 @@ use Oliverde8\Component\PhpEtl\ChainOperation\ChainSplitOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Extract\CsvExtractOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\Extract\JsonExtractOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Grouping\SimpleGroupingOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Loader\FileWriterOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\CallbackTransformerOperation; @@ -14,6 +15,7 @@ use Oliverde8\Component\PhpEtl\OperationConfig\ChainSplitConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Extract\CsvExtractConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\Extract\JsonExtractConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Grouping\SimpleGroupingConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Loader\CsvFileWriterConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\CallBackTransformerConfig; @@ -51,5 +53,6 @@ new GenericChainFactory(SimpleGroupingOperation::class, SimpleGroupingConfig::class), new GenericChainFactory(FilterDataOperation::class, FilterDataConfig::class, injections: ['ruleApplier' => $ruleApplier]), new GenericChainFactory(ChainSplitOperation::class, ChainSplitConfig::class), + new GenericChainFactory(JsonExtractOperation::class, JsonExtractConfig::class), ], ); \ No newline at end of file diff --git a/examples/00-SimpleCases/07-JsonToCsv.php b/examples/00-SimpleCases/07-JsonToCsv.php new file mode 100644 index 0000000..a89982e --- /dev/null +++ b/examples/00-SimpleCases/07-JsonToCsv.php @@ -0,0 +1,26 @@ +addLink(new JsonExtractConfig()) + ->addLink((new RuleTransformConfig(false)) + ->addColumn('productId', [['get' => ['field' => 'productId']]]) + ->addColumn('sku', [['get' => ['field' => 'sku']]]) + ->addColumn('name-{@context/locales}', [['get' => ['field' => ['name', '@context/locales']]]]) + ) + ->addLink(new CsvFileWriterConfig('products.csv')); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem('data/products.json')]), + ['locales' => ['fr_FR', 'en_US']] +); diff --git a/examples/00-SimpleCases/data/products.json b/examples/00-SimpleCases/data/products.json new file mode 100644 index 0000000..11dc8ec --- /dev/null +++ b/examples/00-SimpleCases/data/products.json @@ -0,0 +1,26 @@ +[ + { + "productId": 1, + "sku": "sku1", + "name": { + "fr_FR": "Mon Produit 1", + "en_US": "My Product 1" + } + }, + { + "productId": 2, + "sku": "sku2", + "name": { + "fr_FR": "Mon Produit 2", + "en_US": "My Product 2" + } + }, + { + "productId": 3, + "sku": "sku3", + "name": { + "fr_FR": "Mon Produit 3", + "en_US": "My Product 3" + } + } +] diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Extract/JsonExtractOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Extract/JsonExtractOperation.php index 5bfb6bd..744cb89 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Extract/JsonExtractOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Extract/JsonExtractOperation.php @@ -4,6 +4,7 @@ use oliverde8\AssociativeArraySimplified\AssociativeArray; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\FileExtractedItem; @@ -11,31 +12,21 @@ use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Item\MixItem; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Extract\JsonExtractConfig; -class JsonExtractOperation extends AbstractChainOperation implements DataChainOperationInterface +class JsonExtractOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { - protected string $fileKey; - - protected bool $scoped; - - public function __construct(string $fileKey, bool $scoped) - { - $this->fileKey = $fileKey; - $this->scoped = $scoped; - } + public function __construct(private readonly JsonExtractConfig $config) + {} public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface { $filename = $item->getData(); if (is_array($filename)) { - $filename = AssociativeArray::getFromKey($filename, $this->fileKey); + $filename = AssociativeArray::getFromKey($filename, $this->config->fileKey); } - if ($this->scoped) { - $data = json_decode($context->getFileSystem()->read($filename), true); - } else { - $data = json_decode(file_get_contents($filename), true); - } + $data = json_decode($context->getFileSystem()->read($filename), true); return new MixItem([new GroupedItem(new \ArrayIterator($data)), new FileExtractedItem($item->getData())]); } diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/JsonExtractConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/JsonExtractConfig.php new file mode 100644 index 0000000..69f4115 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Extract/JsonExtractConfig.php @@ -0,0 +1,16 @@ + Date: Sun, 16 Nov 2025 15:22:12 +0100 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain=20-=20Updated=20HttpClient=20a?= =?UTF-8?q?nd=20SplitData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/.init.php | 10 ++++ examples/00-SimpleCases/08-ApiToCsv.php | 33 ++++++++++++ examples/00-SimpleCases/09-ApiToCsv2.php | 52 +++++++++++++++++++ .../Transformer/SimpleHttpOperation.php | 49 ++++++----------- .../Transformer/SplitItemOperation.php | 42 ++++++--------- .../Transformer/SimpleHttpConfig.php | 36 +++++++++++++ .../Transformer/SplitItemConfig.php | 29 +++++++++++ 7 files changed, 192 insertions(+), 59 deletions(-) create mode 100644 examples/00-SimpleCases/08-ApiToCsv.php create mode 100644 examples/00-SimpleCases/09-ApiToCsv2.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SimpleHttpConfig.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SplitItemConfig.php diff --git a/examples/.init.php b/examples/.init.php index d63c263..8f6b690 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -10,6 +10,8 @@ use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\CallbackTransformerOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\FilterDataOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\RuleTransformOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\SimpleHttpOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\SplitItemOperation; use Oliverde8\Component\PhpEtl\ExecutionContextFactory; use Oliverde8\Component\PhpEtl\GenericChainFactory; @@ -21,6 +23,8 @@ use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\CallBackTransformerConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\FilterDataConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\RuleTransformConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SimpleHttpConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SplitItemConfig; use Oliverde8\Component\RuleEngine\RuleApplier; use Oliverde8\Component\RuleEngine\Rules\ExpressionLanguage; @@ -29,6 +33,7 @@ use Oliverde8\Component\RuleEngine\Rules\StrToLower; use Oliverde8\Component\RuleEngine\Rules\StrToUpper; use Psr\Log\NullLogger; +use Symfony\Component\HttpClient\HttpClient; require __DIR__ . '/../vendor/autoload.php'; @@ -43,6 +48,9 @@ ] ); +$client = HttpClient::create(['headers' => ['Accept' => 'application/json']]); + + $chainBuilder = new ChainBuilderV2( new ExecutionContextFactory(), [ @@ -54,5 +62,7 @@ new GenericChainFactory(FilterDataOperation::class, FilterDataConfig::class, injections: ['ruleApplier' => $ruleApplier]), new GenericChainFactory(ChainSplitOperation::class, ChainSplitConfig::class), new GenericChainFactory(JsonExtractOperation::class, JsonExtractConfig::class), + new GenericChainFactory(SimpleHttpOperation::class, SimpleHttpConfig::class, injections: ['client' => $client]), + new GenericChainFactory(SplitItemOperation::class, SplitItemConfig::class), ], ); \ No newline at end of file diff --git a/examples/00-SimpleCases/08-ApiToCsv.php b/examples/00-SimpleCases/08-ApiToCsv.php new file mode 100644 index 0000000..ac82def --- /dev/null +++ b/examples/00-SimpleCases/08-ApiToCsv.php @@ -0,0 +1,33 @@ +addLink(new SimpleHttpConfig( + method: 'GET', + url: 'https://63b687951907f863aaf90ab1.mockapi.io/test', + responseIsJson: true + )) + ->addLink(new SplitItemConfig( + keys: ['content'], + singleElement: true + )) + ->addLink(new CsvFileWriterConfig('output.csv')); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem([])]), + [] +); + diff --git a/examples/00-SimpleCases/09-ApiToCsv2.php b/examples/00-SimpleCases/09-ApiToCsv2.php new file mode 100644 index 0000000..577a28e --- /dev/null +++ b/examples/00-SimpleCases/09-ApiToCsv2.php @@ -0,0 +1,52 @@ +addLink(new SimpleHttpConfig( + method: 'GET', + url: '@"https://63b687951907f863aaf90ab1.mockapi.io/test/"~data["id"]', + responseIsJson: true, + optionKey: '-placeholder-', + )) + ->addLink((new RuleTransformConfig(false)) + ->addColumn('createdAt', [['get' => ['field' => ['content', 'createdAt']]]]) + ->addColumn('name', [['get' => ['field' => ['content', 'name']]]]) + ->addColumn('avatar', [['get' => ['field' => ['content', 'avatar']]]]) + ->addColumn('id', [['get' => ['field' => ['content', 'id']]]]) + ) + ->addLink(new CsvFileWriterConfig('output.csv')); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([ + ["id" => 1], + ["id" => 2], + ["id" => 3], + ["id" => 4], + ["id" => 5], + ["id" => 6], + ["id" => 7], + ["id" => 8], + ["id" => 9], + ["id" => 10], + ["id" => 11], + ["id" => 12], + ["id" => 13], + ["id" => 14], + ["id" => 15], + ["id" => 16], + ["id" => 17], + ["id" => 18], + ["id" => 19], + ]), + [] +); + diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php index 7559522..d1526fe 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php @@ -6,67 +6,48 @@ use oliverde8\AssociativeArraySimplified\AssociativeArray; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\AsyncHttpClientResponseItem; -use Oliverde8\Component\PhpEtl\Item\DataItem; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SimpleHttpConfig; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Contracts\HttpClient\HttpClientInterface; -class SimpleHttpOperation extends AbstractChainOperation implements DataChainOperationInterface +class SimpleHttpOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { - private HttpClientInterface $client; - - private string $method = "GET"; - private string $url; - - private bool $responseIsJson; - - private ?string $optionsKey; - - protected ?string $responseKey; - private ExpressionLanguage $expressionLanguage; - public function __construct( - HttpClientInterface $client, - string $method, - string $url, - bool $responseIsJson, - ?string $optionsKey, - ?string $responseKey - ) { - $this->client = $client; - $this->method = $method; - $this->url = $url; - $this->responseIsJson = $responseIsJson; - $this->optionsKey = $optionsKey; - $this->responseKey = $responseKey; - + public function __construct(private readonly HttpClientInterface $client, private readonly SimpleHttpConfig $config) + { $this->expressionLanguage = new ExpressionLanguage(); } - public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface { $data = $item->getData(); - if ($this->optionsKey) { - $options = AssociativeArray::getFromKey($data, $this->optionsKey, []); + if ($this->config->optionKey) { + $options = AssociativeArray::getFromKey($data, $this->config->optionKey, []); } else { $options = $data; } - $url = $this->url; + $url = $this->config->url; if (strpos($url, "@") === 0) { $url = ltrim($url, '@'); $url = $this->expressionLanguage->evaluate($url, ['data' => $data]); } - $response = $this->client->request($this->method, $url, $options); + $response = $this->client->request($this->config->method, $url, $options); $response->getInfo(); - return new AsyncHttpClientResponseItem($this->client, $response, $this->responseIsJson, $this->responseKey, $data); + return new AsyncHttpClientResponseItem($this->client, $response, $this->config->responseIsJson, $this->config->responseKey, $data); + } + + public function getConfigurationClass(): string + { + return SimpleHttpConfig::class; } } diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php index 5194a15..106359c 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php @@ -4,6 +4,7 @@ use oliverde8\AssociativeArraySimplified\AssociativeArray; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Exception\ChainOperationException; use Oliverde8\Component\PhpEtl\Item\ChainBreakItem; @@ -12,26 +13,12 @@ use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Item\MixItem; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SplitItemConfig; -class SplitItemOperation extends AbstractChainOperation implements DataChainOperationInterface +class SplitItemOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { - protected bool $singleElement; - - protected bool $keepKeys; - - protected array $keys; - - protected ?string $keyName; - - protected array $duplicateKeys; - - public function __construct(bool $singleElement, array $keys, bool $keepKeys, string $keyName = null, array $duplicatekeys = []) + public function __construct(protected readonly SplitItemConfig $config) { - $this->singleElement = $singleElement; - $this->keepKeys = $keepKeys; - $this->keys = $keys; - $this->keyName = $keyName; - $this->duplicateKeys = $duplicatekeys; } /** @@ -39,8 +26,8 @@ public function __construct(bool $singleElement, array $keys, bool $keepKeys, st */ public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface { - if ($this->singleElement) { - $data = AssociativeArray::getFromKey($item->getData(), $this->keys[0], new ChainBreakItem()); + if ($this->config->singleElement) { + $data = AssociativeArray::getFromKey($item->getData(), $this->config->keys[0], new ChainBreakItem()); if ($data instanceof ItemInterface) { return $data; } @@ -49,14 +36,14 @@ public function processData(DataItemInterface $item, ExecutionContext $context): } $newItemData = []; - foreach ($this->keys as $key) { + foreach ($this->config->keys as $key) { $newItemData[] = AssociativeArray::getFromKey($item->getData(), $key, []); } return $this->createItem($newItemData); } - protected function createItem($itemData, $fullData): ItemInterface + protected function createItem($itemData, $fullData = []): ItemInterface { if (!is_array($itemData)) { throw new ChainOperationException(sprintf('Split operation expects an array to split; "%s', gettype($itemData))); @@ -68,17 +55,17 @@ protected function createItem($itemData, $fullData): ItemInterface $items[] = $datum; } else { $dataItem = []; - if ($this->keyName) { - AssociativeArray::setFromKey($dataItem, $this->keyName, $datum); + if ($this->config->keyName) { + AssociativeArray::setFromKey($dataItem, $this->config->keyName, $datum); } else { $dataItem = $datum; } - foreach ($this->duplicateKeys as $keyStore => $keyFetch) { + foreach ($this->config->duplicateKeys as $keyStore => $keyFetch) { AssociativeArray::setFromKey($dataItem, $keyStore, AssociativeArray::getFromKey($fullData, $keyFetch)); } - if ($this->keepKeys) { + if ($this->config->keepKeys) { $dataItem = ['key' => $datumKey, 'value' => $dataItem]; } $items[] = new DataItem($dataItem); @@ -87,4 +74,9 @@ protected function createItem($itemData, $fullData): ItemInterface return new MixItem($items); } + + public function getConfigurationClass(): string + { + return SplitItemConfig::class; + } } diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SimpleHttpConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SimpleHttpConfig.php new file mode 100644 index 0000000..97cd33f --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SimpleHttpConfig.php @@ -0,0 +1,36 @@ +method, $validMethods, true)) { + throw new \InvalidArgumentException( + "Method must be one of: " . implode(', ', $validMethods) . ". Got: {$this->method}" + ); + } + + if (empty($this->url)) { + throw new \InvalidArgumentException("URL cannot be empty"); + } + } +} + diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SplitItemConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SplitItemConfig.php new file mode 100644 index 0000000..dc518c5 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/SplitItemConfig.php @@ -0,0 +1,29 @@ +keys)) { + throw new \InvalidArgumentException("Keys cannot be empty"); + } + } +} + From 893aa31436766b296ff0aec6cc14d791112572d1 Mon Sep 17 00:00:00 2001 From: oliverde8 Date: Sun, 16 Nov 2025 15:44:49 +0100 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain=20-=20Updated=20LogOperation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/.init.php | 17 ++++++++-- examples/00-SimpleCases/10-CsvWithLogging.php | 29 ++++++++++++++++ .../Transformer/LogOperation.php | 17 +++++----- .../Transformer/SimpleHttpOperation.php | 5 --- .../Transformer/SplitItemOperation.php | 5 --- .../OperationConfig/Transformer/LogConfig.php | 34 +++++++++++++++++++ 6 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 examples/00-SimpleCases/10-CsvWithLogging.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/LogConfig.php diff --git a/examples/.init.php b/examples/.init.php index 8f6b690..aa519ba 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -9,6 +9,7 @@ use Oliverde8\Component\PhpEtl\ChainOperation\Loader\FileWriterOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\CallbackTransformerOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\FilterDataOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\LogOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\RuleTransformOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\SimpleHttpOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\SplitItemOperation; @@ -22,6 +23,7 @@ use Oliverde8\Component\PhpEtl\OperationConfig\Loader\CsvFileWriterConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\CallBackTransformerConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\FilterDataConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\LogConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\RuleTransformConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SimpleHttpConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SplitItemConfig; @@ -37,10 +39,20 @@ require __DIR__ . '/../vendor/autoload.php'; +// Simple logger that outputs to console +class ConsoleLogger extends \Psr\Log\AbstractLogger +{ + public function log($level, $message, array $context = []): void + { + $contextStr = !empty($context) ? ' | Context: ' . json_encode($context) : ''; + echo "[{$level}] {$message}{$contextStr}\n"; + } +} + $ruleApplier = new RuleApplier( new NullLogger(), [ - new Get(new NullLogger()), + new Get(new ConsoleLogger()), new Implode(new NullLogger()), new StrToLower(new NullLogger()), new StrToUpper(new NullLogger()), @@ -62,7 +74,8 @@ new GenericChainFactory(FilterDataOperation::class, FilterDataConfig::class, injections: ['ruleApplier' => $ruleApplier]), new GenericChainFactory(ChainSplitOperation::class, ChainSplitConfig::class), new GenericChainFactory(JsonExtractOperation::class, JsonExtractConfig::class), - new GenericChainFactory(SimpleHttpOperation::class, SimpleHttpConfig::class, injections: ['client' => $client]), + new GenericChainFactory(SimpleHttpOperation::class, SimpleHttpConfig::class), new GenericChainFactory(SplitItemOperation::class, SplitItemConfig::class), + new GenericChainFactory(LogOperation::class, LogConfig::class), ], ); \ No newline at end of file diff --git a/examples/00-SimpleCases/10-CsvWithLogging.php b/examples/00-SimpleCases/10-CsvWithLogging.php new file mode 100644 index 0000000..7c21f9b --- /dev/null +++ b/examples/00-SimpleCases/10-CsvWithLogging.php @@ -0,0 +1,29 @@ +addLink(new LogConfig( + message: 'Starting CSV extraction', + level: 'info' + )) + ->addLink(new CsvExtractConfig()) + ->addLink(new LogConfig( + message: '@"Processing customer: " ~ data["FirstName"]', + level: 'debug' + )); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem('data/customers.csv')]), + [] +); + diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/LogOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/LogOperation.php index 06c10ac..bd604b3 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/LogOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/LogOperation.php @@ -5,39 +5,38 @@ use oliverde8\AssociativeArraySimplified\AssociativeArray; use Oliverde8\Component\PhpEtl\ChainOperation\AbstractChainOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ConfigurableChainOperationInterface; use Oliverde8\Component\PhpEtl\ChainOperation\DataChainOperationInterface; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\LogConfig; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -class LogOperation extends AbstractChainOperation implements DataChainOperationInterface +class LogOperation extends AbstractChainOperation implements DataChainOperationInterface, ConfigurableChainOperationInterface { protected readonly ExpressionLanguage $expressionLanguage; - public function __construct( - protected readonly string $message, - protected readonly string $level, - protected readonly array $context, - ){ + public function __construct(protected readonly LogConfig $config) + { $this->expressionLanguage = new ExpressionLanguage(); } public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface { $data = new AssociativeArray($item->getData()); - $message = $this->message; + $message = $this->config->message; if (strpos($message, "@") === 0) { $message = ltrim($message, "@"); $message = $this->expressionLanguage->evaluate($message, ['data' => $item->getData(), 'context' => $context->getParameters()]); } $logContext = []; - foreach ($this->context as $key => $valueKey) { + foreach ($this->config->context as $key => $valueKey) { $logContext[$key] = $data->get($valueKey); } - switch ($this->level) { + switch ($this->config->level) { case 'debug': $context->getLogger()->debug($message, $logContext); break; diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php index d1526fe..2d58168 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SimpleHttpOperation.php @@ -45,9 +45,4 @@ public function processData(DataItemInterface $item, ExecutionContext $context): return new AsyncHttpClientResponseItem($this->client, $response, $this->config->responseIsJson, $this->config->responseKey, $data); } - - public function getConfigurationClass(): string - { - return SimpleHttpConfig::class; - } } diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php index 106359c..bfaa1c1 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/Transformer/SplitItemOperation.php @@ -74,9 +74,4 @@ protected function createItem($itemData, $fullData = []): ItemInterface return new MixItem($items); } - - public function getConfigurationClass(): string - { - return SplitItemConfig::class; - } } diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/LogConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/LogConfig.php new file mode 100644 index 0000000..bc70e63 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/Transformer/LogConfig.php @@ -0,0 +1,34 @@ +level, $validLevels, true)) { + throw new \InvalidArgumentException( + "Log level must be one of: " . implode(', ', $validLevels) . ". Got: {$this->level}" + ); + } + + if (empty($this->message)) { + throw new \InvalidArgumentException("Message cannot be empty"); + } + } +} + From c1757bd3cd67e026209c73930ed09793ea000343 Mon Sep 17 00:00:00 2001 From: oliverde8 Date: Sun, 16 Nov 2025 15:55:01 +0100 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain=20-=20Updated=20ChainMerge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/.init.php | 3 ++ examples/00-SimpleCases/11-CsvMergeChains.php | 50 +++++++++++++++++++ .../ChainOperation/ChainMergeOperation.php | 41 ++++++++------- .../OperationConfig/ChainMergeConfig.php | 45 +++++++++++++++++ 4 files changed, 120 insertions(+), 19 deletions(-) create mode 100644 examples/00-SimpleCases/11-CsvMergeChains.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/ChainMergeConfig.php diff --git a/examples/.init.php b/examples/.init.php index aa519ba..19c5613 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -2,6 +2,7 @@ use Oliverde8\Component\PhpEtl\ChainBuilderV2; +use Oliverde8\Component\PhpEtl\ChainOperation\ChainMergeOperation; use Oliverde8\Component\PhpEtl\ChainOperation\ChainSplitOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Extract\CsvExtractOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Extract\JsonExtractOperation; @@ -16,6 +17,7 @@ use Oliverde8\Component\PhpEtl\ExecutionContextFactory; use Oliverde8\Component\PhpEtl\GenericChainFactory; +use Oliverde8\Component\PhpEtl\OperationConfig\ChainMergeConfig; use Oliverde8\Component\PhpEtl\OperationConfig\ChainSplitConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Extract\CsvExtractConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Extract\JsonExtractConfig; @@ -72,6 +74,7 @@ public function log($level, $message, array $context = []): void new GenericChainFactory(FileWriterOperation::class, CsvFileWriterConfig::class), new GenericChainFactory(SimpleGroupingOperation::class, SimpleGroupingConfig::class), new GenericChainFactory(FilterDataOperation::class, FilterDataConfig::class, injections: ['ruleApplier' => $ruleApplier]), + new GenericChainFactory(ChainMergeOperation::class, ChainMergeConfig::class), new GenericChainFactory(ChainSplitOperation::class, ChainSplitConfig::class), new GenericChainFactory(JsonExtractOperation::class, JsonExtractConfig::class), new GenericChainFactory(SimpleHttpOperation::class, SimpleHttpConfig::class), diff --git a/examples/00-SimpleCases/11-CsvMergeChains.php b/examples/00-SimpleCases/11-CsvMergeChains.php new file mode 100644 index 0000000..a31e02c --- /dev/null +++ b/examples/00-SimpleCases/11-CsvMergeChains.php @@ -0,0 +1,50 @@ +addLink(new CsvExtractConfig()) + ->addLink((new ChainMergeConfig()) + ->addMerge((new ChainConfig()) + ->addLink((new RuleTransformConfig(false)) + ->addColumn('customer_id', [['get' => ['field' => 'ID']]]) + ->addColumn('full_name', [ + ['implode' => [ + 'values' => [ + [[ 'get' => [ 'field' => 'FirstName' ]]], + [[ 'get' => [ 'field' => 'LastName' ]]], + ], + 'with' => ' ', + ]] + ]) + ) + ) + ->addMerge((new ChainConfig()) + ->addLink((new RuleTransformConfig(false)) + ->addColumn('customer_id', [['get' => ['field' => 'ID']]]) + ->addColumn('status', [['get' => ['field' => 'subscribed']]]) + ) + ) + + ) + ->addLink(new CallBackTransformerConfig(function (DataItem $dataItem) { + var_dump($dataItem->getData()); + return $dataItem; + })); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([new DataItem('data/customers.csv')]), + [] +); diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainMergeOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainMergeOperation.php index f6c1d3a..a10ccb9 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainMergeOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainMergeOperation.php @@ -4,58 +4,56 @@ namespace Oliverde8\Component\PhpEtl\ChainOperation; +use Oliverde8\Component\PhpEtl\ChainBuilderV2; use Oliverde8\Component\PhpEtl\ChainProcessor; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Item\MixItem; use Oliverde8\Component\PhpEtl\Item\StopItem; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\ChainMergeConfig; /** - * Class ChainSplitOperation + * Class ChainMergeOperation * * @author de Cramer Oliver * @copyright 2018 Oliverde8 * @package Oliverde8\Component\PhpEtl\ChainOperation */ -class ChainMergeOperation extends AbstractChainOperation implements DataChainOperationInterface, DetailedObservableOperation +class ChainMergeOperation extends AbstractChainOperation implements DataChainOperationInterface, DetailedObservableOperation, ConfigurableChainOperationInterface { use SplittedChainOperationTrait; - /** @var ChainProcessor[] */ - protected array $chainProcessors; - /** - * ChainSplitOperation constructor. - * - * @param ChainProcessor[] $chainProcessors + * @var ChainProcessor[] */ - public function __construct(array $chainProcessors) + private array $chainProcessors = []; + + public function __construct(ChainBuilderV2 $chainBuilder, ChainMergeConfig $config) { - $this->chainProcessors = $chainProcessors; - $this->onSplittedChainOperationConstruct($chainProcessors); + foreach ($config->getChainConfigs() as $chainConfig) { + $this->chainProcessors[] = $chainBuilder->createChain($chainConfig); + } + $this->onSplittedChainOperationConstruct($this->chainProcessors); } public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface { $returnItems = []; foreach ($this->chainProcessors as $chainProcessor) { - $returnItems[] = $chainProcessor->processItemWithChain($item, 0, $context); + foreach ($chainProcessor->processGenerator($item, $context, withStop: false) as $newItem) { + $returnItems[] = $newItem; + } } - // Nothing to process. + // Return all items merged together. return new MixItem($returnItems); } public function processStop(StopItem $item, ExecutionContext $context): ItemInterface { foreach ($this->chainProcessors as $chainProcessor) { - $result = $chainProcessor->processItemWithChain($item, 0, $context); - - if ($result !== $item) { - // Return a new stop item in order to continue flushing out data with stop items. - $item = new StopItem(); - } + foreach ($chainProcessor->processGenerator($item, $context) as $newItem) {} } return $item; @@ -68,4 +66,9 @@ public function getChainProcessors(): array { return $this->chainProcessors; } + + public function getConfigurationClass(): string + { + return ChainMergeConfig::class; + } } diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainMergeConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainMergeConfig.php new file mode 100644 index 0000000..f73e61d --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainMergeConfig.php @@ -0,0 +1,45 @@ +chainConfigs; + } + + public function addMerge(ChainConfig $chainConfig): self + { + $this->chainConfigs[] = $chainConfig; + return $this; + } + + protected function validate(bool $constructOnly): void + { + if ($constructOnly) { + return; + } + + if (empty($this->chainConfigs)) { + throw new ChainBuilderException("At least one chain config must be provided for ChainMergeConfig"); + } + foreach ($this->chainConfigs as $chainConfig) { + if (!$chainConfig instanceof ChainConfig) { + throw new ChainBuilderException("All chain configs must be instances of ChainConfig"); + } + } + } +} + From d3688351397d5790007aad459adcdc40efc89506 Mon Sep 17 00:00:00 2001 From: oliverde8 Date: Sun, 16 Nov 2025 16:10:53 +0100 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain=20-=20Updated=20ChainRepeat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/.init.php | 3 + examples/00-SimpleCases/12-ApiPagination.php | 66 +++++++++++++++++++ .../ChainOperation/ChainRepeatOperation.php | 25 +++++-- .../OperationConfig/ChainRepeatConfig.php | 33 ++++++++++ 4 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 examples/00-SimpleCases/12-ApiPagination.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/ChainRepeatConfig.php diff --git a/examples/.init.php b/examples/.init.php index 19c5613..29e8dcc 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -3,6 +3,7 @@ use Oliverde8\Component\PhpEtl\ChainBuilderV2; use Oliverde8\Component\PhpEtl\ChainOperation\ChainMergeOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\ChainRepeatOperation; use Oliverde8\Component\PhpEtl\ChainOperation\ChainSplitOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Extract\CsvExtractOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Extract\JsonExtractOperation; @@ -18,6 +19,7 @@ use Oliverde8\Component\PhpEtl\GenericChainFactory; use Oliverde8\Component\PhpEtl\OperationConfig\ChainMergeConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\ChainRepeatConfig; use Oliverde8\Component\PhpEtl\OperationConfig\ChainSplitConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Extract\CsvExtractConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Extract\JsonExtractConfig; @@ -75,6 +77,7 @@ public function log($level, $message, array $context = []): void new GenericChainFactory(SimpleGroupingOperation::class, SimpleGroupingConfig::class), new GenericChainFactory(FilterDataOperation::class, FilterDataConfig::class, injections: ['ruleApplier' => $ruleApplier]), new GenericChainFactory(ChainMergeOperation::class, ChainMergeConfig::class), + new GenericChainFactory(ChainRepeatOperation::class, ChainRepeatConfig::class), new GenericChainFactory(ChainSplitOperation::class, ChainSplitConfig::class), new GenericChainFactory(JsonExtractOperation::class, JsonExtractConfig::class), new GenericChainFactory(SimpleHttpOperation::class, SimpleHttpConfig::class), diff --git a/examples/00-SimpleCases/12-ApiPagination.php b/examples/00-SimpleCases/12-ApiPagination.php new file mode 100644 index 0000000..b0d0084 --- /dev/null +++ b/examples/00-SimpleCases/12-ApiPagination.php @@ -0,0 +1,66 @@ +addLink(new CallBackTransformerConfig(function(DataItemInterface $dataItem) use (&$page) { + $totalPages = 5; // Simulate 5 pages of data + + echo "Fetching page {$page}/{$totalPages}...\n"; + + // Simulate API response with paginated data + $items = []; + for ($i = 1; $i <= 10; $i++) { + $itemId = (($page - 1) * 10) + $i; + $items[] = [ + 'id' => $itemId, + 'name' => "Item {$itemId}", + 'page' => $page, + ]; + } + + $hasNextPage = $page < $totalPages; + + return new DataItem([ + 'items' => $items, + 'page' => $page++, + 'hasNextPage' => $hasNextPage, + ]); + })), + validationExpression: 'data["hasNextPage"] == true', + allowAsynchronous: false +); + +$chainConfig + ->addLink($repeatConfig) + ->addLink(new SplitItemConfig(keys: ['items'])) + ->addLink(new CsvFileWriterConfig('paginated-results.csv')); + +$chainProcessor = $chainBuilder->createChain($chainConfig); +$chainProcessor->process( + new ArrayIterator([ + new DataItem([[]]), + ]), + [] +); + + diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainRepeatOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainRepeatOperation.php index 1327b78..0ccb2c7 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainRepeatOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/ChainRepeatOperation.php @@ -3,25 +3,31 @@ namespace Oliverde8\Component\PhpEtl\ChainOperation; +use Oliverde8\Component\PhpEtl\ChainBuilderV2; use Oliverde8\Component\PhpEtl\ChainProcessor; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\GroupedItem; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\ChainRepeatConfig; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -class ChainRepeatOperation extends AbstractChainOperation implements DetailedObservableOperation +class ChainRepeatOperation extends AbstractChainOperation implements DetailedObservableOperation, ConfigurableChainOperationInterface { use SplittedChainOperationTrait; private ExpressionLanguage $expressionLanguage; + private ChainProcessor $chainProcessor; + private bool $allowAsynchronous; + private string $validationExpression; - public function __construct( - protected readonly ChainProcessor $chainProcessor, - protected readonly string $validationExpression, - protected readonly bool $allowAsynchronous = false, - ) { - $this->onSplittedChainOperationConstruct([$chainProcessor]); + public function __construct(ChainBuilderV2 $chainBuilder, ChainRepeatConfig $config) + { + $this->chainProcessor = $chainBuilder->createChain($config->getChainConfig()); + $this->validationExpression = $config->validationExpression; + $this->allowAsynchronous = $config->allowAsynchronous; + + $this->onSplittedChainOperationConstruct([$this->chainProcessor]); $this->expressionLanguage = new ExpressionLanguage(); } @@ -56,4 +62,9 @@ public function itemIsValid(ItemInterface $item, ExecutionContext $context): boo // If not a data, then it's valid. return true; } + + public function getConfigurationClass(): string + { + return ChainRepeatConfig::class; + } } diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainRepeatConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainRepeatConfig.php new file mode 100644 index 0000000..5ab5292 --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/ChainRepeatConfig.php @@ -0,0 +1,33 @@ +chainConfig; + } + + protected function validate(bool $constructOnly): void + { + + if (empty($this->validationExpression)) { + throw new \InvalidArgumentException("Validation expression cannot be empty"); + } + } +} + From 7786abe1c5c6e850a83bd8b253ba1f18bf2432ad Mon Sep 17 00:00:00 2001 From: oliverde8 Date: Sun, 16 Nov 2025 16:18:25 +0100 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=F0=9F=92=ABNew=20paradigm=20to?= =?UTF-8?q?=20configure=20the=20Etl=20chain=20-=20Updated=20FailSafeOperat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/.init.php | 3 + examples/00-SimpleCases/13-FailSafe.php | 46 ++++++++++++++ .../ChainOperation/FailSafeOperation.php | 60 ++++++++++++------- .../PhpEtl/OperationConfig/FailSafeConfig.php | 37 ++++++++++++ 4 files changed, 124 insertions(+), 22 deletions(-) create mode 100644 examples/00-SimpleCases/13-FailSafe.php create mode 100644 src/Oliverde8/Component/PhpEtl/OperationConfig/FailSafeConfig.php diff --git a/examples/.init.php b/examples/.init.php index 29e8dcc..7fa8a7f 100644 --- a/examples/.init.php +++ b/examples/.init.php @@ -15,6 +15,7 @@ use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\RuleTransformOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\SimpleHttpOperation; use Oliverde8\Component\PhpEtl\ChainOperation\Transformer\SplitItemOperation; +use Oliverde8\Component\PhpEtl\ChainOperation\FailSafeOperation; use Oliverde8\Component\PhpEtl\ExecutionContextFactory; use Oliverde8\Component\PhpEtl\GenericChainFactory; @@ -31,6 +32,7 @@ use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\RuleTransformConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SimpleHttpConfig; use Oliverde8\Component\PhpEtl\OperationConfig\Transformer\SplitItemConfig; +use Oliverde8\Component\PhpEtl\OperationConfig\FailSafeConfig; use Oliverde8\Component\RuleEngine\RuleApplier; use Oliverde8\Component\RuleEngine\Rules\ExpressionLanguage; @@ -83,5 +85,6 @@ public function log($level, $message, array $context = []): void new GenericChainFactory(SimpleHttpOperation::class, SimpleHttpConfig::class), new GenericChainFactory(SplitItemOperation::class, SplitItemConfig::class), new GenericChainFactory(LogOperation::class, LogConfig::class), + new GenericChainFactory(FailSafeOperation::class, FailSafeConfig::class), ], ); \ No newline at end of file diff --git a/examples/00-SimpleCases/13-FailSafe.php b/examples/00-SimpleCases/13-FailSafe.php new file mode 100644 index 0000000..4dbe965 --- /dev/null +++ b/examples/00-SimpleCases/13-FailSafe.php @@ -0,0 +1,46 @@ +addLink(new CallBackTransformerConfig(function(DataItem $dataItem) { + static $attempt = 0; $attempt++; + $data = $dataItem->getData(); + echo "Attempt {$attempt} for id {$data['id']}\n"; + if ($attempt < 3) { + echo " -> Simulating transient error\n"; + throw new RuntimeException('Transient error, please retry'); + } + + echo " -> Success!\n"; + $data['status'] = 'success'; + $data['attempt'] = $attempt; + return new DataItem($data); + })); + +$failSafeConfig = new FailSafeConfig( + chainConfig: $failingChain, + exceptionsToCatch: [RuntimeException::class], + nbAttempts: 5, +); + +$rootChain = new ChainConfig(); +$rootChain->addLink($failSafeConfig); + +$processor = $chainBuilder->createChain($rootChain, new ConsoleLogger()); +$processor->process( + new ArrayIterator([ + new DataItem(['id' => 1]), + ]), + [] +); + diff --git a/src/Oliverde8/Component/PhpEtl/ChainOperation/FailSafeOperation.php b/src/Oliverde8/Component/PhpEtl/ChainOperation/FailSafeOperation.php index d622892..e14beff 100644 --- a/src/Oliverde8/Component/PhpEtl/ChainOperation/FailSafeOperation.php +++ b/src/Oliverde8/Component/PhpEtl/ChainOperation/FailSafeOperation.php @@ -3,35 +3,43 @@ namespace Oliverde8\Component\PhpEtl\ChainOperation; +use Oliverde8\Component\PhpEtl\ChainBuilderV2; use Oliverde8\Component\PhpEtl\ChainProcessor; +use Oliverde8\Component\PhpEtl\Exception\ChainOperationException; use Oliverde8\Component\PhpEtl\Item\DataItemInterface; use Oliverde8\Component\PhpEtl\Item\GroupedItem; use Oliverde8\Component\PhpEtl\Item\ItemInterface; use Oliverde8\Component\PhpEtl\Item\StopItem; use Oliverde8\Component\PhpEtl\Model\ExecutionContext; +use Oliverde8\Component\PhpEtl\OperationConfig\FailSafeConfig; -class FailSafeOperation implements ChainOperationInterface, DetailedObservableOperation +class FailSafeOperation extends AbstractChainOperation implements DataChainOperationInterface, DetailedObservableOperation, ConfigurableChainOperationInterface { use SplittedChainOperationTrait; - protected int $count = 0; + private ChainProcessor $chainProcessor; + private array $exceptionsToCatch = []; + private int $nbAttempts = 1; - public function __construct( - protected readonly ChainProcessor $chainProcessor, - protected readonly array $exceptionsToCatch, - protected readonly int $nbAttempts, - ){} - - public function process(ItemInterface $item, ExecutionContext $context) + public function __construct(ChainBuilderV2 $chainBuilder, FailSafeConfig $config) { - if ($item instanceof StopItem) { - foreach ($this->repeatOnItem($item, $context) as $newItem) {} - return $item; - } + $this->chainProcessor = $chainBuilder->createChain($config->getChainConfig()); + $this->exceptionsToCatch = $config->exceptionsToCatch; + $this->nbAttempts = $config->nbAttempts; + $this->onSplittedChainOperationConstruct([$this->chainProcessor]); + } + public function processData(DataItemInterface $item, ExecutionContext $context): ItemInterface + { return new GroupedItem($this->repeatOnItem($item, $context)); } + public function processStop(StopItem $item, ExecutionContext $context): ItemInterface + { + foreach ($this->repeatOnItem($item, $context) as $ignored) {} + return $item; + } + public function repeatOnItem(ItemInterface $inputItem, ExecutionContext $context): \Generator { $nbAttempts = 0; @@ -40,23 +48,31 @@ public function repeatOnItem(ItemInterface $inputItem, ExecutionContext $context foreach ($this->chainProcessor->processGenerator($inputItem, $context, withStop: false) as $newItem) { yield $newItem; } - return; + return; // success - stop retrying } catch (\Exception $exception) { + $exceptionToHandle = $exception; + if ($exception instanceof ChainOperationException) { + $exceptionToHandle = $exception->getPrevious() ?? $exception; + } + $nbAttempts++; - $exceptionHandled = false; + $handled = false; foreach ($this->exceptionsToCatch as $exceptionType) { - if ($exception instanceof $exceptionType) { - $exceptionHandled = true; + if ($exceptionToHandle instanceof $exceptionType) { + $handled = true; break; } } - - if (!$exceptionHandled || $nbAttempts >= $this->nbAttempts) { - $context->getLogger()->error("Failed to handle exception in fail safe!", ['exception' => $exception]); + if (!$handled || $nbAttempts >= $this->nbAttempts) { + $context->getLogger()->error('FailSafeOperation giving up', ['attempts' => $nbAttempts, 'exception' => $exception]); throw $exception; - } else { - $context->getLogger()->warning("Handling exception with fail safe!", ['exception' => $exception, 'nbAttempts' => $nbAttempts]); } + $context->getLogger()->warning('FailSafeOperation retrying', ['attempts' => $nbAttempts, 'exception' => $exception]); } } while ($nbAttempts < $this->nbAttempts); } + + public function getConfigurationClass(): string + { + return FailSafeConfig::class; + } } \ No newline at end of file diff --git a/src/Oliverde8/Component/PhpEtl/OperationConfig/FailSafeConfig.php b/src/Oliverde8/Component/PhpEtl/OperationConfig/FailSafeConfig.php new file mode 100644 index 0000000..336e5bd --- /dev/null +++ b/src/Oliverde8/Component/PhpEtl/OperationConfig/FailSafeConfig.php @@ -0,0 +1,37 @@ +chainConfig; + } + + protected function validate(bool $constructOnly): void + { + if ($this->nbAttempts < 1) { + throw new \InvalidArgumentException('nbAttempts must be >= 1'); + } + foreach ($this->exceptionsToCatch as $ex) { + if (!is_string($ex) || (!class_exists($ex) && !interface_exists($ex))) { + throw new \InvalidArgumentException('exceptionsToCatch must be class/interface names'); + } + } + } +} +