<?php
/**
 * @link http://psesd.org/
 *
 * @copyright Copyright (c) 2015 Puget Sound ESD
 * @license http://psesd.org/license/
 */

namespace cascade\gii\cascadeTypeModule;

use cascade\components\base\ModuleSetExtension;
use Yii;
use yii\db\ActiveRecord;
use yii\db\Connection;
use yii\db\Schema;
use yii\gii\CodeFile;
use yii\helpers\ArrayHelper;
use yii\helpers\FileHelper;
use yii\helpers\Html;
use yii\helpers\Inflector;

/**
 * Generator [[@doctodo class_description:cascade\gii\cascadeTypeModule\Generator]].
 /**
 *
 * This generator will generate the skeleton code needed for a
 *
 * @property string $migrationClassName The model namespace of the module. This property is read-only.
 * @property string $migrationsAlias The model namespace of the module. This property is read-only.
 * @property string $migrationsNamespace The model namespace of the module. This property is read-only.
 * @property string $modelNamespace The model namespace of the module. This property is read-only.
 * @property boolean $modulePath The directory that contains the module class. This property is read-only.
 * @property string $widgetNamespace The widget namespace of the module. This property is read-only.
 * @property string $migrationClassName The model namespace of the module. This property is read-only.
 * @property string $migrationsAlias The model namespace of the module. This property is read-only.
 * @property string $migrationsNamespace The model namespace of the module. This property is read-only.
 * @property string $modelNamespace The model namespace of the module. This property is read-only.
 * @property boolean $modulePath The directory that contains the module class. This property is read-only.
 * @property string $widgetNamespace The widget namespace of the module. This property is read-only.
 *
 * @author Jacob Morrison <jmorrison@psesd.org>
 *
 * @since 1.0
 */
class Generator extends \yii\gii\Generator
{
    /**
     * @var [[@doctodo var_type:db]] [[@doctodo var_description:db]]
     */
    public $db = 'db';
    /**
     * @var [[@doctodo var_type:moduleSet]] [[@doctodo var_description:moduleSet]]
     */
    public $moduleSet = '';

    //public $moduleName;
    /**
     * @var [[@doctodo var_type:baseClass]] [[@doctodo var_description:baseClass]]
     */
    public $baseClass = 'cascade\components\types\ActiveRecord';

    /**
     * @var [[@doctodo var_type:title]] [[@doctodo var_description:title]]
     */
    public $title;
    /**
     * @var [[@doctodo var_type:uniparental]] [[@doctodo var_description:uniparental]]
     */
    public $uniparental = 0;
    /**
     * @var [[@doctodo var_type:hasDashboard]] [[@doctodo var_description:hasDashboard]]
     */
    public $hasDashboard = 1;
    /**
     * @var [[@doctodo var_type:priority]] [[@doctodo var_description:priority]]
     */
    public $priority = 1000;
    /**
     * @var [[@doctodo var_type:icon]] [[@doctodo var_description:icon]]
     */
    public $icon;

    /**
     * @var [[@doctodo var_type:widgets]] [[@doctodo var_description:widgets]]
     */
    public $widgets = [];

    /**
     * @var [[@doctodo var_type:migrationTimestamp]] [[@doctodo var_description:migrationTimestamp]]
     */
    public $migrationTimestamp;

    /**
     * @var [[@doctodo var_type:generateRelations]] [[@doctodo var_description:generateRelations]]
     */
    public $generateRelations = true;
    /**
     * @var [[@doctodo var_type:generateLabelsFromComments]] [[@doctodo var_description:generateLabelsFromComments]]
     */
    public $generateLabelsFromComments = false;

    /**
     * @var [[@doctodo var_type:tableName]] [[@doctodo var_description:tableName]]
     */
    public $tableName;
    /**
     * @var [[@doctodo var_type:descriptorField]] [[@doctodo var_description:descriptorField]]
     */
    public $descriptorField;
    /**
     * @var [[@doctodo var_type:children]] [[@doctodo var_description:children]]
     */
    public $children = '';
    /**
     * @var [[@doctodo var_type:parents]] [[@doctodo var_description:parents]]
     */
    public $parents = '';
    /**
     * @var [[@doctodo var_type:section]] [[@doctodo var_description:section]]
     */
    public $section;

    /**
     * @inheritdoc
     */
    public function __construct()
    {
        if (is_null($this->migrationTimestamp)) {
            $this->migrationTimestamp = time();
        }

        return parent::__construct();
    }

    /**
     * Get base namespace.
     *
     * @return [[@doctodo return_type:getBaseNamespace]] [[@doctodo return_description:getBaseNamespace]]
     */
    public function getBaseNamespace()
    {
        if (!empty($this->moduleSet) && isset(Yii::$app->extensions[$this->moduleSet])) {
            $bsClass = Yii::$app->extensions[$this->moduleSet]['bootstrap'];
            $bsReflector = new \ReflectionClass(new $bsClass());

            return $bsReflector->getNamespaceName();
        }

        return 'cascade\modules';
    }

    /**
     * Get model class.
     *
     * @return [[@doctodo return_type:getModelClass]] [[@doctodo return_description:getModelClass]]
     */
    public function getModelClass()
    {
        return $this->generateClassName($this->tableName);
    }

    /**
     * Get module name.
     *
     * @return [[@doctodo return_type:getModuleName]] [[@doctodo return_description:getModuleName]]
     */
    public function getModuleName()
    {
        return preg_replace('/^Object/', 'Type', $this->modelClass);
    }

    /**
     * Get module namespace.
     *
     * @return [[@doctodo return_type:getModuleNamespace]] [[@doctodo return_description:getModuleNamespace]]
     */
    public function getModuleNamespace()
    {
        return $this->baseNamespace . '\\' . $this->moduleName;
    }

    /**
     * Get module class.
     *
     * @return [[@doctodo return_type:getModuleClass]] [[@doctodo return_description:getModuleClass]]
     */
    public function getModuleClass()
    {
        return $this->moduleNamespace . '\\' . 'Module';
    }

    /**
     * Get module i d.
     *
     * @return [[@doctodo return_type:getModuleID]] [[@doctodo return_description:getModuleID]]
     */
    public function getModuleID()
    {
        return $this->moduleName;
    }

    /**
     * Get module system i d.
     *
     * @return [[@doctodo return_type:getModuleSystemID]] [[@doctodo return_description:getModuleSystemID]]
     */
    public function getModuleSystemID()
    {
        return preg_replace('/^Type/', '', $this->moduleName);
    }

    // model
    /**
     * Get ns.
     *
     * @return [[@doctodo return_type:getNs]] [[@doctodo return_description:getNs]]
     */
    public function getNs()
    {
        return $this->baseNamespace . '\\models';
    }

    /**
     * @inheritdoc
     */
    public function getName()
    {
        return 'Cascade Object Type Module Generator';
    }

    /**
     * @inheritdoc
     */
    public function getDescription()
    {
        return 'This generator helps you to generate the skeleton code needed by a Cascade object type module.';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return array_merge(parent::rules(), [
            /* Module */
            //['moduleID, moduleClass, baseNamespace', 'filter', 'filter' => 'trim'],
            //['moduleID, moduleClass, baseNamespace', 'required'],
            //['moduleID', 'match', 'pattern' => '/^[\w\\-]+$/', 'message' => 'Only word characters and dashes are allowed.'],
            //['moduleClass, baseNamespace', 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'],
            //['moduleClass', 'validateModuleClass'],

            /* Model */
            //['modelClass', 'filter', 'filter' => 'trim'],
            [['tableName', 'icon', 'title'], 'required'],
            //['db, modelClass', 'match', 'pattern' => '/^\w+$/', 'message' => 'Only word characters are allowed.'],
            // ['ns, baseClass', 'match', 'pattern' => '/^[\w\\\\]+$/', 'message' => 'Only word characters and backslashes are allowed.'],
            [['tableName'], 'match', 'pattern' => '/^(\w+\.)?([\w\*]+)$/', 'message' => 'Only word characters, and optionally an asterisk and/or a dot are allowed.'],
            //['db', 'validateDb'],
            //['ns', 'validateNamespace'],
            [['tableName'], 'validateTableName'],
            [['migrationTimestamp', 'priority'], 'integer'],
            [['section', 'descriptorField', 'parents', 'children', 'uniparental', 'hasDashboard', 'moduleSet'], 'safe'],
            //['baseClass', 'validateClass', 'params' => ['extends' => ActiveRecord::className()]],
            //['generateRelations, generateLabelsFromComments', 'boolean'],

        ]);
    }

    /**
     * @inheritdoc
     */
    public function attributeLabels()
    {
        return [
            'moduleSet' => 'Module Set',

            /* Module */
            'moduleID' => 'Module ID',
            'moduleClass' => 'Module Class',
            'uniparental' => 'Uniparental',
            'hasDashboard' => 'Managed by its own dashboard',
            'priority' => 'Priority',

            /* Model */

            'ns' => 'Namespace',
            'db' => 'Database Connection ID',
            'tableName' => 'Table Name',
            'modelClass' => 'Model Class',
            'baseClass' => 'Base Class',
            'generateRelations' => 'Generate Relations',
            'generateLabelsFromComments' => 'Generate Labels from DB Comments',
        ];
    }

    /**
     * @inheritdoc
     */
    public function hints()
    {
        return [
            /* Module */
            'moduleID' => 'This refers to the ID of the module, e.g., admin.',
            'moduleClass' => 'This is the fully qualified class name of the module, e.g., cascade\modules\admin\Module.',

            'title' => 'Single noun for this object type',
            'section' => 'Which section does this widget fall into?',
            'uniparental' => 'Objects of this type can have only one parent',
            'priority' => 'Priority object appears in sections (lower numbers appear first)',
            'hasDashboard' => 'Objects of this type are managed from their own dashboard.',

            /* Model */

            'ns' => 'This is the namespace of the ActiveRecord class to be generated, e.g., cascade\models',
            'db' => 'This is the ID of the DB application component.',
            'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. tbl_post.
                The table name may consist of the DB schema part if needed, e.g. public.tbl_post.
                The table name may contain an asterisk to match multiple table names, e.g. tbl_*
                will match tables who name starts with tbl_. In this case, multiple ActiveRecord classes
                will be generated, one for each matching table name; and the class names will be generated from
                the matching characters. For example, table tbl_post will generate Post
                class.',
            'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain
                the namespace part as it is specified in "Namespace". You do not need to specify the class name
                if "Table Name" contains an asterisk at the end, in which case multiple ActiveRecord classes will be generated.',
            'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.',
            'generateRelations' => 'This indicates whether the generator should generate relations based on
                foreign key constraints it detects in the database. Note that if your database contains too many tables,
                you may want to uncheck this option to accelerate the code generation proc	ess.',
            'generateLabelsFromComments' => 'This indicates whether the generator should generate attribute labels
                by using the comments of the corresponding DB columns.',
        ];
    }

    /**
     * @inheritdoc
     */
    public function successMessage()
    {
        if (!empty($this->moduleSet)) {
            return "";
        } elseif (Yii::$app->hasModule($this->moduleID)) {
            $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($this->moduleID), ['target' => '_blank']);

            return "The module has been generated successfully. You may $link.";
        }

        $output = <<<EOD

The module has been generated successfully.

To access the module, you need to add this to your application configuration:

EOD;
        $code = <<<EOD
<?php
    ......
    'modules' => [
        '{$this->moduleID}' => [
            'class' => '{$this->moduleClass}',
        ],
    ],
    ......
EOD;

