<?php

namespace Modules\Dynamicfield\Utility\Fields;

use Collective\Html\FormFacade;
use Modules\Dynamicfield\Entities\Entity;
use Modules\Dynamicfield\Entities\RepeaterTranslation;

class Repeater extends FieldBase
{
    protected $options;
    protected $fieldModel;
    protected $value;
    protected $fieldType;
    protected $fieldLabel;
    protected $fieldId;
    protected $groupFieldId;
    protected $locale;
    protected $repeaterHeaders;
    protected $order;
    protected $repeaterFieldData;
    protected $isValid = true;
    protected $htmlItemTemplate = '
%s
';

    protected $groupFields = array();
    protected $fieldValues = array();

    protected $deleteItems = '';
    protected $defaultOrder = '';

    public function init($default = null)
    {
        $this->initRepeatFields($this->field, $default);
    }

    /**
     * Set data per group.
     *
     * @param $fieldInfo
     * @param null $default
     */
    private function initRepeatFields($fieldInfo, $default = null)
    {
        $controls = array();
        $repeaterId = $fieldInfo->id;
        $entity = Entity::getEntity($this->entityId, $this->entityType, $repeaterId);
        $repeaters = Entity::getAllDataTranactionRepeater($this->entityId, $this->entityType, $repeaterId, $this->locale);
        $this->repeaterFieldData = Entity::getAllDataTranactionRepeaterFields($this->entityId, $this->entityType, $repeaterId, $this->locale);
        $post_data = @$default['value'];
        $this->defaultOrder = @$default['order'];
        $repeater_field = $this->field;
        $fieldOfRepeater = $repeater_field->getListFields();
        if (isset($post_data)) {
            unset($post_data['clone']);
            $this->deleteItems = $default['delete'];

            foreach ($post_data as $k => $control) {
                $listDefault = $post_data[$k];
                $controls[$k]['fields'] = $this->createListControlAfterPostData($k, $listDefault, $fieldOfRepeater);
                $controls[$k]['order'] = -1;
            }
        } else {
            if (count($repeaters)) {
                $i = 1;

                foreach ($repeaters as $repeater) {
                    $controls[$repeater->id]['fields'] = $this->createListControl($repeater, $fieldOfRepeater);
                    $controls[$repeater->id]['order'] = $i;
                    ++$i;
                }
            }
        }

        $this->groupFields = $controls;

        $this->repeaterHeaders = $this->createHeaderRepeater($fieldOfRepeater);
    }

    /**
     * create list controller when user post data from browser.
     *
     * @param $repeaterId
     * @param null $default
     *
     * @return array
     */
    private function createListControlAfterPostData($repeaterId, $default = null, $fieldOfRepeater)
    {
        $controls = array();
        $nameFormat = '%s[fields]' . sprintf('[%s][value][%s]', $this->getFieldId(), $repeaterId) . '[%s][value]';
        $idFormat = '%s_' . sprintf('%s_%s_', $this->getFieldId(), $repeaterId) . '_%s_value';

        if ($fieldOfRepeater->count()) {
            foreach ($fieldOfRepeater as $field) {
                $listDefault = $default[$field->id];
                $fieldControl = $this->createFieldControl($field, $repeaterId, $listDefault);
                $fieldControl->setHtmlNameFormat($nameFormat);
                $fieldControl->setHtmlIdFormat($idFormat);
                $controls[$field->id] = $fieldControl;
            }
        }

        return $controls;
    }

    /**
     * Set list control of repeater.
     *
     * @param $repeater
     *
     * @return mixed
     */
    private function createListControl($repeater, $fieldOfRepeater)
    {
        $nameFormat = '%s[fields]' . sprintf('[%s][value][%s]', $this->getFieldId(), $repeater->id) . '[%s][value]';
        $idFormat = '%s_' . sprintf('%s_%s_', $this->getFieldId(), $repeater->id) . '_%s_value';

        if ($fieldOfRepeater->count()) {
            foreach ($fieldOfRepeater as $field) {
                $dbData = $this->getFieldDataInDB($field->id, $repeater->id);
                $fieldControl = $this->createFieldControl($field, $repeater->id);
                $fieldControl->setHtmlItemTemplate('%s');
                $fieldControl->setHtmlNameFormat($nameFormat);
                $fieldControl->setHtmlIdFormat($idFormat);

               // $value = $repeater->getFieldValue($field->id);
                $fieldControl->setValue($dbData);

                $controls[$field->id] = $fieldControl;
            }
        }

        return $controls;
    }

    /**
     * Set field control data.
     *
     * @param $field
     * @param $translateId
     * @param null $default
     *
     * @return File|Image|Number|Text|Textarea|Wysiwyg|null
     */
    private function createFieldControl($field, $translateId, $default = null)
    {
        $fieldControl = null;
        $fieldValue = $default;
        switch ($field->type) {
            case 'text':
                $fieldControl = new Text($field, $this->entityId, $this->locale);
                break;
            case 'number':
                $fieldControl = new Number($field, $this->entityId, $this->locale);
                break;
            case 'textarea':
                $fieldControl = new Textarea($field, $this->entityId, $this->locale);
                break;
            case 'wysiwyg':
                $fieldControl = new Wysiwyg($field, $this->entityId, $this->locale);
                break;
            case 'file':
                $fieldControl = new File($field, $this->entityId, $this->locale);
                break;
            case 'image':
                $fieldControl = new Image($field, $this->entityId, $this->locale);
                break;
        }
        // assign entity type class to field to use for save data;
        $fieldControl->setEntityType($this->entityType);
        $fieldControl->setRepeaterId($this->fieldId);
        $fieldControl->setTranslateId($translateId);

        $fieldControl->init($fieldValue);

        return $fieldControl;
    }

    /**
     * Set header repeater.
     *
     * @return mixed
     */
    private function createHeaderRepeater($fieldOfRepeater)
    {
        $repeater = new RepeaterTranslation();
        //TODO Here was ='clone'
        $repeater->id = 'clone';
        $controls = $this->createListControl($repeater, $fieldOfRepeater);

        return $controls;
    }

    /**
     * Valid for group field.
     *
     * @return bool
     */
    public function valid()
    {
        $isValid = true;

        return $isValid;
    }

    /**
     * Render for group field.
     *
     * @return string
     */
    public function render()
    {
        $htmlRepeaterTemplate = '';
        $tdFirstBody = "
                                %s
                                <input type='hidden' name='%s' id='%s' value='%s'/>
                            	";
        $repeaterDeleteId = 'repeater_delete_' . $this->locale . '_' . $this->getFieldId();
        $repeaterTableId = 'repeater_table_' . $this->locale . '_' . $this->getFieldId();
        $repeaterDeleteName = sprintf('%s[fields][%s][delete]', $this->locale, $this->getFieldId());
        $tdDeleteBtn = "
							locale}_{$this->getFieldId()}',this)\">

							";
        $groups = $this->groupFields;

        $headers = $this->repeaterHeaders;
        $columns = count($headers) + 2;
        $tableHeader = "

							" . $this->getLabel() . '

';

        $columnWidth = 50;
        if (count($headers)) {
            $columnWidth = 100 / ($columns - 2);
             // make header
            $tdHeader = '';
            $tdTemplate = '';
            foreach ($headers as $field) {
                $tdHeader .= sprintf("%s	", $columnWidth . '%', $field->getLabel());
                $field->setLabel('');
                $tdTemplate .= sprintf("%s	", $columnWidth . '%', $field->render());
            }

            $groupName = $this->locale . '[fields][%s][order][%s]';
            $groupName = sprintf($groupName, $this->fieldId, 'clone');
            // assign template to create new item of repeater
            $tdFirstNew = sprintf($tdFirstBody, -1, $groupName, $groupName, -1);
            $tdTemplate = $tdFirstNew . $tdTemplate . $tdDeleteBtn;
            $repeaterFormat = "
%s
";
            $htmlRepeaterTemplate = sprintf($repeaterFormat, $this->locale, $this->getFieldId(), $tdTemplate);

            // assign header lable for repeater
            $tdHeader = ' 	' . $tdHeader . " 	";
            $tableHeader .= $tdHeader;
        }
        $trBody = '';
        if (count($groups)) {
            // make row
            $i = 1;
            foreach ($groups as $groupId => $group) {
                $groupName = $this->locale . '[fields][%s][order][%s]';
                $groupOrder = $group['order'];
                $groupName = sprintf($groupName, $this->fieldId, $groupId);

                $fields = $group['fields'];
                $tdBody = '';
                foreach ($fields as $field) {
                    $field->setLabel('');
                    $tdBody .= sprintf("%s	", $columnWidth . '%', $field->render());
                }

                $tdFirstNew = sprintf($tdFirstBody, $i, $groupName, $groupName, $groupOrder);
                $tdBody = $tdFirstNew . $tdBody . $tdDeleteBtn;
                $trBody .= sprintf("
%s
", $groupId, $tdBody);
                ++$i;
            }
        }
        $tableBody = $trBody . $htmlRepeaterTemplate;
        $htmlAddNewBtn = "locale}_{$this->getFieldId()}')\">" . trans('Add Item') . '';
        $tableFooter = sprintf("%s	", $htmlAddNewBtn);

        $tableHtml = "

%s

%s

%s

";

        $html = sprintf($tableHtml, $tableHeader, $tableBody, $tableFooter);

        // assign index for repeater to make new item
        $repeaterIndexId = 'repeater_index_' . $this->locale . '_' . $this->getFieldId();

        $inputIndex = FormFacade::hidden($repeaterIndexId, 0, array('id' => $repeaterIndexId));
        $inputDelete = FormFacade::hidden($repeaterDeleteName, '', array('id' => $repeaterDeleteId));
        $html            .= $inputIndex . $inputDelete;

        $html            .=  sprintf("", $repeaterTableId);

        return $html;
    }

