<?php
/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (C) 2017-2018 Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Transformer;

use CleverAge\ProcessBundle\Exception\TransformerException;
use CleverAge\ProcessBundle\Registry\TransformerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
 * Maps properties of an array/object to an other array/object
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class MappingTransformer implements ConfigurableTransformerInterface
{
    use TransformerTrait;

    /** @var LoggerInterface */
    protected $logger;

    /** @var PropertyAccessorInterface */
    protected $accessor;

    /**
     * @param TransformerRegistry       $transformerRegistry
     * @param LoggerInterface           $logger
     * @param PropertyAccessorInterface $accessor
     */
    public function __construct(
        TransformerRegistry $transformerRegistry,
        LoggerInterface $logger,
        PropertyAccessorInterface $accessor
    ) {
        $this->transformerRegistry = $transformerRegistry;
        $this->logger = $logger;
        $this->accessor = $accessor;
    }

    /**
     * Must return the transformed $value
     *
     * @param mixed $input
     * @param array $options
     *
     * @throws \Exception
     *
     * @return mixed $value
     */
    public function transform($input, array $options = [])
    {
        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);
        $options = $resolver->resolve($options);

        if (!empty($options['initial_value']) && $options['keep_input']) {
            throw new InvalidOptionsException(
                'The options "initial_value" and "keep_input" can\'t be both enabled.'
            );
        }

        $result = $options['initial_value'];
        if ($options['keep_input']) {
            $result = $input;
        }

        /** @noinspection ForeachSourceInspection */
        foreach ($options['mapping'] as $targetProperty => $mapping) {
            $targetProperty = (string) $targetProperty;
            if (null !== $mapping['constant']) {
                $transformedValue = $mapping['constant'];
            } elseif ($mapping['set_null']) {
                $transformedValue = null;
            } else {
                $sourceProperty = $mapping['code'] ?? $targetProperty;
                if (\is_array($sourceProperty)) {
                    $transformedValue = [];
                    /** @var array $sourceProperty */
                    foreach ($sourceProperty as $destKey => $srcKey) {
                        try {
                            $transformedValue[$destKey] = $this->accessor->getValue($input, $srcKey);
                        } catch (\RuntimeException $missingPropertyError) {
                            // @TODO no error if framework.property_access.throw_exception_on_invalid_index = false (default)
                            if ($mapping['ignore_missing'] || $options['ignore_missing']) {
                                continue;
                            }
                            $this->logger->debug(
                                'Mapping exception',
                                [
                                    'srcKey' => $srcKey,
                                    'message' => $missingPropertyError->getMessage(),
                                ]
                            );
                            throw $missingPropertyError;
                        }
                    }
                } else {
                    try {
                        $transformedValue = $this->accessor->getValue($input, $sourceProperty);
                    } catch (\RuntimeException $missingPropertyError) {
                        // @TODO no error if framework.property_access.throw_exception_on_invalid_index = false (default)
                        if ($mapping['ignore_missing'] || $options['ignore_missing']) {
                            continue;
                        }
                        $this->logger->debug(
                            'Mapping exception',
                            [
                                'message' => $missingPropertyError->getMessage(),
                            ]
                        );
                        throw $missingPropertyError;
                    }
                }
            }

            try {
                $transformedValue = $this->applyTransformers($mapping['transformers'], $transformedValue);
            } catch (TransformerException $exception) {
                $exception->setTargetProperty($targetProperty);
                $this->logger->debug(
                    'Transformation exception',
                    [
                        'message' => $exception->getPrevious()->getMessage(),
                        'file' => $exception->getPrevious()->getFile(),
                        'line' => $exception->getPrevious()->getLine(),
                        'trace' => $exception->getPrevious()->getTraceAsString(),
                    ]
                );

                throw $exception;
            }

            if (\is_callable($options['merge_callback'])) {
                $options['merge_callback']($result, $targetProperty, $transformedValue);
            } elseif ($this->accessor->isWritable($result, $targetProperty)) {
                $this->accessor->setValue($result, $targetProperty, $transformedValue);
            } elseif (\is_array($result)) {
                $result[$targetProperty] = $transformedValue;
            } else {
                throw new \UnexpectedValueException("Property '{$targetProperty}' is not writable");
            }
        }

        return $result;
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
     * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException
     * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
     * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'mapping',
            ]
        );
        $resolver->setAllowedTypes('mapping', ['array']);
        $resolver->setDefaults(
            [
                'ignore_missing' => false,
                'keep_input' => false,
                'initial_value' => [],
                'merge_callback' => null,
            ]
        );
        $resolver->setAllowedTypes('ignore_missing', ['boolean']);
        $resolver->setAllowedTypes('keep_input', ['boolean']);
        $resolver->setAllowedTypes('merge_callback', ['NULL', 'callable']);

        /** @noinspection PhpUnusedParameterInspection */
        $resolver->setNormalizer(
            'mapping',
            function (Options $options, $value) {
                $resolvedMapping = [];
                $mappingResolver = new OptionsResolver();
                $this->configureMappingOptions($mappingResolver);
                /** @var array $value */
                foreach ($value as $property => $mappingConfig) {
                    $resolvedMapping[$property] = $mappingResolver->resolve(
                        $mappingConfig ?? []
                    );
                }

                return $resolvedMapping;
            }
        );
    }

    /**
     * Returns the unique code to identify the transformer
     *
     * @return string
     */
    public function getCode()
    {
        return 'mapping';
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException
     * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException
     * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException
     * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     */
    protected function configureMappingOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'code' => null, // Source property
                'constant' => null,
                'set_null' => false, // Because the "null" value cannot be covered by the constant option
                'ignore_missing' => false,
            ]
        );
        $resolver->setAllowedTypes('code', ['NULL', 'string', 'array']);
        $resolver->setAllowedTypes('set_null', ['boolean']);
        $resolver->setAllowedTypes('ignore_missing', ['boolean']);

        $this->configureTransformersOptions($resolver);
    }
}