        return $output . '
' . highlight_string($code, true) . '
';
    }

    /**
     * @inheritdoc
     */
    public function requiredTemplates()
    {
        return ['module.php', 'detail_list_widget.php', 'simple_link_list_widget.php',  'model.php', 'migration.php', 'module_set.php'];
    }

    /**
     * @inheritdoc
     */
    public function autoCompleteData()
    {
        return [
            'tableName' => function () {
                return $this->getDbConnection()->getSchema()->getTableNames();
            },
        ];
    }

    /**
     * @inheritdoc
     */
    public function stickyAttributes()
    {
        return ['moduleSet'];
    }

    /**
     * @inheritdoc
     */
    public function generate()
    {
        $files = [];
        $modulePath = $this->getModulePath();
        $widgets = [];
        $systemId = ucfirst($this->getModuleSystemID());
        if (!empty($this->section) && substr($this->section, 0, 1) === '_') {
            if ($this->section === '_side') {
                $files[] = new CodeFile(
                    $modulePath . '/widgets/EmbeddedList.php',
                    $this->render("side_list_widget.php")
                );
            } else {
                $files[] = new CodeFile(
                    $modulePath . '/widgets/EmbeddedList.php',
                    $this->render("embedded_list_widget.php")
                );
            }
            $widgets[] = "Embedded{$systemId}Browse";
        } else {
            if (!empty($this->children)) {
                $widgets[] = "Children{$systemId}Browse";
            }
            if (!empty($this->parents)) {
                $widgets[] = "Parents{$systemId}Browse";
            }
            $files[] = new CodeFile(
                $modulePath . '/widgets/DetailList.php',
                $this->render("detail_list_widget.php")
            );
        }
        if (!empty($this->hasDashboard)) {
            $files[] = new CodeFile(
                $modulePath . '/widgets/SimpleLinkList.php',
                $this->render("simple_link_list_widget.php")
            );
            $widgets[] = "{$systemId}Summary";
        }

        $this->widgets = $widgets;

        $files[] = new CodeFile(
            $modulePath . '/Module.php',
            $this->render("module.php")
        );

        if (!empty($this->moduleSet) && isset(Yii::$app->extensions[$this->moduleSet])) {
            $files[] = new CodeFile(
                $this->moduleSetPath . '/Extension.php',
                $this->render("module_set.php")
            );
        }

        $relations = $this->generateRelations();
        $db = $this->getDbConnection();
        foreach ($this->getTableNames() as $tableName) {
            $className = $this->generateClassName($tableName);
            $tableSchema = $db->getTableSchema($tableName);
            $myRelations = isset($relations[$className]) ? $relations[$className] : [];
            $params = [
                'tableName' => $tableName,
                'className' => $className,
                'tableSchema' => $tableSchema,
                'descriptorField' => $this->generateDescriptorField($tableSchema),
                'labels' => $this->generateLabels($tableSchema),
                'rules' => $this->generateRules($tableSchema),
                'relations' => $myRelations,
                'uses' => $this->generateRelationUses($myRelations),
                'migrationClassName' => $this->migrationClassName,
                'migrationsNamespace' => $this->migrationsNamespace,
                'createTableSyntax' => $this->generateCreateTableColumns($tableSchema),
                'createTableIndices' => $this->generateTableIndices($tableSchema),
                'columnSettingSkel' => $this->generateColumnSettings($tableSchema),
            ];
            $files[] = new CodeFile(
                Yii::getAlias('@' . str_replace('\\', '/', $this->modelNamespace)) . '/' . $className . '.php',
                $this->render('model.php', $params)
            );
            $files[] = $migration = new CodeFile(
                $this->getMigrationPath(),
                $this->render('migration.php', $params)
            );
        }

        return $files;
    }

    /**
     * Validates [[moduleClass]] to make sure it is a fully qualified class name.
     */
    public function validateModuleClass()
    {
        if (strpos($this->moduleClass, '\\') === false || Yii::getAlias('@' . str_replace('\\', '/', $this->moduleClass)) === false) {
            $this->addError('moduleClass', 'Module class must be properly namespaced.');
        }
        if (substr($this->moduleClass, -1, 1) == '\\') {
            $this->addError('moduleClass', 'Module class name must not be empty. Please enter a fully qualified class name. e.g. "cascade\modules\admin\Module".');
        }
    }

    /**
     * [[@doctodo method_description:generateDescriptorField]].
     *
     * @param [[@doctodo param_type:table]] $table [[@doctodo param_description:table]]
     *
     * @return [[@doctodo return_type:generateDescriptorField]] [[@doctodo return_description:generateDescriptorField]]
     */
    public function generateDescriptorField($table)
    {
        $field = '';
        if (!empty($this->descriptorField)) {
            if (strpos($this->descriptorField, ',') === false) {
                $field = "\tpublic \$descriptorField = '{$this->descriptorField}';\n";
            } else {
                $parts = explode(',', str_replace(' ', '', $this->descriptorField));
                $field = "\tpublic \$descriptorField = ['" . implode("', '", $parts) . "'];\n";
            }
        } else {
            $check = ['name', 'title'];
            foreach ($check as $column) {
                if (isset($table->columns[$column])) {
                    $field = "\tpublic \$descriptorField = '$column';\n";
                }
            }
        }

        return $field;
    }

    /**
     * [[@doctodo method_description:possibleSections]].
     *
     * @return [[@doctodo return_type:possibleSections]] [[@doctodo return_description:possibleSections]]
     */
    public function possibleSections()
    {
        $s = ['' => '(self)'];
        foreach (Yii::$app->collectors['sections']->getAll() as $section) {
            $s[$section->systemId] = $section->sectionTitle;
        }

        return $s;
    }

    /**
     * [[@doctodo method_description:possibleModuleSets]].
     *
     * @return [[@doctodo return_type:possibleModuleSets]] [[@doctodo return_description:possibleModuleSets]]
     */
    public function possibleModuleSets()
    {
        $s = ['' => '(core)'];
        foreach (Yii::$app->extensions as $id => $ext) {
            if (!isset($ext['bootstrap'])) {
                continue;
            }
            $bsClass = $ext['bootstrap'];
            $bs = new $bsClass();
            if ($bs instanceof ModuleSetExtension) {
                $s[$id] = $id;
            }
        }

        return $s;
    }

    /**
     * [[@doctodo method_description:possibleIcons]].
     *
     * @return [[@doctodo return_type:possibleIcons]] [[@doctodo return_description:possibleIcons]]
     */
    public function possibleIcons()
    {
        $path = Yii::getAlias("@vendor/fortawesome/font-awesome/src/icons.yml");
        $data = \Spyc::YAMLLoad($path);
        $icons = [];
        ArrayHelper::multisort($data['icons'], 'name');
        foreach ($data['icons'] as $icon) {
            $group = $icon['categories'][0];
            if (!isset($icons[$group])) {
                $icons[$group] = [];
            }
            $icons[$group]['fa fa-' . $icon['id']] = $icon['name'] . "	" . '&#x' . $icon['unicode'] . ';';
        }
        ksort($icons);

        return $icons;
    }

    /**
     * Get module path.
     *
     * @return boolean the directory that contains the module class
     */
    public function getModulePath()
    {
        return Yii::getAlias('@' . str_replace('\\', '/', $this->moduleNamespace));
    }

    /**
     * Get module set path.
     *
     * @return [[@doctodo return_type:getModuleSetPath]] [[@doctodo return_description:getModuleSetPath]]
     */
    public function getModuleSetPath()
    {
        return Yii::getAlias('@' . str_replace('\\', '/', $this->baseNamespace));
    }

    /**
     * Get widget namespace.
     *
     * @return string the widget namespace of the module.
     */
    public function getWidgetNamespace()
    {
        return $this->moduleNamespace . '\widgets';
    }

    /**
     * Get model namespace.
     *
     * @return string the model namespace of the module.
     */
    public function getModelNamespace()
    {
        return $this->moduleNamespace . '\models';
    }

    /**
     * Get migrations namespace.
     *
     * @return string the model namespace of the module.
     */
    public function getMigrationsNamespace()
    {
        return $this->moduleNamespace . '\migrations';
    }

    /**
     * Get migration path.
     *
     * @return [[@doctodo return_type:getMigrationPath]] [[@doctodo return_description:getMigrationPath]]
     */
    public function getMigrationPath()
    {
        return Yii::getAlias('@' . str_replace('\\', '/', $this->migrationsNamespace)) . '/' . $this->migrationClassName . '.php';
    }

    /**
     * Get migration directory.
     *
     * @return [[@doctodo return_type:getMigrationDirectory]] [[@doctodo return_description:getMigrationDirectory]]
     */
    public function getMigrationDirectory()
    {
        return Yii::getAlias('@' . str_replace('\\', '/', $this->migrationsNamespace));
    }
    /**
     * Get migrations alias.
     *
     * @return string the model namespace of the module.
     */
    public function getMigrationsAlias()
    {
        return '@' . str_replace('\\', '/', $this->migrationsNamespace);
    }

    /**
     * Get migration class name.
     *
     * @return string the model namespace of the module.
     */
    public function getMigrationClassName()
    {
        $postfix = '_initial_' . $this->tableName;
        if (is_dir($this->migrationDirectory)) {
            $searchExisting = FileHelper::findFiles($this->migrationDirectory, ['only' => [$postfix . '.php']]);
            if (!empty($searchExisting)) {
                return strstr(basename($searchExisting[0]), '.php', true);
            }
        }

        return  'm' . gmdate('ymd_His', $this->migrationTimestamp) . $postfix;
    }

    /**
     * Collects the foreign key column details for the given table.
     *
     * @param TableSchema $table the table metadata
     *
     * @return [[@doctodo return_type:findTableKeys]] [[@doctodo return_description:findTableKeys]]
     */
    protected function findTableKeys($table)
    {
        $r = [
                'foreignKeys' => [],
                'indices' => [],
                'primaryKeys' => [],
            ];
        $row = $this->dbConnection->createCommand('SHOW CREATE TABLE ' . $this->dbConnection->getSchema()->quoteSimpleTableName($table->name))->queryOne();
        if (isset($row['Create Table'])) {
            $sql = $row['Create Table'];
        } else {
            $row = array_values($row);
            $sql = $row[1];
        }
        $regexp = '/(PRIMARY KEY)\s+\(([^\)]+)\)/mi';
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $pks = array_map('trim', explode(',', str_replace('`', '', $match[2])));
                $r['primaryKeys'][] = ['keys' => $pks];
            }
        }

        $regexp = '/(UNIQUE KEY|INDEX|KEY)\s+([^\s]+)\s+\(([^\)]+)\)/mi';
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $unique = $match[1] === 'UNIQUE KEY';
                $fname = trim(str_replace('`', '', $match[2]));
                $pks = array_map('trim', explode(',', str_replace('`', '', $match[3])));
                $r['indices'][$fname] = ['keys' => $pks, 'unique' => $unique];
            }
        }

        $regexp = '/CONSTRAINT\s+([^\s]+)\sFOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)\s+ON DELETE\s+((?:(?!\sON).)*)\s+ON UPDATE\s+((?:(?!,).)*)/mi';
        if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $fname = trim(str_replace('`', '', $match[1]));
                $fks = array_map('trim', explode(',', str_replace('`', '', $match[2])));
                $pks = array_map('trim', explode(',', str_replace('`', '', $match[4])));
                $constraint = ['name' => $fname, 'table' => str_replace('`', '', $match[3]), 'keys' => []];
                $constraint['onDelete'] = trim($match[5]);
                $constraint['onUpdate'] = trim($match[6]);
                foreach ($fks as $k => $name) {
                    $constraint['keys'][$name] = $pks[$k];
                }
                $r['foreignKeys'][] = $constraint;
            }
        }

        return $r;
    }

    /**
     * Get primary key location.
     *
     * @param [[@doctodo param_type:table]] $table [[@doctodo param_description:table]]
     *
     * @return [[@doctodo return_type:getPrimaryKeyLocation]] [[@doctodo return_description:getPrimaryKeyLocation]]
     */
    public function getPrimaryKeyLocation($table)
    {
        // if multiple, put the primary key in the indicies section
        $count = 0;
        foreach ($table->columns as $column) {
            if ($column->isPrimaryKey) {
                $count++;
            }
            if ($count > 1) {
                return 'index';
            }
        }

        return 'table_build';
    }

    /**
     * [[@doctodo method_description:generateTableIndices]].
     *
     * @param [[@doctodo param_type:table]] $table [[@doctodo param_description:table]]
     *
     * @return [[@doctodo return_type:generateTableIndices]] [[@doctodo return_description:generateTableIndices]]
     */
    public function generateTableIndices($table)
    {
        $tableName = $table->name;
        $meta = $this->findTableKeys($table);
        $niceName = lcfirst(Inflector::id2camel($tableName, '_'));

        if ($this->getPrimaryKeyLocation($table) === 'index') {
            foreach ($meta['primaryKeys'] as $name => $parts) {
                $keys = $parts['keys'];
                $unique = !empty($parts['unique']);
                $i[] = "\$this->addPrimaryKey('{$niceName}Pk', '{$tableName}', '" . implode(',', $keys) . "');";
            }
        }

        foreach ($meta['indices'] as $name => $parts) {
            $keys = $parts['keys'];
            $name = $this->fixIndexName($name, $table, $keys);
            $unique = !empty($parts['unique']);
            $i[] = "\$this->createIndex('{$name}', '{$tableName}', '" . implode(',', $keys) . "', " . ($unique ? 'true' : 'false') . ");";
        }

        foreach ($meta['foreignKeys'] as $fk) {
            $keys = $fk['keys'];
            $unique = !empty($fk['unique']);
            $name = $this->fixIndexName($fk['name'], $table, array_values($keys));
            $i[] = "\$this->addForeignKey('{$fk['name']}', '{$tableName}', '" . implode(',', array_keys($fk['keys'])) . "', '{$fk['table']}', '" . implode(',', array_values($fk['keys'])) . "', '{$fk['onDelete']}', '{$fk['onUpdate']}');";
        }

        return implode("\n\t\t", $i);
    }

    /**
     * Get module set modules.
     *
     * @return [[@doctodo return_type:getModuleSetModules]] [[@doctodo return_description:getModuleSetModules]]
     */
    public function getModuleSetModules()
    {
        if (empty($this->moduleSet) || !isset(Yii::$app->extensions[$this->moduleSet])) {
            return '';
        }
        $bsClass = Yii::$app->extensions[$this->moduleSet]['bootstrap'];
        $p = [];
        $bs = new $bsClass();
        $modules = $bs->getModules();
        $modules[$this->moduleID] = ['class' => $this->moduleClass];
        foreach ($modules as $id => $module) {
            $e = '$m[\'' . $id . '\'] = [';
            if (!is_array($module)) {
                $module = ['class' => $module];
            }
            $n = 0;
            foreach ($module as $k => $v) {
                $e .= "\n\t\t\t'{$k}' => ";
                if (is_string($v)) {
                    $e .= "'" . addslashes($v) . "'";
                } elseif (is_numeric($v)) {
                    $e .= $v;
                }
                $n++;
                if ($n !== count($module)) {
                    $e .= ',';
                }
            }
            $e .= "\n\t\t];";
            $p[] = $e;
        }

        return implode("\n\t\t", $p);
    }

    /**
     * [[@doctodo method_description:fixIndexName]].
     *
     * @param [[@doctodo param_type:name]]  $name  [[@doctodo param_description:name]]
     * @param [[@doctodo param_type:table]] $table [[@doctodo param_description:table]]
     * @param [[@doctodo param_type:keys]]  $keys  [[@doctodo param_description:keys]]
     *
     * @return [[@doctodo return_type:fixIndexName]] [[@doctodo return_description:fixIndexName]]
     */
    public function fixIndexName($name, $table, $keys)
    {
        if (strpos($name, '_') === false) {
            return $name;
        }
        $niceName = preg_replace('/object/', '', $table->name);
        $niceName = lcfirst(Inflector::id2camel($niceName, '_'));
        $indices = [];
        foreach ($keys as $key) {
            $indices[] = Inflector::id2camel(substr($key, 0, strpos($key, '_id')), '_');
        }

        return $niceName . implode('', $indices);
    }

    /**
     * Generates Create table schema.
     *
     * @param \yii\db\TableSchema $table the table schema
     *
     * @return array the generated validation rules
     */
    public function generateCreateTableColumns($table)
    {
        $fields = [];
        $queryBuilder = $this->getDbConnection()->getQueryBuilder();
    //	\d($table->columns );
        foreach ($table->columns as $column) {
            $nullExtra = $signedExtra = $defaultExtra = $primaryKeyExtra  = $autoIncrementExtra = '';
            $size = $column->size;
            if (!is_null($column->scale)) {
                $size .= ',' . $column->scale;
            }

            if (!$column->allowNull) {
                $nullExtra = ' NOT NULL';
            }

            if ($column->unsigned) {
                $signedExtra = ' unsigned';
            }
            if ($column->autoIncrement) {
                $autoIncrementExtra = ' AUTO_INCREMENT';
            }

            if ($column->isPrimaryKey and $this->getPrimaryKeyLocation($table) === 'table_build') {
                $primaryKeyExtra = ' PRIMARY KEY';
            }

            if (!empty($column->enumValues)) {
                $size = '\'' . implode('\',\'', $column->enumValues) . '\'';
            }

            $stringValue = $column->dbTypecast($column->defaultValue);
            if (is_null($stringValue)) {
                if ($column->allowNull) {
                    $stringValue = 'NULL';
                } else {
                    $stringValue = false;
                }
            } elseif (is_bool($stringValue)) {
                $stringValue = ($stringValue) ? 1 : 0;
            } else {
                $stringValue = $this->getDbConnection()->getSchema()->quoteValue($stringValue);
            }
            if ($stringValue !== false) {
                $defaultExtra = ' DEFAULT ' . addslashes($stringValue);
            }

            $field = "'{$column->name}' => '";

            // \canis\base\Debug::d($column);exit;
            $fieldType = $column->dbType;
            preg_match('/^(\w+)(\((.+?)\))?\s*(.+)$/', $fieldType, $fieldTypeParts);
            if (!isset($fieldTypeParts[1])) {
                var_dump($fieldTypeParts);
                var_dump($column);
                exit;
            }
            $fieldTypeBare = $fieldTypeParts[1];
            // if (isset($fieldTypeBare[4]) AND $fieldTypeBare[4] === 'unsigned') {
            // 	$signedExtra = ' unsigned';
            // }

            if ($fieldType === 'char(36)') {
                $fieldType = $column->dbType . ' CHARACTER SET ascii COLLATE ascii_bin';
            } elseif ($fieldType === 'tinyint(1)') {
                $fieldType = 'boolean';
            } elseif (substr($column->dbType, 0, 4) === 'enum') {
                $fieldType = $column->dbType;
            } elseif (isset($this->getDbConnection()->getSchema()->typeMap[$fieldTypeBare])) {
                $fieldType = $this->getDbConnection()->getSchema()->typeMap[$fieldTypeBare];
                $qb = $this->getDbConnection()->getQueryBuilder();
                if (isset($qb->typeMap[$fieldType])) {
                    preg_match('/^(\w+)(\((.+?)\))?\s*(.+)$/', $qb->typeMap[$fieldType], $baseTypeParts);
                    if (isset($fieldTypeParts[4]) && isset($baseTypeParts[4]) && $fieldTypeParts[4] !== $baseTypeParts[4]) {
                        $size = preg_replace('/[^0-9\,]/', '', $fieldTypeParts[4]);
                    }
                }
                if ($size !== false) {
                    $fieldType .= '(' . $size . ')';
                }
            }

            $fieldComplete = trim(addslashes($fieldType) . $signedExtra . $nullExtra . $defaultExtra . $autoIncrementExtra . $primaryKeyExtra);
            $field .= $fieldComplete . '\'';

            $fields[] = $field;
        }

        return implode(",\n\t\t\t", $fields);
    }

    /**
     * Generates the attribute labels for the specified table.
     *
     * @param \yii\db\TableSchema $table the table schema
     *
     * @return array the generated attribute labels (name => label)
     */
    public function generateLabels($table)
    {
        $labels = [];
        foreach ($table->columns as $column) {
            if ($this->generateLabelsFromComments && !empty($column->comment)) {
                $labels[$column->name] = $column->comment;
            } elseif (!strcasecmp($column->name, 'id')) {
                $labels[$column->name] = 'ID';
            } else {
                $label = Inflector::camel2words($column->name);
                if (strcasecmp(substr($label, -3), ' id') === 0) {
                    $label = substr($label, 0, -3) . ' ID';
                }
                $labels[$column->name] = $label;
            }
        }

        return $labels;
    }

    /**
     * [[@doctodo method_description:generateColumnSettings]].
     *
     * @param [[@doctodo param_type:table]] $table [[@doctodo param_description:table]]
     *
     * @return [[@doctodo return_type:generateColumnSettings]] [[@doctodo return_description:generateColumnSettings]]
     */
    public function generateColumnSettings($table)
    {
        $types = [];
        foreach ($table->columns as $column) {
            if (in_array($column->name, ['id', 'created', 'modified', 'archived', 'deleted']) || strstr($column->name, '_id') !== false) {
                continue;
            }
            $types[] = "'{$column->name}' => []";
        }

        return $types;
    }

    /**
     * Generates validation rules for the specified table.
     *
     * @param \yii\db\TableSchema $table the table schema
     *
     * @return array the generated validation rules
     */
    public function generateRules($table)
    {
        $types = [];
        $lengths = [];
        foreach ($table->columns as $column) {
            if ($column->autoIncrement) {
                continue;
            }
            if (!$column->allowNull && $column->defaultValue === null && !$column->isPrimaryKey) {
                $types['required'][] = $column->name;
            }
            switch ($column->type) {
                case Schema::TYPE_SMALLINT:
                case Schema::TYPE_INTEGER:
                case Schema::TYPE_BIGINT:
                    $types['integer'][] = $column->name;
                    break;
                case Schema::TYPE_BOOLEAN:
                    $types['boolean'][] = $column->name;
                    break;
                case Schema::TYPE_FLOAT:
                case Schema::TYPE_DECIMAL:
                case Schema::TYPE_MONEY:
                    $types['number'][] = $column->name;
                    break;
                case Schema::TYPE_DATE:
                case Schema::TYPE_TIME:
                case Schema::TYPE_DATETIME:
                case Schema::TYPE_TIMESTAMP:
                    if (!in_array($column->name, ['created', 'deleted', 'modified'])) {
                        $types['safe'][] = $column->name;
                    }
                    break;
                default: // strings
                    if ($column->size > 0) {
                        $lengths[$column->size][] = $column->name;
                    } else {
                        $types['string'][] = $column->name;
                    }
            }
        }

        $rules = [];
        foreach ($types as $type => $columns) {
            $rules[] = "[['" . implode("', '", $columns) . "'], '$type']";
        }
        foreach ($lengths as $length => $columns) {
            $rules[] = "[['" . implode("', '", $columns) . "'], 'string', 'max' => $length]";
        }

        return $rules;
    }

    /**
     * [[@doctodo method_description:generateRelationUses]].
     *
     * @param [[@doctodo param_type:relations]] $relations [[@doctodo param_description:relations]]
     *
     * @return [[@doctodo return_type:generateRelationUses]] [[@doctodo return_description:generateRelationUses]]
     */
    public function generateRelationUses($relations)
    {
        $uses = [];
        foreach ($relations as $relation) {
            if (isset($this->modelMap[$relation[1]])) {
                $uses[] = 'use ' . $this->modelMap[$relation[1]] . ';';
            }
        }

        return implode("\n", array_unique($uses));
    }

    /**
     * [[@doctodo method_description:generateRelations]].
     *
     * @return array the generated relation declarations
     */
    protected function generateRelations()
    {
        if (!$this->generateRelations) {
            return [];
        }

        $db = $this->getDbConnection();

        if (($pos = strpos($this->tableName, '.')) !== false) {
            $schemaName = substr($this->tableName, 0, $pos);
        } else {
            $schemaName = '';
        }

        $relations = [];
        foreach ($db->getSchema()->getTableSchemas($schemaName) as $table) {
            $tableName = $table->name;
            $className = $this->generateClassName($tableName);
            foreach ($table->foreignKeys as $refs) {
                $refTable = $refs[0];
                unset($refs[0]);
                $fks = array_keys($refs);
                $refClassName = $this->generateClassName($refTable);

                // Add relation for this table
                $link = $this->generateRelationLink(array_flip($refs));
                $relationName = $this->generateRelationName($relations, $className, $table, $fks[0], false);
                if ($relationName === 'Id0') {
                    $relationName = 'Registry';
                }
                $relations[$className][$relationName] = [
                    "return \$this->hasOne($refClassName::className(), $link);",
                    $refClassName,
                    false,
                ];

                // Add relation for the referenced table
                $hasMany = false;
                foreach ($fks as $key) {
                    if (!in_array($key, $table->primaryKey, true)) {
                        $hasMany = true;
                        break;
                    }
                }
                $link = $this->generateRelationLink($refs);
                $relationName = $this->generateRelationName($relations, $refClassName, $refTable, $className, $hasMany);
                $relations[$refClassName][$relationName] = [
                    "return \$this->" . ($hasMany ? 'hasMany' : 'hasOne') . "($className::className(), $link);",
                    $className,
                    $hasMany,
                ];
            }

            if (($fks = $this->checkPivotTable($table)) === false) {
                continue;
            }
            $table0 = $fks[$table->primaryKey[0]][0];
            $table1 = $fks[$table->primaryKey[1]][0];
            $className0 = $this->generateClassName($table0);
            $className1 = $this->generateClassName($table1);

            $link = $this->generateRelationLink([$fks[$table->primaryKey[1]][1] => $table->primaryKey[1]]);
            $viaLink = $this->generateRelationLink([$table->primaryKey[0] => $fks[$table->primaryKey[0]][1]]);
            $relationName = $this->generateRelationName($relations, $className0, $db->getTableSchema($table0), $table->primaryKey[1], true);
            $relations[$className0][$relationName] = [
                "return \$this->hasMany($className1::className(), $link)->viaTable('{$table->name}', $viaLink);",
                $className0,
                true,
            ];

            $link = $this->generateRelationLink([$fks[$table->primaryKey[0]][1] => $table->primaryKey[0]]);
            $viaLink = $this->generateRelationLink([$table->primaryKey[1] => $fks[$table->primaryKey[1]][1]]);
            $relationName = $this->generateRelationName($relations, $className1, $db->getTableSchema($table1), $table->primaryKey[0], true);
            $relations[$className1][$relationName] = [
                "return \$this->hasMany($className0::className(), $link)->viaTable('{$table->name}', $viaLink);",
                $className1,
                true,
            ];
        }

        return $relations;
    }

    /**
     * Generates the link parameter to be used in generating the relation declaration.
     *
     * @param array $refs reference constraint
     *
     * @return string the generated link parameter.
     */
    protected function generateRelationLink($refs)
    {
        $pairs = [];
        foreach ($refs as $a => $b) {
            $pairs[] = "'$a' => '$b'";
        }

        return '[' . implode(', ', $pairs) . ']';
    }

    /**
     * Checks if the given table is a pivot table.
     * For simplicity, this method only deals with the case where the pivot contains two PK columns,
     * each referencing a column in a different table.
     *                       or false if the table is not a pivot table.
     *
     * @param [[@doctodo param_type:table]] $table [[@doctodo param_description:table]]
     *
     * @return array|boolean the relevant foreign key constraint information if the table is a pivot table,
     */
    protected function checkPivotTable($table)
    {
        $pk = $table->primaryKey;
        if (count($pk) !== 2) {
            return false;
        }
        $fks = [];
        foreach ($table->foreignKeys as $refs) {
            if (count($refs) === 2) {
                if (isset($refs[$pk[0]])) {
                    $fks[$pk[0]] = [$refs[0], $refs[$pk[0]]];
                } elseif (isset($refs[$pk[1]])) {
                    $fks[$pk[1]] = [$refs[0], $refs[$pk[1]]];
                }
            }
        }
        if (count($fks) === 2 && $fks[$pk[0]][0] !== $fks[$pk[1]][0]) {
            return $fks;
        } else {
            return false;
        }
    }

    /**
     * Generate a relation name for the specified table and a base name.
     *
     * @param array               $relations the relations being generated currently.
     * @param string              $className the class name that will contain the relation declarations
     * @param \yii\db\TableSchema $table     the table schema
     * @param string              $key       a base name that the relation name may be generated from
     * @param boolean             $multiple  whether this is a has-many relation
     *
     * @return string the relation name
     */
    protected function generateRelationName($relations, $className, $table, $key, $multiple)
    {
        if (strcasecmp(substr($key, -2), 'id') === 0 && strcasecmp($key, 'id')) {
            $key = rtrim(substr($key, 0, -2), '_');
        }
        if ($multiple) {
            $key = Inflector::pluralize($key);
        }
        $name = $rawName = Inflector::id2camel($key, '_');
        $i = 0;
        while (isset($table->columns[lcfirst($name)])) {
            $name = $rawName . ($i++);
        }
        while (isset($relations[$className][lcfirst($name)])) {
            $name = $rawName . ($i++);
        }

        return $name;
    }

    /**
     * Validates the [[db]] attribute.
     */
    public function validateDb()
    {
        if (Yii::$app->hasComponent($this->db) === false) {
            $this->addError('db', 'There is no application component named "db".');
        } elseif (!Yii::$app->getComponent($this->db) instanceof Connection) {
            $this->addError('db', 'The "db" application component must be a DB connection instance.');
        }
    }

    /**
     * Validates the [[ns]] attribute.
     */
    public function validateNamespace()
    {
        $this->ns = ltrim($this->ns, '\\');
        $path = Yii::getAlias('@' . str_replace('\\', '/', $this->ns), false);
        if ($path === false) {
            $this->addError('ns', 'Namespace must be associated with an existing directory.');
        }
    }

    /**
     * Validates the [[modelClass]] attribute.
     */
    public function validateModelClass()
    {
        if ($this->isReservedKeyword($this->modelClass)) {
            $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.');
        }
        if (strpos($this->tableName, '*') === false && $this->modelClass == '') {
            $this->addError('modelClass', 'Model Class cannot be blank.');
        }
    }

    /**
     * Validates the [[tableName]] attribute.
     *
     * @return [[@doctodo return_type:validateTableName]] [[@doctodo return_description:validateTableName]]
     */
    public function validateTableName()
    {
        if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) {
            $this->addError('tableName', 'At most one asterisk is allowed.');

            return;
        }
        $tables = $this->getTableNames();
        if (empty($tables)) {
            $this->addError('tableName', "Table '{$this->tableName}' does not exist.");
        } else {
            foreach ($tables as $table) {
                $class = $this->generateClassName($table);
                if ($this->isReservedKeyword($class)) {
                    $this->addError('tableName', "Table '$table' will generate a class which is a reserved PHP keyword.");
                    break;
                }
            }
        }
    }

    /**
     * @var [[@doctodo var_type:_tableNames]] [[@doctodo var_description:_tableNames]]
     */
    private $_tableNames;
    /**
     * @var [[@doctodo var_type:_classNames]] [[@doctodo var_description:_classNames]]
     */
    private $_classNames;

    /**
     * Get table names.
     *
     * @return array the table names that match the pattern specified by [[tableName]].
     */
    protected function getTableNames()
    {
        if ($this->_tableNames !== null) {
            return $this->_tableNames;
        }
        $db = $this->getDbConnection();
        if ($db === null) {
            return [];
        }
        $tableNames = [];
        if (strpos($this->tableName, '*') !== false) {
            if (($pos = strrpos($this->tableName, '.')) !== false) {
                $schema = substr($this->tableName, 0, $pos);
                $pattern = '/^' . str_replace('*', '\w+', substr($this->tableName, $pos + 1)) . '$/';
            } else {
                $schema = '';
                $pattern = '/^' . str_replace('*', '\w+', $this->tableName) . '$/';
            }

            foreach ($db->schema->getTableNames($schema) as $table) {
                if (preg_match($pattern, $table)) {
                    $tableNames[] = $schema === '' ? $table : ($schema . '.' . $table);
                }
            }
        } elseif (($table = $db->getTableSchema($this->tableName, true)) !== null) {
            $tableNames[] = $this->tableName;
            $this->_classNames[$this->tableName] = $this->modelClass;
        }

        return $this->_tableNames = $tableNames;
    }

    /**
     * Generates a class name from the specified table name.
     *
     * @param string $tableName the table name (which may contain schema prefix)
     *
     * @return string the generated class name
     */
    protected function generateClassName($tableName)
    {
        if (isset($this->_classNames[$tableName])) {
            return $this->_classNames[$tableName];
        }

        if (($pos = strrpos($tableName, '.')) !== false) {
            $tableName = substr($tableName, $pos + 1);
        }

        $db = $this->getDbConnection();
        $patterns = [];
        if (strpos($this->tableName, '*') !== false) {
            $pattern = $this->tableName;
            if (($pos = strrpos($pattern, '.')) !== false) {
                $pattern = substr($pattern, $pos + 1);
            }
            $patterns[] = '/^' . str_replace('*', '(\w+)', $pattern) . '$/';
        }
        if (!empty($db->tablePrefix)) {
            $patterns[] = "/^{$db->tablePrefix}(.*?)$/";
            $patterns[] = "/^(.*?){$db->tablePrefix}$/";
        } else {
            $patterns[] = "/^tbl_(.*?)$/";
        }

        $className = $tableName;
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $tableName, $matches)) {
                $className = $matches[1];
                break;
            }
        }

        return $this->_classNames[$tableName] = Inflector::id2camel($className, '_');
    }

    /**
     * Get search models.
     *
     * @return [[@doctodo return_type:getSearchModels]] [[@doctodo return_description:getSearchModels]]
     */
    public function getSearchModels()
    {
        return ['@cascade/models' => 'cascade\models'];
    }

    /**
     * Get model map.
     *
     * @return [[@doctodo return_type:getModelMap]] [[@doctodo return_description:getModelMap]]
     */
    public function getModelMap()
    {
        $m = [];
        $search = [];
        foreach ($this->searchModels as $path => $namespace) {
            $files = FileHelper::findFiles(Yii::getAlias($path), ['only' => ['.php']]);
            foreach ($files as $file) {
                $baseName = strstr(basename($file), '.php', true);
                $className = $namespace . '\\' . $baseName;
                if (class_exists($className)) {
                    $reflector = new \ReflectionClass($className);
                    if ($reflector->isSubclassOf('yii\base\Model')) {
                        $m[$baseName] = $className;
                    }
                }
            }
        }

        return $m;
    }
    /**
     * Get db connection.
     *
     * @return Connection the DB connection as specified by [[db]].
     */
    protected function getDbConnection()
    {
        return Yii::$app->{$this->db};
    }
}


