<?php
/*
* This file is part of Chevere.
*
* (c) Rodolfo Berrios <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chevere\Workflow;
use ArgumentCountError;
use Chevere\Action\Interfaces\ActionInterface;
use Chevere\DataStructure\Interfaces\VectorInterface;
use Chevere\DataStructure\Vector;
use Chevere\Parameter\Interfaces\ParameterInterface;
use Chevere\Parameter\Interfaces\ParametersInterface;
use Chevere\Workflow\Interfaces\CallerInterface;
use Chevere\Workflow\Interfaces\JobInterface;
use Chevere\Workflow\Interfaces\ResponseReferenceInterface;
use Chevere\Workflow\Interfaces\VariableInterface;
use InvalidArgumentException;
use OverflowException;
use ReflectionClass;
use function Chevere\Action\getParameters;
use function Chevere\Message\message;
use function Chevere\Parameter\assertNamedArgument;
final class Job implements JobInterface
{
/**
* @var array<string, mixed>
*/
private array $arguments;
/**
* @var VectorInterface<string>
*/
private VectorInterface $dependencies;
private ParametersInterface $parameters;
/**
* @var VectorInterface<ResponseReferenceInterface|VariableInterface>
*/
private VectorInterface $runIf;
private bool $isSync;
private CallerInterface $caller;
/**
* Creates a Job
* DO NOT use this method directly, use `sync` or `async` functions instead.
*
* @param ActionInterface $_ The action to run
* @param mixed ...$argument Action arguments for its run method (raw, reference or variable)
*/
public function __construct(
private ActionInterface $_,
mixed ...$argument
) {
$debugBacktrace = debug_backtrace(options: 0, limit: 2);
$callerFunction = $debugBacktrace[1]['function'] ?? '';
$index = (int) in_array(
$callerFunction,
['Chevere\Workflow\sync', 'Chevere\Workflow\async']
);
$debugBacktrace = $debugBacktrace[$index];
$file = $debugBacktrace['file'] ?? 'unknown';
$line = $debugBacktrace['line'] ?? 0;
$this->caller = new Caller($file, (int) $line);
$this->isSync = false;
$this->runIf = new Vector();
$this->dependencies = new Vector();
$this->parameters = getParameters($_::class);
$this->arguments = [];
$this->setArguments(...$argument);
}
public function caller(): CallerInterface
{
return $this->caller;
}
public function withArguments(mixed ...$argument): JobInterface
{
$new = clone $this;
$new->setArguments(...$argument);
return $new;
}
public function withRunIf(ResponseReferenceInterface|VariableInterface ...$context): JobInterface
{
$new = clone $this;
$new->runIf = new Vector();
$known = new Vector();
foreach ($context as $item) {
if ($known->contains($item->__toString())) {
throw new OverflowException(
(string) message(
'Condition `%condition%` is already defined',
condition: $item->__toString()
)
);
}
$new->inferDependencies($item);
$new->runIf = $new->runIf->withPush($item);
$known = $known->withPush($item->__toString());
}
return $new;
}
public function withIsSync(bool $flag = true): JobInterface
{
$new = clone $this;
$new->isSync = $flag;
return $new;
}
public function withDepends(string ...$jobs): JobInterface
{
$new = clone $this;
$new->addDependencies(...$jobs);
return $new;
}
public function action(): ActionInterface
{
return $this->_;
}
public function arguments(): array
{
return $this->arguments;
}
public function dependencies(): VectorInterface
{
return $this->dependencies;
}
public function runIf(): VectorInterface
{
return $this->runIf;
}
public function isSync(): bool
{
return $this->isSync;
}
private function setArguments(mixed ...$argument): void
{
if (! $this->parameters->isVariadic()) {
$this->assertArgumentsCount($argument);
}
$lastKey = array_key_last($this->parameters->keys());
$lastName = $this->parameters->keys()[$lastKey] ?? null;
$values = [];
foreach ($this->parameters as $name => $parameter) {
if ($name === $lastName && $this->parameters->isVariadic()) {
$variadicKeys = array_diff_key(
$argument,
array_flip($this->parameters->keys())
);
foreach ($variadicKeys as $key => $value) {
$key = strval($key);
$values[$key] = $value;
$this->inferDependencies($value);
$this->assertParameter($name, $parameter, $value);
}
break;
}
if (array_key_exists($name, $argument)) {
$value = $argument[$name];
$values[$name] = $value;
$this->inferDependencies($value);
$this->assertParameter($name, $parameter, $value);
}
}
$this->arguments = $values;
}
/**
* @param mixed[] $arguments
*/
private function assertArgumentsCount(array $arguments): void
{
$countProvided = count($arguments);
$requiredKeys = $this->parameters->requiredKeys()->toArray();
$intersectKeys = array_intersect(array_keys($arguments), $requiredKeys);
$countIntersect = count($intersectKeys);
$missing = array_map(
fn (string $item) => $this->formatAsVariable($item),
array_diff($requiredKeys, $intersectKeys)
);
if ($missing !== []) {
$reflection = new ReflectionClass($this->_);
$class = "`{$reflection->getName()}`";
if ($reflection->isAnonymous()) {
$class = 'anon class in '
. $reflection->getFileName() . ':' . $reflection->getStartLine();
}
throw new ArgumentCountError(
(string) message(
'Missing argument(s) [`%arguments%`] for %action%',
arguments: implode(', ', $missing),
action: $class
)
);
}
if (count($requiredKeys) > $countProvided
|| count($requiredKeys) !== $countIntersect
|| $countProvided > count($this->parameters)
) {
$requiredVars = array_map(
fn (string $item) => $this->formatAsVariable($item),
$requiredKeys
);
$parameters = implode(', ', $requiredVars);
$parameters = $parameters === '' ? '' : "[{$parameters}]";
throw new ArgumentCountError(
(string) message(
'`%symbol%` requires %countRequired% argument(s)%parameters%',
symbol: $this->_::class . '::' . $this->_::mainMethod(),
countRequired: strval(count($requiredKeys)),
parameters: $parameters === '' ? '' : " `{$parameters}`"
)
);
}
}
private function formatAsVariable(string $name): string
{
return $this->parameters->get($name)->type()->typeHinting()
. " \${$name}";
}
private function assertParameter(string $name, ParameterInterface $parameter, mixed $value): void
{
if ($value instanceof ResponseReferenceInterface || $value instanceof VariableInterface) {
return;
}
assertNamedArgument($name, $parameter, $value);
}
private function inferDependencies(mixed $argument): void
{
$condition = $argument instanceof ResponseReferenceInterface;
if (! $condition) {
return;
}
if ($this->dependencies->contains($argument->job())) {
return;
}
$this->dependencies = $this->dependencies
->withPush($argument->job());
}
private function addDependencies(string ...$jobs): void
{
$this->assertDependencies(...$jobs);
foreach ($jobs as $job) {
if ($this->dependencies->contains($job)) {
continue;
}
$this->dependencies = $this->dependencies->withPush($job);
}
}
private function assertDependencies(string ...$dependencies): void
{
$uniques = array_unique($dependencies);
if ($uniques !== $dependencies) {
throw new OverflowException(
(string) message(
'Job dependencies must be unique (repeated **%dependencies%**)',
dependencies: implode(', ', array_diff_assoc($dependencies, $uniques))
)
);
}
foreach ($dependencies as $dependency) {
if (empty($dependency) || ctype_digit($dependency) || ctype_space($dependency)) {
throw new InvalidArgumentException();
}
}
}
}
|