<?php

namespace Reaction\Db\Orm;

use Reaction;
use Reaction\Db\ConnectionInterface;
use Reaction\Db\DatabaseInterface;
use Reaction\Db\Expressions\Expression;
use Reaction\Db\StaleObjectException;
use Reaction\Db\TableSchema;
use Reaction\Exceptions\InvalidArgumentException;
use Reaction\Exceptions\InvalidConfigException;
use Reaction\Exceptions\Model\ValidationError;
use Reaction\Helpers\ArrayHelper;
use Reaction\Helpers\Inflector;
use Reaction\Helpers\StringHelper;
use Reaction\Promise\ExtendedPromiseInterface;
use Reaction\Promise\LazyPromise;
use Reaction\Promise\LazyPromiseInterface;
use function Reaction\Promise\reject;
use function Reaction\Promise\rejectLazy;

/**
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
 *
 * Active Record implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record).
 * The premise behind Active Record is that an individual [[ActiveRecord]] object is associated with a specific
 * row in a database table. The object's attributes are mapped to the columns of the corresponding table.
 * Referencing an Active Record attribute is equivalent to accessing the corresponding table column for that record.
 *
 * As an example, say that the `Customer` ActiveRecord class is associated with the `customer` table.
 * This would mean that the class's `name` attribute is automatically mapped to the `name` column in `customer` table.
 * Thanks to Active Record, assuming the variable `$customer` is an object of type `Customer`, to get the value of
 * the `name` column for the table row, you can use the expression `$customer->name`.
 * In this example, Active Record is providing an object-oriented interface for accessing data stored in the database.
 * But Active Record provides much more functionality than this.
 *
 * To declare an ActiveRecord class you need to extend [[\Reaction\Db\Orm\ActiveRecord]] and
 * implement the `tableName` method:
 *
 * ```php
 * <?php
 *
 * class Customer extends \Reaction\Db\Orm\ActiveRecord
 * {
 *     public static function tableName()
 *     {
 *         return 'customer';
 *     }
 * }
 * ```
 *
 * The `tableName` method only has to return the name of the database table associated with the class.
 *
 * > Tip: You may also use the [Gii code generator](guide:start-gii) to generate ActiveRecord classes from your
 * > database tables.
 *
 * Class instances are obtained in one of two ways:
 *
 * * Using the `new` operator to create a new, empty object
 * * Using a method to fetch an existing record (or records) from the database
 *
 * Below is an example showing some typical usage of ActiveRecord:
 *
 * ```php
 * $user = new User();
 * $user->name = 'Qiang';
 * $user->save();  // a new row is inserted into user table
 *
 * // the following will retrieve the user 'CeBe' from the database
 * $user = User::find()->where(['name' => 'CeBe'])->one();
 *
 * // this will get related records from orders table when relation is defined
 * $orders = $user->orders;
 * ```
 *
 * For more details and usage information on ActiveRecord, see the [guide article on ActiveRecord](guide:db-active-record).
 *
 * @method ActiveQuery hasMany($class, array $link) see [[BaseActiveRecord::hasMany()]] for more info
 * @method ActiveQuery hasOne($class, array $link) see [[BaseActiveRecord::hasOne()]] for more info
 */
class ActiveRecord extends BaseActiveRecord
{
    /**
     * The insert operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
     */
    const OP_INSERT = 0x01;
    /**
     * The update operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
     */
    const OP_UPDATE = 0x02;
    /**
     * The delete operation. This is mainly used when overriding [[transactions()]] to specify which operations are transactional.
     */
    const OP_DELETE = 0x04;
    /**
     * All three operations: insert, update, delete.
     * This is a shortcut of the expression: OP_INSERT | OP_UPDATE | OP_DELETE.
     */
    const OP_ALL = 0x07;

    /**
     * Loads default values from database table schema.
     *
     * You may call this method to load default values after creating a new instance:
     *
     * ```php
     * // class Customer extends \Reaction\Db\Orm\ActiveRecord
     * $customer = new Customer();
     * $customer->loadDefaultValues();
     * ```
     *
     * @param bool $skipIfSet whether existing value should be preserved.
     * This will only set defaults for attributes that are `null`.
     * @return $this the model instance itself.
     */
    public function loadDefaultValues($skipIfSet = true)
    {
        foreach (static::getTableSchema()->columns as $column) {
            if ($column->defaultValue !== null && (!$skipIfSet || $this->{$column->name} === null)) {
                $this->{$column->name} = $column->defaultValue;
            }
        }

        return $this;
    }

    /**
     * Returns the database connection used by this AR class.
     * By default, the "db" application component is used as the database connection.
     * You may override this method if you want to use a different database connection.
     * @return DatabaseInterface the database connection used by this AR class.
     */
    public static function getDb()
    {
        return Reaction::$app->getDb();
    }

    /**
     * Creates an [[ActiveQuery]] instance with a given SQL statement.
     *
     * Note that because the SQL statement is already specified, calling additional
     * query modification methods (such as `where()`, `order()`) on the created [[ActiveQuery]]
     * instance will have no effect. However, calling `with()`, `asArray()` or `indexBy()` is
     * still fine.
     *
     * Below is an example:
     *
     * ```php
     * $customers = Customer::findBySql('SELECT * FROM customer')->all();
     * ```
     *
     * @param string $sql the SQL statement to be executed
     * @param array $params parameters to be bound to the SQL statement during execution.
     * @return ActiveQuery the newly created [[ActiveQuery]] instance
     */
    public static function findBySql($sql, $params = [])
    {
        $query = static::find();
        $query->sql = $sql;

        return $query->params($params);
    }

    /**
     * Finds ActiveRecord instance(s) by the given condition.
     * This method is internally called by [[findOne()]] and [[findAll()]].
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
     * @return ActiveQueryInterface the newly created [[ActiveQueryInterface|ActiveQuery]] instance.
     * @throws InvalidConfigException if there is no primary key defined.
     * @internal
     */
    protected static function findByCondition($condition)
    {
        $query = static::find();

        if (!ArrayHelper::isAssociative($condition)) {
            // query by primary key
            $primaryKey = static::primaryKey();
            if (isset($primaryKey[0])) {
                $pk = $primaryKey[0];
                if (!empty($query->join) || !empty($query->joinWith)) {
                    $pk = static::tableName() . '.' . $pk;
                }
                // if condition is scalar, search for a single primary key, if it is array, search for multiple primary key values
                $condition = [$pk => is_array($condition) ? array_values($condition) : $condition];
            } else {
                throw new InvalidConfigException('"' . get_called_class() . '" must have a primary key.');
            }
        } elseif (is_array($condition)) {
            $condition = static::filterCondition($condition);
        }

        return $query->andWhere($condition);
    }