<?php

/**
 * ItePHP: Framework PHP (http://itephp.com)
 * Copyright (c) NewClass (http://newclass.pl)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the file LICENSE
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) NewClass (http://newclass.pl)
 * @link          http://itephp.com ItePHP Project
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */

namespace ItePHP\Component\Form;

use ItePHP\Core\FileNotUploadedException;
use ItePHP\Core\Request;
use ItePHP\Core\ValidatorService;

/**
 * Generator form. Support for mapping data, validation and generate html code.
 *
 * @author Michal Tomczak (michal.tomczak@itephp.com)
 */
class FormBuilder{

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

    /**
     * @var FormField[]
     */
	private $fields=[];

    /**
     * @var string[]
     */
	private $formTags=[];

    /**
     * @var string[]
     */
	private $submitTags=[];

    /**
     * @var ValidatorService
     */
	private $validatorService;

    /**
     * @var bool
     */
	private $isConfirmed=false;

    /**
     * @var Designer
     */
	private $designer;

    /**
     * @var FormFormatter
     */
	private $transformer;

    /**
     * FormBuilder constructor.
     */
	public function __construct(){
		$this->formatter=new BasicFormFormatter();
		$this->transformer=new BasicFormTransformer();
		$this->formTags=[
			'method'=>'post'
			,'id'=>null
			,'class'=>null
			,'enctype'=>null
        ];

		$this->submitTags=[
			'value'=>'Apply'
			,'id'=>null
			,'class'=>null
        ];
	}