<?php
/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (C) 2017-2018 Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Task\Doctrine;

use CleverAge\ProcessBundle\Model\ProcessState;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManagerInterface;

/**
 * Detach Doctrine entities from unit of work
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class DoctrineDetacherTask extends AbstractDoctrineTask
{
    /**
     * @param ProcessState $state
     *
     * @throws \UnexpectedValueException
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \InvalidArgumentException
     */
    public function execute(ProcessState $state)
    {
        $entity = $state->getInput();
        if (null === $entity) {
            throw new \RuntimeException('DoctrineWriterTask does not allow null input');
        }
        $class = ClassUtils::getClass($entity);
        $entityManager = $this->doctrine->getManagerForClass($class);
        if (!$entityManager instanceof EntityManagerInterface) {
            throw new \UnexpectedValueException("No manager found for class {$class}");
        }
        $entityManager->detach($entity);
    }
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\Command;

use CleverAge\ProcessBundle\Manager\ProcessManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\VarDumper\VarDumper;
use Symfony\Component\Yaml\Parser;

/**
 * Run a process from the command line interface
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ExecuteProcessCommand extends Command
{
    /** @var ProcessManager */
    protected $processManager;

    /**
     * @param ProcessManager $processManager
     *
     * @throws \Symfony\Component\Console\Exception\LogicException
     */
    public function __construct(ProcessManager $processManager)
    {
        $this->processManager = $processManager;
        parent::__construct();
    }

    /**
     * {@inheritdoc}
     *
     * @throws \Symfony\Component\Console\Exception\InvalidArgumentException
     */
    protected function configure()
    {
        $this->setName('cleverage:process:execute');
        $this->addArgument(
            'processCodes',
            InputArgument::IS_ARRAY | InputArgument::REQUIRED,
            'The code(s) of the process(es) to execute, separated by a space'
        );
        $this->addOption('input', 'i', InputOption::VALUE_REQUIRED, 'Pass input data to the first task of the process');
        $this->addOption('input-from-stdin', null, InputOption::VALUE_NONE, 'Read input data from stdin');
        $this->addOption('context', 'c', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Contextual value', []);
    }

    /**
     * @param InputInterface  $input
     * @param OutputInterface $output
     *
     * @throws \Exception
     *
     * @return int|null
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $inputData = $input->getOption('input');
        if ($input->getOption('input-from-stdin')) {
            $inputData = '';
            while (!feof(STDIN)) {
                $inputData .= fread(STDIN, 8192);
            }
        }

        $context = $this->parseContextValues($input);

        /** @noinspection ForeachSourceInspection */
        foreach ($input->getArgument('processCodes') as $code) {
            if (!$output->isQuiet()) {
                $output->writeln("<comment>Starting process '{$code}'...</comment>");
            }
            $returnValue = $this->processManager->execute($code, $inputData, $context);
            if ($output->isVeryVerbose() && class_exists(VarDumper::class)) {
                VarDumper::dump($returnValue); // @todo remove this please
            }
            if (!$output->isQuiet()) {
                $output->writeln("<info>Process '{$code}' executed successfully</info>");
            }
        }

        return 0;
    }

    /**
     * @param InputInterface $input
     *
     * @throws \Symfony\Component\Console\Exception\InvalidArgumentException
     *
     * @return array
     */
    protected function parseContextValues(InputInterface $input)
    {
        $parser = new Parser();

        $pattern = '/([\w]+):(.*)/';
        $contextValues = $input->getOption('context');
        $context = [];
        foreach ($contextValues as $contextValue) {
            preg_match($pattern, $contextValue, $parts);
            if (3 !== \count($parts)
                || $parts[0] !== $contextValue) {
                throw new \InvalidArgumentException(sprintf('Invalid context %s', $contextValue));
            }
            $context[$parts[1]] = $parser->parse($parts[2]);
        }

        return $context;
    }
}


<?php
/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (C) 2017-2018 Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Task\File\Csv;

use CleverAge\ProcessBundle\Filesystem\CsvResource;
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\FinalizableTaskInterface;
use CleverAge\ProcessBundle\Model\ProcessState;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Generic abstract task to handle CSV resources
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
abstract class AbstractCsvResourceTask extends AbstractConfigurableTask implements FinalizableTaskInterface
{
    /** @var CsvResource */
    protected $csv;

    /**
     * @param ProcessState $state
     */
    public function finalize(ProcessState $state)
    {
        if ($this->csv instanceof CsvResource) {
            $this->csv->close();
        }
    }

