<?php

namespace PeskyCMF\Scaffold;

use PeskyCMF\Scaffold\DataGrid\DataGridColumn;
use PeskyCMF\Scaffold\Form\FormInput;
use PeskyCMF\Scaffold\ItemDetails\ValueCell;
use PeskyCMF\Scaffold\MenuItem\CmfMenuItem;
use PeskyCMF\Scaffold\MenuItem\CmfRedirectMenuItem;
use PeskyCMF\Scaffold\MenuItem\CmfRequestMenuItem;
use PeskyORM\ORM\Column;
use PeskyORM\ORM\Relation;
use PeskyORM\ORM\TableInterface;
use Swayok\Html\Tag;

abstract class ScaffoldSectionConfig {
    /**
     * @var TableInterface
     */
    protected $table;
    /**
     * This scaffold config uses no db columns
     * @var bool
     */
    protected $thereIsNoDbColumns = false;
    /**
     * @var array
     */
    protected $fieldsConfigs = [];
    /**
     * Fields list
     * @var array|AbstractValueViewer[]
     */
    protected $valueViewers = [];
    /**
     * @var string
     */
    protected $title;
    /**
     * @var array
     */
    protected $relationsToRead = [];
    /**
     * @var null|\Closure
     */
    protected $defaultFieldRenderer = null;
    /**
     * @var null|\Closure
     */
    protected $rawRecordDataModifier;
    /**
     * Container width (percents)
     * @var int
     */
    protected $width = 100;
    /**
     * @var \Closure|null
     */
    protected $toolbarItems = null;
    /**
     * @var array|\Closure
     */
    protected $specialConditions = [];
    /**
     * @var ScaffoldConfig
     */
    protected $scaffoldConfig;
    /**
     * @var array|\Closure
     */
    protected $dataToAddToRecord;
    /**
     * @var array|\Closure
     */
    protected $dataToSendToTemplate;
    /**
     * @var string
     */
    protected $template = null;
    /**
     * @var string
     */
    protected $jsInitiator = null;
    protected $isFinished = false;
    /** @var  bool */
    protected $allowRelationsInValueViewers = false;
    /**
     * Allow viewer names like "column_name:key_name" for json/jsonb DB columns
     * @var bool
     */
    protected $allowComplexValueViewerNames = false;
    /** @var array */
    protected $additionalColumnsToSelect = [];

    /**
     * @param TableInterface $table
     * @param ScaffoldConfig $scaffoldConfig
     * @return $this
     */
    static public function create(TableInterface $table, ScaffoldConfig $scaffoldConfig) {
        $class = get_called_class();
        return new $class($table, $scaffoldConfig);
    }

    /**
     * ScaffoldSectionConfig constructor.
     * @param TableInterface $table
     * @param ScaffoldConfig $scaffoldConfig
     */
    public function __construct(TableInterface $table, ScaffoldConfig $scaffoldConfig) {
        $this->table = $table;
        $this->scaffoldConfig = $scaffoldConfig;
    }

    /**
     * @return ScaffoldConfig
     */
    public function getScaffoldConfig() {
        return $this->scaffoldConfig;
    }

    public function setTemplate($template) {
        $this->template = $template;
        return $this;
    }

    public function getTemplate() {
        if (empty($this->template)) {
            throw new ScaffoldSectionConfigException($this, 'Scaffold section view file not set');
        }
        return $this->template;
    }

    /**
     * Translate resource-related items (column names, input labels, etc.)
     * @param AbstractValueViewer|null $viewer
     * @param string $suffix
     * @param array $parameters
     * @return string
     */
    public function translate(AbstractValueViewer $viewer = null, $suffix = '', array $parameters = []) {
        if ($viewer) {
            return $this
                ->getScaffoldConfig()
                ->translateForViewer($this->getSectionTranslationsPrefix('value_viewer'), $viewer, $suffix, $parameters);
        } else {
            return $this
                ->getScaffoldConfig()
                ->translate($this->getSectionTranslationsPrefix(), $suffix, $parameters);
        }
    }

    /**
     * Translate general UI elements (button labels, tooltips, messages, etc..)
     * @param $path
     * @param array $parameters
     * @return mixed
     */
    public function translateGeneral($path, array $parameters = []) {
        $prefix = $this->getSectionTranslationsPrefix();
        $text = $this->getScaffoldConfig()->translate($prefix, $path, $parameters);
        if (preg_match('%\.' . preg_quote($prefix . '.' . $path, '%') . '$%', $text)) {
            $text = cmfTransGeneral($this->getSectionTranslationsPrefix('general') . '.' . $path, $parameters);
        }
        return $text;
    }

    /**
     * Prefix for this section's translations (ex: 'datagrid', 'item_details', 'form')
     * Will be used in $this->translate() and $this->translateGeneral()
     * @param bool $subtype - null, 'value_viewer', 'general'.
     *      - null - common translations for section. Path will be like 'resource.{section}.translation'
     *      - 'value_viewer' - translations for value viewers labels. It is preferred to nest
     *          value viewer labels deeper in section (ex: 'datagrid.columns') or outside of section (ex: 'columns').
     *          Path will be like 'resource.{section}.value_viewers.translation'
     *      - 'general' - translations for general UI elements ($this->translateGeneral()). Used only when there is
     *          no custom translation (example path: 'resource.{section}.translation') and system needs to get
     *          translation from cmfTransGeneral(). By default it should be same as for $subtype = null
     * @return string
     */
    abstract protected function getSectionTranslationsPrefix($subtype = null);

    /**
     * Disables any usage of TableStructure (validation, automatic field type guessing, etc)
     * @return $this
     */
    public function thereIsNoDbColumns() {
        $this->thereIsNoDbColumns = true;
        return $this;
    }

    /**
     * @param array $viewers
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \BadMethodCallException
     * @throws \PeskyCMF\Scaffold\ScaffoldException
     * @throws \InvalidArgumentException
     */
    protected function setValueViewers(array $viewers) {
        /** @var AbstractValueViewer|null $config */
        foreach ($viewers as $name => $config) {
            $valueConverter = null;
            if (is_int($name)) {
                $name = $config;
                $config = null;
            } else if ($config instanceof \Closure) {
                $valueConverter = $config;
                $config = null;
            }
            $this->addValueViewer($name, $config);
            if (!empty($valueConverter)) {
                $this->getValueViewer($name)->setValueConverter($valueConverter);
            }
        }
        return $this;
    }

    /**
     * @return AbstractValueViewer[]
     */
    public function getValueViewers() {
        return $this->valueViewers;
    }

    /**
     * Get only viewers that are linked to table columns defined in TableConfig class ($valueViewer->isDbColumn() === true)
     * @param bool $includeViewersForRelations - false: only vievers linked to main table's columns will be returned
     * @return AbstractValueViewer[]|DataGridColumn[]|FormInput[]|ValueCell[]
     */
    public function getViewersLinkedToDbColumns($includeViewersForRelations = false) {
        $ret = [];
        foreach ($this->getValueViewers() as $key => $viewer) {
            if ($viewer->isLinkedToDbColumn() && ($includeViewersForRelations || !$viewer->hasRelation())) {
                $ret[$key] = $viewer;
            }
        }
        return $ret;
    }

    /**
     * Get only viewers that are not linked to columns defined in TableConfig class ($valueViewer->isDbColumn() === false)
     * @return AbstractValueViewer[]|DataGridColumn[]|ValueCell[]|FormInput[]
     */
    public function getStandaloneViewers() {
        $ret = [];
        foreach ($this->getValueViewers() as $key => $viewer) {
            if (!$viewer->isLinkedToDbColumn()) {
                $ret[$key] = $viewer;
            }
        }
        return $ret;
    }

    /**
     * Get only viewers that are linked to main table's relation
     * @return AbstractValueViewer[]|DataGridColumn[]|ValueCell[]|FormInput[]
     */
    public function getViewersForRelations() {
        $ret = [];
        foreach ($this->getValueViewers() as $key => $viewer) {
            if ($viewer->isLinkedToDbColumn() && $viewer->hasRelation()) {
                $ret[$key] = $viewer;
            }
        }
        return $ret;
    }

    /**
     * @return AbstractValueViewer
     */
    abstract public function createValueViewer();

    /**
     * @param string $name
     * @return DataGridColumn|ValueCell|FormInput|AbstractValueViewer
     * @throws \InvalidArgumentException
     */
    public function getValueViewer($name) {
        if (!$this->hasValueViewer($name)) {
            throw new \InvalidArgumentException('Scaffold ' . get_class($this) . " has no viewer with name [$name]");
        }
        return $this->valueViewers[$name];
    }

    /**
     * @param string $name
     * @return bool
     */
    public function hasValueViewer($name) {
        return !empty($name) && !empty($this->valueViewers[$name]);
    }

    /**
     * @param string $name
     * @param null|AbstractValueViewer $viewer
     * @return $this
     * @throws \UnexpectedValueException
     * @throws \BadMethodCallException
     * @throws \InvalidArgumentException
     */
    public function addValueViewer($name, AbstractValueViewer $viewer = null) {
        $usesRelation = false;
        if (
            !$this->thereIsNoDbColumns
            && (!$viewer || $viewer->isLinkedToDbColumn())
            && !$this->getTable()->getTableStructure()->hasColumn($name)
            && !$this->isValidComplexValueViewerName($name) //< $name is something like "column_name:key_name"
        ) {
            if ($this->allowRelationsInValueViewers) {
                list($relation, $relationColumnName) = $this->validateRelationValueViewerName($name);
                $usesRelation = true;
            } else {
                throw new \InvalidArgumentException("Table {$this->getTable()->getName()} has no column '{$name}'");
            }
        }
        if (empty($viewer)) {
            $viewer = $this->createValueViewer();
        }
        if ($this->thereIsNoDbColumns) {
            $viewer->setIsLinkedToDbColumn(false);
        }
        $viewer->setName($name);
        $viewer->setPosition($this->getNextValueViewerPosition($viewer));
        $viewer->setScaffoldSectionConfig($this);
        if ($usesRelation) {
            $viewer->setRelation($relation, $relationColumnName);
        }
        $this->valueViewers[$name] = $viewer;
        return $this;
    }

    /**
     * @param $name
     * @return array - array(Relation, column_name)
     * @throws \UnexpectedValueException
     * @throws \BadMethodCallException
     * @throws \InvalidArgumentException
     */
    protected function validateRelationValueViewerName($name) {
        $nameParts = explode('.', $name);
        $hasRelation = $this->getTable()->getTableStructure()->hasRelation($nameParts[0]);
        if (!$hasRelation && count($nameParts) === 1) {
            throw new \InvalidArgumentException("Table {$this->getTable()->getName()} has no column '{$name}'");
        } else if (!$hasRelation) {
            throw new \InvalidArgumentException("Table {$this->getTable()->getName()} has no relation '{$nameParts[0]}'");
        }
        $relation = $this->getTable()->getTableStructure()->getRelation($nameParts[0]);
        if (count($nameParts) === 1) {
            $columnName = $relation->getForeignColumnName();
        } else {
            $columnName = end($nameParts); //< allows something like RelationName.i.column_name
            if (!$relation->getForeignTable()->getTableStructure()->hasColumn($columnName)) {
                throw new \InvalidArgumentException(
                    "Relation {$nameParts[0]} of table {$this->getTable()->getName()} has no column '{$columnName}'"
                );
            }
        }
        return [$relation, $columnName];
    }