	/**
	 * Set service to validate field data
	 *
	 * @param ValidatorService $validatorService
	 */
	public function setValidatorService(ValidatorService $validatorService){
		$this->validatorService=$validatorService;
	}

	/**
	 * Set formatter with html rule pattern
	 *
	 * @param FormFormatter $formatter
	 */
	public function setFormatter(FormFormatter $formatter){
		$this->formatter=$formatter;
	}

	/**
	 * Set designer with rule to generate fields
	 *
	 * @param Designer $designer
	 */
	public function setDesigner(Designer $designer){
		$this->designer=$designer;
		$this->designer->build($this);
	}

	/**
	 * Set transformer with rule to encode/decode data
	 *
	 * @param Transformer $transformer
	 */
	public function setTransformer(Transformer $transformer){
		$this->transformer=$transformer;
	}

	/**
	 * Get transformer with rule to encode/decode data
	 *
	 * @return Transformer
	 */
	public function getTransformer(){
		return $this->transformer;
	}

	/**
	 * Set form tags
	 *
	 * @param string[] $tags - array with data (all field is optional):
	 * [
	 *	'method'=>'post' //"post" or "get"
	 *	,'id'=>'id1' //html tag id
	 *	,'class'=>'class1' //html tag class
	 *	,'enctype'=>'multipart/form-data' //html tag enctype eg. "text/plain", "multipart/form-data" or "application/x-www-form-urlencoded"
	 *	]
	 */
	public function setFormTags($tags){
		$this->formTags=array_merge($this->formTags,$tags);
	}