    /**
     * Filters array condition before it is assiged to a Query filter.
     *
     * This method will ensure that an array condition only filters on existing table columns.
     *
     * @param array $condition condition to filter.
     * @return array filtered condition.
     * @throws InvalidArgumentException in case array contains unsafe values.
     * @internal
     */
    protected static function filterCondition(array $condition)
    {
        $result = [];
        // valid column names are table column names or column names prefixed with table name
        $columnNames = static::getTableSchema()->getColumnNames();
        $tableName = static::tableName();
        $columnNames = array_merge($columnNames, array_map(function($columnName) use ($tableName) {
            return "$tableName.$columnName";
        }, $columnNames));
        foreach ($condition as $key => $value) {
            if (is_string($key) && !in_array($key, $columnNames, true)) {
                throw new InvalidArgumentException('Key "' . $key . '" is not a column name and can not be used as a filter');
            }
            $result[$key] = is_array($value) ? array_values($value) : $value;
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function refresh()
    {
        $query = static::find();
        $tableName = key($query->getTablesUsedInFrom());
        $pk = [];
        // disambiguate column names in case ActiveQuery adds a JOIN
        foreach ($this->getPrimaryKey(true) as $key => $value) {
            $pk[$tableName . '.' . $key] = $value;
        }
        $query->where($pk);

        return $query->one()->then(
            function($record) {
                /* @var $record BaseActiveRecord */
                $result = $this->refreshInternal($record);
                return $result ? true : reject(false);
            }
        );
    }

    /**
     * Updates the whole table using the provided attribute values and conditions.
     *
     * For example, to change the status to be 1 for all customers whose status is 2:
     *
     * ```php
     * Customer::updateAll(['status' => 1], 'status = 2');
     * ```
     *
     * > Warning: If you do not specify any condition, this method will update **all** rows in the table.
     *
     * Note that this method will not trigger any events. If you need [[EVENT_BEFORE_UPDATE]] or
     * [[EVENT_AFTER_UPDATE]] to be triggered, you need to [[find()|find]] the models first and then
     * call [[update()]] on each of them. For example an equivalent of the example above would be:
     *
     * ```php
     * $models = Customer::find()->where('status = 2')->all();
     * foreach ($models as $model) {
     *     $model->status = 1;
     *     $model->update(false); // skipping validation as no user input is involved
     * }
     * ```
     *
     * For a large set of models you might consider using [[ActiveQuery::each()]] to keep memory usage within limits.
     *
     * @param array                    $attributes attribute values (name-value pairs) to be saved into the table
     * @param string|array             $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
     * Please refer to [[Query::where()]] on how to specify this parameter.
     * @param array                    $params the parameters (name => value) to be bound to the query.
     * @param ConnectionInterface|null $connection
     * @return LazyPromiseInterface with int the number of rows updated
     */
    public static function updateAll($attributes, $condition = '', $params = [], $connection = null)
    {
        $command = static::getDb()->createCommand();
        $command->update(static::tableName(), $attributes, $condition, $params);
        if (isset($connection)) {
            $command->setConnection($connection);
        }

        return $command->execute();
    }

    /**
     * Updates the whole table using the provided counter changes and conditions.
     *
     * For example, to increment all customers' age by 1,
     *
     * ```php
     * Customer::updateAllCounters(['age' => 1]);
     * ```
     *
     * Note that this method will not trigger any events.
     *
     * @param array $counters the counters to be updated (attribute name => increment value).
     * Use negative values if you want to decrement the counters.
     * @param string|array $condition the conditions that will be put in the WHERE part of the UPDATE SQL.
     * Please refer to [[Query::where()]] on how to specify this parameter.
     * @param array $params the parameters (name => value) to be bound to the query.
     * Do not name the parameters as `:bp0`, `:bp1`, etc., because they are used internally by this method.
     * @return LazyPromiseInterface with int the number of rows updated
     */
    public static function updateAllCounters($counters, $condition = '', $params = [])
    {
        $n = 0;
        foreach ($counters as $name => $value) {
            $counters[$name] = new Expression("[[$name]]+:bp{$n}", [":bp{$n}" => $value]);
            $n++;
        }
        $command = static::getDb()->createCommand();
        $command->update(static::tableName(), $counters, $condition, $params);

        return $command->execute();
    }

    /**
     * Deletes rows in the table using the provided conditions.
     *
     * For example, to delete all customers whose status is 3:
     *
     * ```php
     * Customer::deleteAll('status = 3');
     * ```
     *
     * > Warning: If you do not specify any condition, this method will delete **all** rows in the table.
     *
     * Note that this method will not trigger any events. If you need [[EVENT_BEFORE_DELETE]] or
     * [[EVENT_AFTER_DELETE]] to be triggered, you need to [[find()|find]] the models first and then
     * call [[delete()]] on each of them. For example an equivalent of the example above would be:
     *
     * ```php
     * $models = Customer::find()->where('status = 3')->all();
     * foreach ($models as $model) {
     *     $model->delete();
     * }
     * ```
     *
     * For a large set of models you might consider using [[ActiveQuery::each()]] to keep memory usage within limits.
     *
     * @param string|array             $condition the conditions that will be put in the WHERE part of the DELETE SQL.
     * Please refer to [[Query::where()]] on how to specify this parameter.
     * @param array                    $params the parameters (name => value) to be bound to the query.
     * @param ConnectionInterface|null $connection
     * @return LazyPromiseInterface with int the number of rows deleted
     */
    public static function deleteAll($condition = null, $params = [], $connection = null)
    {
        $command = static::getDb()->createCommand();
        $command->delete(static::tableName(), $condition, $params);
        if (isset($connection)) {
            $command->setConnection($connection);
        }

        return $command->execute();
    }

    /**
     * {@inheritdoc}
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
     */
    public static function find()
    {
        return Reaction::create(ActiveQuery::class, [get_called_class()]);
    }

    /**
     * Declares the name of the database table associated with this AR class.
     * By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]
     * with prefix [[Connection::tablePrefix]]. For example if [[Connection::tablePrefix]] is `tbl_`,
     * `Customer` becomes `tbl_customer`, and `OrderItem` becomes `tbl_order_item`. You may override this method
     * if the table is not named after this convention.
     * @return string the table name
     */
    public static function tableName()
    {
        return '{{%' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_') . '}}';
    }

    /**
     * Returns the schema information of the DB table associated with this AR class.
     * @return TableSchema the schema information of the DB table associated with this AR class.
     * @throws InvalidConfigException if the table for the AR class does not exist.
     */
    public static function getTableSchema()
    {
        $tableSchema = static::getDb()
            ->getSchema()
            ->getTableSchema(static::tableName());

        if ($tableSchema === null) {
            throw new InvalidConfigException('The table does not exist: ' . static::tableName());
        }

        return $tableSchema;
    }

    /**
     * Returns the primary key name(s) for this AR class.
     * The default implementation will return the primary key(s) as declared
     * in the DB table that is associated with this AR class.
     *
     * If the DB table does not declare any primary key, you should override
     * this method to return the attributes that you want to use as primary keys
     * for this AR class.
     *
     * Note that an array should be returned even for a table with single primary key.
     *
     * @return string[] the primary keys of the associated database table.
     */
    public static function primaryKey()
    {
        return static::getTableSchema()->primaryKey;
    }

    /**
     * Returns the list of all attribute names of the model.
     * The default implementation will return all column names of the table associated with this AR class.
     * @return array list of attribute names.
     */
    public function attributes()
    {
        return array_keys(static::getTableSchema()->columns);
    }

    /**
     * Declares which DB operations should be performed within a transaction in different scenarios.
     * The supported DB operations are: [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]],
     * which correspond to the [[insert()]], [[update()]] and [[delete()]] methods, respectively.
     * By default, these methods are NOT enclosed in a DB transaction.
     *
     * In some scenarios, to ensure data consistency, you may want to enclose some or all of them
     * in transactions. You can do so by overriding this method and returning the operations
     * that need to be transactional. For example,
     *
     * ```php
     * return [
     *     'admin' => self::OP_INSERT,
     *     'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
     *     // the above is equivalent to the following:
     *     // 'api' => self::OP_ALL,
     *
     * ];
     * ```
     *
     * The above declaration specifies that in the "admin" scenario, the insert operation ([[insert()]])
     * should be done in a transaction; and in the "api" scenario, all the operations should be done
     * in a transaction.
     *
     * @return array the declarations of transactional operations. The array keys are scenarios names,
     * and the array values are the corresponding transaction operations.
     */
    public function transactions()
    {
        return [];
    }

    /**
     * {@inheritdoc}
     */
    public static function populateRecord($record, $row)
    {
        $columns = static::getTableSchema()->columns;
        foreach ($row as $name => $value) {
            if (isset($columns[$name])) {
                $row[$name] = $columns[$name]->phpTypecast($value);
            }
        }
        parent::populateRecord($record, $row);
    }

    /**
     * Inserts a row into the associated database table using the attribute values of this record.
     *
     * This method performs the following steps in order:
     *
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
     *    returns `false`, the rest of the steps will be skipped;
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
     *    failed, the rest of the steps will be skipped;
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
     *    the rest of the steps will be skipped;
     * 4. insert the record into database. If this fails, it will skip the rest of the steps;
     * 5. call [[afterSave()]];
     *
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_INSERT]], and [[EVENT_AFTER_INSERT]]
     * will be raised by the corresponding methods.
     *
     * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database.
     *
     * If the table's primary key is auto-incremental and is `null` during insertion,
     * it will be populated with the actual value after insertion.
     *
     * For example, to insert a customer record:
     *
     * ```php
     * $customer = new Customer;
     * $customer->name = $name;
     * $customer->email = $email;
     * $customer->insert();
     * ```
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $attributes list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return LazyPromiseInterface with bool whether the attributes are valid and the record is inserted successfully.
     * @throws \Exception|\Throwable in case insert failed.
     */
    public function insert($runValidation = true, $attributes = null)
    {
        $validationPromise = $runValidation
            ? new LazyPromise(function() use ($attributes) {
                return $this->validate($attributes);
            })
            : Reaction\Promise\resolveLazy(true);
        return $validationPromise
            ->otherwiseLazy(function() {
                Reaction::info('Model not inserted due to validation error in {method}.', ['method' => __METHOD__]);
                return reject(new ValidationError("Model validation error"));
            })->thenLazy(function() use ($attributes) {
                if (!$this->isTransactional(self::OP_INSERT)) {
                    return new LazyPromise(function() use ($attributes) {
                        return $this->insertInternal($attributes);
                    });
                }

                $transaction = static::getDb()->createTransaction();
                return $transaction->begin()->then(
                    function(ConnectionInterface $connection) use ($attributes) {
                        return $this->insertInternal($attributes, $connection);
                    }
                )->then(
                    function($result) use ($transaction) {
                        return $transaction->commit()->then(function() use ($result) {
                            return $result;
                        });
                    },
                    function($exception) use ($transaction) {
                        return $transaction->rollBack()->then(
                            function() use ($exception) { throw $exception; },
                            function($e) use ($exception) { throw $exception; }
                        );
                    }
                );
            });
    }

    /**
     * Inserts an ActiveRecord into DB without considering transaction.
     * @param array                    $attributes list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @param ConnectionInterface|null $connection
     * @return ExtendedPromiseInterface with bool whether the record is inserted successfully.
     */
    protected function insertInternal($attributes = null, $connection = null)
    {
        if (!$this->beforeSave(true)) {
            return reject(false);
        }
        $values = $this->getDirtyAttributes($attributes);
        return static::getDb()->getSchema()->insert(static::tableName(), $values, $connection)->then(
            function($primaryKeys) use ($values) {
                if ($primaryKeys === false) {
                    return reject(false);
                }
                foreach ($primaryKeys as $name => $value) {
                    $id = static::getTableSchema()->columns[$name]->phpTypecast($value);
                    $this->setAttribute($name, $id);
                    $values[$name] = $id;
                }

                $changedAttributes = array_fill_keys(array_keys($values), null);
                $this->setOldAttributes($values);
                $this->afterSave(true, $changedAttributes);
                return true;
            }
        );
    }