    /**
     * Check if $name is complex column name like "column_name:key_name"
     * @param string $name
     * @return bool
     */
    protected function isValidComplexValueViewerName($name) {
        if ($this->allowComplexValueViewerNames && AbstractValueViewer::isComplexViewerName($name)) {
            list($colName, ) = AbstractValueViewer::splitComplexViewerName($name);
            if ($this->getTable()->getTableStructure()->hasColumn($colName)) {
                $type = $this->getTable()->getTableStructure()->getColumn($colName)->getType();
                return in_array($type, [Column::TYPE_JSON, Column::TYPE_JSONB], true);
            }
        }
        return false;
    }

    /**
     * @param AbstractValueViewer $viewer
     * @return int
     */
    protected function getNextValueViewerPosition(AbstractValueViewer $viewer) {
        return count($this->valueViewers);
    }

    /**
     * @return TableInterface
     */
    public function getTable() {
        return $this->table;
    }

    /**
     * @param string $default
     * @return string
     */
    public function getTitle($default = '') {
        return empty($this->title) ? $default : $this->title;
    }

    /**
     * @param string $title
     * @return $this
     */
    public function setTitle($title) {
        $this->title = $title;
        return $this;
    }

    /**
     * @param array $record
     * @return array
     * @throws \UnexpectedValueException
     */
    protected function modifyRawRecordData(array $record) {
        if ($this->rawRecordDataModifier !== null) {
            $record = call_user_func($this->rawRecordDataModifier, $record, $this);
            if (!is_array($record)) {
                throw new \UnexpectedValueException('modifyRawDataRecord must return array');
            }
        }
        return $record;
    }

    /**
     * Modify record's data before it is processed by ScaffoldSectionConfig->prepareRecord()
     * Must return array
     * @param \Closure $modifier - function (array $record, ScaffoldSectionConfig $sectionConfig) { return $record }
     * @return $this
     */
    public function setRawRecordDataModifier(\Closure $modifier) {
        $this->rawRecordDataModifier = $modifier;
        return $this;
    }

    /**
     * @param array $record
     * @param array $virtualColumns - list of columns that are provided in TableStructure but marked as not existing in DB
     * @return array
     * @throws \PeskyORM\Exception\InvalidDataException
     * @throws \PDOException
     * @throws \PeskyORM\Exception\OrmException
     * @throws \UnexpectedValueException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     * @throws \PeskyCMF\Scaffold\ValueViewerConfigException
     */
    public function prepareRecord(array $record, array $virtualColumns = []) {
        $pkKey = $this->getTable()->getPkColumnName();
        /** @noinspection UnnecessaryParenthesesInspection */
        $permissionsAndServiceData = [
            '___create_allowed' => $this->isCreateAllowed(),
            '___delete_allowed' => (
                $this->isDeleteAllowed()
                && $this->getScaffoldConfig()->isRecordDeleteAllowed($record)
            ),
            '___edit_allowed' => (
                $this->isEditAllowed()
                && $this->getScaffoldConfig()->isRecordEditAllowed($record)
            ),
            '___details_allowed' => (
                $this->isDetailsViewerAllowed()
                && $this->getScaffoldConfig()->isRecordDetailsAllowed($record)
            ),
            '___cloning_allowed' => $this->isCloningAllowed(),
            '__modal' => $this->isUsingDialog(),
            'DT_RowId' => 'item-' . preg_replace('%[^a-zA-Z0-9_-]+%', '-', $record[$pkKey]),
            '___pk_value' => $record[$pkKey]
        ];
        if (!empty($virtualColumns)) {
            $recordObj = $this->getTable()->newRecord()->enableTrustModeForDbData()->fromDbData($record);
            foreach ($virtualColumns as $virtualColumn) {
                $record[$virtualColumn] = $recordObj->getValue($virtualColumn);
            }
        }
        $record = $this->modifyRawRecordData($record);
        $customData = $this->getCustomDataForRecord($record);
        $valueViewers = $this->getValueViewers();
        // backup values
        $recordWithBackup = [];
        foreach ($record as $key => $value) {
            $recordWithBackup[$key] = $recordWithBackup['__' . $key] = $value;
        }
        foreach ($record as $key => $value) {
            if (!$this->hasValueViewer($key) && $this->getTable()->getTableStructure()->hasRelation($key)) {
                unset($recordWithBackup['__' . $key]);
                if ($this->getTable()->getTableStructure()->getRelation($key)->getType() === Relation::HAS_MANY) {
                    $recordWithBackup[$key] = [];
                    foreach ($record[$key] as $index => $relationData) {
                        $recordWithBackup[$key][$index] = $this->prepareRelatedRecord($key, $relationData, $index);
                    }
                } else {
                    $recordWithBackup[$key] = $this->prepareRelatedRecord($key, $record[$key]);
                }
                continue;
            }
            if (empty($valueViewers[$key])) {
                if ($key !== $pkKey) {
                    unset($recordWithBackup[$key], $recordWithBackup['__' . $key]);
                }
                continue;
            }
            $valueViewer = $valueViewers[$key];
            if (
                is_object($valueViewer)
                && method_exists($valueViewer, 'convertValue')
                && (
                    !method_exists($valueViewer, 'isVisible')
                    || $valueViewer->isVisible()
                )
            ) {
                $convertedValue = $valueViewer->convertValue($recordWithBackup[$key], $record);
                if ($valueViewer instanceof DataGridColumn) {
                    $recordWithBackup[$valueViewer::convertNameForDataTables($key)] = $convertedValue;
                } else {
                    $recordWithBackup[$key] = $convertedValue;
                }
            }
        }
        foreach ($valueViewers as $key => $valueViewer) {
            if ($valueViewer->isLinkedToDbColumn() && $valueViewer::isComplexViewerName($key)) {
                $colName = $valueViewer->getTableColumn()->getName();
                if (!array_has($recordWithBackup, $colName) && array_has($record, $colName)) {
                    $value = array_get($record, $colName);
                    if (is_string($value) && mb_strlen($value) >= 2) {
                        $value = json_decode($value, true);
                    }
                    array_set($recordWithBackup, $colName, $value);
                    array_set($recordWithBackup, '__' . $colName, $value);

                }
                if (
                    method_exists($valueViewer, 'convertValue')
                    && (
                        !method_exists($valueViewer, 'isVisible')
                        || $valueViewer->isVisible()
                    )
                ) {
                    $key = implode('.', $valueViewer::splitComplexViewerName($key));
                    $convertedValue = $valueViewer->convertValue(array_get($recordWithBackup, $key), $record);
                    if ($valueViewer instanceof DataGridColumn) {
                        array_set($recordWithBackup, $valueViewer::convertNameForDataTables($key), $convertedValue);
                    } else {
                        array_set($recordWithBackup, $key, $convertedValue);
                    }
                }
            } else if (
                !$valueViewer->isLinkedToDbColumn()
                && !array_key_exists($key, $recordWithBackup)
                && method_exists($valueViewer, 'convertValue')
                && (
                    !method_exists($valueViewer, 'isVisible')
                    || $valueViewer->isVisible()
                )
            ) {
                $convertedValue = $valueViewer->convertValue(null, $record);
                if ($valueViewer instanceof DataGridColumn) {
                    $recordWithBackup[$valueViewer::convertNameForDataTables($key)] = $convertedValue;
                } else {
                    $recordWithBackup[$key] = $convertedValue;
                }
            }
        }
        if (!empty($customData) && is_array($customData)) {
            $recordWithBackup = array_merge($recordWithBackup, $customData);
        }
        $recordWithBackup = array_merge($recordWithBackup, $permissionsAndServiceData);
        return $recordWithBackup;
    }

    /**
     * Process related record's data by viewers attached to it
     * @param string $relationName
     * @param array $relationRecordData
     * @param null|string $index - string: passed for HAS_MANY relation | null: for relations other then HAS_MANY
     * @return array
     * @throws \UnexpectedValueException
     * @throws \PeskyCMF\Scaffold\ValueViewerConfigException
     * @throws \InvalidArgumentException
     * @throws \BadMethodCallException
     */
    protected function prepareRelatedRecord($relationName, array $relationRecordData, $index = null) {
        $recordWithBackup = $relationRecordData;
        $valueViewers = $this->getViewersForRelations();
        foreach ($relationRecordData as $columnName => $value) {
            $viewerName = $relationName . '.' . ($index === null ? '' : $index . '.') . $columnName;
            if (
                array_key_exists($viewerName, $valueViewers)
                && $valueViewers[$viewerName]->getRelation()->getName() === $relationName
            ) {
                $recordWithBackup[$columnName] = $recordWithBackup['__' . $columnName] = $value;
                $valueViewer = $valueViewers[$viewerName];
                if (
                    is_object($valueViewer)
                    && method_exists($valueViewer, 'convertValue')
                    && (
                        !method_exists($valueViewer, 'isVisible')
                        || $valueViewer->isVisible()
                    )
                ) {
                    $recordWithBackup[$columnName] = $valueViewer->convertValue(
                        $recordWithBackup[$columnName],
                        $relationRecordData
                    );
                }
            }
        }
        return $recordWithBackup;
    }

    /**
     * @param array|\Closure $arrayOrClosure
     *      - \Closure: funciton (array $record, ScaffoldSectionConfig $scaffoldSectionConfig) { return []; }
     * @return $this
     * @throws ScaffoldException
     */
    public function setDataToAddToRecord($arrayOrClosure) {
        if (!is_array($arrayOrClosure) && !($arrayOrClosure instanceof \Closure)) {
            throw new ScaffoldException($this, 'setDataToAddToRecord($arrayOrClosure) accepts only array or \Closure');
        }
        $this->dataToAddToRecord = $arrayOrClosure;
        return $this;
    }

    /**
     * @param array $record
     * @return array|mixed
     */
    public function getCustomDataForRecord(array $record) {
        if (empty($this->dataToAddToRecord)) {
            return [];
        } else if ($this->dataToAddToRecord instanceof \Closure) {
            return call_user_func($this->dataToAddToRecord, $record, $this);
        } else {
            return $this->dataToAddToRecord;
        }
    }

    /**
     * @param array|\Closure $arrayOrClosure - function (ScaffoldSectionConfig $sectionConfig) { return [] }
     * @return $this
     * @throws ScaffoldException
     */
    public function sendDataToTemplate($arrayOrClosure) {
        if (!is_array($arrayOrClosure) && !($arrayOrClosure instanceof \Closure)) {
            throw new ScaffoldException($this, 'setDataToAddToRecord($arrayOrClosure) accepts only array or \Closure');
        }
        $this->dataToSendToTemplate = $arrayOrClosure;
        return $this;
    }

    /**
     * @return array|mixed
     */
    public function getAdditionalDataForTemplate() {
        if (empty($this->dataToSendToTemplate)) {
            return [];
        } else if ($this->dataToSendToTemplate instanceof \Closure) {
            return call_user_func($this->dataToSendToTemplate, $this);
        } else {
            return $this->dataToSendToTemplate;
        }
    }

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

    /**
     * @return bool
     */
    public function hasRelationsToRead() {
        return !empty($this->relationsToRead);
    }