	/**
	 * Set submit button tags
	 *
	 * @param string[] $tags - array with data:
	 * [
	 * 	'value'=> 'Apply' //label button, default: Apply
	 * 	,'id'=>'id1' //html tag id
	 * 	,'class'=>'class1' //html tag class
	 * ]
	 */
	public function setSubmitTags($tags){
		$this->submitTags=array_merge($this->submitTags,$tags);
	}

	/**
	 * Add form field
	 *
	 * @param FormField $field
	 */
	public function addField(FormField $field){

		$this->fields[]=$field;

		if($field->getName()==null){
			$field->setName('name_'.count($this->fields));
		}

		if($field->getId()==null){
			$field->setId('id_'.count($this->fields));
		}

		if($field instanceof FileField){
			$this->formTags['enctype']='multipart/form-data';
		}

	}

	/**
	 * Remove field from generator
	 *
	 * @param string $name - field name
	 */
	public function removeField($name){
		for($i=0; $i<count($this->fields); $i++){
			if($this->fields[$i]->getName()==$name){
				array_splice($this->fields,$i,1);
				break;
			}
		}
	}

	/**
	 * Generate html form string
	 *
	 * @return string - with html form
	 */
	public function render(){
		$html=$this->formatter->renderFormBegin($this->formTags);
		foreach($this->fields as $field){
			$html.=$this->formatter->renderField($field);
		}

		$html.=$this->renderSubmit();
		$html.=$this->renderEnd();
		return $html;
	}

	/**
	 * Generate html string for fields
	 *
	 * @return string with html fields
	 */
	public function renderFields(){
		$html='';

		foreach($this->fields as $field){
			$html.=$this->formatter->renderField($field);
		}

		return $html;
	}

	/**
	 * Generate html string for selected field
	 *
	 * @param string $name - field name
	 * @return string with html field
	 */
	public function renderField($name){
		$html='';

		$field=$this->getField($name);
		$html.=$this->formatter->renderField($field);

		return $html;
	}

	/**
	 * Generate html string for open form tag
	 *
	 * @return string - with html open form tag
	 */
	public function renderBegin(){
		return $this->formatter->renderFormBegin($this->formTags);
	}

	/**
	 * Generate html string for close form tag
	 *
	 * @return string - with html close form tag
	 */
	public function renderEnd(){
		return $this->formatter->renderFormEnd();
	}

	/**
	 * Generate html string for open form submit
	 *
	 * @return string - with html open form submit
	 */
	public function renderSubmit(){
		return $this->formatter->renderSubmit($this->submitTags);
	}

	/**
	 * @return string - with html form
	 */
	public function __toString(){
		return $this->render();
	}

    /**
     * Get field object
     *
     * @param string $name - field name (html name tag)
     * @return FormField
     * @throws FieldNotFoundException
     */
	public function getField($name){
		foreach($this->fields as $field){
			if($field->getName()==$name)
				return $field;
		}

		throw new FieldNotFoundException($name);
	}

	/**
	 * Get all fields object
	 *
	 * @return FormField[]
	 */
	public function getFields(){
		return $this->fields;
	}

	/**
	 * Check confirmed form (clicked submit button in frontend/mailed fields value)
	 *
	 * @return boolean - if success then true else false
	 */
	public function isConfirmed(){
		return $this->isConfirmed;
	}

	/**
	 * Check valid form
	 *
	 * @return boolean - if success then true else false
	 * @since 0.13.0
	 */
	public function isValid(){
		if(!$this->isConfirmed())
			return false;

		$errors=$this->getErrors();

		return count($errors)==0;

	}

	/**
	 * Set default values for fields
	 *
	 * @param mixed[] $data eg:
	 * [
	 * '{text field name 1}'=>'{text value name 1}'
	 * ,'{text field name 2}'=>'{text value name 2}'
	 * ]
	 */
	public function setData($data){
		$data=$this->transformer->encode($data);
		foreach($this->fields as $field){
			if(isset($data[$field->getName()])){
				$field->setData($data[$field->getName()]);
			}
		}
	}

    /**
     * Get data from fields
     * @return mixed[]
     * @throws \Exception
     */
	public function getData(){
		$data=[];
		foreach($this->fields as $field){
			if(preg_match('/^(.*?)(\[.*\])$/',$field->getName(),$result)){
				if($result[2]==''){
					//FIXME autoincrement field
				}
				else{

					if(!preg_match_all("/\[(.*?)\]/", $result[2], $resultDeep)){
						throw new \Exception('Invalid field name.');//FIXME dedicate exception
					}
					$storage=&$data[$result[1]];
					foreach($resultDeep[1] as $deep){
						if(!isset($storage[$deep])){
							$storage[$deep]=[];
						}
						$storage=&$storage[$deep];
					}
					$storage=$field->getData();
				}
			}
			else{
				$data[$field->getName()]=$field->getData();
			}
		}

		return $this->transformer->decode($data);
	}

	/**
	 * Remove all field data
	 */
	public function clearData(){
		foreach($this->fields as $field){
			$field->clearData();
		}
	}

    /**
     * Submit form. Check http confirm and validate fields
     *
     * @param Request $request
     * @throws \Exception
     */
	public function submit(Request $request){
		$this->isConfirmed=false;

		if($this->formTags['method']=='post' && $request->getType()=='POST'){
			$this->isConfirmed=true;
		}

		$query=$request->getQuery();
		if(count($this->fields)>0 && $this->formTags['method']=='get' && isset($query[$this->fields[0]->getName()])){
			$this->isConfirmed=true;
		}

		if(!$this->isConfirmed)
			return;

		if($this->formTags['method']=='post'){
			$storage=$request->getData();
		}
		else{
			$storage=$request->getQuery();
		}

		//set field data

		$result=[];
		foreach($this->fields as $field){

			if(isset($storage[$field->getName()])){
				$field->setData($storage[$field->getName()]);
			}
			else if($field instanceof FileField){
				try{
					$field->setData($request->getFile($field->getName()));
				}
				catch(FileNotUploadedException $e){
					$field->setData('');
				}
			}
			else if(preg_match('/^(.*?)(\[.*\])$/',$field->getName(),$result) && isset($storage[$result[1]])){//array
				if(!preg_match_all("/\[(.*?)\]/", $result[2], $resultDeep)){
					throw new \Exception('Invalid field name.');//FIXME dedicate exception
				}

				$value=$storage[$result[1]];
				foreach($resultDeep[1] as $deep){
					if(!isset($value[$deep])){
						$value=null;
						break;
					}
					$value=$value[$deep];
				}

				if($result[2]==''){
					//FIXME autoincrement field
				}
				else{
					$field->setData($value);
				}
			}
			else{//for checkbox or disabled field
				$field->setData(null);
			}
		}

		//validate
		if($request->isFullUploadedData()){
			foreach($this->fields as $field){
				if($field->getValidator()){
					if($error=$field->getValidator()->validate($field->getData())){
						$field->setError($error);
					}

				}
			}
		}
		else{
			foreach($this->fields as $field){
				$field->setError('Request data is too large.');
			}
		}
	}

	/**
	 * Validate fields and get errors
	 *
	 * @return array - with errors if success then empty array
	 */
	public function getErrors(){
		$errors=[];
		foreach($this->fields as $field){
			if(!$field->isValid()){
				$errors[]=['field'=>$field->getLabel(),'message'=>$field->getError()];
			}
		}

		return $errors;
	}
}


<?php
/**
 * User: Alex Gusev <alex@flancer64.com>
 */

namespace Praxigento\BonusHybrid\Service\Calc\Forecast\Z;

use Praxigento\BonusBase\Service\Period\Calc\IAdd as PPeriodAdd;

/**
 * Local process to register new period & calculation for forecast calculations (plain & compressed).
 */