    /**
     * Saves the changes to this active record into the associated database table.
     *
     * This method performs the following steps in order:
     *
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
     *    returns `false`, the rest of the steps will be skipped;
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
     *    failed, the rest of the steps will be skipped;
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
     *    the rest of the steps will be skipped;
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
     * 5. call [[afterSave()]];
     *
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
     * will be raised by the corresponding methods.
     *
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
     *
     * For example, to update a customer record:
     *
     * ```php
     * $customer = Customer::findOne($id);
     * $customer->name = $name;
     * $customer->email = $email;
     * $customer->update();
     * ```
     *
     * Note that it is possible the update does not affect any row in the table.
     * In this case, this method will return 0. For this reason, you should use the following
     * code to check if update() is successful or not:
     *
     * ```php
     * if ($customer->update() !== false) {
     *     // update successful
     * } else {
     *     // update failed
     * }
     * ```
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $attributeNames list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return LazyPromiseInterface with int|false the number of rows affected, or false if validation fails
     * or [[beforeSave()]] stops the updating process.
     * being updated is outdated.
     * @throws \Exception|\Throwable in case update failed.
     */
    public function update($runValidation = true, $attributeNames = null)
    {
        $validationPromise = $runValidation
            ? new LazyPromise(function() use ($attributeNames) {
                return $this->validate($attributeNames);
            })
            : Reaction\Promise\resolveLazy(true);
        return $validationPromise
            ->otherwiseLazy(function() {
                Reaction::info('Model not updated due to validation error in {method}.', ['method' => __METHOD__]);
                return reject(new ValidationError("Model validation error"));
            })->thenLazy(function() use ($attributeNames) {
                if (!$this->isTransactional(self::OP_UPDATE)) {
                    return $this->updateInternal($attributeNames);
                }

                $transaction = static::getDb()->createTransaction();
                return $transaction->begin()->then(
                    function(ConnectionInterface $connection) use ($attributeNames) {
                        return $this->updateInternal($attributeNames, $connection);
                    }
                )->then(
                    function($result) use ($transaction) {
                        return $transaction->commit()->then(function() use ($result) {
                            return $result;
                        });
                    },
                    function($exception) use ($transaction) {
                        return $transaction->rollBack()->then(
                            function() use ($exception) { throw $exception; },
                            function($e) use ($exception) { throw $exception; }
                        );
                    }
                );
            });
    }

    /**
     * Deletes the table row corresponding to this active record.
     *
     * This method performs the following steps in order:
     *
     * 1. call [[beforeDelete()]]. If the method returns `false`, it will skip the
     *    rest of the steps;
     * 2. delete the record from the database;
     * 3. call [[afterDelete()]].
     *
     * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]]
     * will be raised by the corresponding methods.
     *
     * @return LazyPromiseInterface with int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
     * @throws \Exception|\Throwable in case delete failed.
     */
    public function delete()
    {
        if (!$this->isTransactional(self::OP_DELETE)) {
            return $this->deleteInternal();
        }

        $transaction = static::getDb()->createTransaction();
        return $transaction->begin()->thenLazy(
            function(ConnectionInterface $connection) {
                return $this->deleteInternal($connection);
            }
        )->thenLazy(
            function($result) use ($transaction) {
                return $transaction->commit()->then(function() use ($result) {
                    return $result;
                });
            },
            function($exception) use ($transaction) {
                return $transaction->rollBack()->then(
                    function() use ($exception) { throw $exception; },
                    function($e) use ($exception) { throw $exception; }
                );
            }
        );
    }

    /**
     * Deletes an ActiveRecord without considering transaction.
     * @param ConnectionInterface|null $connection
     * @return LazyPromiseInterface with int|false the number of rows deleted, or `false` if the deletion is unsuccessful for some reason.
     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
     */
    protected function deleteInternal($connection = null)
    {
        if (!$this->beforeDelete()) {
            return Reaction\Promise\rejectLazy(false);
        }

        // we do not check the return value of deleteAll() because it's possible
        // the record is already deleted in the database and thus the method will return 0
        $condition = $this->getOldPrimaryKey(true);
        $lock = $this->optimisticLock();
        if ($lock !== null) {
            $condition[$lock] = $this->$lock;
        }
        return static::deleteAll($condition, [], $connection)->thenLazy(
            function($result) use ($lock) {
                if ($lock !== null && !$result) {
                    throw new StaleObjectException('The object being deleted is outdated.');
                }
                $this->setOldAttributes(null);
                $this->afterDelete();

                return $result;
            }
        );
    }

    /**
     * Returns a value indicating whether the given active record is the same as the current one.
     * The comparison is made by comparing the table names and the primary key values of the two active records.
     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
     * @param ActiveRecord $record record to compare to
     * @return bool whether the two active records refer to the same row in the same database table.
     */
    public function equals($record)
    {
        if ($this->isNewRecord || $record->isNewRecord) {
            return false;
        }

        return static::tableName() === $record->tableName() && $this->getPrimaryKey() === $record->getPrimaryKey();
    }

    /**
     * Returns a value indicating whether the specified operation is transactional in the current [[$scenario]].
     * @param int $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]].
     * @return bool whether the specified operation is transactional in the current [[scenario]].
     */
    public function isTransactional($operation)
    {
        $scenario = $this->getScenario();
        $transactions = $this->transactions();

        return isset($transactions[$scenario]) && ($transactions[$scenario] & $operation);
    }
}


<?php

/*
 * This file is part of madewithlove/service-providers
 *
 * (c) madewithlove <heroes@madewithlove.be>
 *
 * For the full copyright and license information, please view the LICENSE
 */

namespace Madewithlove\ServiceProviders\Http;

use Interop\Container\ServiceProviderInterface;
use Madewithlove\ServiceProviders\Utilities\Alias;
use Psr\Container\ContainerInterface;
use Relay\Relay;
use Relay\RelayBuilder;

class RelayServiceProvider implements ServiceProviderInterface
{
    /**
     * @var array
     */
    protected $middlewares = [];

    /**
     * @param array $middlewares
     */
    public function __construct(array $middlewares = [])
    {
        $this->middlewares = $middlewares;
    }

    /**
     * {@inheritdoc}
     */
    public function getServices()
    {
        return [
            RelayBuilder::class => [$this, 'getFactory'],
            Relay::class => [$this, 'getRelay'],
            'pipeline' => new Alias(Relay::class),
        ];
    }

    /**
     * @param ContainerInterface $container
     *
     * @return RelayBuilder
     */
    public function getFactory(ContainerInterface $container)
    {
        return new RelayBuilder(function ($middleware) use ($container) {
            return is_string($middleware) ? $container->get($middleware) : $middleware;
        });
    }

    /**
     * @param ContainerInterface $container
     *
     * @return Relay
     */
    public function getRelay(ContainerInterface $container)
    {
        /** @var RelayBuilder $builder */
        $builder = $container->get(RelayBuilder::class);

        return $builder->newInstance($this->middlewares);
    }
}


<?php

namespace Simply\Container;

/**
 * Abstract entry provider that provides the most basic provider initialization functionality.
 *
 * @author Riikka Kalliomäki <riikka.kalliomaki@gmail.com>
 * @copyright Copyright (c) 2018 Riikka Kalliomäki
 * @license http://opensource.org/licenses/mit-license.php MIT License
 */
abstract class AbstractEntryProvider implements EntryProvider
{
    /**
     * Returns a new instance of the entry provider.
     * @return EntryProvider New entry provider instance initialized with default constructor
     */
    public static function initialize(): EntryProvider
    {
        return new static();
    }

    /**
     * Returns provided list of all classes returned by the container methods.
     * @return string[] Provided list of all classes returned by the container methods
     */
    public function getMethods(): array
    {
        $methods = [];
        $reflection = new \ReflectionClass($this);

        foreach ($reflection->getMethods() as $method) {
            $identifier = $this->getMethodIdentifier($method);

            if (!\is_null($identifier)) {
                $methods[$identifier] = $method->getName();
            }
        }

        return $methods;
    }

    /**
     * Tells the identifier to use for the given provider method.
     * @param \ReflectionMethod $method The provider method
     * @return string|null The identifier for the method or null if not applicable
     */
    private function getMethodIdentifier(\ReflectionMethod $method): ?string
    {
        if (!$method->isPublic() || $method->isStatic()) {
            return null;
        }

        if (preg_match('/^__[^_]/', $method->getName())) {
            return null;
        }

        $type = $method->getReturnType();

        if (!$type instanceof \ReflectionType || $type->isBuiltin()) {
            return null;
        }

        return $type->getName();
    }
}


<?php

namespace Gwsn\Prismic\Models;

use Gwsn\Transformer\Mapping\BaseMapping;

/**
 * Class ResponseItemData
 * @package Gwsn\Prismic\Models
 */
class ResponseItemData extends BaseMapping
{
    /** @var string $id */
    public $guid;

    /** @var string $type */
    public $type;

    /** @var string $slug */
    public $slug;

    /** @var mixed $value */
    public $value;

    /** @var string $href */
    public $href;

    /** @var array $tags */
    public $tags = [];

    /** @var \DateTime $first_publication_date */
    public $firstPublicationDate;

    /** @var \DateTime $last_publication_date */
    public $lastPublicationDate;

    /** @var array $slugs */
    public $slugs = [];

    /** @var array $linked_documents */
    public $linkedDocuments = [];

    /** @var string $lang */
    public $lang;

    /** @var array $alternate_languages */
    public $alternateLanguages = [];

    /** @var array $data */
    public $data;

    /** @var array $custom */
    public $custom;

    /**
     * Return the default transformer.
     *
     * @return array
     */
    public function getMapping(): array
    {
        return [
            'guid' => 'id',
            'slug' => ['slugs', 'callbackSlug'],
            'type' => 'type',
            'href' => 'href',
            'tags' => 'tags',
            'slugs' => 'slugs',
            'lang' => 'lang',
            'firstPublicationDate' => ['first_publication_date', 'callbackDateTime'],
            'lastPublicationDate' => ['last_publication_date', 'callbackDateTime'],
            'data' => ['data', 'callbackParsePrismicDataParts'],
            'custom' => ['data', 'callbackCustomData'],
        ];
    }

    /**
     * Get the first slug
     *
     * @param $value
     * @param $raw
     * @return string
     */
    public function callbackSlug($value, $raw)
    {
        if (is_array($value) && !empty($value)) {
            return array_values($value)[0];
        }
        return '';
    }