    /**
     * Set relations to join.
     * Notes:
     * - For data grid you need to provide key-value pairs in same format as for columns selection:
     *   ['Relation' => ['col1', 'col2', ...]]. More info: @see AbstractSelect::columns()
     *   HAS MANY relations are forbidden.
     * - For item edit form and item details view you need to provide only names of the relations you need to read
     *   with the item. All types of relations allowed but there is no automatic possibility to get deeper relations
     * @param array $relationNames
     * @return $this
     */
    public function readRelations(array $relationNames) {
        $this->relationsToRead = $relationNames;
        return $this;
    }

    /**
     * @return \Closure|null
     * @throws \PeskyCMF\Scaffold\ScaffoldSectionConfigException
     */
    public function getDefaultValueRenderer() {
        if (!empty($this->defaultFieldRenderer)) {
            return $this->defaultFieldRenderer;
        } else {
            $this->setDefaultValueRenderer(function (RenderableValueViewer $valueViewer, $sectionConfig, array $dataForView) {
                $rendererConfig = $this->createValueRenderer()->setData($dataForView);
                $this->configureDefaultValueRenderer($rendererConfig, $valueViewer);
                return $rendererConfig;
            });
            return $this->defaultFieldRenderer;
        }
    }

    /**
     * @return ValueRenderer
     */
    abstract protected function createValueRenderer();

    /**
     * @param ValueRenderer $renderer
     * @param RenderableValueViewer $valueViewer
     */
    protected function configureDefaultValueRenderer(
        ValueRenderer $renderer,
        RenderableValueViewer $valueViewer
    ) {
        $valueViewer->configureDefaultRenderer($renderer);
    }

    /**
     * @param \Closure $defaultFieldRenderer
     * @return $this
     */
    public function setDefaultValueRenderer(\Closure $defaultFieldRenderer) {
        $this->defaultFieldRenderer = $defaultFieldRenderer;
        return $this;
    }

    /**
     * @return bool
     */
    public function hasDefaultValueRenderer() {
        return !empty($this->defaultFieldRenderer);
    }

    /**
     * @return string[]
     * @throws \UnexpectedValueException
     */
    public function getToolbarItems() {
        if (empty($this->toolbarItems)) {
            return [];
        }
        $toolbarItems = call_user_func($this->toolbarItems, $this);
        if (!is_array($toolbarItems)) {
            throw new \UnexpectedValueException(get_class($this) . '->toolbarItems closure must return an array');
        }
        /**
         * @var array $toolbarItems
         * @var Tag|string $item
         */
        foreach ($toolbarItems as &$item) {
            if (is_object($item)) {
                if ($item instanceof CmfMenuItem) {
                    // do nothing
                } else if (method_exists($item, 'build')) {
                    $item = $item->build();
                } else if (method_exists($item, '__toString')) {
                    $item = (string) $item;
                } else {
                    throw new \UnexpectedValueException(
                        get_class($this) . '->toolbarItems: array may contain only strings and objects with build() or __toString() methods'
                    );
                }
            } else if (!is_string($item)) {
                throw new \UnexpectedValueException(
                    get_class($this) . '->toolbarItems: array may contain only strings and objects with build() or __toString() methods'
                );
            }
        }
        return $toolbarItems;
    }

    /**
     * Note: common actions: 'details', 'edit', 'clone', 'delete', 'create' will be added automatically
     * before custom menu items. You can manipulate positioning of common items using actions names as keys
     * Example: 'details' => null.
     * @param \Closure $callback - function (ScaffoldSectionConfig $scaffoldSectionConfig) { return []; }
     * Callback must return an array.
     * Array may contain only strings, Tag class instances, or any object with build() or __toString() method
     * Note: you can change positions of default toolbar items ('create', 'bulk_actions') using item names as a key in
     * resulting array of items while leaving value to be null:
     * function ($scaffoldSectionConfig) { return [$myItem, 'create' => null, $otherItem, 'bulk_actions' => 'null'] }
     * Examples:
     * 1. ToolbarItem:
     * a. Preferred usage:
     * - CmfMenuItem::redirect(cmfRoute('route', [], false))
            ->setTitle($this->translate('action.details'))
     * - CmfMenuItem::request(cmfRoute('route', [], false), 'delete')
            ->setTitle($this->translate('action.delete'))
            ->setConfirm($this->translate('message.delete_confirm'));
     * - CmfMenuItem::bulkActionOnSelectedRows(cmfRoute('route', [], false), 'delete')
            ->setTitle($this->translate('action.delete'))
            ->setPrimaryKeyColumnName('id')
            ->setConfirm($this->translate('message.delete_confirm'));
     * - CmfMenuItem::bulkActionOnFilteredRows(cmfRoute('route', [], false), 'delete')
            ->setTitle($this->translate('action.delete'))
            ->setConfirm($this->translate('message.delete_confirm'));
     * b. Alternative usage:
     * - call some url via ajax and then run "callback(json)"
         Tag::a()
             ->setContent(trans('path.to.translation'))
             ->setClass('btn btn-warning')
             ->setDataAttr('action', 'request')
             ->setDataAttr('url', cmfRoute('route', [], false))
             ->setDataAttr('method', 'put')
             ->setDataAttr('data', 'id={{= it.id }}')
             ->setDataAttr('on-success', 'callbackFuncitonName')
             //^ callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
             //^ It will receive 3 args: data, $link, defaultOnSuccessCallback
             ->setHref('javascript: void(0)');
     * - redirect to other url
         Tag::a()
             ->setContent(trans('path.to.translation'))
             ->setClass('btn btn-warning')
             ->setHref(cmfRoute('route', [], false))
             ->setTarget('_blank')
     * c. ONLY FOR DATA GRIDS:
     * - call some url via ajax passing all selected ids and then run "callback(json)"
         Tag::a()
             ->setContent(trans('path.to.translation'))
             //^ you can use ':count' in label to insert selected items count
             ->setDataAttr('action', 'bulk-selected')
             ->setDataAttr('confirm', trans('path.to.translation'))
             //^ confirm action before sending request to server
             ->setDataAttr('url', cmfRoute('route', [], false))
             ->setDataAttr('method', 'delete')
             //^ can be 'post', 'put', 'delete' depending on action type
             ->setDataAttr('id-field', 'id')
             //^ for bulk actions on selected items id field name to use to get rows ids, default: 'id'
             ->setDataAttr('on-success', 'callbackFuncitonName')
             //^ callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
             //^ It will receive 3 args: data, $link, defaultOnSuccessCallback
             ->setDataAttr('response-type', 'json')
             //^ one of: json, html, xml. Default: 'json'
             ->setHref('javascript: void(0)');
     * Values will be received in the 'ids' key of the request as array
     * - call some url via ajax passing filter conditions and then run "callback(json)"
         Tag::a()
             ->setContent(trans('path.to.translation'))
             //^ you can use ':count' in label to insert filtered items count
             ->setDataAttr('action', 'bulk-filtered')
             ->setDataAttr('confirm', trans('path.to.translation'))
             //^ confirm action before sending request to server
             ->setDataAttr('url', cmfRoute('route', [], false))
             ->setDataAttr('method', 'put')
             //^ can be 'post', 'put', 'delete' depending on action type
             ->setDataAttr('on-success', 'callbackFuncitonName')
             //^ callbackFuncitonName must be a function name: 'funcName' or 'Some.funcName' allowed
             //^ It will receive 3 args: data, $link, defaultOnSuccessCallback
             ->setDataAttr('response-type', 'json')
             //^ one of: json, html, xml. Default: 'json'
             ->setHref('javascript: void(0)');
     * - bulk actions with custom on-click handler
         Tag::button()
             ->setContent(trans('path.to.translation'))
             //^ you can use ':count' in label to insert selected items count or filtered items count
             //^ depending on 'data-type' attribute
             ->setClass('btn btn-success')
             ->setDataAttr('type', 'bulk-selected')
             //^ 'bulk-selected' or 'bulk-filtered'
             ->setDataAttr('url', cmfRoute('route', [], false))
             ->setDataAttr('id-field', 'id')
             //^ id field name to use to get rows ids, default: 'id'
             ->setOnClick('someFunction(this)')
             //^ for 'bulk-selected': inside someFunction() you can get selected rows ids via $(this).data('data').ids
     * 2. List of toolbar items:
     *      [
     *          ToolbarItem1,
     *          ToolbarItem2,
     *          'delete' => null
     *      ]
     * @return $this
     */
    public function setToolbarItems(\Closure $callback) {
        $this->toolbarItems = $callback;
        return $this;
    }

    /**
     * @return boolean
     */
    public function isCreateAllowed() {
        return $this->getScaffoldConfig()->isCreateAllowed();
    }

    /**
     * @return boolean
     */
    public function isEditAllowed() {
        return $this->getScaffoldConfig()->isEditAllowed();
    }

    /**
     * @return boolean
     */
    public function isCloningAllowed() {
        return $this->getScaffoldConfig()->isCloningAllowed();
    }

    /**
     * @return boolean
     */
    public function isDeleteAllowed() {
        return $this->getScaffoldConfig()->isDeleteAllowed();
    }

    /**
     * @return boolean
     */
    public function isDetailsViewerAllowed() {
        return $this->getScaffoldConfig()->isDetailsViewerAllowed();
    }

    /**
     * @return bool
     */
    public function hasSpecialConditions() {
        return !empty($this->specialConditions);
    }

    /**
     * @return array
     */
    public function getSpecialConditions() {
        return $this->specialConditions instanceof \Closure
            ? call_user_func($this->specialConditions, $this)
            : $this->specialConditions;
    }

    /**
     * @param array|\Closure $specialConditions - array or function (ScaffoldSectionConfig $scaffoldSectionConfig) {}
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setSpecialConditions($specialConditions) {
        if (!is_array($specialConditions) && !($specialConditions instanceof \Closure)) {
            throw new \InvalidArgumentException('setSpecialConditions expects array or \Closure');
        }
        $this->specialConditions = $specialConditions;
        return $this;
    }

    /**
     * @return string
     */
    public function getWidth() {
        return min($this->width, 100);
    }

    /**
     * @param $percents
     * @return $this
     */
    public function setWidth($percents) {
        $this->width = $percents;
        return $this;
    }

    /**
     * Get css classes for content container.
     * Uses witdh to collect col-??-?? and col-??-offset?? classes
     * @return string
     */
    public function getCssClassesForContainer() {
        $colsXl = $this->getWidth() >= 100 ? 12 : ceil(12 * ($this->getWidth() / 100));
        $colsXlLeft = floor((12 - $colsXl) / 2);
        $colsLg = $colsXl >= 10 ? 12 : $colsXl + 2;
        $colsLgLeft = floor((12 - $colsLg) / 2);
        return "col-xs-12 col-xl-{$colsXl} col-lg-{$colsLg} col-xl-offset-{$colsXlLeft} col-lg-offset-{$colsLgLeft}";
    }

    /**
     * @return string
     */
    public function getJsInitiator() {
        return $this->jsInitiator;
    }

    /**
     * @return bool
     */
    public function hasJsInitiator() {
        return !empty($this->jsInitiator);
    }