class Register
    implements \Praxigento\Core\Api\App\Service\Process
{
    /** string  */
    const IN_CALC_TYPE_CODE = 'calcTypeCode';
    const IN_PERIOD = 'period';

    /** int */
    const OUT_CALC_ID = 'calcId';

    /** @var  \Praxigento\Core\Api\Helper\Period */
    private $hlpPeriod;
    /** @var \Praxigento\BonusBase\Service\Period\Calc\IAdd */
    private $procPeriodAdd;

    public function __construct(
        \Praxigento\Core\Api\Helper\Period $hlpPeriod,
        \Praxigento\BonusBase\Service\Period\Calc\IAdd $procPeriodAdd
    )
    {
        $this->hlpPeriod = $hlpPeriod;
        $this->procPeriodAdd = $procPeriodAdd;
    }

    public function exec(\Praxigento\Core\Data $ctx)
    {
        /* get working data from context */
        $calcTypeCode = $ctx->get(self::IN_CALC_TYPE_CODE);
        $period = $ctx->get(self::IN_PERIOD);

        /* get calculation period (begin, end dates) */
        list($dateFrom, $dateTo) = $this->getPeriod($period);

        /* register new calculation for period */
        $calcId = $this->registerNewCalc($dateFrom, $dateTo, $calcTypeCode);

        $result = new \Praxigento\Core\Data();
        $result->set(self::OUT_CALC_ID, $calcId);
        return $result;
    }

    /**
     * Return 2 dates (period being/end): first day of the month and yesterday.
     *
     * @param string|null $period 'YYYYMM'
     * @return array
     */
    private function getPeriod($period)
    {
        /* get current month as MONTH period */
        if (!$period) {
            $month = $this->hlpPeriod->getPeriodCurrent(null, 0, \Praxigento\Core\Api\Helper\Period::TYPE_MONTH);
            /* get current date then get yesterday date as end of period */
            $begin = $this->hlpPeriod->getPeriodFirstDate($month);
            $today = $this->hlpPeriod->getPeriodCurrent();
            $end = $this->hlpPeriod->getPeriodPrev($today);
        } else {
            $month = $period;
            $begin = $this->hlpPeriod->getPeriodFirstDate($month);
            $end = $this->hlpPeriod->getPeriodLastDate($month);
        }
        $result = [$begin, $end];
        return $result;
    }

    /**
     * Register new period and related calculation.
     *
     * @param string $from begin of the period (YYYYMMDD)
     * @param string $to end of the period (YYYYMMDD)
     * @param string $calcTypeCode
     * @return int registered calculation ID
     * @throws \Exception
     */
    private function registerNewCalc($from, $to, $calcTypeCode)
    {
        $result = null;
        $ctx = new \Praxigento\Core\Data();
        $ctx->set(PPeriodAdd::CTX_IN_DSTAMP_BEGIN, $from);
        $ctx->set(PPeriodAdd::CTX_IN_DSTAMP_END, $to);
        $ctx->set(PPeriodAdd::CTX_IN_CALC_TYPE_CODE, $calcTypeCode);
        $this->procPeriodAdd->exec($ctx);
        $success = $ctx->get(PPeriodAdd::CTX_OUT_SUCCESS);
        if ($success) {
            $result = $ctx->get(PPeriodAdd::CTX_OUT_CALC_ID);
        }
        return $result;
    }
}


<?php
/**
 * @copyright   2010-2014, The Titon Project
 * @license     http://opensource.org/licenses/bsd-license.php
 * @link        http://titon.io
 */

namespace Titon\Db\Sqlite;

use Titon\Db\Driver\AbstractPdoDriver;
use Titon\Db\Driver\Type;
use Titon\Db\Driver\Type\AbstractType;

/**
 * A driver that represents the SQLite database and uses PDO.
 *
 * @package Titon\Db\Sqlite
 */
class SqliteDriver extends AbstractPdoDriver {

    /**
     * Configuration.
     *
     * @type array {
     *         @type string $path    Path to database file
     *         @type bool $memory    Toggle between in memory only database
     * }
     */
    protected $_config = [
        'path' => '',
        'memory' => false
    ];

    /**
     * Set the dialect.
     */
    public function initialize() {
        $this->setDialect(new SqliteDialect($this));
    }

    /**
     * {@inheritdoc}
     *
     * @uses Titon\Db\Type\AbstractType
     */
    public function describeTable($table) {
        return $this->cacheQuery([__METHOD__, $table], function(SqliteDriver $driver) use ($table) {
            $columns = $driver->executeQuery('PRAGMA table_info("' . $table  . '");')->find();
            $schema = [];

            if (!$columns) {
                return $schema;
            }

            foreach ($columns as $column) {
                $field = $column['name'];
                $type = strtolower($column['type']);
                $length = '';

                // Determine type and length
                if (preg_match('/([a-z]+)(?:\(([0-9,]+)\))?/is', $type, $matches)) {
                    $type = strtolower($matches[1]);

                    if (isset($matches[2])) {
                        $length = $matches[2];
                    }
                }

                // Inherit type defaults
                $data = $driver->getType($type)->getDefaultOptions();

                // Overwrite with custom
                $data = [
                    'field' => $field,
                    'type' => $type,
                    'length' => $length,
                    'null' => !$column['notnull'],
                    'default' => $column['dflt_value']
                ] + $data;

                if ($column['pk']) {
                    $data['primary'] = true;
                    $data['ai'] = true;
                }

                if ($data['default'] === 'NULL') {
                    $data['default'] = null;
                }

                $schema[$field] = $data;
            }

            return $schema;
        }, '+1 year');
    }

    /**
     * {@inheritdoc}
     */
    public function getDriver() {
        return 'sqlite';
    }

    /**
     * {@inheritdoc}
     */
    public function getDsn() {
        if ($dsn = $this->getConfig('dsn')) {
            return $dsn;
        }

        $dsn = $this->getDriver() . ':';

        if ($path = $this->getConfig('path')) {
            $dsn .= $path;

        } else if ($this->getConfig('memory')) {
            $dsn .= ':memory:';
        }

        return $dsn;
    }

    /**
     * {@inheritdoc}
     */
    public function getSupportedTypes() {
        return [
            'string' => 'Titon\Db\Driver\Type\StringType',
            'real' => 'Titon\Db\Driver\Type\FloatType'
        ] + parent::getSupportedTypes();
    }

    /**
     * {@inheritdoc}
     */
    public function isEnabled() {
        return extension_loaded('pdo_sqlite');
    }

    /**
     * {@inheritdoc}
     */
    public function listTables($database = null) {
        $database = $database ?: $this->getDatabase();

        return $this->cacheQuery([__METHOD__, $database], function(SqliteDriver $driver) use ($database) {
            $tables = $driver->executeQuery('SELECT * FROM sqlite_master WHERE type = ?;', ['table'])->find();
            $schema = [];

            foreach ($tables as $table) {
                if ($table['name'] === 'sqlite_sequence') {
                    continue;
                }

                $schema[] = $table['name'];
            }

            return $schema;
        }, '+1 year');
    }

    /**
     * {@inheritdoc}
     */
    public function newQuery($string) {
        return new SqliteQuery($string);
    }

}


<?php
/**
 * User: Alex Gusev <alex@flancer64.com>
 */

namespace Praxigento\BonusHybrid\Service\Calc\Compress;

use Praxigento\BonusBase\Api\Service\Period\Calc\Get\Dependent\Request as AGetPeriodRequest;
use Praxigento\BonusBase\Api\Service\Period\Calc\Get\Dependent\Response as AGetPeriodResponse;
use Praxigento\BonusHybrid\Config as Cfg;
use Praxigento\BonusHybrid\Repo\Data\Downline as EBonDwnl;
use Praxigento\BonusHybrid\Service\Calc\Bonus\Z\Proc\Compress\Phase1 as PPhase1;
use Praxigento\BonusHybrid\Service\Calc\Compress\Z\Repo\Query\GetPhase1Pv as QBldGetPv;
use Praxigento\Downline\Repo\Query\Snap\OnDate\Builder as QBSnap;

class Phase1
    implements \Praxigento\Core\Api\App\Service\Process
{
    /** @var \Praxigento\Downline\Api\Helper\Tree */
    private $hlpDwnlTree;
    /** @var \Praxigento\Core\Api\App\Logger\Main */
    private $logger;
    /** @var \Praxigento\BonusBase\Api\Service\Period\Calc\Get\Dependent */
    private $servPeriodGet;
    /** @var \Praxigento\BonusHybrid\Service\Calc\Bonus\Z\Proc\Compress\Phase1 */
    private $procPhase1;
    /** @var \Praxigento\BonusHybrid\Service\Calc\Compress\Z\Repo\Query\GetPhase1Pv */
    private $qbGetPv;
    /** @var \Praxigento\Downline\Repo\Query\Snap\OnDate\Builder */
    private $qbSnapOnDate;
    /** @var \Praxigento\BonusHybrid\Repo\Dao\Downline */
    private $daoBonDwnl;
    /** @var \Praxigento\BonusBase\Repo\Dao\Calculation */
    private $daoCalc;
    /** @var \Praxigento\BonusBase\Repo\Dao\Rank */
    private $daoRank;
    /** @var \Praxigento\BonusHybrid\Repo\Dao\Compression\Phase1\Transfer\Pv */
    private $daoTransPv;

    public function __construct(
        \Praxigento\Core\Api\App\Logger\Main $logger,
        \Praxigento\Downline\Api\Helper\Tree $hlpDwnlTree,
        \Praxigento\BonusBase\Repo\Dao\Calculation $daoCalc,
        \Praxigento\BonusBase\Repo\Dao\Rank $daoRank,
        \Praxigento\BonusHybrid\Repo\Dao\Downline $daoBonDwnl,
        \Praxigento\BonusHybrid\Repo\Dao\Compression\Phase1\Transfer\Pv $daoTransPv,
        \Praxigento\BonusHybrid\Service\Calc\Compress\Z\Repo\Query\GetPhase1Pv $qbGetPv,
        \Praxigento\Downline\Repo\Query\Snap\OnDate\Builder $qbSnapOnDate,
        \Praxigento\BonusBase\Api\Service\Period\Calc\Get\Dependent $servPeriodGet,
        \Praxigento\BonusHybrid\Service\Calc\Bonus\Z\Proc\Compress\Phase1 $procPhase1
    ) {
        $this->logger = $logger;
        $this->hlpDwnlTree = $hlpDwnlTree;
        $this->daoCalc = $daoCalc;
        $this->daoRank = $daoRank;
        $this->daoBonDwnl = $daoBonDwnl;
        $this->daoTransPv = $daoTransPv;
        $this->qbGetPv = $qbGetPv;
        $this->qbSnapOnDate = $qbSnapOnDate;
        $this->servPeriodGet = $servPeriodGet;
        $this->procPhase1 = $procPhase1;
    }

    /**
     * Wrapper for compression sub-process.
     *
     * @param array $dwnlSnap see \Praxigento\Downline\Repo\Query\Snap\OnDate\Builder
     * @param array $pv [custId => pv]
     * @param int $calcId
     * @return array [$updates, $pvTransfers]
     */
    private function compress($dwnlSnap, $pv, $calcId)
    {
        $in = new \Praxigento\Core\Data();
        $in->set(PPhase1::IN_DWNL_PLAIN, $dwnlSnap);
        $in->set(PPhase1::IN_PV, $pv);
        $in->set(PPhase1::IN_CALC_ID, $calcId);
        $in->set(PPhase1::IN_KEY_CALC_ID, EBonDwnl::A_CALC_REF);
        $in->set(PPhase1::IN_KEY_CUST_ID, QBSnap::A_CUST_ID);
        $in->set(PPhase1::IN_KEY_PARENT_ID, QBSnap::A_PARENT_ID);
        $in->set(PPhase1::IN_KEY_DEPTH, QBSnap::A_DEPTH);
        $in->set(PPhase1::IN_KEY_PATH, QBSnap::A_PATH);
        $in->set(PPhase1::IN_KEY_PV, EBonDwnl::A_PV);
        $out = $this->procPhase1->exec($in);
        $updates = $out->get(PPhase1::OUT_COMPRESSED);
        $pvTransfers = $out->get(PPhase1::OUT_PV_TRANSFERS);
        $result = [$updates, $pvTransfers];
        return $result;
    }

    public function exec(\Praxigento\Core\Data $ctx)
    {
        /**
         * perform processing
         */
        $ctx->set(self::CTX_OUT_SUCCESS, false);
        $this->logger->info("Phase1 compression is started.");
        /* get dependent calculation data */
        /** @var \Praxigento\BonusBase\Repo\Data\Calculation $writeOffCalc */
        /** @var \Praxigento\BonusBase\Repo\Data\Period $compressPeriod */
        /** @var \Praxigento\BonusBase\Repo\Data\Calculation $compressCalc */
        list($writeOffCalc, $compressPeriod, $compressCalc) = $this->getCalcData();
        $compressPeriodId = $compressPeriod->getId();
        $dsBegin = $compressPeriod->getDstampBegin();
        $dsEnd = $compressPeriod->getDstampEnd();
        $writeOffCalcId = $writeOffCalc->getId();
        $compressCalcId = $compressCalc->getId();
        $this->logger->info("Phase1 compression period #$compressPeriodId ($dsBegin-$dsEnd)");
        /* load source data for calculation */
        /* TODO: move source data collection into sub-class */
        $dwnlSnap = $this->getDownlineSnapshot($dsEnd);
        $dataPv = $this->getPv($writeOffCalcId);
        /** @var \Praxigento\Downline\Repo\Data\Snap[] $updates */
        /** @var \Praxigento\BonusHybrid\Repo\Data\Compression\Phase1\Transfer\Pv[] $pvTransfers */
        list($updates, $pvTransfers) = $this->compress($dwnlSnap, $dataPv, $compressCalcId);
        /* save compressed downline & PV transfers into DB */
        $this->saveBonusDownline($updates, $compressCalcId);
        $this->savePvTransfers($pvTransfers);
        /* mark this calculation complete */
        $this->daoCalc->markComplete($compressCalcId);
        /* mark process as successful */
        $ctx->set(self::CTX_OUT_SUCCESS, true);
        $this->logger->info("Phase1 compression is completed.");
    }

    /**
     * Get data for calculations/periods.
     *
     * @return array [$writeOffCalc, $compressPeriod, $compressCalc]
     * @throws \Exception
     */
    private function getCalcData()
    {
        /**
         * Get data for compression & PV write off calculations.
         */
        $req = new AGetPeriodRequest();
        $req->setBaseCalcTypeCode(Cfg::CODE_TYPE_CALC_PV_WRITE_OFF);
        $req->setDepCalcTypeCode(Cfg::CODE_TYPE_CALC_COMPRESS_PHASE1);
        /** @var AGetPeriodResponse $resp */
        $resp = $this->servPeriodGet->exec($req);
        /** @var \Praxigento\BonusBase\Repo\Data\Calculation $compressCalc */
        $writeOffCalc = $resp->getBaseCalcData();
        /** @var \Praxigento\BonusBase\Repo\Data\Period $compressPeriod */
        $compressPeriod = $resp->getDepPeriodData();
        /** @var \Praxigento\BonusBase\Repo\Data\Calculation $depCalcData */
        $compressCalc = $resp->getDepCalcData();
        /**
         * Compose result.
         */
        $result = [$writeOffCalc, $compressPeriod, $compressCalc];
        return $result;
    }

    /**
     * Get ID for default rank.
     *
     * @return int
     */
    private function getDefaultRankId()
    {
        $result = $this->daoRank->getIdByCode(Cfg::RANK_DISTRIBUTOR);
        return $result;
    }

    /**
     * Get Downline Tree snapshot on the $datestamp. Result is an array [$customerId => [...], ...]
     *
     * @param $dateOn 'YYYYMMDD'
     *
     * @return array|null
     */
    private function getDownlineSnapshot($dateOn)
    {
        /* collect downline data to given date */
        $query = $this->qbSnapOnDate->build();
        $conn = $query->getConnection();
        $bind = [$this->qbSnapOnDate::BND_ON_DATE => $dateOn];
        $result = $conn->fetchAll($query, $bind);
        return $result;
    }

    /**
     * Get PV that are debited inside 'PV Write Off' operation related for the $calcId.
     * @param int $calcId
     * @return array
     */
    private function getPv($calcId)
    {
        $query = $this->qbGetPv->build();
        $conn = $query->getConnection();
        $bind = [QBldGetPv::BND_CALC_ID => $calcId];
        $data = $conn->fetchAll($query, $bind);
        $result = $this->hlpDwnlTree->mapValueById($data, QBldGetPv::A_CUST_ID, QBldGetPv::A_PV);
        return $result;
    }

    /**
     * @param array $snap snap data with PV (see
     *     \Praxigento\BonusHybrid\Service\Calc\Bonus\Z\Proc\Compress\Phase1::populateCompressedSnapWithPv)
     * @param int $calcId
     */
    private function saveBonusDownline($snap, $calcId)
    {
        /* 'distributor' is the minimal rank in compressed trees */
        $rankId = $this->getDefaultRankId();

        foreach ($snap as $one) {
            /* get working vars */
            $custId = $one[QBSnap::A_CUST_ID];
            $depth = $one[QBSnap::A_DEPTH];
            $parentId = $one[QBSnap::A_PARENT_ID];
            $path = $one[QBSnap::A_PATH];
            $pv = $one[EBonDwnl::A_PV];
            /* compose new entity to save */
            $entity = new EBonDwnl();
            $entity->setCalculationRef($calcId);
            $entity->setCustomerRef($custId);
            $entity->setDepth($depth);
            $entity->setOv(0);
            $entity->setParentRef($parentId);
            $entity->setPath($path);
            $entity->setPv($pv);
            $entity->setRankRef($rankId);
            $entity->setTv(0);
            $this->daoBonDwnl->create($entity);
        }
    }

    /**
     * @param \Praxigento\BonusHybrid\Repo\Data\Compression\Phase1\Transfer\Pv[] $data
     */
    private function savePvTransfers($data)
    {
        /** @var \Praxigento\BonusHybrid\Repo\Data\Compression\Phase1\Transfer\Pv $one */
        foreach ($data as $one) {
            $this->daoTransPv->create($one);
        }
    }
}