    /**
     * Parse the date time into a Date object
     * @todo Parse the date
     *
     * @param $value
     * @param $raw
     * @return null
     */
    public function callbackDateTime($value, $raw)
    {

        if (!empty($value)) {
            return $value;
        }
        return null;
    }

    /**
     * Parse the data (html content and links)
     *
     * @param $value
     * @param $raw
     * @return array|null
     */
    public function callbackParsePrismicDataParts($value, $raw)
    {

        if (!empty($value)) {
            return Fragment::parseFragments($value);
        }
        return null;
    }

    /**
     * Parse the data (html content and links)
     *
     * @param $value
     * @param $raw
     * @return array|null
     */
    public function callbackParsePrismicDataPart($value, $raw)
    {

        if (!empty($value)) {
            return Fragment::parseFragments([0 => $value]);
        }
        return null;
    }

    /**
     * Return all the values that are not default in Prismic
     *
     * @param $value
     * @param $raw
     * @return mixed
     */
    public function callbackCustomData($value, $raw) {
        $defaultKeys = ['id','slugs','data','type','href','tags','lang','alternate_languages','linked_documents','first_publication_date','last_publication_date'];
        foreach($raw as $key => $value) {
            if(in_array($key, $defaultKeys)) {
                unset($raw[$key]);
            }
        }
        return $raw;
    }

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

    /**
     * @param string $guid
     */
    public function setGuid(string $guid)
    {
        $this->guid = $guid;
    }

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

    /**
     * @param string $type
     */
    public function setType(string $type)
    {
        $this->type = $type;
    }

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

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

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

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

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

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

    /**
     * @param string $slug
     */
    public function setSlug(string $slug)
    {
        $this->slug = $slug;
    }

    /**
     * @param string $href
     */
    public function setHref(string $href)
    {
        $this->href = $href;
    }

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

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

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

    /**
     * @param \DateTime $firstPublicationDate
     */
    public function setFirstPublicationDate($firstPublicationDate = null)
    {
        $this->firstPublicationDate = $firstPublicationDate;
    }

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

    /**
     * @param \DateTime $lastPublicationDate
     */
    public function setLastPublicationDate($lastPublicationDate = null)
    {
        $this->lastPublicationDate = $lastPublicationDate;
    }

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

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

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

    /**
     * @param string $lang
     */
    public function setLang(string $lang)
    {
        $this->lang = $lang;
    }

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

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

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

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

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

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

}


<?php

namespace Reaction\Cache;

use Reaction\Exceptions\Exception;
use Reaction\Helpers\ArrayHelper;
use function Reaction\Promise\reject;
use function Reaction\Promise\resolve;

/**
 * Class ArrayCache
 * @package Reaction\Cache
 */
class ArrayCache extends ExtendedCache
{
    /** @var array Data storage */
    protected $storage = [];
    /** @var array Tags associated with data keys */
    protected $tags = [];

    /**
     * {@inheritdoc}
     */
    public function get($key, $default = null)
    {
        $key = $this->processKey($key);
        if ($this->existInternal($key)) {
            return resolve($this->storage[$key]);
        }
        return resolve($default);
    }

    /**
     * {@inheritdoc}
     */
    public function set($key, $value, $ttl = null, $tags = [])
    {
        $key = $this->processKey($key);
        $this->storage[$key] = $value;
        $this->addKeyTags($key, $tags);
        return resolve(true);
    }

    /**
     * {@inheritdoc}
     */
    public function delete($key)
    {
        $key = $this->processKey($key);
        if ($this->existInternal($key)) {
            unset($this->storage[$key]);
            foreach ($this->tags as $tag => $keys) {
                if (isset($keys[$key])) {
                    unset($this->tags[$tag][$key]);
                }
            }
        }
        return resolve(true);
    }

    /**
     * {@inheritdoc}
     */
    public function deleteByTag($tag)
    {
        if (!isset($this->tags[$tag])) {
            return resolve(true);
        }
        $keys = $this->tags[$tag];
        unset($this->tags[$tag]);
        return !empty($keys) ? $this->deleteMultiple($keys) : resolve(true);
    }

    /**
     * {@inheritdoc}
     */
    public function exists($key)
    {
        $key = $this->processKey($key);
        if ($this->existInternal($key)) {
            return resolve(true);
        }
        return reject(new Exception(sprintf('Cache for key "%s" not exists', $key)));
    }

    /**
     * {@inheritdoc}
     */
    public function flush()
    {
        $this->storage = [];
        $this->tags = [];
        return resolve(true);
    }

    /**
     * Internal check that record exists in storage
     * @internal
     * @param string $key
     * @return bool
     */
    protected function existInternal($key) {
        return ArrayHelper::keyExists($key, $this->storage);
    }

    /**
     * Save key tags
     * @param string   $key
     * @param string[] $tags
     */
    protected function addKeyTags($key, $tags = [])
    {
        foreach ($tags as $tag) {
            if (!isset($this->tags[$tag])) {
                $this->tags[$tag] = [];
            }
            $this->tags[$tag][$key] = $key;
        }
    }
}


<?php

namespace Reaction\Console\Controllers;

use Reaction;
use Reaction\RequestApplicationInterface;
use Reaction\Console\Routes\Controller;
use Reaction\Console\Exception;
use Reaction\Helpers\Console;
use Reaction\Helpers\Inflector;

/**
 * Provides help information about console commands.
 *
 * This command displays the available command list in
 * the application or the detailed instructions about using
 * a specific command.
 *
 * This command can be used as follows on command line:
 *
 * ```
 * console help [command name]
 * ```
 *
 * In the above, if the command name is not provided, all
 * available commands will be displayed.
 *
 * @property array $commands All available command names. This property is read-only.
 */
class HelpController extends Controller
{

    /**
     * Displays available commands or the detailed information
     * about a particular command.
     *
     * @param RequestApplicationInterface $app
     * @param string                               $command The name of the command to show help about.
     * If not provided, all available commands will be displayed.
     * @throws Exception if the command for help is unknown
     */
    public function actionIndex(RequestApplicationInterface $app, $command = null)
    {
        if ($command !== null) {
            $result = $this->createController($command);
            if ($result[0] === null) {
                $name = $this->ansiFormat($command, Console::FG_YELLOW);
                throw new Exception("No help for unknown command \"$name\".");
            }

            /** @var $controller Controller */
            list($controller, $actionId) = $result;

            $actions = $this->getActions($controller);
            if ($actionId !== '' || (count($actions) === 1 && $actions[0] === $controller->defaultAction)) {
                $this->getSubCommandHelp($controller, $actionId);
            } else {
                $this->getCommandHelp($controller);
            }
        } else {
            $this->getDefaultHelp();
        }
    }

    /**
     * List all available controllers and actions in machine readable format.
     * This is used for shell completion.
     * @param RequestApplicationInterface $app
     */
    public function actionList(RequestApplicationInterface $app)
    {
        foreach ($this->getCommandDescriptions() as $command => $description) {
            $result = $this->createController($command);
            if ($result === false || !($result[0] instanceof Controller)) {
                continue;
            }
            /** @var $controller Controller */
            list($controller, $actionId) = $result;
            $actions = $this->getActions($controller);
            if (!empty($actions)) {
                $prefix = $controller->getUniqueId();
                $this->stdout("$prefix\n");
                foreach ($actions as $action) {
                    $this->stdout("$prefix/$action\n");
                }
            }
        }
    }

    /**
     * List all available options for the $action in machine readable format.
     * This is used for shell completion.
     *
     * @param RequestApplicationInterface $app
     * @param string                      $action route to action
     */
    public function actionListActionOptions(RequestApplicationInterface $app, $action)
    {
        $result = $this->createController($action);

        if ($result === false || !($result[0] instanceof Controller)) {
            return;
        }

        /** @var Controller $controller */
        list($controller, $actionId) = $result;
        if (!$controller->hasAction($actionId)) {
            return;
        }

        foreach ($controller->getActionArgsHelp($actionId) as $argument => $help) {
            $description = str_replace("\n", '', addcslashes($help['comment'], ':')) ?: $argument;
            $this->stdout($argument . ':' . $description . "\n");
        }

        $this->stdout("\n");
        foreach ($controller->getActionOptionsHelp($actionId) as $argument => $help) {
            $description = str_replace("\n", '', addcslashes($help['comment'], ':'));
            $this->stdout('--' . $argument . ($description ? ':' . $description : '') . "\n");
        }
    }