    /**
     * Save field data to database.
     *
     * @return bool
     */
    public function save()
    {
        $bResult = false;
        try {
            if (!empty($this->deleteItems)) {
                $items = explode(',', $this->deleteItems);
                RepeaterTranslation::destroy($items);
            }
            if (count($this->groupFields)) {
                foreach ($this->groupFields as $groupId => $group) {
                    $fields = $group['fields'];
                    $order = $this->defaultOrder[$groupId];

                    $translate_id = 0;
                    foreach ($fields as $field) {
                        if ($translate_id) {
                            $field->setTranslateId($translate_id);
                        }
                        $field->save();
                        $translate_id = $field->getTranslateId();
                    }
                     // update order
                     $translate = RepeaterTranslation::find($translate_id);
                    $translate->order = $order;
                    $translate->save();
                }
            }
            $bResult = true;
        } catch (\Exception $e) {
            //exception handling
        }

        return $bResult;
    }

    /**
     * Show group field.
     *
     * @return array
     */
    public function getDisplayValue()
    {
        $values = array();

        if (count($this->groupFields)) {
            $i = 0;
            foreach ($this->groupFields as $key => $group) {
                $fields = $group['fields'];
                $item = array();
                foreach ($fields as $field) {
                    $item[$field->getFieldName()] = $field->getDisplayValue();
                }
                $item['id'] = $key;
                $values[$i] = $item;
                ++$i;
            }
        }

        return $values;
    }

    /**
     * Get error message.
     *
     * @return string
     */
    public function getErrorMessage()
    {
        $error = $this->getOption('error_message');

        return $error;
    }

    public function getFieldDataInDB($fieldId, $translationId)
    {
        $fieldValue = "";
        $index = -1;
        foreach ($this->repeaterFieldData as $k=>$v) {
            if ($v->translation_id == $translationId && $v->field_id == $fieldId) {
                $fieldValue = $v->value;
                $index = $k ;
                break;
            }
        }
        // remove item to improve performance
        if ($index) {
            unset($this->repeaterFieldData[$index]);
        }

        return $fieldValue;
    }

    /**
     * Get order data.
     *
     * @return mixed
     */
    protected function getOrder()
    {
        return $this->order;
    }
}


<?php

namespace Modules\Dynamicfield\Entities;

use Illuminate\Database\Eloquent\Model;

class RepeaterTranslation extends Model
{
    protected $table = 'dynamicfield__repeater_translations';
    protected $fillable = ['entity_repeater_id', 'locale', 'order'];

    /**
     * Relationship with Entity.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function entity()
    {
        return $this->belongsTo('Modules\Dynamicfield\Entities\Entity', 'entity_repeater_id', 'id');
    }

    /**
     * Relationship with RepeaterValue.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function fieldValues()
    {
        return $this->hasMany('Modules\Dynamicfield\Entities\RepeaterValue', 'translation_id', 'id');
    }

    /**
     * Get repeater data by fieldId.
     *
     * @param $fieldId
     *
     * @return RepeaterValue
     */
    public function getFieldValue($fieldId)
    {
        $object = new RepeaterValue();
        if (is_numeric($this->id)) {
            $repeatValues = $this->fieldValues()->where('field_id', $fieldId)->get();
            if ($repeatValues->count()) {
                $object = $repeatValues[0];
            }
        }

        return $object;
    }
}


<?php

namespace Modules\Dynamicfield\Utility;

use Illuminate\Filesystem\Filesystem;
use Modules\Core\Foundation\Theme\ThemeManager;

class Template
{
    /**
     * @var ThemeManager
     */
    private $themeManager;
    /**
     * @var Filesystem
     */
    private $finder;

    public function __construct(ThemeManager $themeManager, Filesystem $finder)
    {
        $this->themeManager = $themeManager;
        $this->finder = $finder;
    }

    public function getTemplates()
    {
        $path = $this->getCurrentThemeBasePath();

        $templates = [];

        foreach ($this->finder->allFiles($path . '/views') as $template) {
            $relativePath = $template->getRelativePath();

            if ($this->isLayoutOrPartial($relativePath)) {
                continue;
            }

            $templateName = $this->getTemplateName($template);
            $file = $this->removeExtensionsFromFilename($template);

            if ($this->hasSubdirectory($relativePath)) {
                $templates[str_replace('/', '.', $relativePath) . '.' . $file] = $templateName;
            } else {
                $templates[$file] = $templateName;
            }
        }

        return $templates;
    }

    /**
     * Get the base path of the current theme.
     *
     * @return string
     */
    private function getCurrentThemeBasePath()
    {
        return $this->themeManager->find(setting('core::template'))->getPath();
    }

    /**
     * Read template name defined in comments.
     *
     * @param $template
     *
     * @return string
     */
    private function getTemplateName($template)
    {
        preg_match('/{{-- Template: (.*) --}}/', $template->getContents(), $templateName);

        if (count($templateName) > 1) {
            return $templateName[1];
        }

        return $this->getDefaultTemplateName($template);
    }

    /**
     * If the template name is not defined in comments, build a default.
     *
     * @param $template
     *
     * @return mixed
     */
    private function getDefaultTemplateName($template)
    {
        $relativePath = $template->getRelativePath();
        $fileName = $this->removeExtensionsFromFilename($template);

        return $this->hasSubdirectory($relativePath) ? $relativePath . '/' . $fileName : $fileName;
    }

    /**
     * Check if the given path is a layout or a partial.
     *
     * @param string $relativePath
     *
     * @return bool
     */
    private function isLayoutOrPartial($relativePath)
    {
        return preg_match('#(layouts|partials)#i', $relativePath) === 1;
    }

    /**
     * Remove the extension from the filename.
     *
     * @param $template
     *
     * @return mixed
     */
    private function removeExtensionsFromFilename($template)
    {
        return str_replace('.blade.php', '', $template->getFilename());
    }

    /**
     * Check if the relative path is not empty (meaning the template is in a directory).
     *
     * @param $relativePath
     *
     * @return bool
     */
    private function hasSubdirectory($relativePath)
    {
        return !empty($relativePath);
    }
}


<?php

namespace Modules\Dynamicfield\Utility\Fields;

use Collective\Html\FormFacade;

class Textarea extends FieldBase
{
    public function __construct($fieldInfo, $entityId, $locale, $dbData = null)
    {
        parent:: __construct($fieldInfo, $entityId, $locale, $dbData);
    }

    /**
     * Check validator with field.
     *
     * @return bool
     */
    public function valid()
    {
        $bResult = false;
        if ($this->getOption('required') == 'true') {
            $value = $this->getValue();
            if (empty($value)) {
                $this->isValid = false;
            } else {
                $bResult = true;
            }
        } else {
            $bResult = true;
        }

        return $bResult;
    }

    /**
     * Render html of field.
     *
     * @return string
     */
    public function render()
    {
        $attrs = array();
        $id = $this->getHtmlId();
        $name = $this->getHtmlName();
        $value = $this->getValue();
        $label = $this->getLabel();

        $cssClass = 'textarea form-control';
        $errorMessage = '';
        $attrs['id'] = $id;
        if (!$this->isValid) {
            $errorMessage = sprintf("%s", $this->getErrorMessage());
            $cssClass .= ' error';
        }

        $defaultValue = $this->getOption('default_value');
        if (!strlen($value)) {
            $value = $defaultValue;
        }

        if ($this->getOption('required') == 'true') {
            $cssClass .= ' required';
        }
        if ($this->getOption('placeholder')) {
            $attrs['placeholder'] = $this->getOption('placeholder');
        }
        if ($this->getOption('rows')) {
            $attrs['rows'] = $this->getOption('rows');
        }
        $attrs['class'] = $cssClass;

        $html = '';
        if (!empty($label)) {
            $html .= FormFacade::label($label);
        }
        $html                .= FormFacade::textarea($name, $value, $attrs) . $errorMessage;
        $html = sprintf($this->htmlItemTemplate, $html);

        return $html;
    }

    /**
     * Get error message.
     *
     * @return string
     */
    public function getErrorMessage()
    {
        $error = $this->getOption('error_message');

        return $error;
    }

    public function loadFieldData()
    {
    }
}


<?php

namespace Modules\Dynamicfield\Utility\Fields;