<?php namespace Ordercloud\Cart\Entities\Policies;

use Ordercloud\Cart\Entities\CartItem;
use Ordercloud\Cart\Exceptions\MaxMerchantsException;

class LimitedMerchantCartPolicy extends BaseCartPolicy implements CartPolicy
{
    /**
     * @var int
     */
    private $maxNrMerchants;

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

    public function add(CartItem $newItem, array $items)
    {
        $newItems = parent::add($newItem, $items);

        if ($this->getMerchantCount($newItems) > $this->maxNrMerchants) {
            throw new MaxMerchantsException($newItem, $this->maxNrMerchants);
        }

        return $newItems;
    }

    /**
     * @param array|CartItem[] $items
     *
     * @return int
     */
    protected function getMerchantCount(array $items)
    {
        $merchants = [];

        foreach ($items as $item) {
            $merchants[] = $item->getProduct()->getOrganisation()->getId();
        }

        $uniqueMerchants = array_unique($merchants);

        return count($uniqueMerchants);
    }
}


<?php

namespace Isolate\LazyObjects\Proxy\Adapter\OcramiusProxyManager\Factory;

use Isolate\LazyObjects\Exception\InvalidArgumentException;
use Isolate\LazyObjects\Proxy\Adapter\OcramiusProxyManager\ProxyGenerator\LazyObjectsProxyGenerator;
use Isolate\LazyObjects\Proxy\LazyProperty;
use Isolate\LazyObjects\Proxy\MethodReplacement;
use ProxyManager\Configuration;
use ProxyManager\Generator\ClassGenerator;
use ProxyManager\Version;
use ReflectionClass;

/**
 * Factory responsible of producing proxy lazy objects
 */
class LazyObjectsFactory
{
    const GENERATE_NEVER = 0;
    const GENERATE_WHEN_NOT_EXISTS = 1;
    const GENERATE_ALWAYS = 2;
    /**
     * @var \ProxyManager\Configuration
     */
    private $configuration;

    /**
     * Cached checked class names
     *
     * @var string[]
     */
    private $checkedClasses = array();

    /**
     * @var LazyObjectsProxyGenerator|null
     */
    private $generator;

    /**
     * @var int
     */
    private $generationStrategy;

    /**
     * @param \ProxyManager\Configuration $configuration
     * @param int $generationStrategy
     */
    public function __construct(Configuration $configuration = null, $generationStrategy = self::GENERATE_WHEN_NOT_EXISTS)
    {
        if ($generationStrategy !== self::GENERATE_NEVER && $generationStrategy !== self::GENERATE_WHEN_NOT_EXISTS
            && $generationStrategy !== self::GENERATE_ALWAYS) {
            throw new \InvalidArgumentException("Invalid generation strategy.");
        }

        $this->configuration = $configuration ?: new Configuration();
        $this->generator = new LazyObjectsProxyGenerator();
        $this->generationStrategy = $generationStrategy;
    }

    /**
     * @param $instance
     * @param array $lazyProperties
     * @param array $methodReplacements
     * @return \Isolate\LazyObjects\WrappedObject
     * @throws InvalidArgumentException
     */
    public function createProxy($instance, array $lazyProperties = array(), array $methodReplacements = [])
    {
        foreach ($lazyProperties as $lazyProperty) {
            if (!$lazyProperty instanceof LazyProperty) {
                throw new InvalidArgumentException("Lazy property needs to be an instance of Isolate\\LazyObjects\\Proxy\\LazyProperty");
            }
        }

        foreach ($methodReplacements as $methodReplacement) {
            if (!$methodReplacement instanceof MethodReplacement) {
                throw new InvalidArgumentException("Method replacement needs to be an instance of Isolate\\LazyObjects\\Proxy\\MethodReplacement");
            }
        }

        $proxyClassName = $this->generateProxy(get_class($instance));

        return new $proxyClassName($instance, $lazyProperties, $methodReplacements);
    }

    /**
     * Return proxy class name
     *
     * @param string $className
     * @return string
     */
    public function createProxyClass($className)
    {
        return $this->generateProxy($className);
    }

    /**
     * Generate a proxy from a class name
     * @param  string $className
     * @return string proxy class name
     */
    private function generateProxy($className)
    {
        if (isset($this->checkedClasses[$className])) {
            return $this->checkedClasses[$className];
        }

        $proxyParameters = array(
            'className'           => $className,
            'factory'             => get_class($this),
            'proxyManagerVersion' => Version::VERSION
        );
        $proxyClassName  = $this
            ->configuration
            ->getClassNameInflector()
            ->getProxyClassName($className, $proxyParameters);

        $this->generateProxyClass($proxyClassName, $className, $proxyParameters);

        $this
            ->configuration
            ->getSignatureChecker()
            ->checkSignature(new ReflectionClass($proxyClassName), $proxyParameters);

        return $this->checkedClasses[$className] = $proxyClassName;
    }

    /**
     * Generates the provided `$proxyClassName` from the given `$className` and `$proxyParameters`
     * @param string $proxyClassName
     * @param string $className
     * @param array  $proxyParameters
     *
     * @return void
     */
    private function generateProxyClass($proxyClassName, $className, array $proxyParameters)
    {
        if ($this->generationStrategy === self::GENERATE_NEVER) {
            return ;
        }

        if ($this->generationStrategy === self::GENERATE_WHEN_NOT_EXISTS && class_exists($proxyClassName)) {
            return ;
        }

        $className = $this->configuration->getClassNameInflector()->getUserClassName($className);
        $phpClass  = new ClassGenerator($proxyClassName);

        $this->generator->generate(new ReflectionClass($className), $phpClass);

        $phpClass = $this->configuration->getClassSignatureGenerator()->addSignature($phpClass, $proxyParameters);

        $this->configuration->getGeneratorStrategy()->generate($phpClass);
        $this->configuration->getProxyAutoloader()->__invoke($proxyClassName);
    }
}


<?php

abstract class SDIS62_Model_Abstract implements SDIS62_Model_Interface_Abstract
{
    /**
     * @var int|string|null $id
     */
    protected $id;

    /**
     * Constructeur (et hydratation de l'entité si le $data est fourni)
     *
     * @param array $data Optionnel
     */
    public function __construct($data = array())
    {
        $this->hydrate($data);
    }

    /**
     * Récupération de l'id de l'entité
     *
     * @return int|string|null
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Définition de l'id de l'entité
     *
     * @param int|string|null $id
     * @return SDIS62_Model_Abstract Interface fluide
     * @throws Zend_Exception Si l'id de l'entité est déjà spécifié (prévention d'une modification de l'identifiant)
     */
    public function setId($id)
    {
        if($this->getId($id) == null)
        {
            $this->id = $id;
        }
        else
        {
            throw new Zend_Exception("L'id d'une entité ne peut pas être modifié");
        }

        return $this;
    }