    /**
     * Displays usage information for $action.
     *
     * @param RequestApplicationInterface $app
     * @param string                      $action route to action
     */
    public function actionUsage(RequestApplicationInterface $app, $action)
    {
        $result = $this->createController($action);

        if ($result === false || !($result[0] instanceof Controller)) {
            return;
        }

        /** @var Controller $controller */
        list($controller, $actionId) = $result;
        if (!$controller->hasAction($actionId)) {
            return;
        }

        $scriptName = $this->getScriptName();
        if ($actionId === $controller->defaultAction) {
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
        } else {
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId() . '/' . $actionId, Console::FG_YELLOW));
        }

        foreach ($controller->getActionArgsHelp($actionId) as $name => $arg) {
            if ($arg['required']) {
                $this->stdout(' <' . $name . '>', Console::FG_CYAN);
            } else {
                $this->stdout(' [' . $name . ']', Console::FG_CYAN);
            }
        }

        $this->stdout("\n");
    }

    /**
     * Returns all available command names.
     * @param bool $withInternal Whenever to show internal controllers
     * @return array all available command names
     */
    public function getCommands($withInternal = false)
    {$commands = [];
        $namespaces = Reaction::$app->router->controllerNamespaces;
        $controllers = Reaction\Helpers\ClassFinderHelper::findClassesPsr4($namespaces, true);
        foreach ($controllers as $controllerClass) {
            if (!$withInternal && Reaction\Helpers\ReflectionHelper::isImplements($controllerClass, 'Reaction\Routes\ControllerInternalInterface')) {
                continue;
            }
            if ($this->validateControllerClass($controllerClass)) {
                $relativePath = Reaction::$app->router->getRelativeControllerNamespace($controllerClass);
                $pathArray = explode('\\', $relativePath);
                $classBasename = array_pop($pathArray);
                $command = Inflector::camel2id(substr($classBasename, 0, -10), '-', true);
                if (!empty($pathArray)) {
                    $classPath = ltrim(implode('/', $pathArray), DIRECTORY_SEPARATOR);
                    $command = $classPath . DIRECTORY_SEPARATOR . $command;
                }
                $commands[] = $command;
            }
        }
        sort($commands);
        return array_unique($commands);
    }

    /**
     * Returns an array of commands an their descriptions.
     * @return array all available commands as keys and their description as values.
     */
    protected function getCommandDescriptions()
    {
        $descriptions = [];
        foreach ($this->getCommands() as $command) {
            $description = '';

            $result = $this->createController($command);
            if ($result !== false && $result[0] instanceof Controller) {
                list($controller, $actionID) = $result;
                /** @var Controller $controller */
                $description = $controller->getHelpSummary();
            }

            $descriptions[$command] = $description;
        }

        return $descriptions;
    }

    /**
     * Returns all available actions of the specified controller.
     * @param Controller $controller the controller instance
     * @return array all available action IDs.
     */
    public function getActions($controller)
    {
        return $controller->actions();
    }

    /**
     * Validates if the given class is a valid console controller class.
     * @param string $controllerClass
     * @return bool
     */
    protected function validateControllerClass($controllerClass)
    {
        if (class_exists($controllerClass)) {
            $class = new \ReflectionClass($controllerClass);
            return !$class->isAbstract() && $class->isSubclassOf('Reaction\Console\Routes\Controller');
        }

        return false;
    }

    /**
     * Displays all available commands.
     */
    protected function getDefaultHelp()
    {
        $commands = $this->getCommandDescriptions();
        $this->stdout($this->getDefaultHelpHeader());
        if (!empty($commands)) {
            $this->stdout("\nThe following commands are available:\n\n", Console::BOLD);
            $len = 0;
            foreach ($commands as $command => $description) {
                $result = $this->createController($command);
                if ($result !== false && $result[0] instanceof Controller) {
                    /** @var $controller Controller */
                    list($controller, $actionId) = $result;
                    $actions = $this->getActions($controller);
                    if (!empty($actions)) {
                        $prefix = $controller->getUniqueId();
                        foreach ($actions as $action) {
                            $string = $prefix . '/' . $action;
                            if ($action === $controller->defaultAction) {
                                $string .= ' (default)';
                            }
                            if (($l = strlen($string)) > $len) {
                                $len = $l;
                            }
                        }
                    }
                } elseif (($l = strlen($command)) > $len) {
                    $len = $l;
                }
            }
            foreach ($commands as $command => $description) {
                $this->stdout('- ' . $this->ansiFormat($command, Console::FG_YELLOW));
                $this->stdout(str_repeat(' ', $len + 4 - strlen($command)));
                $this->stdout(Console::wrapText($description, $len + 4 + 2), Console::BOLD);
                $this->stdout("\n");

                $result = $this->createController($command);
                if ($result !== false && $result[0] instanceof Controller) {
                    list($controller, $actionId) = $result;
                    $actions = $this->getActions($controller);
                    if (!empty($actions)) {
                        $prefix = $controller->getUniqueId();
                        foreach ($actions as $action) {
                            $string = '  ' . $prefix . '/' . $action;
                            $this->stdout('  ' . $this->ansiFormat($string, Console::FG_GREEN));
                            if ($action === $controller->defaultAction) {
                                $string .= ' (default)';
                                $this->stdout(' (default)', Console::FG_YELLOW);
                            }
                            $summary = $controller->getActionHelpSummary($action);
                            if ($summary !== '') {
                                $this->stdout(str_repeat(' ', $len + 4 - strlen($string)));
                                $this->stdout(Console::wrapText($summary, $len + 4 + 2));
                            }
                            $this->stdout("\n");
                        }
                    }
                    $this->stdout("\n");
                }
            }
            $scriptName = $this->getScriptName();
            $this->stdout("\nTo see the help of each command, enter:\n", Console::BOLD);
            $this->stdout("\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
                . $this->ansiFormat('<command-name>', Console::FG_CYAN) . "\n\n");
        } else {
            $this->stdout("\nNo commands are found.\n\n", Console::BOLD);
        }
    }

    /**
     * Displays the overall information of the command.
     * @param Controller                  $controller the controller instance
     */
    protected function getCommandHelp($controller)
    {
        $controller->color = $this->color;

        $this->stdout("\nDESCRIPTION\n", Console::BOLD);
        $comment = $controller->getHelp();
        if ($comment !== '') {
            $this->stdout("\n$comment\n\n");
        }

        $actions = $this->getActions($controller);
        if (!empty($actions)) {
            $this->stdout("\nSUB-COMMANDS\n\n", Console::BOLD);
            $prefix = $controller->getUniqueId();

            $maxlen = 5;
            foreach ($actions as $action) {
                $len = strlen($prefix . '/' . $action) + 2 + ($action === $controller->defaultAction ? 10 : 0);
                if ($maxlen < $len) {
                    $maxlen = $len;
                }
            }
            foreach ($actions as $action) {
                $this->stdout('- ' . $this->ansiFormat($prefix . '/' . $action, Console::FG_YELLOW));
                $len = strlen($prefix . '/' . $action) + 2;
                if ($action === $controller->defaultAction) {
                    $this->stdout(' (default)', Console::FG_GREEN);
                    $len += 10;
                }
                $summary = $controller->getActionHelpSummary($action);
                if ($summary !== '') {
                    $this->stdout(str_repeat(' ', $maxlen - $len + 2) . Console::wrapText($summary, $maxlen + 2));
                }
                $this->stdout("\n");
            }
            $scriptName = $this->getScriptName();
            $this->stdout("\nTo see the detailed information about individual sub-commands, enter:\n");
            $this->stdout("\n  $scriptName " . $this->ansiFormat('help', Console::FG_YELLOW) . ' '
                . $this->ansiFormat('<sub-command>', Console::FG_CYAN) . "\n\n");
        }
    }

    /**
     * Displays the detailed information of a command action.
     * @param Controller $controller the controller instance
     * @param string     $actionId action ID
     * @throws Exception if the action does not exist
     */
    protected function getSubCommandHelp($controller, $actionId)
    {
        if (!$controller->hasAction($actionId)) {
            $name = $this->ansiFormat(rtrim($controller->getUniqueId() . '/' . $actionId, '/'), Console::FG_YELLOW);
            throw new Exception("No help for unknown sub-command \"$name\".");
        }

        $description = $controller->getActionHelp($actionId);
        if ($description !== '') {
            $this->stdout("\nDESCRIPTION\n", Console::BOLD);
            $this->stdout("\n$description\n\n");
        }

        $this->stdout("\nUSAGE\n\n", Console::BOLD);
        $scriptName = $this->getScriptName();
        if ($actionId === $controller->defaultAction) {
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId(), Console::FG_YELLOW));
        } else {
            $this->stdout($scriptName . ' ' . $this->ansiFormat($controller->getUniqueId() . '/' . $actionId, Console::FG_YELLOW));
        }

        $args = $controller->getActionArgsHelp($actionId);
        foreach ($args as $name => $arg) {
            if ($arg['required']) {
                $this->stdout(' <' . $name . '>', Console::FG_CYAN);
            } else {
                $this->stdout(' [' . $name . ']', Console::FG_CYAN);
            }
        }

        $options = $controller->getActionOptionsHelp($actionId);
        ksort($options);

        if (!empty($options)) {
            $this->stdout(' [...options...]', Console::FG_RED);
        }
        $this->stdout("\n\n");

        if (!empty($args)) {
            foreach ($args as $name => $arg) {
                $this->stdout($this->formatOptionHelp(
                        '- ' . $this->ansiFormat($name, Console::FG_CYAN),
                        $arg['required'],
                        $arg['type'],
                        $arg['default'],
                        $arg['comment']) . "\n\n");
            }
        }

        if (!empty($options)) {
            $this->stdout("\nOPTIONS\n\n", Console::BOLD);
            foreach ($options as $name => $option) {
                $this->stdout($this->formatOptionHelp(
                        $this->ansiFormat('--' . $name . $this->formatOptionAliases($controller, $name),
                            Console::FG_RED, empty($option['required']) ? Console::FG_RED : Console::BOLD),
                        !empty($option['required']),
                        $option['type'],
                        $option['default'],
                        $option['comment']) . "\n\n");
            }
        }
    }

    /**
     * Generates a well-formed string for an argument or option.
     * @param string $name the name of the argument or option
     * @param bool $required whether the argument is required
     * @param string $type the type of the option or argument
     * @param mixed $defaultValue the default value of the option or argument
     * @param string $comment comment about the option or argument
     * @return string the formatted string for the argument or option
     */
    protected function formatOptionHelp($name, $required, $type, $defaultValue, $comment)
    {
        $comment = trim($comment);
        $type = trim($type);
        if (strncmp($type, 'bool', 4) === 0) {
            $type = 'boolean, 0 or 1';
        }

        if ($defaultValue !== null && !is_array($defaultValue)) {
            if ($type === null) {
                $type = gettype($defaultValue);
            }
            if (is_bool($defaultValue)) {
                // show as integer to avoid confusion
                $defaultValue = (int)$defaultValue;
            }
            if (is_string($defaultValue)) {
                $defaultValue = "'" . $defaultValue . "'";
            } else {
                $defaultValue = var_export($defaultValue, true);
            }
            $doc = "$type (defaults to $defaultValue)";
        } else {
            $doc = $type;
        }

        if ($doc === '') {
            $doc = $comment;
        } elseif ($comment !== '') {
            $doc .= "\n" . preg_replace('/^/m', '  ', $comment);
        }

        $name = $required ? "$name (required)" : $name;

        return $doc === '' ? $name : "$name: $doc";
    }

    /**
     * @param Controller $controller the controller instance
     * @param string $option the option name
     * @return string the formatted string for the alias argument or option
     */
    protected function formatOptionAliases($controller, $option)
    {
        foreach ($controller->optionAliases() as $name => $value) {
            if ($value === $option) {
                return ', -' . $name;
            }
        }

        return '';
    }

    /**
     * @param RequestApplicationInterface $app
     * @return string the name of the cli script currently running.
     */
    protected function getScriptName()
    {
        return basename($this->app->reqHelper->getScriptFile());
    }

    /**
     * Return a default help header.
     * @return string default help header.
     */
    protected function getDefaultHelpHeader()
    {
        return "\nThis is Reaction version " . \Reaction::getVersion() . ".\n";
    }

    /**
     * Create controller helper
     * @param string $command
     * @param array  $config
     * @param bool   $defaultOnFault
     * @return array
     */
    protected function createController($command, $config = [], $defaultOnFault = true) {
        $config['app'] = $this->app;
        return Reaction::$app->router->createController($command, $config, $defaultOnFault);
    }
}