    /**
     * For data grids:
     * - JS function will be called instead of ScaffoldDataGridHelper.init()
     * - JS function will receive 2 arguments: dataGridSelector, dataTablesConfig
     * - You may want to call ScaffoldDataGridHelper.init(dataGridSelector, dataTablesConfig) in this
     *   function to continue normal initialization
     * - Function MUST return DataTables object
     * For forms:
     * - JS function will be called before default form init (see FormHelper.initForm)
     * - JS function will receive 3 arguments: $form, $formContainer, onSubmitSuccess
     * - JS function context is form's DOM element
     * - If JS function returns false - default init will not proceed
     * For item details viewer:
     * - JS function will be called after data was loaded and content rendered
     * - JS function will receive 1 argument depending on situation: $content or $modal
     * @param string $jsFunctionName - name of existing JS function without braces, for example: 'initSomething' or 'SomeVar.init'
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setJsInitiator($jsFunctionName) {
        if (!is_string($jsFunctionName) && !preg_match('%^[$_a-zA-Z][a-zA-Z0-9_.\[\]\'"]+$%', $jsFunctionName)) {
            throw new \InvalidArgumentException("Invalid JavaScript funciton name: [$jsFunctionName]");
        }
        $this->jsInitiator = $jsFunctionName;
        return $this;
    }

    /**
     * @return bool
     */
    public function isUsingDialog() {
        return false;
    }

    /**
     * Called before scaffold template rendering
     */
    public function beforeRender() {

    }

    /**
     * Finish building config.
     * This may trigger some actions that should be applied after all configurations were provided
     * @throws \BadMethodCallException
     */
    public function finish() {
        if ($this->isFinished) {
            throw new \BadMethodCallException('Attempt to call ' . get_class($this) . '->finish() twice');
        } else {
            $this->isFinished = true;
        }
    }

    /**
     * @param array $columnNames
     * @return $this
     */
    public function setAdditionalColumnsToSelect(...$columnNames) {
        if (count($columnNames) && is_array($columnNames[0])) {
            $columnNames = $columnNames[0];
        }
        $this->additionalColumnsToSelect = $columnNames;
        return $this;
    }

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

    /**
     * @param string $section
     * @return CmfRedirectMenuItem
     */
    public function getItemEditMenuItem($section = 'toolbar') {
        return CmfMenuItem::redirect(routeToCmfItemEditForm($this->getScaffoldConfig()->getResourceName(), '{{= it.___pk_value}}', false, null, true))
            ->setTitle($this->translateGeneral($section . '.edit_item'))
            ->setIconClasses('glyphicon glyphicon-edit')
            ->setIconColorClass('text-green')
            ->setButtonClasses('btn btn-success item-edit')
            ->setAccessProvider(function () {
                return $this->isEditAllowed();
            })
            ->setConditionToShow('it.___edit_allowed');
    }

    /**
     * @param string $section
     * @return CmfRedirectMenuItem
     */
    public function getItemCloneMenuItem($section = 'toolbar') {
        return CmfMenuItem::redirect(routeToCmfItemCloneForm($this->getScaffoldConfig()->getResourceName(), '{{= it.___pk_value}}', false, null, true))
            ->setTitle($this->translateGeneral($section . '.clone_item'))
            ->setIconClasses('fa fa-copy')
            ->setIconColorClass('text-primary')
            ->setButtonClasses('btn btn-primary item-clone')
            ->setAccessProvider(function () {
                return $this->isCloningAllowed();
            })
            ->setConditionToShow('it.___cloning_allowed');
    }

    /**
     * @param string $section
     * @return CmfRedirectMenuItem
     */
    public function getItemCreateMenuItem($section = 'toolbar') {
        return CmfMenuItem::redirect(routeToCmfItemAddForm($this->getScaffoldConfig()->getResourceName(), [], false, null, true))
            ->setTitle($this->translateGeneral($section . '.create_item'))
            ->setIconClasses('fa fa-file-o')
            ->setIconColorClass('text-primary')
            ->setButtonClasses('btn btn-primary item-add')
            ->setAccessProvider(function () {
                return $this->isCreateAllowed();
            })
            ->setConditionToShow('it.___create_allowed');

    }

    /**
     * @param string $section
     * @return CmfRedirectMenuItem
     */
    public function getItemDetailsMenuItem($section = 'toolbar') {
        return CmfMenuItem::redirect(routeToCmfItemDetails($this->getScaffoldConfig()->getResourceName(), '{{= it.___pk_value}}', false, null, true))
            ->setTitle($this->translateGeneral($section . '.view_item'))
            ->setIconClasses('glyphicon glyphicon-info-sign')
            ->setIconColorClass('text-light-blue')
            ->setButtonClasses('btn btn-info')
            ->setAccessProvider(function () {
                return $this->isDetailsViewerAllowed();
            })
            ->setConditionToShow('it.___details_allowed');
    }

    /**
     * @param string $section
     * @return CmfRequestMenuItem
     */
    public function getItemDeleteMenuItem($section = 'toolbar') {
        return CmfMenuItem::request(routeToCmfItemDelete($this->getScaffoldConfig()->getResourceName(), '{{= it.___pk_value}}', false, null, true), 'delete')
            ->setTitle($this->translateGeneral($section . '.delete_item'))
            ->setIconClasses('glyphicon glyphicon-trash')
            ->setIconColorClass('text-red')
            ->setButtonClasses('btn btn-danger item-delete')
            ->setBlockDataGrid(true)
            ->setConfirm($this->translateGeneral('message.delete_item_confirm'))
            ->setAccessProvider(function () {
                return $this->isDeleteAllowed();
            })
            ->setConditionToShow('it.___delete_allowed');
    }

}


<?php

namespace SnooPHP\Http;

/**
 * A Route for the application
 *
 * Defines an HTTP route identified by method and url
 * with an optional action and name
 *
 * @author sneppy
 */
class Route
{
	/**
	 * @var string $url route url
	 */
	protected $url;

	/**
	 * @var string $method route method
	 */
	protected $method;

	/**
	 * @var Callable $action action to execute on match
	 */
	protected $action;

	/**
	 * @var string $name route name
	 */
	protected $name;

	/**
	 * @var array $args route arguments
	 */
	protected $args = [];

	/**
	 * Create a new route
	 *
	 * @param string	$url	route url
	 * @param string	$method	route method
	 * @param Callable	$action	route action
	 * @param string	$name	route name
	 */
	public function __construct($url = "/", $method = "GET", Callable $action = null, $name = null)
	{
		$this->url		= trim($url);
		$this->method	= trim($method);
		$this->action	= $action;
		$this->name		= $name ?: $this->url;
	}

	/**
	 * Get or set route url
	 *
	 * @param string $url if not null updated url property
	 *
	 * @return string
	 */
	public function url($url = null)
	{
		if ($url) $this->url = trim($url);
		return $this->url;
	}

	/**
	 * Get or set route method
	 *
	 * @param string $method if not null update method
	 *
	 * @return string
	 */
	public function method($method = null)
	{
		if ($method) $this->method = trim($method);
		return $this->method;
	}

	/**
	 * Get or set route action
	 *
	 * @param Callable $action if specified replace current action
	 *
	 * @return Callable
	 */
	public function action(Callable $action = null)
	{
		if ($action) $this->action = $action;
		return $this->action;
	}

	/**
	 * Get or set route name
	 *
	 * @param string $name if specified replace current route name
	 *
	 * @return string
	 */
	public function name($name = null)
	{
		if ($name) $this->name = $name;
		return $this->name;
	}

	/**
	 * Get route arguments
	 *
	 * @return array
	 */
	public function args()
	{
		return $this->args;
	}

	/**
	 * Determine if test string matches this route
	 *
	 * @param string $test test url string
	 *
	 * @return array
	 */
	public function match($test)
	{
		// Generate pattern
		$isArray	= [];
		$pattern = preg_replace_callback(
			"~/\{(?<arg>\w+)(?<arr>\[\])?\}(?<num>\?|\+|\*|\{[0-9,]+\})?~",
			function($matches) use (&$isArray) {

				$name			= $matches["arg"];
				$num			= $matches["num"] ?? "";
				$isArray[$name]	= !empty($matches[2]);
				return "(?<$name>(?:/[^\\s/?]+)$num)";
			},
			$this->url
		);

		// Pattern error
		if (!$pattern || empty($pattern))
		{
			// Pattern error
			error_log("pattern error: found in route with pattern: {$this->url}");
			return false;
		}

		// Append start, parameters and end
		$pattern = "^$pattern/?(?:\?.*)?$";

		// Match
		if (preg_match("~$pattern~", $test, $matches))
		{
			foreach ($matches as $name => $val)
			{
				// Decode url
				$val = urldecode($val);

				// Discard non-associative indexes
				if (is_int($name))
				{
					if ($name === 0) $this->args[$name] = $val;
				}
				else
				{
					// Get values as array or as string
					$val = ltrim($val, "/");
					$this->args[$name] = $isArray[$name] ? explode("/", $val) : $val;
				}
			}

			// It's a match!
			return true;
		}

		// Better luck next time!
		return false;
	}
}


<?php