use Modules\Dynamicfield\Entities\Entity;
use Modules\Dynamicfield\Entities\Field;
use Modules\Dynamicfield\Entities\RepeaterTranslation;
use Modules\Dynamicfield\Entities\RepeaterValue;

class FieldBase
{
    protected $field;
    protected $options;
    protected $fieldModel;
    protected $value;
    protected $fieldType;
    protected $fieldLabel;
    protected $fieldId;
    protected $repeaterId;
    protected $translationId;
    protected $entityId;
    protected $locale;
    protected $dataInDB;
    protected $model;
    protected $entityType;
    protected $isValid = true;
    protected $htmlNameFormat = '%s[fields][%s][value]';
    protected $htmlIdFormat = '%s_%s_value';
    protected $htmlItemTemplate = "
%s
";

    public function __construct($fieldInfo, $entityId, $locale, $dbData = null)
    {
        $this->field = $fieldInfo;
        $option_data = (array) json_decode($fieldInfo->data);
        $this->options = $option_data;
        $this->entityId = $entityId;
        $this->locale = $locale;
        $this->dataInDB = $dbData;
        $this->fieldId = $this->field->id;
    }

    /**
     * Set model and value.
     *
     * @param null $default
     */
    public function init($default = null)
    {
        //$this->model = $this->getModel();
        if (!isset($default)) {
            //$this->value = $this->model->value;
            if (empty($this->dataInDB)) {
                $this->value = $this->getOption('default');
            } else {
                $this->value =  $this->dataInDB;
            }
        } else {
            $this->value = $default['value'];
        }
    }

    /**
     * Get Entity or RepeaterTranslation model by locale.
     *
     * @return RepeaterValue
     */
    public function getModel()
    {
        if (is_a($this->field, 'Modules\Dynamicfield\Entities\Field')) {
            $entity = Entity::getEntity($this->entityId, $this->entityType, $this->fieldId);
            $model = $entity->getFieldByLocale($this->locale);
        } else {
            $model = new RepeaterValue();
            if (is_numeric($this->translationId)) {
                $repeaterTranslate = RepeaterTranslation::firstOrNew(array('id' => $this->translationId));
                $model = $repeaterTranslate->getFieldValue($this->fieldId);
            }
        }

        return $model;
    }

    /**
     * Get value.
     *
     * @return mixed
     */
    public function getValue()
    {
        return  $this->value;
    }

    /**
     * Get display value.
     *
     * @return mixed
     */
    public function getDisplayValue()
    {
        return $this->value;
    }

    /**
     * Set value.
     *
     * @param $value
     */
    public function setValue($value)
    {
        $this->value = $value;
    }

    /**
     * Check valid.
     *
     * @return bool
     */
    public function valid()
    {
        return true;
    }

    public function getErrorMessage()
    {
    }
    public function render()
    {
    }
    public function loadFieldData()
    {
    }

    /**
     * Get FieldId.
     *
     * @return mixed
     */
    public function getFieldId()
    {
        return $this->fieldId;
    }
    /**
     * Get field name.
     *
     * @return mixed
     */
    public function getFieldName()
    {
        return $this->field->name;
    }

    /**
     * Get htmlId.
     *
     * @return string
     */
    public function getHtmlId()
    {
        $strHtmlId = sprintf($this->htmlIdFormat, $this->locale, $this->fieldId);

        return $strHtmlId;
    }

    /**
     * get html name.
     *
     * @return string
     */
    public function getHtmlName()
    {
        $strHtmlName = sprintf($this->htmlNameFormat, $this->locale, $this->fieldId);

        return $strHtmlName;
    }

    /**
     * Set html name format.
     *
     * @param $strFormat
     */
    public function setHtmlNameFormat($strFormat)
    {
        $this->htmlNameFormat = $strFormat;
    }

    /**
     * Set htmlId format.
     *
     * @param $strFormat
     */
    public function setHtmlIdFormat($strFormat)
    {
        $this->htmlIdFormat = $strFormat;
    }

    /**
     * Get label.
     *
     * @return string
     */
    public function getLabel()
    {
        $strLabel = $this->getOption('label');

        return $strLabel;
    }

    /**
     * Set Label.
     *
     * @param $value
     */
    public function setLabel($value)
    {
        $this->options['label'] = $value;
    }

    /**
     * Set repeaterId.
     *
     * @param $value
     */
    public function setRepeaterId($value)
    {
        $this->repeaterId = $value;
    }

    /**
     * Set translateId.
     *
     * @param $value
     */
    public function setTranslateId($value)
    {
        $this->translationId = $value;
    }

    /**
     * Get translateId.
     *
     * @return mixed
     */
    public function getTranslateId()
    {
        return $this->translationId;
    }

    /**
     * Set Html item template.
     *
     * @param $strFormat
     */
    public function setHtmlItemTemplate($strFormat)
    {
        $this->htmlItemTemplate = $strFormat;
    }

    /**
     * Save field or repeater fild.
     */
    public function save()
    {
        $this->model = $this->getModel();
        if (is_a($this->model, 'Modules\Dynamicfield\Entities\FieldTranslation')) {
            $this->saveField();
        } else {
            // save sub control of repeater
            $this->saveRepeaterField();
        }
    }

    /**
     * Save normal field.
     */
    public function saveField()
    {
        $entity = Entity::getEntity($this->entityId, $this->entityType, $this->fieldId);
        if (!$entity->id) {
            $entity->entity_id = $this->entityId;
            $entity->field_id = $this->fieldId;
            $entity->entity_type = $this->entityType;
            $entity->save();
        }
        $this->model->entity_field_id = $entity->id;
        $this->model->locale = $this->locale;
        $this->model->value = $this->getValue();
        $this->model->save();
    }

    /**
     * Save repeat data for field of repeater.
     */
    public function saveRepeaterField()
    {
        $repeaterId = $this->repeaterId;

        $repeaterTranslate = RepeaterTranslation::firstOrNew(array('id' => $this->translationId));

        if (!$repeaterTranslate->id) {
            $entity = Entity::getEntity($this->entityId, $this->entityType, $repeaterId);
            if (!$entity->id) {
                $entity->entity_id = $this->entityId;
                $entity->entity_type = $this->entityType;
                $entity->field_id = $repeaterId;
                $entity->save();
            }
            $repeaterTranslate->entity_repeater_id = $entity->id;
            $repeaterTranslate->locale = $this->locale;
            $repeaterTranslate->save();
        }
        $this->model->translation_id = $repeaterTranslate->id;
        $this->model->field_id = $this->fieldId;

        $this->model->value = $this->getValue();
        $this->model->save();
        // assign translate_id to same group;
        $this->translationId = $this->model->translation_id;
    }

    /**
     * Set entity type.
     *
     * @param $type
     */
    public function setEntityType($type)
    {
        $this->entityType = $type;
    }

    /**
     * get value of option.
     *
     * @param $key
     *
     * @return string
     */
    public function getOption($key)
    {
        $value = '';
        try {
            $value = $this->options[$key];
        } catch (\Exception $e) {
        }

        return $value;
    }
}


<?php

/*
 * This file is part of php-drawille
 *
 * (c) Jeff Welch <whatthejeff@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Drawille;

/**
 * Basic turtle graphics interface
 *
 * @author Jeff Welch <whatthejeff@gmail.com>
 * @see    http://en.wikipedia.org/wiki/Turtle_graphics
 */
class Turtle extends Canvas
{
    /**
     * Current x position
     *
     * @var integer
     */
    private $x = 0;
    /**
     * Current y position
     *
     * @var integer
     */
    private $y = 0;
    /**
     * Current canvas rotation
     *
     * @var integer
     */
    private $rotation = 0;

    /**
     * If the pen is up
     *
     * @var boolean
     */
    private $up = false;

    /**
     * Constructor
     *
     * @param int $y starting x position
     * @param int $y starting y position
     */
    public function __construct($x = 0, $y = 0) {
        $this->x = $x;
        $this->y = $y;
    }

    /**
     * Gets the current x position.
     *
     * @return integer x position
     */
    public function getX() {
        return $this->x;
    }

    /**
     * Gets the current y position.
     *
     * @return integer y position
     */
    public function getY() {
        return $this->y;
    }

    /**
     * Gets the current canvas rotation
     *
     * @return integer current canvas rotation
     */
    public function getRotation() {
        return $this->rotation;
    }

    /**
     * Push the pen down
     */
    public function down() {
        $this->up = false;
    }

    /**
     * Pull the pen up
     */
    public function up() {
        $this->up = true;
    }

    /**
     * Move the pen forward
     *
     * @param integer $length distance to move forward
     */
    public function forward($length) {
        $theta = $this->rotation / 180.0 * M_PI;
        $x     = $this->x + $length * cos($theta);
        $y     = $this->y + $length * sin($theta);

        $this->move($x, $y);
    }

    /**
     * Move the pen backwards
     *
     * @param integer $length distance to move backwards
     */
    public function back($length) {
        $this->forward(-$length);
    }