<?php

namespace Reaction\Base;

use Reaction;
use Reaction\Exceptions\ErrorException;
use Reaction\Exceptions\Exception;
use Reaction\Exceptions\HttpException;
use Reaction\Exceptions\UserException;
use Reaction\Helpers\Console;
use Reaction\Helpers\VarDumper;

/**
 * ErrorHandler handles uncaught PHP errors and exceptions.
 *
 * ErrorHandler is configured as an application component in [[\Reaction\StaticApplicationInterface]] by default.
 * You can access that instance via `Reaction::$app->errorHandler`.
 *
 * @package Reaction\Base
 */
abstract class ErrorHandler extends RequestAppComponent implements ErrorHandlerInterface
{
    /**
     * @var bool whether to discard any existing page output before error display. Defaults to true.
     */
    public $discardExistingOutput = true;
    /**
     * @var int the size of the reserved memory. A portion of memory is pre-allocated so that
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
     * Defaults to 256KB.
     */
    public $memoryReserveSize = 262144;
    /**
     * @var \Exception|null the exception that is being handled currently.
     */
    public $exception;

    /**
     * @var string Used to reserve memory for fatal error handler.
     */
    private $_memoryReserve;

    /**
     * Handles uncaught PHP exceptions.
     *
     * This method is implemented as a PHP exception handler.
     *
     * @param \Exception $exception the exception that is not caught
     * @return Reaction\Web\Response
     */
    public function handleException($exception)
    {
        $this->exception = $exception;

        try {
            $this->logException($exception);
            if ($this->discardExistingOutput) {
                $this->clearOutput();
            }
            return $this->renderException($exception);
        } catch (\Exception $e) {
            // an other exception could be thrown while displaying the exception
            $this->handleFallbackExceptionMessage($e, $exception);
        } catch (\Throwable $e) {
            // additional check for \Throwable introduced in PHP 7
            $this->handleFallbackExceptionMessage($e, $exception);
        }

        $this->exception = null;
        return new Reaction\Web\Response(500, [], 'Server error');
    }

    /**
     * Handles PHP execution errors such as warnings and notices.
     *
     * This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
     *
     * @param int $code the level of the error raised.
     * @param string $message the error message.
     * @param string $file the filename that the error was raised in.
     * @param int $line the line number the error was raised at.
     * @return bool whether the normal error handler continues.
     *
     * @throws ErrorException
     */
    public function handleError($code, $message, $file, $line)
    {
        if (error_reporting() & $code) {
            static::loadErrorExceptionClass();
            $exception = new ErrorException($message, $code, $code, $file, $line);

            // in case error appeared in __toString method we can't throw any exception
            $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
            array_shift($trace);
            foreach ($trace as $frame) {
                if ($frame['function'] === '__toString') {
                    $this->handleException($exception);
                }
            }

            throw $exception;
        }

        return false;
    }

    /**
     * Handles fatal PHP errors.
     */
    public function handleFatalError()
    {
        unset($this->_memoryReserve);
        static::loadErrorExceptionClass();
        $error = error_get_last();

        if (ErrorException::isFatalError($error)) {
            $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
            $this->exception = $exception;
            $this->logException($exception);

            if ($this->discardExistingOutput) {
                $this->clearOutput();
            }
            $this->renderException($exception);
        }
    }

    /**
     * Renders the exception.
     * @param \Exception $exception the exception to be rendered.
     */
    abstract protected function renderException($exception);

    /**
     * Logs the given exception.
     * @param \Exception $exception the exception to be logged
     */
    public function logException($exception)
    {
        $category = get_class($exception);
        if ($exception instanceof HttpException) {
            $category = 'Reaction\\Exceptions\\HttpException:' . $exception->statusCode;
        } elseif ($exception instanceof \ErrorException) {
            $category .= ':' . $exception->getSeverity();
        }
        $message = $exception->getMessage();
        $fileLine = '';
        if ($exception->getFile() && $exception->getLine()) {
            $fileLine = "\n" . $exception->getFile() . ':' . $exception->getLine();
        }
        $logMessage = $category . "\n" . $message . $fileLine;
        if (Console::streamSupportsAnsiColors(STDOUT)) {
            $logMessage = Console::ansiFormat($logMessage, [Console::FG_RED]);
        }
        Reaction::$app->logger->logRaw($logMessage);
    }

    /**
     * Removes all output echoed before calling this method.
     */
    public function clearOutput()
    {
        // the following manual level counting is to deal with zlib.output_compression set to On
        for ($level = ob_get_level(); $level > 0; --$level) {
            if (!@ob_end_clean()) {
                ob_clean();
            }
        }
    }

    /**
     * Handles exception thrown during exception processing in [[handleException()]].
     * @param \Exception|\Throwable $exception Exception that was thrown during main exception processing.
     * @param \Exception $previousException Main exception processed in [[handleException()]].
     */
    protected function handleFallbackExceptionMessage($exception, $previousException)
    {
        $msg = "An Error occurred while handling another error:\n";
        $msg .= (string)$exception;
        $msg .= "\nPrevious exception:\n";
        $msg .= (string)$previousException;
        if (Reaction::isDebug()) {
            if (Reaction::isConsoleApp()) {
                echo $msg . "\n";
            } else {
                echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Reaction::$app->charset) . '</pre>';
            }
        } else {
            echo 'An internal server error occurred.';
        }
        $msg .= "\n\$_SERVER (GLOBAL) = " . VarDumper::export($_SERVER);
        $msg .= "\n\$_SERVER (LOCAL) = " . VarDumper::export($this->app->reqHelper->getServerParams());
        error_log($msg);
    }

    /**
     * Converts an exception into a PHP error.
     *
     * This method can be used to convert exceptions inside of methods like `__toString()`
     * to PHP errors because exceptions cannot be thrown inside of them.
     * @param \Exception $exception the exception to convert to a PHP error.
     */
    public static function convertExceptionToError($exception)
    {
        trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
    }

    /**
     * Converts an exception into a simple string.
     * @param \Exception|\Error $exception the exception being converted
     * @return string the string representation of the exception.
     */
    public static function convertExceptionToString($exception)
    {
        if ($exception instanceof UserException) {
            return "{$exception->getName()}: {$exception->getMessage()}";
        }

        if (Reaction::isDebug()) {
            return static::convertExceptionToVerboseString($exception);
        }

        return 'An internal server error occurred.';
    }

    /**
     * Converts an exception into a string that has verbose information about the exception and its trace.
     * @param \Exception|\Error $exception the exception being converted
     * @return string the string representation of the exception.
     */
    public static function convertExceptionToVerboseString($exception)
    {
        if ($exception instanceof Exception) {
            $message = "Exception ({$exception->getName()})";
        } elseif ($exception instanceof ErrorException) {
            $message = (string)$exception->getName();
        } else {
            $message = 'Exception';
        }
        $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
            . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
            . "Stack trace:\n" . $exception->getTraceAsString();

        return $message;
    }

    /**
     * Get trace from exception
     * @param \Throwable $exception
     * @param bool       $asString
     * @return array|string
     */
    public static function getExceptionTrace(\Throwable $exception, $asString = true) {
        $trace = $exception->getTrace();
        $trace = static::reduceStackTrace($exception, $trace);
        return $asString ? static::getExceptionTraceAsString($trace) : $trace;
    }

    /**
     * Reduce length of exceptions stack trace
     * @param \Throwable $exception
     * @param array $trace
     * @return array
     */
    public static function reduceStackTrace(\Throwable $exception, $trace)
    {
        $exclude = [
            'Reaction\Promise\Promise' => ['settle'],
            'React\Promise\Promise' => ['settle'],
            'Rx\Observer\AbstractObserver' => [],
            'Rx\Observer\AutoDetachObserver' => [],
            'Rx\Observer\CallbackObserver' => [],
        ];
        $file = $exception->getFile();
        $traceNew = [];
        foreach ($trace as $num => $row) {
            $rowFile = isset($row['file']) ? $row['file'] : null;
            $rowClass = isset($row['class']) ? $row['class'] : null;
            $rowFunction = isset($row['function']) ? $row['function'] : '{closure}';
            if (
                isset($exclude[$rowClass])
                && (empty($exclude[$rowClass]) || in_array($rowFunction, $exclude[$rowClass]))
                && $rowFile !== $file
            ) {
                continue;
            }
            $traceNew[] = $row;
        }
        return $traceNew;
    }

    /**
     * @param array $trace
     * @return string
     */
    protected static function getExceptionTraceAsString($trace = [])
    {
        $ansiEnabled = Reaction::isConsoleApp() && Console::streamSupportsAnsiColors(\STDOUT);
        $traceStr = '';
        $num = 0;
        foreach ($trace as $row) {
            $rowFile = isset($row['file']) ? $row['file'] : '[internal function]';
            $rowLine = isset($row['line']) ? '(' . $row['line'] . ')' : '';
            $rowClass = isset($row['class']) ? $row['class'] : '';
            $rowFunction = isset($row['function']) ? $row['function'] : '{closure}';
            $callType = isset($row['type']) ? $row['type'] : '';
            $rowFileLine = $rowFile . $rowLine;
            if ($ansiEnabled) {
                $rowFunction = Console::ansiFormat($rowFunction, [Console::FG_PURPLE]);
                $rowClass = $rowClass !== "" ? Console::ansiFormat($rowClass, [Console::FG_PURPLE]) : $rowClass;
                $callType = Console::ansiFormat($callType, [Console::FG_GREEN]);
                $rowFileLine = Console::ansiFormat($rowFileLine, [Console::FG_GREY]);
            }
            $rowStr = '#' . $num . ' ' . $rowFileLine . ': '
                . ($rowClass !== '' ? $rowClass . $callType : '') . $rowFunction;
            if (!empty($row['args'])) {
                $argsArray = [];
                foreach ($row['args'] as $arg) {
                    $argsArray[] = static::getValueType($arg, $ansiEnabled);
                }
                $rowStr .= '(' . implode(', ', $argsArray) . ')';
            } else {
                $rowStr .= '()';
            }
            $traceStr .= $rowStr . "\n";
            $num++;
        }
        return $traceStr;
    }

    /**
     * @param mixed $value
     * @param bool  $withValue
     * @param bool  $ansi
     * @return int|null|string
     */
    protected static function getValueType($value, $withValue = true, $ansi = true) {
        $type = gettype($value);
        $valuePrint = null;
        if ($type === 'object') {
            $type = get_class($value);
        } elseif ($type === 'string') {
            $length = mb_strlen($value);
            if ($length <= 20) {
                $valuePrint = "'" . preg_replace('/\s/', ' ', $value) . "'";
                $type = null;
            } else {
                $valuePrint = 'ln:' . $length;
            }
        } elseif ($type === 'integer') {
            $valuePrint = (int)$value;
            $type = null;
        } elseif ($type === 'array') {
            $length = count($value);
            if ($length === 0) {
                $valuePrint = '[]';
                $type = null;
            } else {
                $valuePrint = 'ln:' . $length . '';
            }
        } elseif ($type === 'boolean') {
            $valuePrint = $value ? 'TRUE' : 'FALSE';
        }

        if ($ansi) {
            if ($valuePrint !== null) {
                $valuePrint = Console::ansiFormat($valuePrint, [Console::FG_BLUE]);
            }
            if ($type !== null) {
                $type = Console::ansiFormat($type, [Console::FG_YELLOW]);
            }
        }

        if ($type === null) {
            return $valuePrint;
        }

        $str = $withValue && isset($valuePrint) ? $type . '(' . $valuePrint . ')' : $type;

        return $str;
    }

    /**
     * Manual loading ErrorException class file.
     * Load ErrorException manually here because autoloading them will not work
     * when error occurs while autoloading a class
     */
    protected static function loadErrorExceptionClass()
    {
        if (!class_exists('Reaction\\Exceptions\\ErrorException', false)) {
            require_once __DIR__ . '/../Exceptions/ErrorException.php';
        }
    }
}