/*
 * Copyright (C) 2015 Ashley Marando
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

namespace Marando\AstroDate\Traits;

use \Marando\AstroDate\TimeScale;

trait FormatTrait {

  /**
   * Formats this instance to a string based on a PHP DateTime format string. A
   * few additional formats have been added for the AstroDate library, for more
   * informatin see the documentation.
   *
   * @param  string $format Format, e.g. 'D, d M Y H:i:s'
   * @return string http://php.net/manual/en/function.date.php
   * @see
   */
  public function format($format) {
    // Persist format
    $this->format = $format;

    // Escape format key letters and escaped characters
    $format = preg_replace('/([a-zA-Z])/', '%$1', $format);
    $format = preg_replace('/\\\\%(.)/', '\\\\$1', $format);

    /////////
    // DAY //
    /////////

    $this->format_d($format);
    $this->formatD($format);
    $this->format_j($format);
    $this->format_l($format);
    $this->formatL($format);
    $this->formatN($format);
    $this->formatS($format);
    $this->format_w($format);
    $this->format_z($format);

    //////////
    // WEEK //
    //////////

    $this->formatW($format);

    ///////////
    // MONTH //
    ///////////

    $this->formatF($format);
    $this->format_m($format);
    $this->formatM($format);
    $this->format_n($format);
    $this->format_t($format);

    //////////
    // YEAR //
    //////////

    $this->formatY($format);
    $this->format_y($format);

    //////////
    // TIME //
    //////////

    $this->format_a($format);
    $this->formatA($format);
    $this->format_g($format);
    $this->formatG($format);
    $this->format_h($format);
    $this->formatH($format);
    $this->format_i($format);
    $this->format_s($format);
    $this->format_u($format);

    //////////////
    // TIMEZONE //
    //////////////

    $this->format_e($format);
    $this->formatO($format);
    $this->formatP($format);
    $this->formatZ($format);

    ///////////
    // OTHER //
    ///////////

    $this->format_r($format);
    $this->format_c($format);
    $this->formatC($format);

    // Replace escape character and return formatted string
    return str_replace('\\', '', $format);
  }

  // // // // // // // // // //
  // // //  Protected  // // //
  // // // // // // // // // //

  /**
   * Day of the month, 2 digits with leading zeros
   * @param string $str
   */
  private function format_d(&$str) {
    if (strstr($str, '%d'))
      $str = str_replace('%d', sprintf('%02d', $this->day), $str);
  }

  /**
   * Textual representation of a day, three letters
   * @param string $str
   */
  private function formatD(&$str) {
    if (strstr($str, '%D'))
      $str = str_replace('%D', $this->dayName(false), $str);
  }

  /**
   * Day of the month without leading zeros
   * @param type $str
   */
  private function format_j(&$str) {
    if (strstr($str, '%j'))
      $str = str_replace('%j', sprintf('%01d', $this->day), $str);
  }

  /**
   * A full textual uppercase representation of the day of the week
   * @param type $str
   */
  private function format_l(&$str) {
    if (strstr($str, '%l'))
      $str = str_replace('%l', $this->dayName(true), $str);
  }

  /**
   * A full textual lowercase representation of the day of the week
   * @param type $str
   */
  private function formatL(&$str) {
    if (strstr($str, '%L'))
      $str = str_replace('%L', strtolower($this->dayName(true)), $str);
  }

  /**
   * ISO-8601 numeric representation of the day of the week 1=Mon, 7=Sun
   * @param type $str
   */
  private function formatN(&$str) {
    if (strstr($str, '%N')) {
      $wdn = $this->weekDayNum();  // Convert 0=Mon 6=Sun to above format
      $str = str_replace('%N', $wdn == 0 ? 7 : $wdn, $str);
    }
  }

  /**
   * English ordinal suffix for the day of the month, 2 characters
   * @param type $str
   */
  private function formatS(&$str) {
    if (strstr($str, '%S'))
      $str = str_replace('%S', static::ordinal($this->day), $str);
  }

  /**
   * Numeric representation of the day of the week 0=Sun, 6=Sat
   * @param type $str
   */
  private function format_w(&$str) {
    if (strstr($str, '%w'))
      $str = str_replace('%w', $this->weekDayNum(), $str);
  }

  /**
   * The day of the year (starting from 1)
   * @param type $str
   */
  private function format_z(&$str) {
    if (strstr($str, '%z'))
      $str = str_replace('%z', $this->dayOfYear(), $str);
  }

  /**
   * ISO-8601 week number of year, weeks starting on Monday
   * @param type $str
   */
  private function formatW(&$str) {
    if (strstr($str, '%W'))
      $str = str_replace('%W', null, $str);
  }

  /**
   * A full textual representation of a month, such as January or March
   * @param type $str
   */
  private function formatF(&$str) {
    if (strstr($str, '%F'))
      $str = str_replace('%F', $this->monthName(true), $str);
  }

  /**
   * Numeric representation of a month, with leading zeros
   * @param type $str
   */
  private function format_m(&$str) {
    if (strstr($str, '%m'))
      $str = str_replace('%m', sprintf('%02d', $this->month), $str);
  }

  /**
   * A short textual representation of a month, three letters
   * @param type $str
   */
  private function formatM(&$str) {
    if (strstr($str, '%M'))
      $str = str_replace('%M', $this->monthName(), $str);
  }

  /**
   * Numeric representation of a month, without leading zeros
   * @param type $str
   */
  private function format_n(&$str) {
    if (strstr($str, '%n'))
      $str = str_replace('%n', sprintf('%01d', $this->month), $str);
  }

  /**
   * Number of days in the given month
   * @param type $str
   */
  private function format_t(&$str) {
    if (strstr($str, '%t'))
      $str = str_replace('%t', null, $str);
  }

  /**
   * A full numeric representation of a year, 4 digits
   * @param type $str
   */
  private function formatY(&$str) {
    if (strstr($str, '%Y'))
      $str = str_replace('%Y', $this->year, $str);
  }

  /**
   * A two digit representation of a year
   * @param type $str
   */
  private function format_y(&$str) {
    if (strstr($str, '%y'))
      $str = str_replace(
              '%y', substr($this->year, strlen($this->year) - 2, 2), $str);
  }

  /**
   * Lowercase Ante meridiem and Post meridiem
   * @param type $str
   */
  private function format_a(&$str) {
    if (strstr($str, '%a'))
      $str = str_replace('%a', $this->hour < 12 ? 'am' : 'pm', $str);
  }

  /**
   * Uppercase Ante meridiem and Post meridiem
   * @param type $str
   */
  private function formatA(&$str) {
    if (strstr($str, '%A'))
      $str = str_replace('%A', $this->hour < 12 ? 'AM' : 'PM', $str);
  }

  /**
   * 12-hour format of an hour without leading zeros
   * @param type $str
   */
  private function format_g(&$str) {
    if (strstr($str, '%g')) {
      $h   = $this->hour > 12 ? $this->hour - 12 : $this->hour;
      $str = str_replace('%g', sprintf('%1d', $h), $str);
    }
  }

  /**
   * 24-hour format of an hour without leading zeros
   * @param type $str
   */
  private function formatG(&$str) {
    if (strstr($str, '%G'))
      $str = str_replace('%G', sprintf('%1d', $this->hour), $str);
  }

  /**
   * 12-hour format of an hour with leading zeros
   * @param type $str
   */
  private function format_h(&$str) {
    if (strstr($str, '%h')) {
      $h   = $this->hour > 12 ? $this->hour - 12 : $this->hour;
      $str = str_replace('%h', sprintf('%02d', $h), $str);
    }
  }

  /**
   * 24-hour format of an hour with leading zeros
   * @param type $str
   */
  private function formatH(&$str) {
    if (strstr($str, '%H'))
      $str = str_replace('%H', sprintf('%02d', $this->hour), $str);
  }

  /**
   * Minutes with leading zeros
   * @param type $str
   */
  private function format_i(&$str) {
    if (strstr($str, '%i'))
      $str = str_replace('%i', sprintf('%02d', $this->min), $str);
  }

  /**
   * Seconds with leading zeros
   * @param type $str
   */
  private function format_s(&$str) {
    if (strstr($str, '%s'))
      $str = str_replace('%s', sprintf('%02d', $this->sec), $str);
  }

  /**
   * Milliseconds
   * @param type $str
   */
  private function format_u(&$str) {
    if (strstr($str, '%u')) {
      $u   = substr($this->micro, 0, 3);
      $str = str_replace('%u', str_pad($u, 3, '0', STR_PAD_LEFT), $str);
    }
  }

  /**
   * Timezone/Timescale identifier
   * @param type $str
   */
  private function format_e(&$str) {
    if (strstr($str, '%e') || strstr($str, '%T')) {
      $t   = $this->timescale == TimeScale::UTC() ? $this->timezone : $this->timescale;
      $str = str_replace('%e', $t, $str);
      $str = str_replace('%T', $t, $str);
    }
  }

  /**
   * Difference to UTC in hours
   * @param type $str
   */
  private function formatO(&$str) {
    if (strstr($str, '%O')) {
      $o   = $this->timezone->offset;
      $os  = $o >= 0 ? '+' : '-';
      $oh  = sprintf('%02d', abs(intval($o)));
      $om  = sprintf('%02d', abs($o - intval($o)) * 60);
      $ofs = "{$os}{$oh}{$om}";
      $str = str_replace('%O', $ofs, $str);
    }
  }

  /**
   * Difference to UTC in hours with colon
   * @param type $str
   */
  private function formatP(&$str) {
    if (strstr($str, '%P')) {
      $o   = $this->timezone->offset;
      $os  = $o >= 0 ? '+' : '-';
      $oh  = sprintf('%02d', abs(intval($o)));
      $om  = sprintf('%02d', abs($o - intval($o)) * 60);
      $ofs = "{$os}{$oh}:{$om}";
      $str = str_replace('%P', $ofs, $str);
    }
  }

  /**
   * Timezone offset in seconds
   * @param type $str
   */
  private function formatZ(&$str) {
    if (strstr($str, '%Z'))
      $str = str_replace('%Z', $this->timezone->offset * 3600, $str);
  }

  /**
   * Era, A.D. or B.C. (added for AstroDate)
   * @param type $str
   */
  private function format_r(&$str) {
    if (strstr($str, '%r'))
      $str = str_replace('%r', $this->era, $str);
  }

  /**
   * Day without leading zero and fraction (added for AstroDate)
   * @param type $str
   */
  private function format_c(&$str) {
    if (strstr($str, '%c'))
      if ($this->dayFrac == 0)
        $str = str_replace('%c', "{$this->day}.0", $str);
      else
        $str = str_replace('%c', round($this->day + $this->dayFrac, 7), $str);
  }

  /**
   * Day with leading zero and fraction (added for AstroDate)
   * @param type $str
   */
  private function formatC(&$str) {
    if (strstr($str, '%C')) {
      if ($this->dayFrac == 0)
        $df = "{$this->day}.0";
      else
        $df = round($this->day + $this->dayFrac, 7);

      $str = str_replace('%C', str_pad($df, 10, 0, STR_PAD_LEFT), $str);
    }
  }

}


<?php

namespace Ekyna\Component\Commerce\Common\Util;

use Ekyna\Component\Commerce\Exception\RuntimeException;

/**
 * Trait FormatterAwareTrait
 * @package Ekyna\Component\Commerce\Common\Util
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
trait FormatterAwareTrait
{
    /**
     * @var FormatterFactory
     */
    protected $formatterFactory;

    /**
     * @var Formatter
     */
    private $formatter;

    /**
     * Sets the formatter factory.
     *
     * @param FormatterFactory $formatterFactory
     */
    public function setFormatterFactory(FormatterFactory $formatterFactory)
    {
        $this->formatterFactory = $formatterFactory;
    }

    /**
     * Returns the formatter.
     *
     * @return Formatter
     */
    protected function getFormatter()
    {
        if ($this->formatter) {
            return $this->formatter;
        }

        if (!$this->formatterFactory) {
            throw new RuntimeException("Please call setFormatterFactory() first.");
        }

        return $this->formatter = $this->formatterFactory->create();
    }
}


<?php
namespace AuthActions\Controller\Component;

use Cake\Controller\Component;
use Cake\Controller\Component\CookieComponent;
use Cake\Datasource\EntityInterface;
use Cake\ORM\TableRegistry;
use Cake\Utility\Hash;
use Cake\Http\Response;

class AuthUtilsComponent extends Component
{
    /**
     * Required components
     *
     * @var array
     */
    public $components = ['Cookie', 'Auth'];

    /**
     * Default configuration
     *
     * @var array
     */
    protected $_defaultConfig = [
        'defaultRedirect' => '/'
    ];

    /**
     * Add a Remeber me cookie
     *
     * @param string $userId UserID
     * @param string $options Options array for the cookie config
     * @return void
     */
    public function addRememberMeCookie($userId, $options = [])
    {
        $options = Hash::merge([
            'expires' => '+14 days',
            'httpOnly' => true,
            'secure' => false
        ], $options);

        $this->Cookie->config($options);
        $this->Cookie->write('User.id', $userId);
    }

    /**
     * Deletes the remember me cookie
     *
     * @return void
     */
    public function destroyRememberMeCookie()
    {
        $this->Cookie->delete('User');
    }

    /**
     * Check if a remeber me cookie exists and login the user
     *
     * @return mixed User ID on success, false if no valid cookie
     */
    public function checkRememberMeCookie()
    {
        if (!$this->loggedIn() && $this->Cookie->read('User.id')) {
            return $this->Cookie->read('User.id');
        }
        return false;
    }

    /**
     * Determines if the user is logged in
     *
     * @return bool
     */
    public function loggedIn()
    {
        return $this->Auth->user() !== null;
    }