    /**
     * Angle the canvas to the right.
     *
     * @param integer $angle degree to angle
     */
    public function right($angle) {
        $this->rotation += $angle;
    }

    /**
     * Angle the canvas to the left.
     *
     * @param integer $angle degree to angle
     */
    public function left($angle) {
        $this->rotation -= $angle;
    }

    /**
     * Move the pen, drawing if the pen is down.
     *
     * @param int $y new x position
     * @param int $y new y position
     */
    public function move($x, $y) {
      if (!$this->up) {
        $x1 = round($this->x);
        $y1 = round($this->y);
        $x2 = $x;
        $y2 = $y;

        $xdiff = max($x1, $x2) - min($x1, $x2);
        $ydiff = max($y1, $y2) - min($y1, $y2);

        $xdir = $x1 <= $x2 ? 1 : -1;
        $ydir = $y1 <= $y2 ? 1 : -1;

        $r = max($xdiff, $ydiff);

        for ($i = 0; $i <= $r; $i++) {
            $x = $x1;
            $y = $y1;

            if ($ydiff > 0) {
                $y += ((float)$i * $ydiff) / $r * $ydir;
            }

            if ($xdiff > 0) {
                $x += ((float)$i * $xdiff) / $r * $xdir;
            }

            $this->set($x, $y);
        }
      }

      $this->x = $x;
      $this->y = $y;
    }

    /**
     * Pull the pen up
     */
    public function pu() {
        $this->up();
    }

    /**
     * Push the pen up
     */
    public function pd() {
        $this->down();
    }

    /**
     * Move the pen forward
     *
     * @param integer $length distance to move forward
     */
    public function fd($length) {
        $this->forward($length);
    }

    /**
     * Move the pen, drawing if the pen is down.
     *
     * @param int $y new x position
     * @param int $y new y position
     */
    public function mv($x, $y) {
        $this->move($x, $y);
    }

    /**
     * Angle the canvas to the right.
     *
     * @param integer $angle degree to angle
     */
    public function rt($angle) {
        $this->right($angle);
    }

    /**
     * Angle the canvas to the left.
     *
     * @param integer $angle degree to angle
     */
    public function lt($angle) {
        $this->left($angle);
    }

    /**
     * Move the pen backwards
     *
     * @param integer $length distance to move backwards
     */
    public function bk($length) {
        $this->back($length);
    }
}


<?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;

/**
 * Slugify a value
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class SlugifyTransformer implements ConfigurableTransformerInterface
{
    /**
     * Must return the transformed $value
     *
     * @param mixed $value
     * @param array $options
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     *
     * @return mixed $value
     */
    public function transform($value, array $options = [])
    {
        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);
        $options = $resolver->resolve($options);

        $transliterator = \Transliterator::create($options['transliterator']);
        $string = $transliterator->transliterate($value);

        return trim(
            preg_replace(
                $options['replace'],
                $options['separator'],
                strtolower(trim(strip_tags($string)))
            ),
            $options['separator']
        );
    }

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

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'transliterator' => 'NFD; [:Nonspacing Mark:] Remove; NFC',
                'replace' => '/[^a-z0-9]+/',
                'separator' => '_',
            ]
        );
    }
}


<?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\Exception;

/**
 * Thrown when multiple independent branches are found in a process
 *
 * @deprecated, we won't send an error for now
 */
class MultiBranchProcessException extends \UnexpectedValueException implements ProcessExceptionInterface
{
    /**
     * @param string $processCode
     * @param array  $branches
     *
     * @return MultiBranchProcessException
     */
    public static function create($processCode, array $branches)
    {
        $branchesStr = [];
        foreach ($branches as $branch) {
            $branchesStr[] = '['.implode(', ', $branch).']';
        }

        $errorStr = '['.implode(', ', $branchesStr).']';
        $errorStr = "Process {$processCode} contains multiple independent branches : {$errorStr}, ";
        $errorStr .= 'which is not allowed. Please create a process for each branch';

        return new self($errorStr);
    }
}


<?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\Configuration;

use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Model\TaskInterface;
use Psr\Log\LogLevel;

