This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Fn;
|
||||
|
||||
/**
|
||||
* Combines callables using right-to-left mathematical composition (f∘g∘h).
|
||||
* The rightmost callable is applied first — the mathematical counterpart to Pipeline.
|
||||
*
|
||||
* Compose::build(f, g, h) is equivalent to Pipeline::build(h, g, f).
|
||||
* An empty composition returns the value unchanged.
|
||||
*/
|
||||
final class Compose
|
||||
{
|
||||
/**
|
||||
* Applies $fns to $value right-to-left immediately and returns the result.
|
||||
*/
|
||||
public static function apply(mixed $value, callable ...$fns): mixed
|
||||
{
|
||||
foreach (array_reverse($fns) as $fn) {
|
||||
$value = $fn($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a reusable right-to-left composition closure.
|
||||
* The returned Closure is compatible with Memoize::wrap() and Retry::run().
|
||||
*/
|
||||
public static function build(callable ...$fns): \Closure
|
||||
{
|
||||
$reversed = array_reverse($fns);
|
||||
|
||||
return static function (mixed $value) use ($reversed): mixed {
|
||||
foreach ($reversed as $fn) {
|
||||
$value = $fn($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Fn;
|
||||
|
||||
/**
|
||||
* Wraps a callable and caches its return value per unique argument signature.
|
||||
* Null return values are cached correctly and do not trigger a second invocation.
|
||||
*
|
||||
* Note: argument and return types are intentionally untyped — a variadic memoizer
|
||||
* cannot express the wrapped callable's full signature at the type-system level.
|
||||
*/
|
||||
final class Memoize
|
||||
{
|
||||
/** @var callable $fn */
|
||||
private $fn;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $cache = [];
|
||||
|
||||
/** */
|
||||
private function __construct(callable $fn)
|
||||
{
|
||||
$this->fn = $fn;
|
||||
}
|
||||
|
||||
public static function wrap(callable $fn): self
|
||||
{
|
||||
return new self($fn);
|
||||
}
|
||||
|
||||
public function __invoke(mixed ...$args): mixed
|
||||
{
|
||||
$key = serialize($args);
|
||||
|
||||
if (!array_key_exists($key, $this->cache)) {
|
||||
$this->cache[$key] = ($this->fn)(...$args);
|
||||
}
|
||||
|
||||
return $this->cache[$key];
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->cache = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Fn;
|
||||
|
||||
/**
|
||||
* Wraps a callable so it is executed exactly once.
|
||||
* Every subsequent call returns the cached return value without re-invoking the callable.
|
||||
*
|
||||
* @template TReturn
|
||||
*/
|
||||
final class Once
|
||||
{
|
||||
/** @var callable(): TReturn */
|
||||
private $fn;
|
||||
|
||||
private bool $called = false;
|
||||
|
||||
private mixed $result = null;
|
||||
|
||||
/** @param callable(): TReturn $fn */
|
||||
private function __construct(callable $fn)
|
||||
{
|
||||
$this->fn = $fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param callable(): T $fn
|
||||
* @return self<T>
|
||||
*/
|
||||
public static function wrap(callable $fn): self
|
||||
{
|
||||
return new self($fn);
|
||||
}
|
||||
|
||||
/** @return TReturn */
|
||||
public function __invoke(): mixed
|
||||
{
|
||||
if (!$this->called) {
|
||||
$this->result = ($this->fn)();
|
||||
$this->called = true;
|
||||
}
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Fn;
|
||||
|
||||
/**
|
||||
* Pre-fills the leading arguments of a callable and returns a new Closure
|
||||
* that accepts the remaining arguments.
|
||||
*/
|
||||
final class Partial
|
||||
{
|
||||
/**
|
||||
* Returns a Closure with $partial pre-filled as the leftmost arguments.
|
||||
* The returned Closure forwards any additional arguments to $fn.
|
||||
*/
|
||||
public static function apply(callable $fn, mixed ...$partial): \Closure
|
||||
{
|
||||
return static function () use ($fn, $partial): mixed {
|
||||
return $fn(...$partial, ...func_get_args());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Fn;
|
||||
|
||||
/**
|
||||
* Pipes a value through an ordered sequence of callables left-to-right.
|
||||
* Each stage receives the return value of the previous stage.
|
||||
* An empty pipeline returns the payload unchanged.
|
||||
*/
|
||||
final class Pipeline
|
||||
{
|
||||
/**
|
||||
* Passes $payload through each stage immediately and returns the result.
|
||||
*/
|
||||
public static function process(mixed $payload, callable ...$stages): mixed
|
||||
{
|
||||
foreach ($stages as $stage) {
|
||||
$payload = $stage($payload);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a reusable pipeline closure that can be invoked later.
|
||||
* The returned Closure is compatible with Memoize::wrap() and Retry::run().
|
||||
*/
|
||||
public static function build(callable ...$stages): \Closure
|
||||
{
|
||||
return static function (mixed $payload) use ($stages): mixed {
|
||||
foreach ($stages as $stage) {
|
||||
$payload = $stage($payload);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Fn;
|
||||
|
||||
final class Retry
|
||||
{
|
||||
/**
|
||||
* Executes a callable, retrying on exception up to $maxAttempts times.
|
||||
*
|
||||
* @template TReturn
|
||||
* @param callable(): TReturn $fn
|
||||
* @param list<class-string<\Throwable>> $only Retry only for these exception types (empty = any)
|
||||
* @param list<class-string<\Throwable>> $except Never retry for these exception types
|
||||
*
|
||||
* @throws \Throwable the last exception when all attempts are exhausted
|
||||
* @return TReturn
|
||||
*/
|
||||
public static function run(
|
||||
callable $fn,
|
||||
int $maxAttempts = 3,
|
||||
int $backoffMs = 0,
|
||||
bool $exponential = false,
|
||||
array $only = [],
|
||||
array $except = [],
|
||||
): mixed {
|
||||
$attempt = 0;
|
||||
$lastError = null;
|
||||
|
||||
while ($attempt < $maxAttempts) {
|
||||
try {
|
||||
return $fn();
|
||||
} catch (\Throwable $e) {
|
||||
$lastError = $e;
|
||||
|
||||
if (!self::shouldRetry($e, $only, $except)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$attempt++;
|
||||
|
||||
if ($attempt < $maxAttempts && $backoffMs > 0) {
|
||||
$delay = $exponential
|
||||
? $backoffMs * (2 ** ($attempt - 1))
|
||||
: $backoffMs * $attempt;
|
||||
|
||||
usleep($delay * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw $lastError ?? new \RuntimeException('Retry failed without exception.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given exception should trigger a retry.
|
||||
* Returns false immediately if $only is set and the exception does not match,
|
||||
* or if the exception matches any entry in $except.
|
||||
*
|
||||
* @param list<class-string<\Throwable>> $only
|
||||
* @param list<class-string<\Throwable>> $except
|
||||
*/
|
||||
private static function shouldRetry(\Throwable $e, array $only, array $except): bool
|
||||
{
|
||||
if ($only !== []) {
|
||||
$matchesOnly = false;
|
||||
|
||||
foreach ($only as $type) {
|
||||
if ($e instanceof $type) {
|
||||
$matchesOnly = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$matchesOnly) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($except as $type) {
|
||||
if ($e instanceof $type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tez\Utils\Fn;
|
||||
|
||||
/**
|
||||
* Executes a side-effect callable on a value and returns the value unchanged.
|
||||
* The return value of $fn is intentionally discarded.
|
||||
*/
|
||||
final class Tap
|
||||
{
|
||||
/**
|
||||
* Calls $fn($value) for its side-effect, then returns $value unchanged.
|
||||
*
|
||||
* @template T
|
||||
* @param T $value
|
||||
* @return T
|
||||
*/
|
||||
public static function value(mixed $value, callable $fn): mixed
|
||||
{
|
||||
$fn($value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user