<?php

namespace Reaction\Validators;

use Reaction;
use Reaction\Base\Model;
use Reaction\Db\Orm\ActiveQuery;
use Reaction\Db\Orm\ActiveQueryInterface;
use Reaction\Db\Orm\ActiveRecord;
use Reaction\Db\Orm\ActiveRecordInterface;
use Reaction\Helpers\Inflector;
use Reaction\I18n\I18N;
use Reaction\Promise\ExtendedPromiseInterface;
use function Reaction\Promise\resolve;

/**
 * UniqueValidator validates that the attribute value is unique in the specified database table.
 *
 * UniqueValidator checks if the value being validated is unique in the table column specified by
 * the ActiveRecord class [[targetClass]] and the attribute [[targetAttribute]].
 *
 * The following are examples of validation rules using this validator:
 *
 * ```php
 * // a1 needs to be unique
 * ['a1', 'unique']
 * // a1 needs to be unique, but column a2 will be used to check the uniqueness of the a1 value
 * ['a1', 'unique', 'targetAttribute' => 'a2']
 * // a1 and a2 need to be unique together, and they both will receive error message
 * [['a1', 'a2'], 'unique', 'targetAttribute' => ['a1', 'a2']]
 * // a1 and a2 need to be unique together, only a1 will receive error message
 * ['a1', 'unique', 'targetAttribute' => ['a1', 'a2']]
 * // a1 needs to be unique by checking the uniqueness of both a2 and a3 (using a1 value)
 * ['a1', 'unique', 'targetAttribute' => ['a2', 'a1' => 'a3']]
 */
class UniqueValidator extends Validator
{
    /**
     * @var string the name of the ActiveRecord class that should be used to validate the uniqueness
     * of the current attribute value. If not set, it will use the ActiveRecord class of the attribute being validated.
     * @see targetAttribute
     */
    public $targetClass;
    /**
     * @var string|array the name of the [[\yii\db\ActiveRecord|ActiveRecord]] attribute that should be used to
     * validate the uniqueness of the current attribute value. If not set, it will use the name
     * of the attribute currently being validated. You may use an array to validate the uniqueness
     * of multiple columns at the same time. The array values are the attributes that will be
     * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated.
     */
    public $targetAttribute;
    /**
     * @var string|array|\Closure additional filter to be applied to the DB query used to check the uniqueness of the attribute value.
     * This can be a string or an array representing the additional query condition (refer to [[\yii\db\Query::where()]]
     * on the format of query condition), or an anonymous function with the signature `function ($query)`, where `$query`
     * is the [[\yii\db\Query|Query]] object that you can modify in the function.
     */
    public $filter;
    /**
     * @var string the user-defined error message.
     *
     * When validating single attribute, it may contain
     * the following placeholders which will be replaced accordingly by the validator:
     *
     * - `{attribute}`: the label of the attribute being validated
     * - `{value}`: the value of the attribute being validated
     *
     * When validating mutliple attributes, it may contain the following placeholders:
     *
     * - `{attributes}`: the labels of the attributes being validated.
     * - `{values}`: the values of the attributes being validated.
     */
    public $message;
    /**
     * @var string and|or define how target attributes are related
     */
    public $targetAttributeJunction = 'and';
    /**
     * @var bool whether this validator is forced to always use master DB
     */
    public $forceMasterDb = true;

    /**
     * {@inheritdoc}
     */
    public function init()
    {
        parent::init();
        if ($this->message !== null) {
            return;
        }
        if (is_array($this->targetAttribute) && count($this->targetAttribute) > 1) {
            $this->message = Reaction::t('rct', 'The combination {values} of {attributes} has already been taken.');
        } else {
            $this->message = Reaction::t('rct', '{attribute} "{value}" has already been taken.');
        }
    }

    /**
     * {@inheritdoc}
     */
    public function validateAttribute($model, $attribute)
    {
        /* @var $targetClass ActiveRecordInterface */
        $targetClass = $this->getTargetClass($model);
        $targetAttribute = $this->targetAttribute === null ? $attribute : $this->targetAttribute;
        $rawConditions = $this->prepareConditions($targetAttribute, $model, $attribute);
        $conditions = [$this->targetAttributeJunction === 'or' ? 'or' : 'and'];

        foreach ($rawConditions as $key => $value) {
            if (is_array($value)) {
                $this->addError($model, $attribute, Reaction::t('rct', '{attribute} is invalid.'));
                return resolve(false);
            }
            $conditions[] = [$key => $value];
        }

        return $this->modelExists($targetClass, $conditions, $model)
            ->then(function() use ($targetAttribute, $attribute, $model) {
                if (is_array($targetAttribute) && count($targetAttribute) > 1) {
                    $this->addComboNotUniqueError($model, $attribute);
                } else {
                    $this->addError($model, $attribute, $this->message);
                }
                return null;
            }, function() {
                return null;
            });
    }

    /**
     * @param Model $model the data model to be validated
     * @return string Target class name
     */
    private function getTargetClass($model)
    {
        return $this->targetClass === null ? get_class($model) : $this->targetClass;
    }

    /**
     * Checks whether the $model exists in the database.
     *
     * @param string $targetClass the name of the ActiveRecord class that should be used to validate the uniqueness
     * of the current attribute value.
     * @param array $conditions conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format.
     * @param Model $model the data model to be validated
     *
     * @return ExtendedPromiseInterface with bool whether the model already exists
     */
    private function modelExists($targetClass, $conditions, $model)
    {
        /** @var ActiveRecordInterface $targetClass $query */
        $query = $this->prepareQuery($targetClass, $conditions);

        if (!$model instanceof ActiveRecordInterface || $model->getIsNewRecord() || get_class($model) !== $targetClass) {
            // if current $model isn't in the database yet then it's OK just to call exists()
            // also there's no need to run check based on primary keys, when $targetClass is not the same as $model's class
            return $query->exists();
        } else {
            // if current $model is in the database already we can't use exists()
            if ($query instanceof \Reaction\Db\Orm\ActiveQuery) {
                // only select primary key to optimize query
                $columnsCondition = array_flip($targetClass::primaryKey());
                $query->select(array_flip($this->applyTableAlias($query, $columnsCondition)));

                // any with relation can't be loaded because related fields are not selected
                $query->with = null;
            }
            return $query->limit(2)->asArray()->all()
                ->then(function($models) use ($targetClass, $model) {
                    $n = count($models);
                    if ($n === 1) {
                        // if there is one record, check if it is the currently validated model
                        $dbModel = reset($models);
                        $pks = $targetClass::primaryKey();
                        $pk = [];
                        foreach ($pks as $pkAttribute) {
                            $pk[$pkAttribute] = $dbModel[$pkAttribute];
                        }
                        $exists = ($pk != $model->getOldPrimaryKey(true));
                    } else {
                        // if there is more than one record, the value is not unique
                        $exists = $n > 1;
                    }
                    return $exists;
                });
        }
    }