/**
 * Represents a task configuration inside a process
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class TaskConfiguration
{
    public const STRATEGY_SKIP = 'skip';
    public const STRATEGY_STOP = 'stop';

    /** @var string */
    protected $code;

    /** @var string */
    protected $serviceReference;

    /** @var TaskInterface */
    protected $task;

    /** @var array */
    protected $options = [];

    /** @var string */
    protected $description = '';

    /** @var string */
    protected $help = '';

    /** @var array */
    protected $outputs = [];

    /** @var array */
    protected $errorOutputs = [];

    /** @var ProcessState */
    protected $state;

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

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

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

    /** @var bool */
    protected $inErrorBranch = false;

    /** @var string */
    protected $errorStrategy;

    /** @var string */
    protected $logLevel;

    /** @var bool */
    protected $logErrors;

    /**
     * @param string $code
     * @param string $serviceReference
     * @param array  $options
     * @param string $description
     * @param string $help
     * @param array  $outputs
     * @param array  $errorOutputs
     * @param string $errorStrategy
     * @param string $logLevel
     */
    public function __construct(
        $code,
        $serviceReference,
        array $options,
        string $description = '',
        string $help = '',
        array $outputs = [],
        array $errorOutputs = [],
        string $errorStrategy = self::STRATEGY_SKIP,
        string $logLevel = LogLevel::CRITICAL
    ) {
        $this->code = $code;
        $this->serviceReference = $serviceReference;
        $this->options = $options;
        $this->description = $description;
        $this->help = $help;
        $this->outputs = $outputs;
        $this->errorOutputs = $errorOutputs;
        $this->errorStrategy = $errorStrategy;
        $this->logLevel = $logLevel;
        $this->logErrors = $logLevel !== LogLevel::DEBUG; // @deprecated, remove me in next version
    }

    /**
     * @return string
     */
    public function getCode(): string
    {
        return $this->code;
    }

    /**
     * @return string
     */
    public function getServiceReference(): string
    {
        return $this->serviceReference;
    }

    /**
     * @return TaskInterface
     */
    public function getTask(): ?TaskInterface
    {
        return $this->task;
    }

    /**
     * @param TaskInterface $task
     */
    public function setTask(TaskInterface $task)
    {
        $this->task = $task;
    }

    /**
     * @return string
     */
    public function getDescription(): string
    {
        return $this->description;
    }

    /**
     * @return string
     */
    public function getHelp(): string
    {
        return $this->help;
    }

    /**
     * @return array
     */
    public function getOptions(): array
    {
        return $this->options;
    }

    /**
     * @param string $code
     * @param mixed  $default
     *
     * @return mixed
     */
    public function getOption($code, $default = null)
    {
        if (array_key_exists($code, $this->options)) {
            return $this->options[$code];
        }

        return $default;
    }

    /**
     * @return array
     */
    public function getOutputs(): array
    {
        return $this->outputs;
    }

    /**
     * @deprecated Use getErrorOutputs method instead
     *
     * @return array
     */
    public function getErrors(): array
    {
        @trigger_error('Deprecated method, use getErrorOutputs instead', E_USER_DEPRECATED);

        return $this->getErrorOutputs();
    }

    /**
     * @return array
     */
    public function getErrorOutputs(): array
    {
        return $this->errorOutputs;
    }

    /**
     * @return ProcessState
     */
    public function getState(): ProcessState
    {
        return $this->state;
    }

    /**
     * @param ProcessState $state
     */
    public function setState(ProcessState $state)
    {
        $this->state = $state;
    }

    /**
     * @return TaskConfiguration[]
     */
    public function getNextTasksConfigurations(): array
    {
        return $this->nextTasksConfigurations;
    }

    /**
     * @param TaskConfiguration $nextTaskConfiguration
     */
    public function addNextTaskConfiguration(TaskConfiguration $nextTaskConfiguration)
    {
        $this->nextTasksConfigurations[] = $nextTaskConfiguration;
    }

    /**
     * @return TaskConfiguration[]
     */
    public function getPreviousTasksConfigurations(): array
    {
        return $this->previousTasksConfigurations;
    }

    /**
     * @param TaskConfiguration $previousTaskConfiguration
     */
    public function addPreviousTaskConfiguration(TaskConfiguration $previousTaskConfiguration)
    {
        $this->previousTasksConfigurations[] = $previousTaskConfiguration;
    }

    /**
     * @return TaskConfiguration[]
     */
    public function getErrorTasksConfigurations(): array
    {
        return $this->errorTasksConfigurations;
    }

    /**
     * @param TaskConfiguration $errorTaskConfiguration
     */
    public function addErrorTaskConfiguration(TaskConfiguration $errorTaskConfiguration)
    {
        $this->errorTasksConfigurations[] = $errorTaskConfiguration;
    }

    /**
     * @return bool
     */
    public function isInErrorBranch(): bool
    {
        return $this->inErrorBranch;
    }

    /**
     * @param bool $inErrorBranch
     */
    public function setInErrorBranch(bool $inErrorBranch)
    {
        $this->inErrorBranch = $inErrorBranch;
    }

    /**
     * @return bool
     */
    public function isRoot(): bool
    {
        return empty($this->getPreviousTasksConfigurations()) && !$this->isInErrorBranch();
    }

    /**
     * Check task ancestors to find if it have a given task as parent
     *
     * @param TaskConfiguration $taskConfig
     *
     * @return bool
     */
    public function hasAncestor(TaskConfiguration $taskConfig)
    {
        foreach ($this->getPreviousTasksConfigurations() as $previousTaskConfig) {
            // Avoid errors for direct ancestors
            if ($this->getCode() === $previousTaskConfig->getCode()) {
                continue;
            }

            if ($previousTaskConfig->getCode() === $taskConfig->getCode()) {
                return true;
            }

            if ($previousTaskConfig->hasAncestor($taskConfig)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check task ancestors to find if it have a given task as child
     *
     * @param TaskConfiguration $taskConfig
     * @param bool              $checkErrors
     *
     * @return bool
     */
    public function hasDescendant(TaskConfiguration $taskConfig, $checkErrors = true)
    {
        foreach ($this->getNextTasksConfigurations() as $nextTaskConfig) {
            // Avoid errors for direct descendant
            if ($this->getCode() === $nextTaskConfig->getCode()) {
                continue;
            }

            if ($nextTaskConfig->getCode() === $taskConfig->getCode()) {
                return true;
            }

            if ($nextTaskConfig->hasDescendant($taskConfig, $checkErrors)) {
                return true;
            }
        }

        if ($checkErrors) {
            foreach ($this->getErrorTasksConfigurations() as $errorTasksConfig) {
                // Avoid errors for direct error descendant
                if ($this->getCode() === $errorTasksConfig->getCode()) {
                    continue;
                }

                if ($errorTasksConfig->getCode() === $taskConfig->getCode()) {
                    return true;
                }

                if ($errorTasksConfig->hasDescendant($taskConfig, $checkErrors)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @return string
     */
    public function getErrorStrategy(): string
    {
        return $this->errorStrategy;
    }

    /**
     * @return string
     */
    public function getLogLevel(): string
    {
        return $this->logLevel;
    }

    /**
     * @deprecated Use getLogLevel instead
     *
     * @return bool
     */
    public function isLogErrors(): bool
    {
        @trigger_error('Deprecated method, use getLogLevel instead', E_USER_DEPRECATED);

        return $this->logErrors;
    }
}


<?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\Registry\TransformerRegistry;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Applies transformers to each element of an array
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ArrayMapTransformer implements ConfigurableTransformerInterface
{
    /** @var TransformerRegistry */
    protected $transformerRegistry;

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

    /**
     * Must return the transformed $value
     *
     * @param array $values
     * @param array $options
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \UnexpectedValueException
     * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException
     *
     * @return mixed $value
     */
    public function transform($values, array $options = [])
    {
        if (!\is_array($values) && !$values instanceof \Traversable) {
            throw new \UnexpectedValueException('Input value must be an array or traversable');
        }

        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);
        /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
        $options = $resolver->resolve($options);
        /** @var array $transformers */
        $transformers = $options['transformers'];

        $results = [];
        /** @noinspection ForeachSourceInspection */
        foreach ($values as $key => $item) {
            foreach ($transformers as $transformerCode => $transformerOptions) {
                if (null === $transformerOptions) {
                    $transformerOptions = [];
                }
                $transformer = $this->transformerRegistry->getTransformer($transformerCode);
                $item = $transformer->transform($item, $transformerOptions);
            }
            if (null === $item && $options['skip_null']) {
                continue;
            }
            $results[$key] = $item;
        }

        return $results;
    }

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

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'transformers',
            ]
        );
        $resolver->setAllowedTypes('transformers', ['array']);
        $resolver->setDefaults([
            'skip_null' => false,
        ]);
        $resolver->setAllowedTypes('skip_null', ['boolean']);
        /** @noinspection PhpUnusedParameterInspection */
        $resolver->setNormalizer(
            'transformers',
            function (Options $options, $transformers) {
                /** @var array $transformers */
                foreach ($transformers as $transformerCode => &$transformerOptions) {
                    $transformerOptionsResolver = new OptionsResolver();
                    /** @noinspection ExceptionsAnnotatingAndHandlingInspection */// @todo remove me sometimes
                    $transformer = $this->transformerRegistry->getTransformer($transformerCode);
                    if ($transformer instanceof ConfigurableTransformerInterface) {
                        $transformer->configureOptions($transformerOptionsResolver);
                        $transformerOptions = $transformerOptionsResolver->resolve(
                            $transformerOptions ?? []
                        );
                    }
                }

                return $transformers;
            }
        );
    }
}


<?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\CsvFile;
use CleverAge\ProcessBundle\Model\ProcessState;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Reads the file path from configuration and iterates over it
 * Ignores any input
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
abstract class AbstractCsvTask extends AbstractCsvResourceTask
{
    /**
     * @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 CsvFile(
            $options['file_path'],
            $options['delimiter'],
            $options['enclosure'],
            $options['escape'],
            $headers,
            $options['mode']
        );
    }

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


<?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;

/**
 * Return the first element of an array
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ArrayFirstTransformer implements ConfigurableTransformerInterface
{
    /**
     * Must return the transformed $value
     *
     * @param mixed $value
     * @param array $options
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     *
     * @return mixed $value
     */
    public function transform($value, array $options = [])
    {
        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);
        $options = $resolver->resolve($options);

        if ($options['allow_not_iterable'] && !is_iterable($value)) {
            return $value;
        }

        return reset($value);
    }

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

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(
            [
                'allow_not_iterable' => false,
            ]
        );
    }
}


<?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\Model;

use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;