    /**
     * Attempts to auto login a user and returns a redirect on success.
     *
     * @param \Cake\Datasource\EntityInterface $user User
     * @return \Cake\Http\Response|null
     */
    public function autoLogin(EntityInterface $user): ?Response
    {
        $controller = $this->getController();
        $request = $controller->request;
        $token = $request->getQuery('t');
        if (empty($token)) {
            return null;
        }

        $this->Auth->logout();
        $tokenData = $user->validateLoginToken($token, $user->getKey(), $user->getSalt());
        if (!is_array($tokenData)) {
            return null;
        }
        if (!empty($tokenData['addRememberMeCookie']) && $tokenData['addRememberMeCookie']) {
            $this->addRememberMeCookie((string)$user->id);
        }
        $userData = $user->toArray();
        $userData['user'] = $user;
        $this->Auth->setUser($userData);
        if (!empty($tokenData['url'])) {
            return $controller->redirect($tokenData['url']);
        }

        return $controller->redirect($this->getConfig('defaultRedirect'));
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Doctrine\DependencyInjection;

use Ekyna\Component\Commerce\Bridge\Doctrine\ORM\Type\OpeningHours;
use Ekyna\Component\Commerce\Cart;
use Ekyna\Component\Commerce\Customer;
use Ekyna\Component\Commerce\Order;
use Ekyna\Component\Commerce\Payment;
use Ekyna\Component\Commerce\Pricing;
use Ekyna\Component\Commerce\Quote;
use Ekyna\Component\Commerce\Shipment;
use Ekyna\Component\Commerce\Supplier;
use Ekyna\Component\Commerce\Support;

/**
 * Class DoctrineBundleMapping
 * @package Ekyna\Component\Commerce\Bridge\Doctrine\DependencyInjection
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class DoctrineBundleMapping
{
    /**
     * Returns the doctrine ORM mapping config for the doctrine bundle (doctrine.dbal.types)
     *
     * @return array
     */
    static function buildTypesConfiguration()
    {
        return [
            OpeningHours::NAME => [
                'class'     => OpeningHours::class,
                'commented' => true,
            ],
        ];
    }

    /**
     * Returns the doctrine ORM mapping config for the doctrine bundle (doctrine.orm.mappings)
     *
     * @return array
     */
    static function buildMappingConfiguration()
    {
        return [
            'EkynaCommerce' => [
                'type'      => 'xml',
                'dir'       => realpath(__DIR__ . '/../ORM/Resources/mapping'),
                'is_bundle' => false,
                'prefix'    => 'Ekyna\Component\Commerce',
                'alias'     => 'EkynaCommerce',
            ],
        ];
    }

    /**
     * Returns the default models implementations.
     *
     * @return array
     */
    static function getDefaultImplementations()
    {
        return [
            Cart\Model\CartInterface::class                 => Cart\Entity\Cart::class,
            Cart\Model\CartAddressInterface::class          => Cart\Entity\CartAddress::class,

            Customer\Model\CustomerInterface::class         => Customer\Entity\Customer::class,
            Customer\Model\CustomerGroupInterface::class    => Customer\Entity\CustomerGroup::class,
            Customer\Model\CustomerAddressInterface::class  => Customer\Entity\CustomerAddress::class,

            Order\Model\OrderInterface::class               => Order\Entity\Order::class,
            Order\Model\OrderAddressInterface::class        => Order\Entity\OrderAddress::class,

            Payment\Model\PaymentMethodInterface::class     => Payment\Entity\PaymentMethod::class,
            Payment\Model\PaymentTermInterface::class       => Payment\Entity\PaymentTerm::class,

            Quote\Model\QuoteInterface::class               => Quote\Entity\Quote::class,
            Quote\Model\QuoteAddressInterface::class        => Quote\Entity\QuoteAddress::class,

            Shipment\Model\ShipmentMethodInterface::class   => Shipment\Entity\ShipmentMethod::class,

            Supplier\Model\SupplierInterface::class         => Supplier\Entity\Supplier::class,
            Supplier\Model\SupplierAddressInterface::class  => Supplier\Entity\SupplierAddress::class,
            Supplier\Model\SupplierDeliveryInterface::class => Supplier\Entity\SupplierDelivery::class,
            Supplier\Model\SupplierOrderInterface::class    => Supplier\Entity\SupplierOrder::class,
            Supplier\Model\SupplierProductInterface::class  => Supplier\Entity\SupplierProduct::class,

            Support\Model\TicketInterface::class            => Support\Entity\Ticket::class,
            Support\Model\TicketMessageInterface::class     => Support\Entity\TicketMessage::class,
        ];
    }
}


<?php

namespace Ekyna\Component\Commerce\Stock\Model;

/**
 * Class Availability
 * @package Ekyna\Component\Commerce\Stock\Model
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class Availability
{
    /**
     * @var string
     */
    private $overflowMessage;

    /**
     * @var float
     */
    private $minimumQuantity;

    /**
     * @var string
     */
    private $minimumMessage;

    /**
     * @var float
     */
    private $maximumQuantity;

    /**
     * @var string
     */
    private $maximumMessage;

    /**
     * @var float
     */
    private $availableQuantity;

    /**
     * @var string
     */
    private $availableMessage;

    /**
     * @var float
     */
    private $resupplyQuantity;

    /**
     * @var string
     */
    private $resupplyMessage;

    /**
     * Constructor.
     *
     * @param string $overflowMessage
     * @param float  $minimumQuantity
     * @param string $minimumMessage
     * @param float  $maximumQuantity
     * @param string $maximumMessage
     * @param float  $availableQuantity
     * @param string $availableMessage
     * @param float  $resupplyQuantity
     * @param string $resupplyMessage
     */
    public function __construct(
        string $overflowMessage,
        float $minimumQuantity = 0,
        string $minimumMessage = null,
        float $maximumQuantity = 0,
        string $maximumMessage = null,
        float $availableQuantity,
        string $availableMessage = null,
        float $resupplyQuantity = 0,
        string $resupplyMessage = null
    ) {
        $this->overflowMessage = $overflowMessage;
        $this->minimumQuantity = $minimumQuantity;
        $this->minimumMessage = $minimumMessage;
        $this->maximumQuantity = $maximumQuantity;
        $this->maximumMessage = $maximumMessage;
        $this->availableQuantity = $availableQuantity;
        $this->availableMessage = $availableMessage;
        $this->resupplyQuantity = $resupplyQuantity;
        $this->resupplyMessage = $resupplyMessage;
    }

    /**
     * Returns the messages for the given quantity.
     *
     * @param float $quantity
     *
     * @return array
     */
    public function getMessagesForQuantity(float $quantity)
    {
        $messages = [];

        if ($quantity < $this->minimumQuantity) {
            $messages[] = $this->minimumMessage;
        } elseif (0 < $this->maximumQuantity && $quantity > $this->maximumQuantity) {
            $messages[] = $this->maximumMessage;
        } else {
            if (null !== $this->availableMessage) {
                $messages[] = $this->availableMessage;
            }

            if ($quantity > $this->availableQuantity) {
                if (null !== $this->resupplyMessage) {
                    $messages[] = $this->resupplyMessage;
                    if ($quantity > $this->availableQuantity + $this->resupplyQuantity) {
                        $messages[] = $this->overflowMessage;
                    }
                } else {
                    $messages[] = $this->overflowMessage;
                }
            }
        }

        if (empty($messages)) {
            $messages[] = $this->overflowMessage;
        }

        return $messages;
    }

    /**
     * Returns
     *
     * @param float $quantity
     *
     * @return bool
     */
    public function isAvailableForQuantity(float $quantity)
    {
        if ($quantity < $this->minimumQuantity) {
            return false;
        } elseif ($quantity > $this->availableQuantity + $this->resupplyQuantity) {
            return false;
        }

        return true;
    }

    /**
     * Returns the overflow message.
     *
     * @return string
     */
    public function getOverflowMessage()
    {
        return $this->overflowMessage;
    }

    /**
     * Returns the minimum quantity.
     *
     * @return float
     */
    public function getMinimumQuantity()
    {
        return $this->minimumQuantity;
    }

    /**
     * Returns the minimum message.
     *
     * @return string
     */
    public function getMinimumMessage()
    {
        return $this->minimumMessage;
    }

    /**
     * Returns the maximum quantity.
     *
     * @return float
     */
    public function getMaximumQuantity()
    {
        return $this->maximumQuantity;
    }

    /**
     * Returns the maximum message.
     *
     * @return string
     */
    public function getMaximumMessage()
    {
        return $this->maximumMessage;
    }

    /**
     * Returns the available quantity.
     *
     * @return float
     */
    public function getAvailableQuantity()
    {
        return $this->availableQuantity;
    }

    /**
     * Returns the available message.
     *
     * @return string
     */
    public function getAvailableMessage()
    {
        return $this->availableMessage;
    }

    /**
     * Returns the resupply quantity.
     *
     * @return float
     */
    public function getResupplyQuantity()
    {
        return $this->resupplyQuantity;
    }

    /**
     * Returns the resupply message.
     *
     * @return string
     */
    public function getResupplyMessage()
    {
        return $this->resupplyMessage;
    }

