PHP Classes

File: src/Job.php

Recommend this page to a friend!
  Classes of Rodolfo Berrios Arce   Workflow   src/Job.php   Download  
File: src/Job.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Workflow
Create and run action workflows
Author: By
Last change:
Date: 1 month ago
Size: 9,456 bytes
 

Contents

Class file image Download
<?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();
            }
        }
    }
}