/**
 * Used to pass information between tasks
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ProcessState
{
    public const STATUS = [self::STATUS_NEW, self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_RESOLVED];
    public const STATUS_NEW = 'new';
    public const STATUS_PENDING = 'pending';
    public const STATUS_PROCESSING = 'processing';
    public const STATUS_RESOLVED = 'resolved';

    /** @var ProcessConfiguration */
    protected $processConfiguration;

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

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

    /** @var mixed */
    protected $input;

    /** @var mixed */
    protected $output;

    /** @var mixed */
    protected $errorOutput;

    /** @var bool */
    protected $stopped = false;

    /** @var \Throwable */
    protected $exception;

    /** @var array */
    protected $errorContext = [];

    /** @var int */
    protected $returnCode;

    /** @var bool */
    protected $skipped;

    /** @var array */
    protected $context;

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

    /** @var array */
    protected $contextualizedOptions;

    /** @var ProcessState|null */
    protected $previousState;

    /** @var string */
    protected $status = self::STATUS_NEW;

    /**
     * @param ProcessConfiguration $processConfiguration
     * @param ProcessHistory       $processHistory
     */
    public function __construct(ProcessConfiguration $processConfiguration, ProcessHistory $processHistory)
    {
        $this->processConfiguration = $processConfiguration;
        $this->processHistory = $processHistory;
    }

    /**
     * @param ContextualOptionResolver $contextualOptionResolver
     */
    public function setContextualOptionResolver(ContextualOptionResolver $contextualOptionResolver): void
    {
        $this->contextualOptionResolver = $contextualOptionResolver;
    }

    /**
     * Clone the current object and keep a back reference
     *
     * @return ProcessState
     */
    public function duplicate()
    {
        $newState = clone $this;
        $newState->setPreviousState($this);

        return $newState;
    }

    /**
     * Reset the state object
     * To be used before execution
     *
     * @param bool $cleanInput
     */
    public function reset($cleanInput)
    {
        $this->setOutput(null);
        $this->setSkipped(false);
        $this->setException(null);
        $this->setErrorOutput(null);

        if($cleanInput) {
            $this->setInput(null);
            $this->setPreviousState(null);
        }
    }

    /**
     * @return ProcessConfiguration
     */
    public function getProcessConfiguration()
    {
        return $this->processConfiguration;
    }

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

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

    /**
     * @param TaskConfiguration $taskConfiguration
     */
    public function setTaskConfiguration(TaskConfiguration $taskConfiguration)
    {
        $this->taskConfiguration = $taskConfiguration;
    }

    /**
     * @return mixed
     */
    public function getInput()
    {
        return $this->input;
    }

    /**
     * @param mixed $input
     */
    public function setInput($input)
    {
        $this->input = $input;
    }

    /**
     * @return mixed
     */
    public function getOutput()
    {
        return $this->output;
    }

    /**
     * @param mixed $output
     */
    public function setOutput($output)
    {
        $this->output = $output;
    }

    /**
     * @deprecated Use getErrorOutput instead
     *
     * @return mixed
     */
    public function getError()
    {
        @trigger_error('Deprecated method, use getErrorOutput instead', E_USER_DEPRECATED);

        return $this->getErrorOutput();
    }

    /**
     * @deprecated Use setErrorOutput instead
     *
     * @param mixed $error
     */
    public function setError($error)
    {
        @trigger_error('Deprecated method, use setErrorOutput instead', E_USER_DEPRECATED);

        $this->setErrorOutput($error);
    }

    /**
     * @deprecated Use hasErrorOutput instead
     *
     * @return bool
     */
    public function hasError()
    {
        @trigger_error('Deprecated method, use hasErrorOutput instead', E_USER_DEPRECATED);

        return $this->hasErrorOutput();
    }

    /**
     * @return mixed
     */
    public function getErrorOutput()
    {
        return $this->errorOutput;
    }

    /**
     * @param mixed $errorOutput
     */
    public function setErrorOutput($errorOutput)
    {
        $this->errorOutput = $errorOutput;
    }

    /**
     * @return bool
     */
    public function hasErrorOutput()
    {
        return null !== $this->errorOutput;
    }

    /**
     * @param \Throwable $e
     */
    public function stop(\Throwable $e = null)
    {
        if ($e) {
            $this->setException($e);
        }
        $this->setStopped(true);
    }

    /**
     * @return boolean
     */
    public function isStopped(): bool
    {
        return $this->stopped;
    }

    /**
     * @param boolean $stopped
     */
    public function setStopped(bool $stopped)
    {
        $this->stopped = $stopped;
    }

    /**
     * @return \Throwable
     */
    public function getException()
    {
        return $this->exception;
    }

    /**
     * @param \Throwable|null $exception
     */
    public function setException(\Throwable $exception = null)
    {
        $this->exception = $exception;
    }

    /**
     * @return array
     */
    public function getErrorContext()
    {
        return $this->errorContext;
    }

    /**
     * @param array $errorContext
     */
    public function setErrorContext(array $errorContext)
    {
        $this->errorContext = $errorContext;
    }

    /**
     * @param string|int       $key
     * @param string|int|array $value
     */
    public function addErrorContextValue($key, $value)
    {
        $this->errorContext[$key] = $value;
    }

    /**
     * @param string|int $key
     */
    public function removeErrorContext($key)
    {
        unset($this->errorContext[$key]);
    }

    /**
     * @return int
     */
    public function getReturnCode()
    {
        if (null !== $this->returnCode) {
            return $this->returnCode;
        }

        return 0;
    }

    /**
     * @param int $returnCode
     */
    public function setReturnCode(int $returnCode)
    {
        $this->returnCode = $returnCode;
    }

    /**
     * @return bool
     */
    public function isSkipped()
    {
        return $this->skipped;
    }

    /**
     * @param bool $skipped
     */
    public function setSkipped(bool $skipped)
    {
        $this->skipped = $skipped;
    }

    /**
     * @return ProcessState
     */
    public function getPreviousState()
    {
        return $this->previousState;
    }

    /**
     * @param ProcessState $previousState
     */
    public function setPreviousState($previousState)
    {
        $this->previousState = $previousState;
    }

    /**
     * @return string
     */
    public function getStatus(): string
    {
        return $this->status;
    }

    /**
     * @param string $status
     *
     * @throws \UnexpectedValueException
     */
    public function setStatus(string $status)
    {
        if (!\in_array($status, self::STATUS, true)) {
            throw new \UnexpectedValueException("Unknown status {$status}");
        }

        $this->status = $status;
    }

    /**
     * @return bool
     */
    public function isResolved()
    {
        return $this->status === self::STATUS_RESOLVED;
    }

    /**
     * @return array
     */
    public function getContext(): array
    {
        return $this->context;
    }

    /**
     * @param array $context
     *
     * @throws \RuntimeException
     */
    public function setContext(array $context): void
    {
        if ($this->context) {
            throw new \RuntimeException('Once defined, context is immutable');
        }

        $this->context = $context;
    }

    /**
     * @return array
     */
    public function getContextualizedOptions()
    {
        if (!$this->contextualizedOptions) {
            $options = $this->getTaskConfiguration()->getOptions();
            $this->contextualizedOptions = $this->contextualOptionResolver->contextualizeOptions(
                $options,
                $this->context
            );
        }

        return $this->contextualizedOptions;
    }

    /**
     * @param string $code
     * @param mixed  $default
     *
     * @return mixed
     */
    public function getContextualizedOption($code, $default = null)
    {
        $contextualizedOptions = $this->getContextualizedOptions();
        if (array_key_exists($code, $contextualizedOptions)) {
            return $contextualizedOptions[$code];
        }

        return $default;
    }

    /**
     * @deprecated Use monolog processors instead
     *
     * @return array
     */
    public function getLogContext()
    {
        @trigger_error('Deprecated method, use monolog processors instead', E_USER_DEPRECATED);
        $context = [
            'process_id' => $this->processHistory->getId(),
            'process_code' => $this->processConfiguration->getCode(),
            'process_context' => $this->context,
            'task_code' => $this->taskConfiguration->getCode(),
            'task_service' => $this->taskConfiguration->getServiceReference(),
        ];

        if ($this->hasErrorOutput()) {
            $context['error'] = $this->getErrorOutput();
        }

        if ($this->exception) {
            $context['exception'] = $this->exception;
        }

        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\Doctrine;

use CleverAge\ProcessBundle\Model\ProcessState;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Persists and flush Doctrine entities
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class DoctrineWriterTask extends AbstractDoctrineTask
{
    /**
     * @param ProcessState $state
     *
     * @throws \Doctrine\ORM\ORMException
     */
    public function execute(ProcessState $state)
    {
        $state->setOutput($this->writeEntity($state));
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \UnexpectedValueException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        parent::configureOptions($resolver);
        $resolver->setDefaults(
            [
                'global_flush' => true,
            ]
        );
        $resolver->setAllowedTypes('global_flush', ['boolean']);
    }

    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Doctrine\ORM\ORMException
     *
     * @return mixed
     */
    protected function writeEntity(ProcessState $state)
    {
        $options = $this->getOptions($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->persist($entity);

        if ($options['global_flush']) {
            $entityManager->flush();
        } else {
            if (!$entityManager instanceof EntityManager) {
                throw new \UnexpectedValueException("Manager for class {$class} does not support unitary flush");
            }
            $entityManager->flush($entity);
        }

        return $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\Transformer;

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

/**
 * Unset a given property
 */
class UnsetTransformer implements ConfigurableTransformerInterface
{
    use ConditionTrait;

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

    /**
     * {@inheritdoc}
     */
    public function transform($value, array $options = [])
    {
        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);
        $options = $resolver->resolve($options);

        if (!\is_array($value)) {
            throw new \UnexpectedValueException('Given value must be an array');
        }

        if (!array_key_exists($options['property'], $value)) {
            throw new \UnexpectedValueException("Property {$options['property']} does not exists");
        }

        if ($this->checkCondition($value, $options['condition'])) {
            unset($value[$options['property']]);
        }

        return $value;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired('property');
        $resolver->setAllowedTypes('property', 'string');

        $this->configureWrappedConditionOptions('condition', $resolver);
    }

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


<?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;

/**
 * Configurable set of conditions to use in tasks or transformers options
 */
trait ConditionTrait
{
    /** @var PropertyAccessorInterface */
    protected $accessor;

    /**
     * Test the input with the given set of conditions
     * True by default
     *
     * @param mixed $input
     * @param array $conditions
     *
     * @return bool
     */
    protected function checkCondition($input, $conditions)
    {
        foreach ($conditions['match'] as $key => $value) {
            if (!$this->checkValue($input, $key, $value)) {
                return false;
            }
        }

        foreach ($conditions['empty'] as $key => $value) {
            if (!$this->checkEmpty($input, $key)) {
                return false;
            }
        }

        foreach ($conditions['match_regexp'] as $key => $value) {
            if (!$this->checkValue($input, $key, $value, true, true)) {
                return false;
            }
        }

        foreach ($conditions['not_match'] as $key => $value) {
            if (!$this->checkValue($input, $key, $value, false)) {
                return false;
            }
        }

        foreach ($conditions['not_empty'] as $key => $value) {
            if ($this->checkEmpty($input, $key)) {
                return false;
            }
        }

        foreach ($conditions['not_match_regexp'] as $key => $value) {
            if (!$this->checkValue($input, $key, $value, false, true)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Configure available condition rules in a wrapper option
     *
     * @param string          $wrapperKey
     * @param OptionsResolver $resolver
     */
    protected function configureWrappedConditionOptions(string $wrapperKey, OptionsResolver $resolver)
    {
        $resolver->setDefault($wrapperKey, []);
        $resolver->setAllowedTypes($wrapperKey, ['array']);
        $resolver->setNormalizer($wrapperKey, function (OptionsResolver $options, $value) {
            $conditionResolver = new OptionsResolver();
            $this->configureConditionOptions($conditionResolver);

            return $conditionResolver->resolve($value);
        });
    }

    /**
     * Configure available condition rules
     *
     * @param OptionsResolver $resolver
     */
    protected function configureConditionOptions(OptionsResolver $resolver)
    {
        $resolver->setDefault('not_match', []);
        $resolver->setDefault('match', []);
        $resolver->setDefault('not_empty', []);
        $resolver->setDefault('empty', []);
        $resolver->setDefault('not_match_regexp', []);
        $resolver->setDefault('match_regexp', []);
        $resolver->setAllowedTypes('not_match', 'array');
        $resolver->setAllowedTypes('match', 'array');
        $resolver->setAllowedTypes('not_match_regexp', 'array');
        $resolver->setAllowedTypes('match_regexp', 'array');
    }

    /**
     * Softly check if an input key match a value, or not
     *
     * @param object|array $input
     * @param string       $key
     * @param mixed        $value
     * @param bool         $shouldMatch
     * @param bool         $regexpMode
     *
     * @throws \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException
     * @throws \Symfony\Component\PropertyAccess\Exception\AccessException
     * @throws \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException
     *
     * @return bool
     */
    protected function checkValue($input, $key, $value, $shouldMatch = true, $regexpMode = false)
    {
        $currentValue = $this->getValue($input, $key);

        /** @noinspection TypeUnsafeComparisonInspection */
        if ($shouldMatch && !$regexpMode && $currentValue != $value) {
            return false;
        }

        /** @noinspection TypeUnsafeComparisonInspection */
        if (!$shouldMatch && !$regexpMode && $currentValue == $value) {
            return false;
        }

        if ($regexpMode) {
            $pregMatch = preg_match($value, $currentValue);

            if ($shouldMatch && (false === $pregMatch || 0 === $pregMatch)) {
                return false;
            }

            if (!$shouldMatch && (false === $pregMatch || $pregMatch > 0)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if the input property is empty or not
     *
     * @param array|object $input
     * @param string       $key
     *
     * @return bool
     */
    protected function checkEmpty($input, $key)
    {
        $currentValue = $this->getValue($input, $key);

        return empty($currentValue);
    }

    /**
     * Soft value getter (return the value or null)
     *
     * @param array|object $input
     * @param string       $key
     *
     * @return mixed|null
     */
    protected function getValue($input, $key)
    {
        if ('' === $key) {
            $currentValue = $input;
        } elseif ($this->accessor->isReadable($input, $key)) {
            $currentValue = $this->accessor->getValue($input, $key);
        } else {
            $currentValue = null;
        }

        return $currentValue;
    }
}


<?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\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessState;
use Psr\Log\LoggerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
 * Get a property on the input and return it with PropertyAccessor
 *
 * @author Corentin Bouix <cbouix@clever-age.com>
 */
class PropertyGetterTask extends AbstractConfigurableTask
{
    /** @var LoggerInterface */
    protected $logger;

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

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

    /**
     * @param ProcessState $state
     *
     * @throws \Exception
     */
    public function execute(ProcessState $state)
    {
        $options = $this->getOptions($state);
        $input = $state->getInput();
        $property = $options['property'];

        try {
            $output = $this->accessor->getValue($input, $property);
        } catch (\Exception $e) {
            $state->addErrorContextValue('property', $property);
            $state->setException($e);

            return;
        }

        $state->setOutput($output);
    }

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


<?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\Database;

use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessState;
use Doctrine\Common\Persistence\ManagerRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Fetch entities from doctrine
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class DatabaseUpdaterTask extends AbstractConfigurableTask
{
    /** @var ManagerRegistry */
    protected $doctrine;

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

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

    /**
     * @param ProcessState $state
     *
     * @throws \InvalidArgumentException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Doctrine\DBAL\DBALException
     */
    public function execute(ProcessState $state)
    {
        $statement = $this->initializeStatement($state);

        if (false === $statement->execute()) {
            throw new \RuntimeException("Error while executing query: {$statement->errorInfo()}");
        }
    }

    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \InvalidArgumentException
     * @throws \Doctrine\DBAL\DBALException
     *
     * @return \Doctrine\DBAL\Driver\Statement
     */
    protected function initializeStatement(ProcessState $state)
    {
        $connection = $this->getConnection($state);

        return $connection->executeQuery($this->getOption($state, 'sql'));
    }

    /**
     * {@inheritdoc}
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'sql',
            ]
        );
        $resolver->setAllowedTypes('sql', ['string']);
        $resolver->setDefaults(
            [
                'connection' => null,
            ]
        );
        $resolver->setAllowedTypes('connection', ['NULL', 'string']);
    }

    /**
     * @param ProcessState $state
     *
     * @throws \InvalidArgumentException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     *
     * @return \Doctrine\DBAL\Connection
     */
    protected function getConnection(ProcessState $state)
    {
        /** @noinspection PhpIncompatibleReturnTypeInspection */

        return $this->doctrine->getConnection($this->getOption($state, 'connection'));
    }
}


<?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\Process;

use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Manager\ProcessManager;
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\Exception\InvalidConfigurationException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * Execute one or many processes while chaining inputs in a iterable way
 */
class ProcessExecutorTask extends AbstractConfigurableTask
{
    /** @var ProcessManager */
    protected $processManager;

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

    /** @var array */
    protected $process;

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

    /**
     * @param ProcessManager               $processManager
     * @param ProcessConfigurationRegistry $processRegistry
     * @param LoggerInterface              $logger
     */
    public function __construct(
        ProcessManager $processManager,
        ProcessConfigurationRegistry $processRegistry,
        LoggerInterface $logger
    ) {
        $this->processManager = $processManager;
        $this->processRegistry = $processRegistry;
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     *
     * @throws \InvalidArgumentException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Exception
     */
    public function execute(ProcessState $state)
    {
        $input = $state->getInput();
        $process = $this->getOption($state, 'process');

        $output = $this->processManager->execute(
            $process,
            $input,
            $this->getOption($state, 'context')
        );
        $state->setOutput($output);
    }

    /**
     * {@inheritdoc}
     *
     * @throws \InvalidArgumentException
     */
    public function initialize(ProcessState $state)
    {
        parent::initialize($state);
        $this->process = $this->getOption($state, 'process');
    }

    /**
     * {@inheritdoc}
     *
     * @throws \Symfony\Component\Form\Exception\InvalidConfigurationException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired('process');
        $resolver->setDefaults(
            [
                'context' => [],
            ]
        );
        $resolver->addAllowedTypes('process', 'string');
        $resolver->setAllowedTypes('context', ['array']);
        $resolver->setNormalizer(
            'process',
            function (Options $options, $processCode) {
                if (!$this->processRegistry->hasProcessConfiguration($processCode)) {
                    throw new InvalidConfigurationException("Unknown process {$processCode}");
                }

                return $processCode;
            }
        );
    }
}


<?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\Debug;

use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Model\TaskInterface;

/**
 * This is a dummy task mostly intended for testing purpose.
 * Forward any input to the error output
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ErrorForwarderTask implements TaskInterface
{
    /**
     * {@inheritdoc}
     */
    public function execute(ProcessState $state)
    {
        $state->setSkipped(true);
        $state->setErrorOutput($state->getInput());
    }
}


<?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\Process;

use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\FlushableTaskInterface;
use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
use Psr\Log\LoggerInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;

/**
 * Launch a new process for each input received, input must be a scalar, a resource or a \Traversable
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ProcessLauncherTask extends AbstractConfigurableTask implements FlushableTaskInterface
{
    /** @var LoggerInterface */
    protected $logger;

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

    /** @var KernelInterface */
    protected $kernel;

    /** @var Process[] */
    protected $launchedProcesses = [];

    /**
     * @param LoggerInterface              $logger
     * @param ProcessConfigurationRegistry $processRegistry
     * @param KernelInterface              $kernel
     */
    public function __construct(
        LoggerInterface $logger,
        ProcessConfigurationRegistry $processRegistry,
        KernelInterface $kernel
    ) {
        $this->logger = $logger;
        $this->processRegistry = $processRegistry;
        $this->kernel = $kernel;
    }

    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\Process\Exception\LogicException
     * @throws \Symfony\Component\Process\Exception\RuntimeException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Symfony\Component\Filesystem\Exception\IOException
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
     */
    public function execute(ProcessState $state)
    {
        $this->handleProcesses($state); // Handler processes first

        $options = $this->getOptions($state);
        while (\count($this->launchedProcesses) >= $options['max_processes']) {
            $this->handleProcesses($state);
            sleep($options['sleep_interval']);
        }

        $process = $this->launchProcess($state);
        $this->launchedProcesses[] = $process;

        $logContext = [
            'input' => $process->getInput(),
        ];

        $this->logger->debug("Running command: {$process->getCommandLine()}", $logContext);

        sleep($options['sleep_interval_after_launch']);
    }

    /**
     * @param ProcessState $state
     *
     * @throws \InvalidArgumentException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Symfony\Component\Process\Exception\RuntimeException
     */
    public function flush(ProcessState $state)
    {
        while (\count($this->launchedProcesses) > 0) {
            $this->handleProcesses($state);
            sleep($this->getOption($state, 'sleep_on_finalize_interval'));
        }

        $state->setSkipped(true);
    }

    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\Process\Exception\LogicException
     * @throws \Symfony\Component\Process\Exception\RuntimeException
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     * @throws \Symfony\Component\Filesystem\Exception\IOException
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     * @throws \Symfony\Component\Process\Exception\InvalidArgumentException
     *
     * @return \Symfony\Component\Process\Process
     */
    protected function launchProcess(ProcessState $state)
    {
        $pathFinder = new PhpExecutableFinder();
        $consolePath = $this->kernel->getRootDir().'/../bin/console';
        $logDir = $this->kernel->getLogDir().'/process';
        $processCode = $this->getOption($state, 'process');
        $processOptions = $this->getOption($state, 'process_options');

        $fs = new Filesystem();
        $fs->mkdir($logDir);
        if (!$fs->exists($consolePath)) {
            throw new \RuntimeException("Unable to resolve path to symfony console '{$consolePath}'");
        }

        $arguments = [
            'nohup',
            $pathFinder->find(),
            $consolePath,
            '--env='.$this->kernel->getEnvironment(),
            'cleverage:process:execute',
            '--input-from-stdin',
        ];

        $arguments = array_merge($arguments, $processOptions);
        $arguments[] = $processCode;

        $process = new Process($arguments, null, null, $state->getInput());
        $process->setCommandLine($process->getCommandLine());
        $process->inheritEnvironmentVariables();
        $process->enableOutput();
        $process->start();

        return $process;
    }

    /**
     * @param ProcessState $state
     *
     * @throws \Symfony\Component\Process\Exception\RuntimeException
     */
    protected function handleProcesses(ProcessState $state)
    {
        foreach ($this->launchedProcesses as $key => $process) {
            if (!$process->isTerminated()) {
                // @todo handle incremental error output properly, specially for terminal where logs are lost
                echo $process->getIncrementalErrorOutput();
                continue;
            }

            $logContext = [
                'cmd' => $process->getCommandLine(),
                'input' => $process->getInput(),
                'exit_code' => $process->getExitCode(),
                'exit_code_text' => $process->getExitCodeText(),
            ];
            $this->logger->debug('Command terminated', $logContext);

            unset($this->launchedProcesses[$key]);
            if (0 !== $process->getExitCode()) {
                $this->logger->critical($process->getErrorOutput(), $logContext);
                $this->killProcesses();

                throw new \RuntimeException("Sub-process has failed: {$process->getExitCodeText()}");
            }
        }
    }

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\AccessException
     * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException
     * @throws \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
     */
    protected function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'process',
            ]
        );
        /** @noinspection PhpUnusedParameterInspection */
        $resolver->setNormalizer(
            'process',
            function (Options $options, $value) {
                if (!$this->processRegistry->hasProcessConfiguration($value)) {
                    throw new InvalidConfigurationException("Unknown process {$value}");
                }

                return $value;
            }
        );
        $resolver->setDefaults(
            [
                'max_processes' => 3,
                'sleep_interval' => 1,
                'sleep_interval_after_launch' => 1,
                'sleep_on_finalize_interval' => 1,
                'process_options' => [],
            ]
        );
        $resolver->setAllowedTypes('max_processes', ['integer']);
        $resolver->setAllowedTypes('sleep_interval', ['integer']);
        $resolver->setAllowedTypes('sleep_interval_after_launch', ['integer']);
        $resolver->setAllowedTypes('process_options', ['array']);
    }

    /**
     * Kill all running processes
     */
    protected function killProcesses()
    {
        foreach ($this->launchedProcesses as $process) {
            $process->stop(5);
        }
    }
}


<?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\Logger;

use CleverAge\ProcessBundle\Manager\ProcessManager;

/**
 * @author  Madeline Veyrenc <mveyrenc@clever-age.com>
 */
class AbstractProcessor
{
    /** @var ProcessManager */
    protected $processManager;

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

    /**
     * @param array $record
     *
     * @return array
     */
    public function __invoke(array $record)
    {
        if (array_key_exists('context', $record)
            && $record['context']) {
            $record['context'] = $this->normalizeRecordData($record['context']);
        }

        $this->processManager->getProcessHistory();
        $recordExtra = array_key_exists('extra', $record) ? $record['extra'] : [];
        $this->addProcessInfoToRecord($recordExtra);
        $record['extra'] = $recordExtra;

        return $record;
    }

    /**
     * @param array $record
     *
     * @return array
     */
    protected function normalizeRecordData(array $record): array
    {
        $newRecord = [];
        foreach ($record as $recordName => $recordValue) {
            $this->addToRecord($newRecord, $recordName, $recordValue);
        }

        return $newRecord;
    }

    /**
     * @param array $record
     *
     * @return void
     */
    protected function addProcessInfoToRecord(array &$record): void
    {
        $processHistory = $this->processManager->getProcessHistory();
        if (!$processHistory) {
            return;
        }

        $this->addToRecord($record, 'process_id', $processHistory->getId());
        $this->addToRecord($record, 'process_code', $processHistory->getProcessCode());
        $this->addToRecord($record, 'process_context', $processHistory->getContext());
    }

    /**
     * @param array $record
     *
     * @return void
     */
    protected function addTaskInfoToRecord(array &$record): void
    {
        $taskConfiguration = $this->processManager->getTaskConfiguration();
        if (!$taskConfiguration) {
            return;
        }
        $this->addToRecord($record, 'task_code', $taskConfiguration->getCode());
        $this->addToRecord($record, 'task_service', $taskConfiguration->getServiceReference());

        $state = $taskConfiguration->getState();
        if (!$state) {
            return;
        }
        if ($state->hasErrorOutput()) {
            $this->addToRecord($record, 'error', $state->getErrorOutput());
        }

        if ($state->getException()) {
            $this->addToRecord($record, 'exception', $state->getException());
        }
    }

    /**
     * @param array  $record
     * @param string $name
     * @param mixed  $data
     *
     * @return void
     */
    protected function addToRecord(array &$record, $name, $data): void
    {
        $record[$name] = $data;
    }
}


<?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;

/**
 * Class PregFilterTransformer
 *
 * @author  Madeline Veyrenc <mveyrenc@clever-age.com>
 */
class PregFilterTransformer implements ConfigurableTransformerInterface
{
    /**
     * Must return the transformed $value
     *
     * @param mixed $value
     * @param array $options
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     *
     * @return mixed $value
     */
    public function transform($value, array $options = [])
    {
        $resolver = new OptionsResolver();
        $this->configureOptions($resolver);
        $options = $resolver->resolve($options);

        $pattern = $options['pattern'];
        $replacement = $options['replacement'];

        return preg_filter($pattern, $replacement, $value);
    }

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

    /**
     * @param OptionsResolver $resolver
     *
     * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(
            [
                'pattern',
                'replacement',
            ]
        );
        $resolver->setAllowedTypes('pattern', ['string', 'array']);
        $resolver->setAllowedTypes('replacement', ['string', 'array']);
    }
}


<?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\Model;

use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;

/**
 * Logs information about a process
 *
 * @author Valentin Clavreul <vclavreul@clever-age.com>
 * @author Vincent Chalnot <vchalnot@clever-age.com>
 */
class ProcessHistory
{
    public const STATE_STARTED = 'started';
    public const STATE_SUCCESS = 'success';
    public const STATE_FAILED = 'failed';

    /**
     * @var int
     */
    protected $id;

    /**
     * @var string
     */
    protected $processCode;

    /**
     * @var array
     */
    protected $context;

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

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

    /**
     * @var string
     */
    protected $state = self::STATE_STARTED;

    /**
     * @param ProcessConfiguration $processConfiguration
     * @param array                $context
     */
    public function __construct(ProcessConfiguration $processConfiguration, array $context = [])
    {
        $this->id = microtime(true);
        $this->processCode = $processConfiguration->getCode();
        $this->startDate = new \DateTime();
        $this->context = $context;
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getProcessCode(): string
    {
        return $this->processCode;
    }

    /**
     * @return array
     */
    public function getContext(): array
    {
        return $this->context;
    }

    /**
     * @return \DateTime
     */
    public function getStartDate(): \DateTime
    {
        return $this->startDate;
    }

    /**
     * @return \DateTime
     */
    public function getEndDate()
    {
        return $this->endDate;
    }

    /**
     * @return string
     */
    public function getState(): string
    {
        return $this->state;
    }

    /**
     * Set the process as failed
     */
    public function setFailed()
    {
        $this->endDate = new \DateTime();
        $this->state = self::STATE_FAILED;
    }

    /**
     * Set the process as succeded
     */
    public function setSuccess()
    {
        $this->endDate = new \DateTime();
        $this->state = self::STATE_SUCCESS;
    }

    /**
     * Is true when the process is running
     *
     * @return bool
     */
    public function isStarted()
    {
        return $this->state === self::STATE_STARTED;
    }

    /**
     * Get process duration in seconds
     *
     * @return int|null
     */
    public function getDuration()
    {
        if ($this->getEndDate()) {
            return $this->getEndDate()->getTimestamp() - $this->getStartDate()->getTimestamp();
        }

        return null;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        $reference = $this->getProcessCode().'['.$this->getState().']';
        $time = $this->getStartDate()->format(\DateTime::ATOM);

        return $reference.': '.$time;
    }
}