diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dfee1a7..fb344aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ## v1.1.x - +- Add Flow\FlowInterface::do notation from https://github.com/fp4php/functional +- Update Flow\FlowInterface::fn to accept as first argument + - Closure : it's the job itself + - array : constructor arguments for Flow instanciation + - array (view as shape) : configuration for Flow instanciation + - FlowInterface : the FlowInterface instance itself ## v1.1.4 diff --git a/README.md b/README.md index 1536d7f8..192edd2e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Flow concept aims to solve ## Installation -PHP 8.2 is the minimal version to use Flow +PHP 8.2 is the minimal version to use Flow The recommended way to install it through [Composer](http://getcomposer.org) and execute ```bash @@ -29,11 +29,21 @@ composer require darkwood/flow use Flow\Flow\Flow; use Flow\Ip; -$flow = (new Flow(fn (object $data) => $data['number'] += 1)) - ->fn(new Flow(fn (object $data) => $data['number'] *= 2)); +class D1 { + public function __construct(public int $n1) {} +} -$ip = new Ip(new ArrayObject(['number' => 4])); -$flow($ip, fn ($ip) => printf("my number %d\n", $ip->data['number'])); // display 'my number 10' +class D2 { + public function __construct(public int $n2) {} +} + +$flow = Flow::do(static function() { + yield fn (D1 $data1) => new D2($data1->n1 += 1); + yield fn (D2 $data2) => $data2->n2 * 2; +}); + +$ip = new Ip(new D1(4)); +$flow($ip, fn ($ip) => printf("my number %d\n", $ip->data->n2)); // display 'my number 10' ``` ## Examples @@ -41,7 +51,7 @@ $flow($ip, fn ($ip) => printf("my number %d\n", $ip->data['number'])); // displa A working script is available in the bundled `examples` directory - Run Flow : `php examples/flow.php` -- Start Server : `php examples/server.php` +- Start Server : `php examples/server.php` Start Client(s) : `php examples/client.php` ## Documentation diff --git a/composer.json b/composer.json index e4dbe47f..3d73b1ba 100644 --- a/composer.json +++ b/composer.json @@ -78,5 +78,10 @@ "phpunit": "Launch PHPUnit test suite", "psalm": "Run Psalm" }, - "minimum-stability": "stable" + "minimum-stability": "stable", + "config": { + "allow-plugins": { + "mcaskill/composer-exclude-files": true + } + } } diff --git a/examples/flow.php b/examples/flow.php index d079a74c..8564ce44 100644 --- a/examples/flow.php +++ b/examples/flow.php @@ -82,9 +82,10 @@ $ip->data->number = null; }; -$flow = (new Flow($job1, $errorJob1, new MaxIpStrategy(2), $driver)) - ->fn(new Flow($job2, $errorJob2, new MaxIpStrategy(2), $driver)) -; +$flow = Flow::do(static function () use ($job1, $job2, $errorJob1, $errorJob2) { + yield [$job1, $errorJob1, new MaxIpStrategy(2)]; + yield [$job2, $errorJob2, new MaxIpStrategy(2)]; +}, ['driver' => $driver]); $ipPool = new SplObjectStorage(); diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 00000000..9e3ef4a6 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,12 @@ +nextIpJob(); } - /** - * @param FlowInterface $flow - * - * @return FlowInterface - */ - public function fn(FlowInterface $flow): FlowInterface + public static function do(callable $callable, ?array $config = null): FlowInterface { + /** + * @var Closure|Generator $generator + */ + $generator = $callable(); + + if ($generator instanceof Generator) { + $flows = []; + + while ($generator->valid()) { + $flow = self::flowUnwrap($generator->current(), $config); + + $generator->send($flow); + + $flows[] = $flow; + } + + $return = $generator->getReturn(); + if (!empty($return)) { + $flows[] = self::flowUnwrap($return, $config); + } + + return self::flowMap($flows); + } + + return self::flowUnwrap($generator, $config); + } + + public function fn(array|Closure|FlowInterface $flow): FlowInterface + { + $flow = self::flowUnwrap($flow); + if ($this->fnFlow) { $this->fnFlow->fn($flow); } else { @@ -133,4 +162,59 @@ private function nextIpJob(): void })($ip->data); } } + + /** + * @template TI + * + * @param array|Closure|FlowInterface $flow + * @param ?array $config + * + * @return FlowInterface + * + * #param ?array{ + * 0: Closure|array, + * 1?: Closure|array, + * 2?: IpStrategyInterface + * 3?: DriverInterface + * }|array{ + * "jobs"?: Closure|array, + * "errorJobs"?: Closure|array, + * "ipStrategy"?: IpStrategyInterface + * "driver"?: DriverInterface + * } $config + */ + private static function flowUnwrap($flow, ?array $config = null): FlowInterface + { + if ($flow instanceof Closure) { + return new self(...[...['jobs' => $flow], ...($config ?? [])]); + } + if (is_array($flow)) { + if (array_key_exists(0, $flow) || array_key_exists('jobs', $flow)) { + return new self(...[...$flow, ...($config ?? [])]); + } + + return self::flowMap($flow); + } + + return $flow; + } + + /** + * @param array> $flows + * + * @return FlowInterface + */ + private static function flowMap(array $flows) + { + $flow = array_shift($flows); + if (null === $flow) { + throw new LogicException('Flow is empty'); + } + + foreach ($flows as $flowIt) { + $flow = $flow->fn($flowIt); + } + + return $flow; + } } diff --git a/src/Flow/FlowDecorator.php b/src/Flow/FlowDecorator.php index 006e3650..78af675c 100644 --- a/src/Flow/FlowDecorator.php +++ b/src/Flow/FlowDecorator.php @@ -23,21 +23,18 @@ public function __construct(private FlowInterface $flow) { } - /** - * @param Ip $ip - */ public function __invoke(Ip $ip, Closure $callback = null): void { ($this->flow)($ip, $callback); } - /** - * @param FlowInterface $flow - * - * @return FlowInterface - */ - public function fn(FlowInterface $flow): FlowInterface + public function fn(array|Closure|FlowInterface $flow): FlowInterface { return $this->flow->fn($flow); } + + public static function do(callable $callable, ?array $config = null): FlowInterface + { + return Flow::do($callable, $config); + } } diff --git a/src/FlowInterface.php b/src/FlowInterface.php index 3fa89f5c..0f05b95e 100644 --- a/src/FlowInterface.php +++ b/src/FlowInterface.php @@ -5,6 +5,7 @@ namespace Flow; use Closure; +use Generator; /** * @template T1 @@ -20,9 +21,59 @@ public function __invoke(Ip $ip, Closure $callback = null): void; /** * @template T2 * - * @param FlowInterface $flow + * @param array|Closure|FlowInterface $flow can be Closure as Job, array constructor arguments for Flow instanciation, array configuration for Flow instanciation or FlowInterface instance + * #param ?array{ + * 0: Closure|array, + * 1?: Closure|array, + * 2?: IpStrategyInterface, + * 3?: DriverInterface + * }|array{ + * "jobs"?: Closure|array, + * "errorJobs"?: Closure|array, + * "ipStrategy"?: IpStrategyInterface, + * "driver"?: DriverInterface + * }|Closure|FlowInterface $config * * @return FlowInterface */ - public function fn(self $flow): self; + public function fn(array|Closure|self $flow): self; + + /** + * Do-notation a.k.a. for-comprehension. + * + * Syntax sugar for sequential {@see FlowInterface::fn()} calls + * + * Syntax "$flow = yield $wrapedFlow" mean: + * 1) $wrapedFlow can be Closure as Job, array constructor arguments for Flow instanciation, array configuration for Flow instanciation or FlowInterface instance + * 2) $flow is assigned as FlowInterface instance + * 3) optionnaly you can return another wrapedFlow + * + * ```php + * $flow = Flow::do(static function() { + * yield new Flow(fn($a) => $a + 1); + * $flow = yield fn($b) => $b * 2; + * $flow = yield $flow->fn([fn($c) => $c * 4]) + * return [$flow, [fn($d) => $d - 8]]; + * }); + * ``` + * $config if provided will be the fallback array configuration for Flow instanciation + * + * @param callable(): Generator|Closure $callable + * @param ?array $config + * + * #param ?array{ + * 0: Closure|array, + * 1?: Closure|array, + * 2?: IpStrategyInterface, + * 3?: DriverInterface + * }|array{ + * "jobs"?: Closure|array, + * "errorJobs"?: Closure|array, + * "ipStrategy"?: IpStrategyInterface, + * "driver"?: DriverInterface + * } $config + * + * @return FlowInterface + */ + public static function do(callable $callable, ?array $config = null): self; } diff --git a/tests/Flow/FlowTest.php b/tests/Flow/FlowTest.php index 71ed71f5..ce6fc116 100644 --- a/tests/Flow/FlowTest.php +++ b/tests/Flow/FlowTest.php @@ -87,6 +87,27 @@ public function testJobs(DriverInterface $driver): void $driver->start(); } + /** + * @dataProvider provideDoCases + */ + public function testDo(DriverInterface $driver, IpStrategyInterface $ipStrategy, callable $callable, ?array $config, int $resultNumber): void + { + $ip = new Ip(new ArrayObject(['number' => 0])); + $flow = Flow::do($callable, [ + ...['driver' => $driver, 'ipStrategy' => $ipStrategy], + ...($config ?? []), + ], $resultNumber); + + ($flow)($ip, static function (Ip $ip) use ($driver, $resultNumber) { + $driver->stop(); + + self::assertSame(ArrayObject::class, $ip->data::class); + self::assertSame($resultNumber, $ip->data['number']); + }); + + $driver->start(); + } + /** * @return array> */ @@ -114,4 +135,21 @@ public static function jobProvider(): iterable }], 0], ]); } + + /** + * @return array> + */ + public static function provideDoCases(): iterable + { + return self::matrix(static fn (DriverInterface $driver) => [ + 'simpleGenerator' => [static function () { + yield static function (ArrayObject $data) { + $data['number'] = 5; + }; + yield static function (ArrayObject $data) { + $data['number'] = 10; + }; + }, null, 10], + ]); + } }