    /**
     * Extraction de l'entité en un tableau de données
     *
     * @return array
     */
    public function extract()
    {
        $vars = get_object_vars($this);
        $this->extractRecursive($vars);

        // On enlève les données parasites
        unset($vars["__initializer__"]);
        unset($vars["__cloner__"]);
        unset($vars["__isInitialized__"]);

        return $vars;
    }

    /**
     * Fonction permettant l'extraction d'un objet de façon récursive
     * Ignore les attributs commençants par "related_"
     *
     * @param array $array Paramètre passé par référence
     */
    protected function extractRecursive(array &$array)
    {
        foreach($array as $key => &$var)
        {
            if(substr($key, 0, 8) == 'related_')
            {
                unset($array[$key]);
                continue;
            }
            elseif(is_object($var))
            {
                if($var instanceof Doctrine\ORM\PersistentCollection)
                {
                    $var = $var->toArray();

                }
                elseif($var instanceof Doctrine\Common\Collections\ArrayCollection)
                {
                    $var = $var->toArray();
                }
                elseif($var instanceof \DateTime)
                {
                    $var = $var->format('H:i:s') == "00:00:00" ? $var->format('Y-m-d') : $var->format('Y-m-d H:i:s');
                }
                else
                {
                    $var = $var->extract();
                }
            }

            if(is_array($var))
            {
                $this->extractRecursive($var);
            }
        }
    }

    /**
     * Hydratation (remplissage) de l'entité à partir d'un tableau de données
     *
     * @return SDIS62_Model_Abstract Interface fluide
     */
    public function hydrate(array $data)
    {
        $this->hydrateRecursive($data);

        foreach($data as $n => $v)
		{
            if(array_key_exists($n, $this->extract()))
            {
                $this->$n = $v;
            }
        }

		return $this;
    }

    /**
     * Fonction permettant d'hydrater un objet de façon récursive
     *
     * @param array $array Paramètre passé par référence
     */
    private function hydrateRecursive(array &$array)
    {
        if(is_array($array))
        {
            foreach($array as &$item)
            {
                if(is_array($item))
                {
                    $this->hydrateRecursive($item);

                    if(array_key_exists("classname", $item))
                    {
                        $object = new $item["classname"];
                        $object->hydrate($item);
                        $item = $object;
                        unset($object);
                    }
                }
            }
        }
    }

    /**
     * Conversion de l'objet en chaine de caractères
     *
     * @return string
     */
    public function __toString()
    {
        return strval($this->id);
    }
}


<?php
/**
 * This source file is part of Xloit project.
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the MIT License that is bundled with this package in the file LICENSE.
 * It is also available through the world-wide-web at this URL:
 * <http://www.opensource.org/licenses/mit-license.php>
 * If you did not receive a copy of the license and are unable to obtain it through the world-wide-web,
 * please send an email to <license@xloit.com> so we can send you a copy immediately.
 *
 * @license   MIT
 * @link      http://xloit.com
 * @copyright Copyright (c) 2016, Xloit. All rights reserved.
 */

namespace Xloit\Bridge\Zend\Form;

use Xloit\Std\ArrayUtils;
use Zend\Form\Form as AbstractForm;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\InputFilterInterface;
use Zend\InputFilter\InputFilterProviderInterface;

/**
 * A {@link Form} class.
 *
 * @package Xloit\Bridge\Zend\Form
 */
class Form extends AbstractForm
{
    use ElementRequireAttributesProviderTrait;

    /**
     * Return element value.
     *
     * @param string $name Name
     *
     * @return string
     */
    public function getValue($name = null)
    {
        if ($this->has($name)) {
            return $this->get($name)->getValue();
        }

        return parent::getValue();
    }

    /**
     * Retrieve input filter used by this form.
     *
     * @return InputFilterInterface
     * @throws \Xloit\Std\Exception\RuntimeException
     * @throws \Zend\InputFilter\Exception\RuntimeException
     * @throws \Zend\InputFilter\Exception\InvalidArgumentException
     */
    public function getInputFilter()
    {
        if ($this->filter) {
            return $this->filter;
        }

        $specifications = [];

        if ($this->object && $this->object instanceof InputFilterProviderInterface) {
            $specifications = $this->object->getInputFilterSpecification();
        }

        if ($this instanceof InputFilterProviderInterface) {
            $specifications = ArrayUtils::merge($specifications, $this->getInputFilterSpecification());
        }

        $this->addRequiredAttributeToFields($specifications);

        /** @noinspection IsEmptyFunctionUsageInspection */
        if (!empty($specifications) && null === $this->baseFieldset) {
            $formFactory  = $this->getFormFactory();
            $inputFactory = $formFactory->getInputFilterFactory();

            if (!($this->filter instanceof InputFilterInterface)) {
                $this->filter = new InputFilter();
                $this->filter->setFactory($inputFactory);
            }

            foreach ($specifications as $name => $specification) {
                $input = $inputFactory->createInput($specification);

                $this->filter->add($input, $name);
            }
        }

        return parent::getInputFilter();
    }

    /**
     * Determine is form multipart.
     *
     * @return bool
     */
    public function isMultipart()
    {
        return $this->getAttribute('enctype') === 'multipart/form-data';
    }
}


<?php
namespace app\manage\block;

class ImageBlock extends Block
{

    /**
     * form
     *
     * @param array $data
     * @return string
     */
    public static function form($data = [])
    {
        if (! isset($data['r_md_num'])) {
            $data['r_md_num'] = 3;
        }

        $data = array_merge(self::$default_form, $data);

        $option = json_decode($data['option'], true);
        $option = $option ? $option : [];
        if (isset($data['width'])) {
            $option['width'] = $data['width'];
        }
        if (isset($data['height'])) {
            $option['height'] = $data['height'];
        }
        $data['option'] = json_encode($option, JSON_UNESCAPED_UNICODE);

        $data['uuid'] = md5(serialize($data));

        $html = '
';
        $html .= '
' . $data['title'] . '
';
        $html .= '
';

        $html .= '<input type="hidden" name="' . $data['name'] . '" value="' . $data['value'] . '" id="upload_file_' . $data['uuid'] . '" />';
        $html .= '
';
        $html .= '<button type="button" class="am-btn am-btn-default am-btn-sm">';
        $html .= '';
        $html .= '选择文件';
        $html .= '</button>';
        $html .= '<input type="file" class="nd-upload-file" nd-target="upload_file_' . $data['uuid'] . '" nd-preview="preview_div_' . $data['uuid'] . '" nd-option=\'' . $data['option'] . '\'>';
        $html .= '
';

        $html .= '
';
        $html .= '
';

        return $html;
    }
}


<?php
/**
 * @link http://psesd.org/
 *
 * @copyright Copyright (c) 2015 Puget Sound ESD
 * @license http://psesd.org/license/
 */

namespace cascade\models;

use cascade\components\db\ActiveRecordTrait;
use cascade\components\types\Module as TypeModule;
use cascade\components\types\Relationship;
use cascade\components\types\RelationshipEvent;
use canis\caching\Cacher;
use Yii;

/**
 * Relation is the model class for table "relation".
 *
 * @author Jacob Morrison <email@ofjacob.com>
 */
class Relation extends \canis\db\models\Relation
{
    use ActiveRecordTrait;

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return array_merge(parent::behaviors(), [
            'Taxonomy' => [
                'class' => 'cascade\components\db\behaviors\ActiveTaxonomy',
                'viaModelClass' => 'RelationTaxonomy',
                'relationKey' => 'relation_id',
            ],
            'PrimaryRelation' => [
                'class' => 'cascade\components\db\behaviors\PrimaryRelation',
            ],
        ]);
    }

    /**
     * @inheritdoc
     */
    public static function queryBehaviors()
    {
        return array_merge(parent::queryBehaviors(),
            [
                'Taxonomy' => [
                    'class' => 'cascade\components\db\behaviors\QueryTaxonomy',
                    'viaModelClass' => 'RelationTaxonomy',
                    'relationKey' => 'relation_id',
                ],
            ]
        );
    }

    /**
     * @inheritdoc
     */
    public function afterSaveRelation($event)
    {
        parent::afterSaveRelation($event);
        if ($this->wasDirty) {
            Cacher::invalidateGroup(['Object', 'relations', $this->parent_object_id]);
            Cacher::invalidateGroup(['Object', 'relations', $this->child_object_id]);
            $parentObject = $this->getParentObject(false);
            $childObject =  $this->getChildObject(false);
            $relationship = $this->relationship;
            $relationshipEvent = new RelationshipEvent(['parentEvent' => $event, 'parentObject' => $parentObject, 'childObject' => $childObject, 'relationship' => $relationship]);
            if (!empty($parentObject) && !empty($parentObject->objectType)) {
                $parentObject->objectType->trigger(TypeModule::EVENT_RELATION_CHANGE, $relationshipEvent);
            }
            if (!empty($childObject) && !empty($childObject->objectType)) {
                $childObject->objectType->trigger(TypeModule::EVENT_RELATION_CHANGE, $relationshipEvent);
            }
        }
    }

    /**
     * @inheritdoc
     */
    public function afterDeleteRelation($event)
    {
        parent::afterDeleteRelation($event);
        Cacher::invalidateGroup(['Object', 'relations', $this->parent_object_id]);
        Cacher::invalidateGroup(['Object', 'relations', $this->child_object_id]);

        $parentObject = $this->getParentObject(false);
        $childObject =  $this->getChildObject(false);
        $relationshipEvent = new RelationshipEvent(['parentEvent' => $event, 'parentObject' => $parentObject, 'childObject' => $childObject, 'relationship' => $this->relationship]);
        if ($parentObject) {
            $parentObject->objectType->trigger(TypeModule::EVENT_RELATION_DELETE, $relationshipEvent);
        }
        if ($childObject) {
            $childObject->objectType->trigger(TypeModule::EVENT_RELATION_DELETE, $relationshipEvent);
        }
    }

    /**
     * [[@doctodo method_description:addFields]].
     *
     * @param [[@doctodo param_type:caller]]       $caller       [[@doctodo param_description:caller]]
     * @param [[@doctodo param_type:fields]]       $fields       [[@doctodo param_description:fields]]
     * @param [[@doctodo param_type:relationship]] $relationship [[@doctodo param_description:relationship]]
     * @param [[@doctodo param_type:owner]]        $owner        [[@doctodo param_description:owner]]
     */
    public function addFields($caller, &$fields, $relationship, $owner)
    {
        $baseField = ['model' => $this];
        if (isset($this->id)) {
            $fields['relation:id'] = $caller->createField('id', $owner, $baseField);
        }
        if (!empty($relationship->taxonomy)
                && ($taxonomyItem = Yii::$app->collectors['taxonomies']->getOne($relationship->taxonomy))
                && ($taxonomy = $taxonomyItem->object)
                && $taxonomy) {
            $fieldName = 'relation:taxonomy_id';
            $fieldSchema = $caller->createColumnSchema('taxonomy_id', ['type' => 'taxonomy', 'phpType' => 'object', 'dbType' => 'taxonomy', 'allowNull' => true]);

            $fields[$fieldName] = $caller->createTaxonomyField($fieldSchema, $taxonomyItem, $owner, $baseField);
        }
    }

    /**
     * Get relationship.
     *
     * @return [[@doctodo return_type:getRelationship]] [[@doctodo return_description:getRelationship]]
     */
    public function getRelationship()
    {
        if (!isset($this->parentObject) || empty($this->parentObject->objectTypeItem)) {
            return false;
        }
        if (!isset($this->childObject) || empty($this->childObject->objectTypeItem)) {
            return false;
        }

        return Relationship::getOne($this->parentObject->objectTypeItem, $this->childObject->objectTypeItem);
    }
}