    /**
     * @param ProcessState $state
     *
     * @throws \UnexpectedValueException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \RuntimeException
     */
    protected function initFile(ProcessState $state)
    {
        if ($this->csv) {
            return;
        }
        $options = $this->getOptions($state);
        $headers = $this->getHeaders($state, $options);
        $this->csv = new CsvResource(
            $state->getInput(),
            $options['delimiter'],
            $options['enclosure'],
            $options['escape'],
            $headers
        );
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'delimiter' => ';',
                'enclosure' => '"',
                'escape' => '\\',
                'headers' => null,
            ]
        );
        $resolver->setAllowedTypes('delimiter', ['string']);
        $resolver->setAllowedTypes('enclosure', ['string']);
        $resolver->setAllowedTypes('escape', ['string']);
        $resolver->setAllowedTypes('headers', ['NULL', 'array']);
    }

    /**
     * @param ProcessState $state
     * @param array        $options
     *
     * @return array
     */
    abstract protected function getHeaders(ProcessState $state, array $options);
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\Task\File;

use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessState;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Yaml\Yaml;

/**
 * Writes a YAML file from an array
 *
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class YamlWriterTask extends AbstractConfigurableTask
{
    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Symfony\Component\Filesystem\Exception\IOException
     */
    public function execute(ProcessState $state)
    {
        $options = $this->getOptions($state);
        file_put_contents($options['file_path'], Yaml::dump($state->getInput(), $options['inline']));
        $state->setOutput($options['file_path']);
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'file_path',
            ]
        );
        $resolver->setAllowedTypes('file_path', ['string']);
        $resolver->setDefaults(
            [
                'inline' => 4,
            ]
        );
        $resolver->setAllowedTypes('inline', ['integer']);
    }
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\Transformer;

use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
 * Array filtering transformer, should match native array_filter behavior
 * @see https://secure.php.net/manual/fr/function.array-filter.php
 */
class ArrayFilterTransformer implements ConfigurableTransformerInterface
{

    use ConditionTrait;

    /**
     * ColumnAggregatorTask constructor.
     *
     * @param PropertyAccessorInterface $accessor
     */
    public function __construct(PropertyAccessorInterface $accessor)
    {
        $this->accessor = $accessor;
    }