    /**
     * Prepares a query by applying filtering conditions defined in $conditions method property
     * and [[filter]] class property.
     *
     * @param ActiveRecordInterface $targetClass the name of the ActiveRecord class that should be used to validate
     * the uniqueness of the current attribute value.
     * @param array $conditions conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format
     *
     * @return ActiveQueryInterface|ActiveQuery
     */
    private function prepareQuery($targetClass, $conditions)
    {
        $query = $targetClass::find();
        $query->andWhere($conditions);
        if ($this->filter instanceof \Closure) {
            call_user_func($this->filter, $query);
        } elseif ($this->filter !== null) {
            $query->andWhere($this->filter);
        }

        return $query;
    }

    /**
     * Processes attributes' relations described in $targetAttribute parameter into conditions, compatible with
     * [[\yii\db\Query::where()|Query::where()]] key-value format.
     *
     * @param string|array $targetAttribute the name of the [[\yii\db\ActiveRecord|ActiveRecord]] attribute that
     * should be used to validate the uniqueness of the current attribute value. You may use an array to validate
     * the uniqueness of multiple columns at the same time. The array values are the attributes that will be
     * used to validate the uniqueness, while the array keys are the attributes whose values are to be validated.
     * If the key and the value are the same, you can just specify the value.
     * @param Model $model the data model to be validated
     * @param string $attribute the name of the attribute to be validated in the $model
     *
     * @return array conditions, compatible with [[\yii\db\Query::where()|Query::where()]] key-value format.
     */
    private function prepareConditions($targetAttribute, $model, $attribute)
    {
        if (is_array($targetAttribute)) {
            $conditions = [];
            foreach ($targetAttribute as $k => $v) {
                $conditions[$v] = is_int($k) ? $model->$v : $model->$k;
            }
        } else {
            $conditions = [$targetAttribute => $model->$attribute];
        }

        $targetModelClass = $this->getTargetClass($model);
        if (!is_subclass_of($targetModelClass, 'Reaction\Db\Orm\ActiveRecord')) {
            return $conditions;
        }

        /** @var ActiveRecord $targetModelClass */
        return $this->applyTableAlias($targetModelClass::find(), $conditions);
    }

    /**
     * Builds and adds [[comboNotUnique]] error message to the specified model attribute.
     * @param \Reaction\Base\Model $model the data model.
     * @param string $attribute the name of the attribute.
     */
    private function addComboNotUniqueError($model, $attribute)
    {
        $attributeCombo = [];
        $valueCombo = [];
        foreach ($this->targetAttribute as $key => $value) {
            if (is_int($key)) {
                $attributeCombo[] = $model->getAttributeLabel($value);
                $valueCombo[] = '"' . $model->$value . '"';
            } else {
                $attributeCombo[] = $model->getAttributeLabel($key);
                $valueCombo[] = '"' . $model->$key . '"';
            }
        }
        $this->addError($model, $attribute, $this->message, [
            'attributes' => Inflector::sentence($attributeCombo),
            'values' => implode('-', $valueCombo),
        ]);
    }

    /**
     * Returns conditions with alias.
     * @param ActiveQuery $query
     * @param array $conditions array of condition, keys to be modified
     * @param null|string $alias set empty string for no apply alias. Set null for apply primary table alias
     * @return array
     */
    private function applyTableAlias($query, $conditions, $alias = null)
    {
        if ($alias === null) {
            $alias = array_keys($query->getTablesUsedInFrom())[0];
        }
        $prefixedConditions = [];
        foreach ($conditions as $columnName => $columnValue) {
            if (strpos($columnName, '(') === false) {
                $prefixedColumn = "{$alias}.[[" . preg_replace(
                    '/^' . preg_quote($alias) . '\.(.*)$/',
                    '$1',
                    $columnName) . ']]';
            } else {
                // there is an expression, can't prefix it reliably
                $prefixedColumn = $columnName;
            }

            $prefixedConditions[$prefixedColumn] = $columnValue;
        }

        return $prefixedConditions;
    }
}


<?php

namespace Reaction\Helpers\Request;

use Reaction\Base\RequestAppComponent;
use Reaction\I18n\TranslationPromise;

/**
 * Class RequestHelperProxy
 * @package Reaction\Helpers\Request
 */
class RequestAppHelperProxy extends RequestAppComponent
{
    /**
     * @var string Helper static class name
     */
    public $helperClass = '';

    /**
     * Proxy to Helper with added charset parameter at given position
     * @param string  $method
     * @param array   $arguments
     * @param integer $position
     * @return mixed
     */
    protected function proxyWithCharset($method, $arguments = [], $position = -1) {
        return $this->proxyWithAppProperty($method, $arguments, $position, 'charset');
    }

    /**
     * Proxy to Helper with added language parameter at given position
     * @param string  $method
     * @param array   $arguments
     * @param integer $position
     * @return mixed
     */
    protected function proxyWithLanguage($method, $arguments = [], $position = -1) {
        return $this->proxyWithAppProperty($method, $arguments, $position, 'language');
    }

    /**
     * Proxy to Helper with added parameter at given position
     * @param string $method
     * @param array  $arguments
     * @param int    $position
     * @param string $propertyName
     * @return mixed
     */
    private function proxyWithAppProperty($method, $arguments = [], $position = -1, $propertyName = 'charset')
    {
        $this->injectVariableToArguments($this->app->{$propertyName}, $arguments, $position);
        return $this->proxy($method, $arguments);
    }

    /**
     * Proxy to Helper with added RequestApplicationInterface parameter at given position
     * @param string $method
     * @param array  $arguments
     * @param int    $position
     * @return mixed
     */
    protected function proxyWithApp($method, $arguments = [], $position = -1)
    {
        $this->injectVariableToArguments($this->app, $arguments, $position);
        return $this->proxy($method, $arguments);
    }

    /**
     * @param string $method
     * @param array  $arguments
     * @return mixed
     */
    protected function proxy($method, $arguments = []) {
        return call_user_func_array([$this->helperClass, $method], $arguments);
    }

    /**
     * Inject variable to arguments array at given position
     * @param mixed $variable
     * @param array $arguments
     * @param int $position
     */
    protected function injectVariableToArguments($variable, &$arguments, $position = -1) {
        $argsCount = count($arguments);
        //Negative value handle (from end)
        if ($position < 0) {
            $position = $argsCount - $position;
        }
        if (!isset($arguments[$position]) && $position >= 0 && $position <= $argsCount) {
           $arguments[$position] = $variable;
        }
    }

    /**
     * Ensure that string is not a Translation promise
     * @param string|$string
     */
    protected function ensureTranslated(&$string)
    {
        if (is_object($string) && $string instanceof TranslationPromise) {
            $string = $string->translate($this->app->language);
        }
    }

    protected function ensureTranslatedArray(&$array, $path = null)
    {
        $checkValues = isset($path) ? \Reaction\Helpers\ArrayHelper::getValue($array, $path) : $array;
        if (!is_array($checkValues) || empty($checkValues)) {
            return;
        }
        foreach ($checkValues as $key => $value) {
            $this->ensureTranslated($value);
            $checkValues[$key] = $value;
        }
        if (isset($path)) {
            \Reaction\Helpers\ArrayHelper::setValue($array, $path, $checkValues);
        } else {
            $array = $checkValues;
        }
    }
}


<?php

namespace AbuseIO\Notification;

use ReflectionClass;
use Log;

/**
 * Class Notification
 * @package AbuseIO\Notification
 */
class Notification
{
    /**
     * Configuration Basename (parser name)
     * @var string
     */
    public $configBase;

    /**
     * Create a new Notification instance
     *
     * @param object $notification instance class
     */
    public function __construct($notification)
    {
        $this->startup($notification);
    }

    /**
     * Generalize the local config based on the parser class object.
     *
     * @param object $notification instance class
     * @return void
     */
    protected function startup($notification)
    {
        $reflect = new ReflectionClass($notification);

        $this->configBase = 'notifications.' . $reflect->getShortName();

        if (empty(config("{$this->configBase}.notification.name"))) {
            $this->failed("Required notification.name is missing in notification configuration");
        }

        Log::info(
            get_class($this) . ': ' .
            'A notification module has been called: ' .
            config("{$this->configBase}.notification.name")
        );

    }

    /**
     * Return failed
     *
     * @param  string $message
     * @return array
     */
    protected function failed($message)
    {
        $this->cleanup();

        Log::warning(
            get_class($this) . ': ' .
            'Notification run failed for module ' . config("{$this->configBase}.notification.name")
            . ' has ended with errors. ' . $message
        );

        return [
            'errorStatus'   => true,
            'errorMessage'  => $message,
        ];
    }

    /**
     * Return success
     *
     * @return array
     */
    protected function success()
    {
        $this->cleanup();

        Log::info(
            get_class($this) . ': ' .
            'Notification run completed for module : ' . config("{$this->configBase}.notification.name")
        );

        return [
            'errorStatus'   => false,
            'errorMessage'  => 'Notification sucessfully send',
        ];
    }

    /**
     * Cleanup anything a parser might have left (basically, remove the working dir)
     *
     * @return void
     */
    protected function cleanup()
    {

    }
}