    /**
     * Returns the array version.
     *
     * @return array
     */
    public function toArray()
    {
        return [
            'o_msg'   => $this->overflowMessage,
            'min_qty' => $this->minimumQuantity,
            'min_msg' => $this->minimumMessage,
            'max_qty' => INF === $this->maximumQuantity ? 'INF' : $this->maximumQuantity,
            'max_msg' => $this->maximumMessage,
            'a_qty'   => INF === $this->availableQuantity ? 'INF' : $this->availableQuantity,
            'a_msg'   => $this->availableMessage,
            'r_qty'   => $this->resupplyQuantity,
            'r_msg'   => $this->resupplyMessage,
        ];
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints;

use Ekyna\Component\Commerce\Invoice\Calculator\InvoiceCalculatorInterface;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceLineInterface;
use Ekyna\Component\Commerce\Document\Model\DocumentLineTypes;
use Ekyna\Component\Commerce\Invoice\Model\InvoiceTypes;
use Ekyna\Component\Commerce\Shipment\Model\ShipmentInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
 * Class InvoiceLineValidator
 * @package Ekyna\Component\Commerce\Bridge\Symfony\Validator\Constraints
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class InvoiceLineValidator extends ConstraintValidator
{
    /**
     * @var InvoiceCalculatorInterface
     */
    private $invoiceCalculator;

    /**
     * Constructor.
     *
     * @param InvoiceCalculatorInterface $calculator
     */
    public function __construct(InvoiceCalculatorInterface $calculator)
    {
        $this->invoiceCalculator = $calculator;
    }

    /**
     * @inheritDoc
     */
    public function validate($line, Constraint $constraint)
    {
        if (null === $line) {
            return;
        }

        if (!$line instanceof InvoiceLineInterface) {
            throw new UnexpectedTypeException($line, InvoiceLineInterface::class);
        }
        if (!$constraint instanceof InvoiceLine) {
            throw new UnexpectedTypeException($constraint, InvoiceLine::class);
        }

        $subject = null;
        $invoice = $line->getInvoice();

        // Good line
        if ($line->getType() === DocumentLineTypes::TYPE_GOOD) {
            if (null === $subject = $line->getSaleItem()) {
                $this
                    ->context
                    ->buildViolation($constraint->null_sale_item)
                    ->setInvalidValue(null)
                    ->atPath('saleItem')
                    ->addViolation();

                return;
            }

            // If invoice is linked to a shipment
            if (null !== $shipment = $invoice->getShipment()) {
                // Check that a matching shipment item exists
                if (null === $shipmentItem = $this->findMatchingShipmentItem($line, $shipment)) {
                    $this
                        ->context
                        ->buildViolation($constraint->hierarchy_integrity)
                        ->addViolation();

                    return;
                }

                // Check that quantities equals
                if ($shipmentItem->getQuantity() != $line->getQuantity()) {
                    $message = InvoiceTypes::isInvoice($invoice)
                        ? $constraint->shipped_miss_match
                        : $constraint->returned_miss_match;

                    $this
                        ->context
                        ->buildViolation($message, [
                            '%qty%' => $shipmentItem->getQuantity(),
                        ])
                        ->setInvalidValue($line->getQuantity())
                        ->atPath('quantity')
                        ->addViolation();

                    return;
                }
            }
        }

        // Discount line
        elseif ($line->getType() === DocumentLineTypes::TYPE_DISCOUNT) {
            if (null === $subject = $line->getSaleAdjustment()) {
                $this
                    ->context
                    ->buildViolation($constraint->null_sale_adjustment)
                    ->setInvalidValue(null)
                    ->atPath('saleAdjustment')
                    ->addViolation();

                return;
            }

        }

        // Shipment line
        elseif ($line->getType() === DocumentLineTypes::TYPE_SHIPMENT) {
            $subject = $invoice->getSale();
        }

        if ($line->getType() !== DocumentLineTypes::TYPE_DISCOUNT) {
            if (empty($line->getDesignation())) {
                $this
                    ->context
                    ->buildViolation($constraint->empty_designation)
                    ->setInvalidValue($line->getDesignation())
                    ->atPath('designation')
                    ->addViolation();

                return;
            }
        }

        if (InvoiceTypes::isInvoice($invoice)) {
            // Invoice case
            $max = $this->invoiceCalculator->calculateInvoiceableQuantity($subject, $invoice);
            $message = $constraint->invoiceable_overflow;
        } else {
            // Credit case
            $max = $this->invoiceCalculator->calculateCreditableQuantity($subject, $invoice);
            $message = $constraint->creditable_overflow;
        }

        // Check quantity integrity
        if ($max < $line->getQuantity()) {
            $this
                ->context
                ->buildViolation($message, ['%max%' => $max])
                ->setInvalidValue($line->getQuantity())
                ->atPath('quantity')
                ->addViolation();

            return;
        }
    }

    /**
     * Finds the shipment item matching the invoice line.
     *
     * @param InvoiceLineInterface $line
     * @param ShipmentInterface    $shipment
     *
     * @return \Ekyna\Component\Commerce\Shipment\Model\ShipmentItemInterface|null
     */
    private function findMatchingShipmentItem(InvoiceLineInterface $line, ShipmentInterface $shipment)
    {
        $saleItem = $line->getSaleItem();

        foreach ($shipment->getItems() as $shipmentItem) {
            if ($saleItem === $shipmentItem->getSaleItem()) {
                return $shipmentItem;
            }
        }

        return null;
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Payum\Offline\Action;

use Ekyna\Component\Commerce\Bridge\Payum\Offline\Constants;
use Ekyna\Component\Commerce\Payment\Model\PaymentInterface;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Request\Convert;

/**
 * Class ConvertAction
 * @package Ekyna\Component\Commerce\Bridge\Payum\Offline\Action
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class ConvertAction implements ActionInterface
{
    /**
     * {@inheritDoc}
     *
     * @param Convert $request
     */
    public function execute($request)
    {
        RequestNotSupportedException::assertSupports($this, $request);

        /** @var PaymentInterface $payment */
        $payment = $request->getSource();

        $details = ArrayObject::ensureArrayObject($payment->getDetails());

        if (isset($details[Constants::FIELD_STATUS])) {
            return;
        }

        $details[Constants::FIELD_STATUS] = false;
        $details['amount'] = $payment->getAmount();
        $details['currency'] = $payment->getCurrency()->getCode();

        $request->setResult((array)$details);
    }

    /**
     * {@inheritDoc}
     */
    public function supports($request)
    {
        return
            $request instanceof Convert &&
            $request->getSource() instanceof PaymentInterface &&
            $request->getTo() === 'array';
    }
}


<?php

namespace PeskyCMF\Scaffold\MenuItem;

use Swayok\Html\Tag;

class CmfRequestMenuItem extends CmfMenuItem {

    /** @var string */
    protected $httpMethod = 'get';
    /** @var array */
    protected $requestData = [];
    /** @var string */
    protected $responseDataType = 'json';
    /** @var string|null */
    protected $onSuccess;
    /** @var bool */
    protected $blockDataGrid = true;
    /** @var string|null */
    protected $confirm;

    /**
     * CmfRequestMenuItem constructor.
     * @param $url
     * @param $httpMethod
     * @throws \InvalidArgumentException
     */
    protected function __construct(string $url, string $httpMethod) {
        parent::__construct($url);
        $this->setHttpMethod($httpMethod);
    }

    /**
     * @return string
     */
    public function getActionType() {
        return 'request';
    }

    /**
     * @param string $httpMethod
     * @return $this
     * @throws \InvalidArgumentException
     */
    protected function setHttpMethod(string $httpMethod) {
        $this->httpMethod = strtolower($httpMethod);
        if (!in_array($this->httpMethod, ['get', 'post', 'put', 'delete'], true)) {
            throw new \InvalidArgumentException(
                '$httpMethod argument must be one of: get, post, put, delete. Received: ' . $httpMethod
            );
        }
        return $this;
    }

    public function getHttpMethod(): string {
        return $this->httpMethod;
    }

    /**
     * @return string|null
     */
    public function getOnSuccess() {
        return $this->onSuccess;
    }

    /**
     * JS function name. This function will be called after successful response with 1 argument - response data.
     * Example: 'SomeVar.onSuccess' will be evalueated as SomeVar.onSuccess(responseData)
     * @param string $onSuccess
     * @return $this
     */
    public function setOnSuccess(string $onSuccess) {
        $this->onSuccess = $onSuccess;
        return $this;
    }

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

    /**
     * Key-value array with data to send with response.
     * You can use Dot.js inserts as values using 'it' variable to access current item data.
     * Example: ['id' => '{{= it.id }}']
     * @param array $requestData
     * @return $this
     */
    public function setRequestData(array $requestData) {
        $this->requestData = $requestData;
        return $this;
    }

    /**
     * @return null|string
     */
    public function makeRequestDataForHtmlAttribute() {
        if (empty($this->requestData)) {
            return null;
        } else {
            return http_build_query($this->requestData, null, '&', PHP_QUERY_RFC1738);
        }
    }

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

    /**
     * @param string $responseDataType
     * @return $this
     * @throws \InvalidArgumentException
     */
    public function setResponseDataType($responseDataType) {
        $this->responseDataType = strtolower($responseDataType);
        if (!in_array($this->responseDataType, ['json', 'text', 'html'], true)) {
            throw new \InvalidArgumentException(
                '$responseDataType argument must be one of: json, text, html. Recieved: ' . $responseDataType
            );
        }
        return $this;
    }

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

    /**
     * Should current data grid be blocked until request's responce received or not?
     * @param bool $blockDataGrid
     * @return $this
     */
    public function setBlockDataGrid($blockDataGrid) {
        $this->blockDataGrid = $blockDataGrid;
        return $this;
    }

    /**
     * @return null|string
     */
    public function getConfirm() {
        return $this->confirm;
    }

    /**
     * Request confirmation of action.
     * @param string $confirm - message (question) to display to user
     * @return $this
     */
    public function setConfirm(string $confirm) {
        $this->confirm = $confirm;
        return $this;
    }

    /**
     * @param Tag $tag
     * @return Tag
     */
    protected function modifyTagBeforeRendering(Tag $tag) {
        return $tag;
    }

    /**
     * Render menu item as <button>
     * @param bool $withIcon
     * @return string
     */
    public function renderAsButton(bool $withIcon = true): string {
        if ($this->isAccessible()) {
            $button = Tag::button(($withIcon ? $this->makeIcon() . ' ' : '') . $this->getTitle())
                ->setClass($this->getButtonClasses())
                ->setType('button')
                ->addCustomRenderedAttributeWithValue($this->makeConditionalDisabledAttribute())
                ->setTitle($this->getTooltip())
                ->setDataAttr('toggle', $this->hasTooltip() ? 'tooltip' : null)
                ->setDataAttr('position', $this->hasTooltip() ? $this->getTooltipPosition() : null)
                ->setDataAttr('action', $this->getActionType())
                ->setDataAttr('url', $this->getUrl())
                ->setDataAttr('method', $this->getHttpMethod())
                ->setDataAttr('data', $this->makeRequestDataForHtmlAttribute())
                ->setDataAttr('response-type', $this->getResponseDataType())
                ->setDataAttr('confirm', $this->getConfirm())
                ->setDataAttr('on-success', $this->getOnSuccess())
                ->setDataAttr('block-datagrid', $this->isBlockDataGrid() ? '1' : null);
            return $this->wrapIntoShowCondition($this->modifyTagBeforeRendering($button)->build());
        } else {
            return '';
        }
    }

    /**
     * Render menu item as  icon (title will be used as tooltip)
     * @param string $additionalClasses
     * @param bool $allowIconColorClass
     * @return string
     */
    public function renderAsIcon(string $additionalClasses = '', bool $allowIconColorClass = true): string {
        if ($this->isAccessible()) {
            $button = Tag::a($this->makeIcon($allowIconColorClass))
                ->addCustomRenderedAttributeWithValue($this->makeConditionalDisabledAttribute())
                ->setTitle($this->getTooltipOrTitle())
                ->setHref('javascript: void(0)')
                ->setClass($additionalClasses)
                ->setDataAttr('toggle', $this->hasTooltipOrTitle() ? 'tooltip' : null)
                ->setDataAttr('position', $this->hasTooltipOrTitle() ? $this->getTooltipPosition() : null)
                ->setDataAttr('action', $this->getActionType())
                ->setDataAttr('url', $this->getUrl())
                ->setDataAttr('method', $this->getHttpMethod())
                ->setDataAttr('data', $this->makeRequestDataForHtmlAttribute())
                ->setDataAttr('response-type', $this->getResponseDataType())
                ->setDataAttr('confirm', $this->getConfirm())
                ->setDataAttr('on-success', $this->getOnSuccess())
                ->setDataAttr('block-datagrid', $this->isBlockDataGrid() ? '1' : null);
            return $this->wrapIntoShowCondition($this->modifyTagBeforeRendering($button)->build());
        } else {
            return '';
        }
    }

    /**
     * Render menu item as
...

     * @param bool $allowTooltip
     * @return string
     */
    public function renderAsBootstrapDropdownMenuItem(): string {
        if ($this->isAccessible()) {
            $button = Tag::a($this->makeIcon(true) . ' ' . $this->getTitle())
                ->setHref('javascript: void(0)')
                ->addCustomRenderedAttributeWithValue($this->makeConditionalDisabledAttribute())
                ->setTitle($this->getTooltip())
                ->setDataAttr('toggle', $this->hasTooltip() ? 'tooltip' : null)
                ->setDataAttr('position', $this->hasTooltip() ? $this->getTooltipPosition() : null)
                ->setDataAttr('action', $this->getActionType())
                ->setDataAttr('url', $this->getUrl())
                ->setDataAttr('method', $this->getHttpMethod())
                ->setDataAttr('data', $this->makeRequestDataForHtmlAttribute())
                ->setDataAttr('response-type', $this->getResponseDataType())
                ->setDataAttr('confirm', $this->getConfirm())
                ->setDataAttr('on-success', $this->getOnSuccess())
                ->setDataAttr('block-datagrid', $this->isBlockDataGrid() ? '1' : null);
            return $this->wrapIntoShowCondition(
                '
makeConditionalDisabledAttribute() . '>'
                    . $this->modifyTagBeforeRendering($button)->build()
                . '
'
            );
        } else {
            return '';
        }
    }
}


<?php

namespace Ekyna\Component\Commerce\Bridge\Payum\Offline\Action;

use Ekyna\Component\Commerce\Bridge\Payum\Offline\Constants;
use Ekyna\Component\Commerce\Bridge\Payum\Request\Accept;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Bridge\Spl\ArrayObject;
use Payum\Core\Exception\RequestNotSupportedException;

/**
 * Class AcceptAction
 * @package Ekyna\Component\Commerce\Bridge\Payum\Offline\Action
 * @author  Etienne Dauvergne <contact@ekyna.com>
 */
class AcceptAction implements ActionInterface
{
    /**
     * {@inheritDoc}
     *
     * @param \Ekyna\Component\Commerce\Bridge\Payum\Request\Accept $request
     */
    public function execute($request)
    {
        RequestNotSupportedException::assertSupports($this, $request);

        $model = ArrayObject::ensureArrayObject($request->getModel());

        $model[Constants::FIELD_STATUS] = Constants::STATUS_CAPTURED;
    }

    /**
     * {@inheritDoc}
     */
    public function supports($request)
    {
        return $request instanceof Accept
            && $request->getModel() instanceof \ArrayAccess;
    }
}


<?php

namespace SnooPHP\Model;

use PDO;
use PDOException;

/**
 * Handles migration for a single database
 *
 * @author Sneppy
 */
class Migration
{
	/**
	 * @var string $dbName name of db connection to use
	 */
	protected $dbName;

	/**
	 * @var Table[] $tables registered tables
	 */
	protected $tables;

	/**
	 * Create a new migration instance
	 *
	 * @param string	$dbName	db connection name
	 * @param Table[]	$tables	list of tables to register
	 */
	public function __construct(array $argv = [], array $tables = [])
	{
		// Parse arguments
		$args = parse_args($argv, [
			"d"	=> basename($argv[0] ?? "master", ".php")
		]);

		// Initial values
		$this->dbName = $args["params"]["d"];
		$this->tables = $tables;

		// Compute tables dependencies
		foreach ($tables as $table) $table->generateDependencies();

		// If command specified, run command
		if ($args["arg"])
			$this->run($args["arg"]);
	}

	/**
	 * Run a migration program
	 *
	 * @param string $prog program to run ('migrate', 'drop' or 'reset')
	 *
	 * @return bool program status
	 */
	public function run($prog = "migrate")
	{
		switch (trim($prog))
		{
			case "migrate":
				return $this->migrate();

			case "drop":
				return $this->drop();

			case "reset":
				return $this->drop() & $this->migrate();

			default:
				error_log("\n\e[1;31m!\e[0m program $prog not applicable\n");
				return false;
		}
	}

	/**
	 * Register a table
	 *
	 * @param Table $table
	 */
	public function register(Table $table)
	{
		// Generate table dependencies
		$table->generateDependencies();
		$this->tables[] = $table;
	}

	/**
	 * Run migration
	 *
	 * @return bool program status
	 */
	protected function migrate()
	{
		// Program status
		$status = true;

		// Compute table dependencies
		// break if circular dependencies are found
		if (($newTables = static::computeDependencies($this->tables)) === false)
		{
			error_log("\e[1;31m!\e[0m circular dependency found; terminating program\n");
			return false;
		}

		// Retrieve last migration
		$migration	= $this->lastMigration();
		$oldTables	= $migration ? unserialize($migration["tables"]) : [];

		// Begin transaction
		// Unfortunately begin transaction has no effect here
		// MySQL adds an implicit commit after DROP TABLE and CREATE TABLE
		printf("\n> starting migration for \e[1;37m'{$this->dbName}'\e[0m database ...\n");
		Db::beginTransaction($this->dbName);

		// Check new tables
		foreach ($newTables as $newTable)
		{
			if (!$newTable->active) continue;

			// Find matching table
			$oldTable = null;
			foreach ($oldTables as $oldTab)
				if ($oldTab->name() === $newTable->name())
				{
					$oldTable = $oldTab;
					break;
				}

			// Run migration
			try
			{
				$oldTable ?
				printf("\n> processing existing table \e[1;37m'{$newTable->name()}'\e[0m:\n") :
				printf("\n> creating new table \e[1;37m'{$newTable->name()}'\e[0m:\n{$newTable->createQuery()}\n");
				$newTable->migrate($oldTable, $this->dbName);
				printf("\e[1;32m✓\e[0m all ok\n");
			}
			catch (PDOException $e)
			{
				error_log("\e[1;31m!\e[0m {$e->getMessage()}\n");
				$status = false;
			}
		}

		// Check old tables
		$dropped = $oldTables;
		foreach ($dropped as $i => $t)
			foreach ($newTables as $newTable)
				if ($newTable->name() === $t->name())
				{
					unset($dropped[$i]);
					break;
				}

		if (!empty($dropped))
		{
			// Get table names
			printf("\n> dropping tables:\n");
			foreach ($dropped as $i => $t)
			{
				printf("    ➤ {$t->name()}\n");
				$dropped[$i] = $t->name();
			}

			// Drop tables
			try
			{
				Db::query("drop table if exists ".implode(", ", $dropped), [], $this->dbName, false);
			}
			catch (PDOException $e)
			{
				error_log("\n\e[1;31m!\e[0m {$e->getMessage()}\n");
				$status = false;
			}
		}

		if ($status)
		{
			// All ok
			// Create migration table if it does not exist
			$this->createMigrationTable();

			// Save migration
			$status = $this->saveMigration($newTables);
			if ($status)
			{
				printf("\n\e[1;32m✓\e[0m migration saved\n");
				Db::commit($this->dbName);
			}
			else
			{

				error_log("\n\e[1;31m!\e[0m error while saving migration; reverting changes ...\n");
				Db::rollBack($this->dbName);
			}
		}
		else
		{
			// Revert changes to database
			// policy is "all or nothing"
			Db::rollBack($this->dbName);
		}

		// Return program status
		return $status;
	}

	/**
	 * Drop all tables migration
	 *
	 * @return bool program status
	 */
	protected function drop()
	{
		// Program status
		$status = true;

		$migration	= $this->lastMigration();
		$tables		= $migration ? unserialize($migration["tables"]) : [];
		if (empty($tables))
		{
			printf("\n\e[1;32m✓\e[0m all ok, nothing to drop\n");
			return true;
		}

		// Compute table dependencies
		// break if circular dependencies are found
		if (($tables = static::computeDependencies($tables)) === false)
		{
			error_log("\n\e[1;31m!\e[0m circular dependency found; terminating program\n");
			return false;
		}

		// Reverse dependency order
		$tables = array_reverse($tables);

		// Get table names
		printf("\n> dropping tables:\n");
		foreach ($tables as $table)
		{
			printf("    ➤ {$table->name()}\n");
			$tableNames[] = $table->name();
		}

		// Begin transaction and run drop query
		// Unfortunately begin transaction has no effect here
		// MySQL adds an implicit commit after DROP TABLE and CREATE TABLE
		Db::beginTransaction($this->dbName);
		try
		{
			Db::query("drop table if exists ".implode(", ", $tableNames), [], $this->dbName, false);
		}
		catch (PDOException $e)
		{
			error_log("\e[1;31m!\e[0m {$e->getMessage()}\n");
			$status = false;
		}

		if ($status)
		{
			// Drop migration table
			if ($this->dropMigrationTable())
			{
				printf("\e[1;32m✓\e[0m tables dropped\n");
				Db::commit($this->dbName);
			}
			else
			{
				printf("\e[1;31m!\e[0m could not drop migration table; reverting all changes ...\n");
				Db::rollBack($this->dbName);
			}
		}
		else
		{
			printf("\e[1;31m!\e[0m something went wrong ...\n");
			Db::rollBack($this->dbName);
		}

		// Return program status
		return $status;
	}

	/**
	 * Return last migration if any
	 *
	 * @return object|bool|null null or false if no migration is found
	 */
	protected function lastMigration()
	{
		try
		{
			// Return last migration in temporal time
			$migration = Db::query("select * from migrations order by created_at desc limit 1", [], $this->dbName);
			return $migration[0];
		}
		catch (PDOException $e)
		{
			if ($e->getCode() === "42S02") return null;
		}

		// Something bad :(
		return false;
	}

	/**
	 * Save migration in database
	 *
	 * @param Table[] $tables current tables
	 *
	 * @return bool false if fails
	 */
	protected function saveMigration(array $tables)
	{
		try
		{
			$tables = serialize($tables);
			return Db::query("insert into migrations(host, tables) values(?, ?)", [gethostname(), $tables], $this->dbName, false);
		}
		catch (PDOException $e)
		{
			error_log($e->getMessage());
			return false;
		}
	}

	/**
	 * Create migration table if doesn't exists
	 *
	 * @return bool query status
	 */
	protected function createMigrationTable()
	{
		$migrations = new Table("migrations", true);
		$migrations->string("host")->notNullable()->primaryComposite();
		$migrations->timestamp("created_at")->notNullable()->primaryComposite(true);
		$migrations->blob("tables");

		try
		{
			return $migrations->create($this->dbName);
		}
		catch (PDOException $e)
		{
			error_log($e->getMessage());
			return false;
		}
	}

	/**
	 * Drop migration table
	 *
	 * @return bool query status
	 */
	protected function dropMigrationTable()
	{
		try
		{
			return Db::query("drop table if exists migrations", [], $this->dbName, false);
		}
		catch (PDOException $e)
		{
			error_log($e->getMessage());
			return false;
		}
	}

	/**
	 * Compute table dependencies
	 *
	 * This method reorders table to avoid dependencies problems
	 * when creating/deleting tables
	 *
	 * @param Table[] $tables list of tables
	 *
	 * @return array|bool return reordered list of tables or false if fails
	 */
	protected static function computeDependencies(array $tables)
	{
		/** @todo vulnerable to circular dependencies */
		$result = [];
		while (!empty($tables))
		{
			// Circular dependency check variable
			$num = count($tables);

			foreach ($tables as $i => $table)
				if (!$table->dependent())
				{
					$result[] = $table;

					// Remove from list
					// and free dependent tables
					unset($tables[$i]);
					foreach ($tables as $t) $t->removeDependency($table->name());
				}

			// If no dependency-free table is found
			// there is probably a circular dependency
			// nothing we can do about it
			if (count($tables) === $num) return false;
		}

		return $result;
	}
}