    /**
     * {@inheritdoc}
     */
    public function transform($value, array $options = [])
    {
        if (!(\is_array($value) || $value instanceof \Iterable)) {
            throw new \UnexpectedValueException('Given value is not iterable');
        }

        $result = [];

        foreach ($value as $key => $item) {
            if ($this->checkCondition($item, $options['condition'])) {
                $result[$key] = $item;
            }
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        return 'array_filter';
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $this->configureWrappedConditionOptions('condition', $resolver);
    }
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\Manager;

use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
use CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException;
use CleverAge\ProcessBundle\Logger\ProcessLogger;
use CleverAge\ProcessBundle\Logger\TaskLogger;
use CleverAge\ProcessBundle\Model\BlockingTaskInterface;
use CleverAge\ProcessBundle\Model\FinalizableTaskInterface;
use CleverAge\ProcessBundle\Model\FlushableTaskInterface;
use CleverAge\ProcessBundle\Model\InitializableTaskInterface;
use CleverAge\ProcessBundle\Model\IterableTaskInterface;
use CleverAge\ProcessBundle\Model\ProcessHistory;
use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Model\TaskInterface;
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Execute processes
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ProcessManager
{
    protected const EXECUTE_PROCESS = 1;
    protected const EXECUTE_PROCEED = 2;
    protected const EXECUTE_FLUSH = 4;

    /** @var ContainerInterface */
    protected $container;

    /** @var ProcessLogger */
    protected $processLogger;

    /** @var TaskLogger */
    protected $taskLogger;

    /** @var EntityManagerInterface */
    protected $entityManager;

    /** @var ProcessConfigurationRegistry */
    protected $processConfigurationRegistry;

    /** @var TaskConfiguration */
    protected $blockingTaskConfiguration;

    /** @var ContextualOptionResolver */
    protected $contextualOptionResolver;

    /** @var TaskConfiguration[] */
    protected $processedIterables = [];

    /** @var TaskConfiguration[] */
    protected $processedBlockings = [];

    /** @var ProcessHistory */
    protected $processHistory;

    /** @var TaskConfiguration */
    protected $taskConfiguration;

    /**
     * @param ContainerInterface           $container
     * @param ProcessLogger                $processLogger
     * @param TaskLogger                   $taskLogger
     * @param EntityManagerInterface       $entityManager
     * @param ProcessConfigurationRegistry $processConfigurationRegistry
     * @param ContextualOptionResolver     $contextualOptionResolver
     */
    public function __construct(
        ContainerInterface $container,
        ProcessLogger $processLogger,
        TaskLogger $taskLogger,
        EntityManagerInterface $entityManager,
        ProcessConfigurationRegistry $processConfigurationRegistry,
        ContextualOptionResolver $contextualOptionResolver
    ) {
        $this->container = $container;
        $this->processLogger = $processLogger;
        $this->taskLogger = $taskLogger;
        $this->entityManager = $entityManager;
        $this->processConfigurationRegistry = $processConfigurationRegistry;
        $this->contextualOptionResolver = $contextualOptionResolver;
    }

    /**
     * @return ProcessHistory|null
     */
    public function getProcessHistory(): ?ProcessHistory
    {
        return $this->processHistory;
    }

    /**
     * @return TaskConfiguration|null
     */
    public function getTaskConfiguration(): ?TaskConfiguration
    {
        return $this->taskConfiguration;
    }

    /**
     * @param string $processCode
     * @param mixed  $input
     * @param array  $context
     *
     * @throws \Exception
     *
     * @return mixed
     */
    public function execute(string $processCode, $input = null, array $context = [])
    {
        $parentProcessHistory = $this->processHistory;
        $processConfiguration = $this->processConfigurationRegistry->getProcessConfiguration($processCode);
        $processHistory = $this->initializeStates($processConfiguration, $context);
        $this->processHistory = $processHistory;
        $this->checkProcess($processConfiguration);

        // First initialize the whole stack in a linear way, tasks are initialized in the order they are configured
        foreach ($processConfiguration->getTaskConfigurations() as $taskConfiguration) {
            $this->initialize($taskConfiguration);
        }

        // If defined, set the input of a task
        if ($processConfiguration->getEntryPoint()) {
            $processConfiguration->getEntryPoint()->getState()->setInput($input);
        }

        // Resolve task from main branch, starting by the end
        /** @var TaskConfiguration[] $taskList */
        $taskList = array_reverse($processConfiguration->getTaskConfigurations());
        $allowedTasks = $processConfiguration->getMainTaskGroup();
        foreach ($taskList as $taskConfiguration) {
            if (\in_array($taskConfiguration->getCode(), $allowedTasks, true)) {
                $this->resolve($taskConfiguration);
            }
        }

        // Finalize the process in a linear way
        foreach ($processConfiguration->getTaskConfigurations() as $taskConfiguration) {
            $this->finalize($taskConfiguration);
        }

        $this->endProcess($processHistory);

        // If defined, return the output of a task
        $returnValue = null;
        if ($processConfiguration->getEndPoint()) {
            $returnValue = $processConfiguration->getEndPoint()->getState()->getOutput();
        }

        $this->processHistory = $parentProcessHistory;

        return $returnValue;
    }

    /**
     * Resolve a task, by checking if parents are resolved and processing roots and BlockingTasks
     *
     * @param TaskConfiguration $taskConfiguration
     *
     * @throws \RuntimeException
     * @throws \UnexpectedValueException
     *
     * @return bool
     */
    protected function resolve(TaskConfiguration $taskConfiguration): bool
    {
        $state = $taskConfiguration->getState();
        if ($state->isResolved()) {
            return true;
        }

        $state->setStatus(ProcessState::STATUS_PENDING);

        // Resolve parents first
        $allParentsResolved = true;
        foreach ($taskConfiguration->getPreviousTasksConfigurations() as $previousTasksConfiguration) {
            if (!$previousTasksConfiguration->getState()->isResolved()) {
                $isResolved = $this->resolve($previousTasksConfiguration);
                $allParentsResolved = $allParentsResolved && $isResolved;
            }
        }

        if (!$allParentsResolved) {
            throw new \UnexpectedValueException('Cannot resolve all parents');
        }

        $state->setStatus(ProcessState::STATUS_PROCESSING);

        // Start processing only roots that are not in error branch
        if ($taskConfiguration->isRoot()) {
            $this->process($taskConfiguration);
        }

        // Then we can proceed with BlockingTask only if they have been processed normally and where never "proceeded"
        if ($taskConfiguration->getTask() instanceof BlockingTaskInterface
            && $this->hasProcessedBlocking($taskConfiguration)
        ) {
            $this->process($taskConfiguration, self::EXECUTE_PROCEED);
        }

        // This task is now finished, we may flush it to test if there is anything lasting
        $this->flush($taskConfiguration);

        $state->setStatus(ProcessState::STATUS_RESOLVED);

        return $state->isResolved();
    }

    /**
     * Fetch task service and run additional setup for InitializableTasks
     *
     * @param TaskConfiguration $taskConfiguration
     *
     * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
     * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException
     * @throws \UnexpectedValueException
     * @throws \RuntimeException
     */
    protected function initialize(TaskConfiguration $taskConfiguration): void
    {
        $this->taskConfiguration = $taskConfiguration;

        if ($taskConfiguration->getErrorStrategy() === TaskConfiguration::STRATEGY_STOP
            && \count($taskConfiguration->getErrorOutputs()) > 0) {
            $m = "Task configuration {$taskConfiguration->getCode()} has error outputs ";
            $m .= "but it's error strategy 'stop' implies they will never be reached.";
            $this->taskLogger->error($m);
        }
        // @todo Refactor this using a Registry with this feature:
        // https://symfony.com/doc/current/service_container/service_subscribers_locators.html
        $serviceReference = $taskConfiguration->getServiceReference();
        if (0 === strpos($serviceReference, '@')) {
            $task = $this->container->get(ltrim($serviceReference, '@'));
        } elseif ($this->container->has($serviceReference)) {
            $task = $this->container->get($serviceReference);
        } else {
            throw new \UnexpectedValueException(
                "Unable to resolve service reference for Task '{$taskConfiguration->getCode()}'"
            );
        }
        if (!$task instanceof TaskInterface) {
            throw new \UnexpectedValueException(
                "Service defined in Task '{$taskConfiguration->getCode()}' is not a TaskInterface"
            );
        }
        $taskConfiguration->setTask($task);

        if ($task instanceof InitializableTaskInterface) {
            $state = $taskConfiguration->getState();
            try {
                $task->initialize($state);
            } catch (\Throwable $e) {
                $logContext = ['exception' => $e];
                $this->taskLogger->critical($e->getMessage(), $logContext);
                $state->stop($e);
            }
        }
        $this->handleState($taskConfiguration->getState());

        $this->taskConfiguration = null;
    }

    /**
     * @param TaskConfiguration $taskConfiguration
     * @param int               $executionFlag
     *
     * @throws \RuntimeException
     */
    protected function process(TaskConfiguration $taskConfiguration, int $executionFlag = self::EXECUTE_PROCESS): void
    {
        $this->taskConfiguration = $taskConfiguration;

        $state = $taskConfiguration->getState();
        do {
            // Execute the current task, and fetch status
            $this->processExecution($taskConfiguration, $executionFlag);

            $this->handleState($state);
            if ($state->isStopped()) {
                return;
            }

            // An error feed cannot be blocked or skipped (except if the process has been stopped)
            if ($state->hasErrorOutput()) {
                foreach ($taskConfiguration->getErrorTasksConfigurations() as $errorTask) {
                    $this->prepareNextProcess($taskConfiguration, $errorTask, true);
                    $this->process($errorTask);

                    // Bubble up the stop signal
                    if ($errorTask->getState()->isStopped()) {
                        $state->setStopped(true);

                        return;
                    }
                }
            }

            // Run child items only if the state is not "skipped" and task is not blocking
            $task = $taskConfiguration->getTask();
            $shouldContinue =
                (!$task instanceof BlockingTaskInterface || self::EXECUTE_PROCEED === $executionFlag)
                && !$state->isSkipped();

            if ($shouldContinue) {
                if ($task instanceof IterableTaskInterface) {
                    // Register the task as not empty
                    $this->addProcessedIterable($taskConfiguration);
                }
                foreach ($taskConfiguration->getNextTasksConfigurations() as $nextTaskConfiguration) {
                    $this->prepareNextProcess($taskConfiguration, $nextTaskConfiguration);
                    $this->process($nextTaskConfiguration);

                    // Bubble up the stop signal
                    if ($nextTaskConfiguration->getState()->isStopped()) {
                        $state->setStopped(true);

                        return;
                    }
                }
                $state->setSkipped(false); // Reset skipped state
            }

            $hasMoreItem = false; // By default, a task is not iterable
            if ($task instanceof IterableTaskInterface) {
                // Check if task has more items
                $hasMoreItem = $task->next($state);
                if (!$hasMoreItem) {
                    if (!$this->hasProcessedIterable($taskConfiguration)) {
                        return; // This means the task is empty
                    }
                    // This means we are over iterating this task so we can remove it from registry
                    $this->removeProcessedIterable($taskConfiguration);
                    if ($state->isStopped()) {
                        return;
                    }
                }
            }
        } while ($hasMoreItem);

        $this->taskConfiguration = null;
    }

    /**
     * @param TaskConfiguration $taskConfiguration
     * @param int               $executionFlag
     */
    protected function processExecution(TaskConfiguration $taskConfiguration, int $executionFlag): void
    {
        $task = $taskConfiguration->getTask();
        $state = $taskConfiguration->getState();

        try {
            if (self::EXECUTE_PROCESS === $executionFlag) {
                $state->reset(false);
                $this->processLogger->debug("Processing task {$taskConfiguration->getCode()}");
                $task->execute($state);
                if ($task instanceof BlockingTaskInterface) {
                    $this->addProcessedBlocking($taskConfiguration);
                }
            } elseif (self::EXECUTE_PROCEED === $executionFlag) {
                $state->reset(true);
                if (!$task instanceof BlockingTaskInterface) {
                    // This exception should never be thrown
                    throw new \UnexpectedValueException("Task {$taskConfiguration->getCode()} is not blocking");
                }
                $this->processLogger->debug("Proceeding task {$taskConfiguration->getCode()}");
                $task->proceed($state);
                $this->removeProcessedBlocking($taskConfiguration);
            } elseif (self::EXECUTE_FLUSH === $executionFlag) {
                $state->reset(true);
                if (!$task instanceof FlushableTaskInterface) {
                    // This exception should never be thrown
                    throw new \UnexpectedValueException("Task {$taskConfiguration->getCode()} is not flushable");
                }
                $this->processLogger->debug("Flushing task {$taskConfiguration->getCode()}");
                $task->flush($state);
            } else {
                throw new \UnexpectedValueException("Unknown execution flag: {$executionFlag}");
            }

            $exception = $state->getException();
        } catch (\Throwable $e) {
            $exception = $e;
        }

        // Manage exception catching and setting the same
        if ($exception) {
            $this->taskLogger->log($taskConfiguration->getLogLevel(), $exception->getMessage(), $state->getErrorContext());
            $state->setException($exception);
            if ($taskConfiguration->getErrorStrategy() === TaskConfiguration::STRATEGY_SKIP) {
                $state->setSkipped(true);
                if (null === $state->getErrorOutput()) {
                    $state->setErrorOutput($state->getInput());
                }
            } elseif ($taskConfiguration->getErrorStrategy() === TaskConfiguration::STRATEGY_STOP) {
                $state->stop($exception);
            }
        }
    }

    /**
     * Browse all children for FlushableTask until a BlockingTask is found
     *
     * @param TaskConfiguration $taskConfiguration
     *
     * @throws \RuntimeException
     */
    protected function flush(TaskConfiguration $taskConfiguration): void
    {
        $task = $taskConfiguration->getTask();
        if ($task instanceof BlockingTaskInterface) {
            return;
        }
        if ($task instanceof FlushableTaskInterface) {
            $this->process($taskConfiguration, self::EXECUTE_FLUSH);
            if ($taskConfiguration->getState()->isStopped()) {
                return;
            }
        }
        // Check outputs
        foreach ($taskConfiguration->getNextTasksConfigurations() as $nextTaskConfiguration) {
            $this->flush($nextTaskConfiguration);
        }
        // Check errors
        foreach ($taskConfiguration->getErrorTasksConfigurations() as $errorTasksConfiguration) {
            $this->flush($errorTasksConfiguration);
        }
    }

    /**
     * @param TaskConfiguration $taskConfiguration
     *
     * @throws \RuntimeException
     */
    protected function finalize(TaskConfiguration $taskConfiguration): void
    {
        $task = $taskConfiguration->getTask();
        if ($task instanceof FinalizableTaskInterface) {
            $this->taskConfiguration = $taskConfiguration;
            $state = $taskConfiguration->getState();
            try {
                $task->finalize($taskConfiguration->getState());
            } catch (\Throwable $e) {
                $logContext = ['exception' => $e];
                $this->taskLogger->critical($e->getMessage(), $logContext);
                $state->stop($e);
            }
            $this->handleState($state);
            $this->taskConfiguration = null;
        }
    }

    /**
     * @param ProcessConfiguration $processConfiguration
     * @param array                $context
     *
     * @throws \RuntimeException
     * @throws \InvalidArgumentException
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
     *
     * @return ProcessHistory
     */
    protected function initializeStates(
        ProcessConfiguration $processConfiguration,
        array $context = []
    ): ProcessHistory {
        $processHistory = new ProcessHistory($processConfiguration, $context);

        foreach ($processConfiguration->getTaskConfigurations() as $taskConfiguration) {
            $state = new ProcessState($processConfiguration, $processHistory);
            $state->setContext($context);
            $state->setContextualOptionResolver($this->contextualOptionResolver);

            $taskConfiguration->setState($state);
            $state->setTaskConfiguration($taskConfiguration);
        }

        return $processHistory;
    }

    /**
     * @param TaskConfiguration $previousTaskConfiguration
     * @param TaskConfiguration $nextTaskConfiguration
     * @param bool              $isError
     */
    protected function prepareNextProcess(
        TaskConfiguration $previousTaskConfiguration,
        TaskConfiguration $nextTaskConfiguration,
        $isError = false
    ): void {
        if ($isError) {
            $input = $previousTaskConfiguration->getState()->getErrorOutput();
        } else {
            $input = $previousTaskConfiguration->getState()->getOutput();
        }

        $nextState = $nextTaskConfiguration->getState();
        $nextState->setInput($input);
        $nextState->setPreviousState($previousTaskConfiguration->getState());
    }

    /**
     * Save the state of the import process
     *
     * @param ProcessState $state
     *
     * @throws \RuntimeException
     */
    protected function handleState(ProcessState $state): void
    {
        $processHistory = $state->getProcessHistory();
        if ($state->getException() && $state->isStopped()) {
            $processHistory->setFailed();

            throw new \RuntimeException(
                "Process {$state->getProcessConfiguration()->getCode()} has failed",
                -1,
                $state->getException()
            );
        }
    }

    /**
     * @param ProcessHistory $history
     *
     */
    protected function endProcess(ProcessHistory $history): void
    {
        // Do not change state if already set
        if ($history->isStarted()) {
            $history->setSuccess();

            $this->processLogger->info(
                "Process {$history->getProcessCode()} succeed",
                [
                    'duration' => $history->getDuration(),
                ]
            );
        }
    }

    /**
     * Validate a process
     *
     * @param ProcessConfiguration $processConfiguration
     *
     * @throws \RuntimeException
     * @throws \CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException
     * @throws \CleverAge\ProcessBundle\Exception\CircularProcessException
     * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException
     */
    protected function checkProcess(ProcessConfiguration $processConfiguration): void
    {
        $processConfiguration->checkCircularDependencies();

        $taskConfigurations = $processConfiguration->getTaskConfigurations();
        $mainTaskList = $processConfiguration->getMainTaskGroup();
        $entryPoint = $processConfiguration->getEntryPoint();
        $endPoint = $processConfiguration->getEndPoint();

        // Check multi-branch processes
        foreach ($taskConfigurations as $taskConfiguration) {
            if (!\in_array($taskConfiguration->getCode(), $mainTaskList, true)) {
                // We won't throw an error to ease development... but there must be some kind of warning
                $state = $taskConfiguration->getState();
                $logContext = ['main_task_list' => $mainTaskList];
                $this->processLogger->warning("Task '{$taskConfiguration->getCode()}' is unreachable", $logContext);
                $this->handleState($state);
            }
        }

        // Check coherence for entry/end points
        $processConfiguration->getEndPoint();
        if ($entryPoint && !\in_array($entryPoint->getCode(), $mainTaskList, true)) {
            throw InvalidProcessConfigurationException::createNotInMain($entryPoint, $mainTaskList);
        }
        if ($endPoint && !\in_array($endPoint->getCode(), $mainTaskList, true)) {
            throw InvalidProcessConfigurationException::createNotInMain($endPoint, $mainTaskList);
        }
    }

    /**
     * When an iterable task returns at least one element, it gets added here
     *
     * @param TaskConfiguration $taskConfiguration
     */
    protected function addProcessedIterable(TaskConfiguration $taskConfiguration): void
    {
        $this->processedIterables[$taskConfiguration->getCode()] = $taskConfiguration;
    }

    /**
     * If true this means that the tasks returned an element at least once
     *
     * @param TaskConfiguration $taskConfiguration
     *
     * @return bool
     */
    protected function hasProcessedIterable(TaskConfiguration $taskConfiguration): bool
    {
        return array_key_exists($taskConfiguration->getCode(), $this->processedIterables);
    }

    /**
     * Once everything was flushed, the task is resolved and can be removed from the stack
     *
     * @param TaskConfiguration $taskConfiguration
     */
    protected function removeProcessedIterable(TaskConfiguration $taskConfiguration): void
    {
        unset($this->processedIterables[$taskConfiguration->getCode()]);
    }

    /**
     * Add blocking tasks that were just processed
     *
     * @param TaskConfiguration $taskConfiguration
     */
    protected function addProcessedBlocking(TaskConfiguration $taskConfiguration): void
    {
        $this->processedBlockings[$taskConfiguration->getCode()] = $taskConfiguration;
    }

    /**
     * If true this means the task was processed normally but was never run with proceed
     *
     * @param TaskConfiguration $taskConfiguration
     *
     * @return bool
     */
    protected function hasProcessedBlocking(TaskConfiguration $taskConfiguration): bool
    {
        return array_key_exists($taskConfiguration->getCode(), $this->processedBlockings);
    }

    /**
     * Once a blocking task has been proceeded, we can remove it from the stack
     *
     * @param TaskConfiguration $taskConfiguration
     */
    protected function removeProcessedBlocking(TaskConfiguration $taskConfiguration): void
    {
        unset($this->processedBlockings[$taskConfiguration->getCode()]);
    }
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\Task;

use CleverAge\ProcessBundle\Model\ProcessState;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Split a single line into multiple lines based on multiple columns and split characters
 *
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class SplitJoinLineTask extends AbstractIterableOutputTask
{
    /**
     * {@inheritdoc}
     */
    public function next(ProcessState $state): bool
    {
        $valid = parent::next($state);
        if (!$valid) {
            $this->iterator = null;
        }

        return $valid;
    }

    /**
     * @param OptionsResolver $resolver
     */
    protected function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setRequired(
            [
                'split_columns',
                'join_column',
            ]
        );
        $resolver->setAllowedTypes('split_columns', ['array']);
        $resolver->setAllowedTypes('join_column', ['string']);
        $resolver->setDefaults(
            [
                'split_character' => ',',
            ]
        );
    }

    /**
     * @param ProcessState $state
     *
     * @return \Iterator
     */
    protected function initializeIterator(ProcessState $state): \Iterator
    {
        $originalLine = $state->getInput();
        $options = $this->getOptions($state);

        $lineCopy = $originalLine;
        foreach ($options['split_columns'] as $splitColumn) {
            unset($lineCopy[$splitColumn]);
        }

        $outputLines = [];
        foreach ($options['split_columns'] as $column) {
            if (!array_key_exists($column, $originalLine)) {
                throw new \UnexpectedValueException("Missing column {$column}");
            }
            $columnValues = explode($options['split_character'], $originalLine[$column]);
            foreach ($columnValues as $columnValue) {
                $outputLine = $lineCopy;
                $outputLine[$options['join_column']] = $columnValue;
                $outputLines[] = $outputLine;
            }
        }

        return new \ArrayIterator($outputLines);
    }
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\Task\Reporting;

use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessState;
use Psr\Log\LoggerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Count the time between 2 iterations
 */
class AdvancedStatCounterTask extends AbstractConfigurableTask
{
    /** @var \Psr\Log\LoggerInterface */
    protected $logger;

    /** @var \DateTime */
    protected $startedAt;

    /** @var \DateTime */
    protected $lastUpdate;

    /** @var int */
    protected $counter = 0;

    /** @var int */
    protected $preInitCounter = 0;

    /**
     * @param LoggerInterface $logger
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \InvalidArgumentException
     */
    public function execute(ProcessState $state)
    {
        $now = new \DateTime();
        if (!$this->startedAt) {
            $this->startedAt = $now;
        }
        if ($this->preInitCounter < $this->getOption($state, 'skip_first')) {
            $this->preInitCounter++;
            $state->setSkipped(true);

            return;
        }
        if ($this->lastUpdate && 0 === $this->counter % $this->getOption($state, 'show_every')) {
            $diff = $now->diff($this->lastUpdate);
            $fullText = "Last iteration {$diff->format('%H:%I:%S')} ago";
            $items = $this->getOption($state, 'num_items') * $this->counter;
            $seconds = $now->getTimestamp() - $this->startedAt->getTimestamp();
            $rate = 'n/a';
            if ($seconds > 0) {
                $rate = number_format($items / $seconds, 2, ',', ' ');
            }
            $fullText .= " - {$rate} items/s - {$items} items processed";
            $fullText .= " in {$now->diff($this->startedAt)->format('%H:%I:%S')}";

            $this->logger->info($fullText);
        } else {
            $state->setSkipped(true);
        }
        $this->counter++;
        $this->lastUpdate = $now;
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'num_items' => 1,
                'skip_first' => 0,
                'show_every' => 1,
            ]
        );
        $resolver->setAllowedTypes('num_items', ['int']);
        $resolver->setAllowedTypes('skip_first', ['int']);
        $resolver->setAllowedTypes('show_every', ['int']);
    }
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\DependencyInjection;

use Sidus\BaseBundle\DependencyInjection\SidusBaseExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;

/**
 * This is the class that loads and manages your bundle configuration.
 *
 * @see http://symfony.com/doc/current/cookbook/bundles/extension.html
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class CleverAgeProcessExtension extends SidusBaseExtension
{
    /**
     * @param array            $configs
     * @param ContainerBuilder $container
     *
     * @throws \Exception
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        parent::load($configs, $container);

        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $processConfigurationRegistry = $container->getDefinition(ProcessConfigurationRegistry::class);
        $processConfigurationRegistry->replaceArgument(0, $config['configurations']);
        $processConfigurationRegistry->replaceArgument(1, $config['default_error_strategy']);
    }
}


<?php
/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (C) 2017-2018 Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Task\File;

use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\IterableTaskInterface;
use CleverAge\ProcessBundle\Model\ProcessState;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Browse a folder an iterate each file for output
 */
class FolderBrowserTask extends AbstractConfigurableTask implements IterableTaskInterface
{
    /** @var LoggerInterface */
    protected $logger;

    /** @var \Iterator|SplFileInfo[] */
    protected $files;

    /**
     * @param LoggerInterface $logger
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    /**
     * @param ProcessState $state
     *
     * @throws \LogicException
     * @throws \InvalidArgumentException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     */
    public function execute(ProcessState $state)
    {
        $options = $this->getOptions($state);
        if (null === $this->files) {
            $finder = new Finder();
            $finder->files();
            if ($options['name_pattern']) {
                $finder->name($options['name_pattern']);
            }
            $this->files = $finder->in($options['folder_path'])->sortByName()->getIterator();
            $this->files->rewind();
        }

        if (!$this->files->valid()) {
            $this->logger->log($options['empty_log_level'], "No item found in path {$options['folder_path']}");
            $state->setSkipped(true);
            $state->setErrorOutput($options['folder_path']);
            $this->files = null;

            return;
        }
        /** @var SplFileInfo $fileInfo */
        $fileInfo = $this->files->current();
        $filePath = $fileInfo->getPathname();
        $state->addErrorContextValue('current_file_path', $filePath);
        $state->setOutput($filePath);
    }

    /**
     * Moves the internal pointer to the next element,
     * return true if the task has a next element
     * return false if the task has terminated it's iteration
     *
     * @param ProcessState $state
     *
     * @return bool
     */
    public function next(ProcessState $state)
    {
        if (!$this->files) {
            return false;
        }
        $this->files->next();
        $state->removeErrorContext('current_file_path');

        return $this->files->valid();
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'folder_path',
            ]
        );
        $resolver->setAllowedTypes('folder_path', ['string']);
        /** @noinspection PhpUnusedParameterInspection */
        $resolver->setNormalizer(
            'folder_path',
            function (Options $options, $value) {
                if (!is_dir($value)) {
                    throw new InvalidConfigurationException(
                        "Folder path does not exists or is not a folder: '{$value}'"
                    );
                }
                if (!is_readable($value)) {
                    throw new InvalidConfigurationException("Folder path is not readable: '{$value}'");
                }

                return $value;
            }
        );
        $resolver->setDefaults(
            [
                'name_pattern' => null,
                'empty_log_level' => LogLevel::WARNING,
            ]
        );
        $resolver->setAllowedTypes('name_pattern', ['NULL', 'string']);
        $resolver->setAllowedValues(
            'empty_log_level',
            [
                LogLevel::ALERT,
                LogLevel::CRITICAL,
                LogLevel::DEBUG,
                LogLevel::EMERGENCY,
                LogLevel::ERROR,
                LogLevel::INFO,
                LogLevel::NOTICE,
                LogLevel::WARNING,
            ]
        );
    }
}


<?php
/*
 * This file is part of the CleverAge/ProcessBundle package.
 *
 * Copyright (C) 2017-2018 Clever-Age
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CleverAge\ProcessBundle\Task\File;

use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessState;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Class FileWriterTask
 *
 * @author  Madeline Veyrenc <mveyrenc@clever-age.com>
 */
class FileWriterTask extends AbstractConfigurableTask
{
    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Symfony\Component\Filesystem\Exception\IOException
     */
    public function execute(ProcessState $state)
    {
        $options = $this->getOptions($state);

        $fs = new Filesystem();
        $fs->dumpFile($options['filename'], $state->getInput());

        $state->setOutput($options['filename']);
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'filename',
            ]
        );
        $resolver->setAllowedTypes('filename', ['string']);
    }
}