diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..523d0af --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +*.iml +out +gen +dev \ No newline at end of file diff --git a/README.md b/README.md index 0f34863..474b4a0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,243 @@ -# pipechain -Pipeline Pattern Implementation +# PipeChain +A linear chainable pipeline pattern implementation with fallbacks. + +## What is a linear pipeline + +A linear pipeline is a object that stacks callbacks and provides a +processing logic to process a payload thru the callback stack, +form the first to the last. + +## Processors + +PipeChain comes out of the box with 4 different processors: + +### The common invoker processor + +The common invoker processor implements a processing of the +queue that considers the provided fallback callback as the +successor of a failed stage callback. + +The common invoker processor is the default processor. + +Example: + +```php +pipe( + # the stage callback + function(string $inbound) { + return 'something'; + }, + # the fallback callback + function(int $inbound) { + return ++$inbound; + } +); + +var_dump( + $pipeline->process(100) // int(3) => 101 +); + +``` + +### The naive invoker processor + +The naive invoker processor implements a processing of the +queue that ignores the provided fallback callback. + +Example: + +```php +pipe(function(int $inbound) { + return ++$inbound; +}); + +var_dump( + $pipeline->process(100) // int(3) => 101 +); + +``` + +### The loyal interface invoker processor + +The loyal interface invoker processor implements a processing +of the queue that acts like a common invoker processor with +an additional interface check of each payload change. + +The interface check has 2 modes: +- `LoyalInterfaceInvokerProcessor::INTERRUPT_ON_MISMATCH` forces + the processor to pass the throwables to the next scope. +- `LoyalInterfaceInvokerProcessor::RESET_ON_MISMATCH` forces + the processor to use the previous payload instead. + +Example: + +```php +modify('+10 hours'); +}; + +$failure = function (string $inbound) use ($prior) { + return $prior(date_create($inbound)); +}; + +$pipeline->pipe($prior, $failure); + +var_dump( + $pipeline + ->process('2017-10-12 18:00:00') + ->format('Y-m-d') // string(10) "2017-10-13" +); + +``` + +### The loyal type invoker processor + +The loyal type invoker processor implements a processing +of the queue that acts like the loyal interface invoker +processor but with type checking of each payload change. + +The type check has 2 modes: +- `LoyalInterfaceInvokerProcessor::INTERRUPT_ON_MISMATCH` forces + the processor to pass the throwables to the next scope. +- `LoyalInterfaceInvokerProcessor::RESET_ON_MISMATCH` forces + the processor to use the previous payload instead. + +Example: + +```php +pipe(function(string $name) { + return strtolower($name); +}); +$pipeline->pipe(function(string $name) { + return ucfirst($name); +}); + +var_dump( + $pipeline->process('jOhN') // string(4) "John" +); + +``` + +## Booting own implementations + +```php +attach( + # stage callable + function() { + + }, + # fallback callable (optional) + function() { + + } + ); + + # ... + + # Don't forget to call the parent, the parent method ensures + # that a processor will be created when $processor is null. + + return parent::boot($container, $processor); + } +} + +# later ... + +echo MyFirstPipeline::create()->process('I can get no satisfaction!'); + +``` + +## Chaining Pipelines + +```php +chain( + MySecondPipeline::create() +)->chain( + MyThirdPipeline::create() +); + +$pipeline->process('This is awesome.'); +``` + +## Maintainer, State of this Package and License + +Those are the Maintainer of this Package: + +- [Matthias Kaschubowski](https://github.com/nhlm) + +This package is released under the MIT license. A copy of the license +is placed at the root of this repository. + +The State of this Package is unstable unless unit tests are added. + +## Todo + +- Adding Unit Tests \ No newline at end of file diff --git a/src/Collections/PipeChainCollection.php b/src/Collections/PipeChainCollection.php new file mode 100644 index 0000000..8bce732 --- /dev/null +++ b/src/Collections/PipeChainCollection.php @@ -0,0 +1,103 @@ +storage = new \SplObjectStorage(); + } + + /** + * Retrieve an external iterator + * @link http://php.net/manual/en/iteratoraggregate.getiterator.php + * @return Traversable|\Generator An instance of an object implementing Iterator or + * Traversable + * @since 5.0.0 + */ + public function getIterator(): \Generator + { + foreach ( $this->storage as $object ) { + yield $object => $this->storage[$object]['fallback']; + } + } + + /** + * Attaches a stage and assigns a fallback to the attached stage. + * + * @param callable $stage + * @param callable|null $fallback + * @return void + */ + public function attach(callable $stage, callable $fallback = null) + { + $this->storage->attach( + $this->marshalClosure($stage), + [ + 'fallback' => is_callable($fallback) ? $this->marshalClosure($fallback) : null + ] + ); + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + return $this->storage->count(); + } + + protected function marshalClosure(callable $callback): \Closure + { + if ( method_exists(\Closure::class, 'fromCallable') ) { + return \Closure::fromCallable($callback); + } + + is_callable($callback, true, $target); + + if ( false === strpos('::', $target) || $callback instanceof \Closure ) { + return (new \ReflectionFunction($target))->getClosure(); + } + else { + list($class, $method) = explode('::', $target); + return (new \ReflectionMethod($class, $method)) + ->getClosure( + is_array($callback) && is_object($callback[0]) + ? $callback[0] + : null + ) + ; + } + } +} \ No newline at end of file diff --git a/src/InvokerProcessorInterface.php b/src/InvokerProcessorInterface.php new file mode 100644 index 0000000..24c382a --- /dev/null +++ b/src/InvokerProcessorInterface.php @@ -0,0 +1,34 @@ +interface = $interface; + $this->mode = $mode; + + if ( ! in_array($mode, [static::INTERRUPT_ON_MISMATCH, static::RESET_ON_MISMATCH]) ) { + throw new InvokerProcessorException( + 'Unknown mode: '.$mode + ); + } + } + + /** + * processes a single stage and its optionally associated fallback with the provided payload. + * + * @param mixed $payload + * @param callable $stage + * @param callable|null $fallback + * @throws InvokerProcessorException|\Throwable + * @return mixed payload + */ + public function process($payload, callable $stage, callable $fallback = null) + { + try { + $newPayload = parent::process($payload, $stage, $fallback); + + return $this->sanitizeLoyalty($newPayload); + } + catch ( \Throwable $exception ) { + if ( $this->mode === static::INTERRUPT_ON_MISMATCH ) { + throw $exception; + } + + return $payload; + } + } + + /** + * validates the loyalty of the payload. + * + * @param $payload + * @throws InvokerProcessorException when the payload is not loyal + */ + protected function validateLoyalty($payload) + { + if ( ! is_a($payload, $this->interface) ) { + throw new InvokerProcessorException( + 'processed payload has incompatible interface, got ' + .get_class($payload).', expecting '.$this->interface + ); + } + } + + /** + * Sanitizes the loyalty of the payload. + * + * @param $payload + * @return mixed + */ + private function sanitizeLoyalty($payload) + { + $this->validateLoyalty($payload); + + return $payload; + } + +} \ No newline at end of file diff --git a/src/InvokerProcessors/LoyalTypeInvokerProcessor.php b/src/InvokerProcessors/LoyalTypeInvokerProcessor.php new file mode 100644 index 0000000..695bfd2 --- /dev/null +++ b/src/InvokerProcessors/LoyalTypeInvokerProcessor.php @@ -0,0 +1,39 @@ +interface ) { + throw new InvokerProcessorException( + 'processed payload has incompatible type, got ' + .gettype($payload).', expecting '.$this->interface + ); + } + } + +} \ No newline at end of file diff --git a/src/InvokerProcessors/NaiveInvokerProcessor.php b/src/InvokerProcessors/NaiveInvokerProcessor.php new file mode 100644 index 0000000..d1f83d9 --- /dev/null +++ b/src/InvokerProcessors/NaiveInvokerProcessor.php @@ -0,0 +1,35 @@ + $fallback ) { + $payload = $this->process($payload, $stage, $fallback); + } + + return $payload; + } + + /** + * processes a single stage and its optionally associated fallback with the provided payload. + * + * @param mixed $payload + * @param callable $stage + * @param callable|null $fallback + * @return mixed payload + */ + abstract public function process($payload, callable $stage, callable $fallback = null); +} \ No newline at end of file diff --git a/src/PipeChain.php b/src/PipeChain.php new file mode 100644 index 0000000..d713c3c --- /dev/null +++ b/src/PipeChain.php @@ -0,0 +1,120 @@ +stack = new PipeChainCollection(); + $this->processor = $this->boot($this->stack, $invokerProcessor); + } + + /** + * pipes a stage with an optional associated fallback. + * + * @param callable $stage + * @param callable $fallback + * @return PipeChainInterface + */ + final public function pipe(callable $stage, callable $fallback = null): PipeChainInterface + { + $this->stack->attach($stage, $fallback); + + return $this; + } + + /** + * processes a payload. + * + * @param $payload + * @return mixed the payload + */ + final public function process($payload) + { + $payload = $this->processor->processStack($payload, $this->stack); + + if ( $this->next instanceof PipeChainInterface ) { + $payload = $this->next->process($payload); + } + + return $payload; + } + + /** + * appends the provided pipe to the end of the chain. + * + * @param PipeChainInterface $chain + * @return PipeChainInterface + */ + final public function chain(PipeChainInterface $chain): PipeChainInterface + { + if ( $this->next instanceof PipeChainInterface ) { + $this->next->chain($chain); + } + else { + $this->next = $chain; + } + + return $this; + } + + /** + * factory method. + * + * @param InvokerProcessorInterface|null $invoker + * @return PipeChainInterface + */ + public static function create(InvokerProcessorInterface $invoker = null): PipeChainInterface + { + return new static($invoker); + } + + /** + * Boots the PipeChainCollection. + * + * This method should be overwritten for own PipeChain implementations pre-filled with pipes. + * + * @param PipeChainCollectionInterface $container + * @return InvokerProcessorInterface + */ + protected function boot(PipeChainCollectionInterface $container, InvokerProcessorInterface $processor = null): InvokerProcessorInterface + { + return $processor ?? new CommonInvokerProcessor(); + } +} \ No newline at end of file diff --git a/src/PipeChainCollectionInterface.php b/src/PipeChainCollectionInterface.php new file mode 100644 index 0000000..09f32e9 --- /dev/null +++ b/src/PipeChainCollectionInterface.php @@ -0,0 +1,24 @@ +