<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Database;

use Closure;
use InvalidArgumentException;
use Leevel\Event\IDispatch;
use PDO;
use PDOException;
use Throwable;

/**
 * 数据库连接抽象层
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2017.03.09
 *
 * @version 1.0
 */
abstract class Connect
{
    /**
     * 所有数据库连接.
     *
     * @var array
     */
    protected $connects = [];

    /**
     * 当前数据库连接.
     *
     * @var array
     */
    protected $connect;

    /**
     * PDO 预处理语句对象
     *
     * @var PDOStatement
     */
    protected $pdoStatement;

    /**
     * 数据查询组件.
     *
     * @var \Leevel\Database\Select
     */
    protected $select;

    /**
     * 数据库连接参数.
     *
     * @var array
     */
    protected $option = [];

    /**
     * sql 最后查询语句.
     *
     * @var string
     */
    protected $sql;

    /**
     * sql 绑定参数.
     *
     * @var array
     */
    protected $bindParams = [];

    /**
     * sql 影响记录数量.
     *
     * @var int
     */
    protected $numRows = 0;

    /**
     * 事务等级.
     *
     * @var int
     */
    protected $transactionLevel = 0;

    /**
     * 是否开启部分事务.
     * 依赖数据库是否支持部分事务.
     *
     * @var bool
     */
    protected $transactionWithSavepoints = false;

    /**
     * 是否仅仅是事务回滚.
     *
     * @var bool
     */
    protected $isRollbackOnly = false;

    /**
     * 断线重连次数.
     *
     * @var int
     */
    protected $reconnectRetry = 0;

    /**
     * 事件处理器.
     *
     * @var \Leevel\Event\IDispatch
     */
    protected $dispatch;

    /**
     * 构造函数.
     *
     * @param array                   $option
     * @param \Leevel\Event\IDispatch $dispatch
     */
    public function __construct(array $option, IDispatch $dispatch = null)
    {
        $this->option = $option;
        $this->dispatch = $dispatch;
    }

    /**
     * 析构方法.
     */
    public function __destruct()
    {
        $this->close();
    }

    /**
     * call.
     *
     * @param string $method
     * @param array  $args
     *
     * @return mixed
     */
    public function __call(string $method, array $args)
    {
        $this->initSelect();

        return $this->select->{$method}(...$args);
    }

    /**
     * 返回 Pdo 查询连接.
     *
     * @param bool|int $master
     * @note bool false (读服务器) true (写服务器)
     * @note 其它去对应服务器连接ID 0 表示主服务器
     *
     * @return mixed
     */
    public function pdo($master = false)
    {
        if (is_bool($master)) {
            if (false === $master) {
                return $this->readConnect();
            }

            return $this->writeConnect();
        }

        return $this->connects[$master] ?? null;
    }

    /**
     * 查询数据记录.
     *
     * @param string   $sql           sql 语句
     * @param array    $bindParams    sql 参数绑定
     * @param bool|int $master
     * @param int      $fetchStyle
     * @param mixed    $fetchArgument
     * @param array    $ctorArgs
     *
     * @return mixed
     */
    public function query(string $sql, array $bindParams = [], $master = false, ?int $fetchStyle = null, $fetchArgument = null, array $ctorArgs = [])
    {
        $this->initSelect();

        $this->setLastSql($sql, $bindParams);

        if (!in_array(($sqlType = $this->normalizeSqlType($sql)), [
            'select',
            'procedure',
        ], true)) {
            throw new InvalidArgumentException(
                'The query method only allows select and procedure SQL statements.'
            );
        }

        $this->pdoStatement = $this->pdo($master)->prepare($sql);

        $this->bindParams($bindParams);

        try {
            $this->pdoStatement->execute();
            $this->reconnectRetry = 0;
        } catch (PDOException $e) {
            if ($this->needReconnect($e)) {
                $this->reconnectRetry++;
                $this->close();

                return self::query($sql, $bindParams, $master, $fetchStyle, $fetchArgument, $ctorArgs);
            }

            throw $e;
        }

        $this->numRows = $this->pdoStatement->rowCount();

        return $this->fetchResult($fetchStyle, $fetchArgument, $ctorArgs, 'procedure' === $sqlType);
    }

    /**
     * 执行 sql 语句.
     *
     * @param string $sql        sql 语句
     * @param array  $bindParams sql 参数绑定
     *
     * @return int|string
     */
    public function execute(string $sql, array $bindParams = [])
    {
        $this->initSelect();

        $this->setLastSql($sql, $bindParams);

        if (in_array(($sqlType = $this->normalizeSqlType($sql)), [
            'select',
            'procedure',
        ], true)) {
            throw new InvalidArgumentException(
                'The query method not allows select and procedure SQL statements.'
            );
        }

        $this->pdoStatement = $this->pdo(true)->prepare($sql);

        $this->bindParams($bindParams);

        try {
            $this->pdoStatement->execute();
            $this->reconnectRetry = 0;
        } catch (PDOException $e) {
            if ($this->needReconnect($e)) {
                $this->reconnectRetry++;
                $this->close();

                return self::execute($sql, $bindParams);
            }

            $this->pdoException($e);
        }

        $this->numRows = $this->pdoStatement->rowCount();

        if (in_array($sqlType, [
            'insert',
            'replace',
        ], true)) {
            return $this->lastInsertId();
        }

        return $this->numRows;
    }

    /**
     * 执行数据库事务
     *
     * @param \Closure $action
     *
     * @return mixed
     */
    public function transaction(Closure $action)
    {
        $this->beginTransaction();

        try {
            $result = call_user_func_array($action, [
                $this,
            ]);

            $this->commit();

            return $result;
        } catch (Throwable $e) {
            $this->rollBack();

            throw $e;
        }
    }

    /**
     * 启动事务.
     */
    public function beginTransaction(): void
    {
        $this->transactionLevel++;

        if (1 === $this->transactionLevel) {
            try { // @codeCoverageIgnore
                $this->pdo(true)->beginTransaction();
            } catch (Exception $e) { // @codeCoverageIgnore
                $this->transactionLevel--; // @codeCoverageIgnore

                throw $e; // @codeCoverageIgnore
            } // @codeCoverageIgnore
        } elseif ($this->transactionLevel > 1 && $this->hasSavepoints()) {
            $this->createSavepoint($this->getSavepointName());
        }
    }

    /**
     * 检查是否处于事务中.
     *
     * @return bool
     */
    public function inTransaction(): bool
    {
        return $this->pdo(true)->inTransaction();
    }

    /**
     * 用于非自动提交状态下面的查询提交.
     */
    public function commit(): void
    {
        if (0 === $this->transactionLevel) {
            throw new InvalidArgumentException('There was no active transaction.');
        }

        if ($this->isRollbackOnly) {
            throw new InvalidArgumentException('Commit failed for rollback only.');
        }

        if (1 === $this->transactionLevel) {
            $this->pdo(true)->commit();
        } elseif ($this->transactionLevel > 1 && $this->hasSavepoints()) {
            $this->releaseSavepoint($this->getSavepointName());
        }

        $this->transactionLevel = max(0, $this->transactionLevel - 1);
    }

    /**
     * 事务回滚.
     */
    public function rollBack(): void
    {
        if (0 === $this->transactionLevel) {
            throw new InvalidArgumentException('There was no active transaction.');
        }

        if (1 === $this->transactionLevel) {
            $this->transactionLevel = 0;

            $this->pdo(true)->rollBack();

            $this->isRollbackOnly = false;
        } elseif ($this->transactionLevel > 1 && $this->hasSavepoints()) {
            $this->rollbackSavepoint($this->getSavepointName());
            $this->transactionLevel--;
        } else {
            $this->isRollbackOnly = true;
            $this->transactionLevel = max(0, $this->transactionLevel - 1);
        }
    }

    /**
     * 设置是否启用部分事务.
     *
     * @param bool $savepoints
     */
    public function setSavepoints(bool $savepoints): void
    {
        $this->transactionWithSavepoints = $savepoints;
    }

    /**
     * 获取最后插入 ID 或者列.
     *
     * @param string $name 自增序列名
     *
     * @return string
     */
    public function lastInsertId(?string $name = null): string
    {
        return $this->connect->lastInsertId($name);
    }

    /**
     * 获取最近一次查询的 sql 语句.
     *
     * @return array
     */
    public function lastSql(): array
    {
        return [$this->sql, $this->bindParams];
    }

    /**
     * 返回影响记录.
     *
     * @return int
     */
    public function numRows(): int
    {
        return $this->numRows;
    }

    /**
     * 关闭数据库.
     */
    public function close(): void
    {
        $this->freePDOStatement();

        $this->closeConnects();
    }

    /**
     * 释放 PDO 预处理查询.
     */
    public function freePDOStatement(): void
    {
        $this->pdoStatement = null;
    }

    /**
     * 关闭数据库连接.
     */
    public function closeConnects(): void
    {
        $this->connects = [];
        $this->connect = null;
    }

    /**
     * sql 表达式格式化.
     *
     * @param string $sql
     * @param string $tableName
     *
     * @return string
     */
    public function normalizeExpression(string $sql, string $tableName): string
    {
        preg_match_all('/\[[a-z][a-z0-9_\.]*\]|\[\*\]/i', $sql,
            $matches, PREG_OFFSET_CAPTURE);

        $matches = reset($matches);

        $out = '';
        $offset = 0;

        foreach ($matches as $value) {
            $length = strlen($value[0]);
            $field = substr($value[0], 1, $length - 2);
            $tmp = explode('.', $field);

            switch (count($tmp)) {
                case 2:
                    $field = $tmp[1];
                    $table = $tmp[0];

                    break;
                default:
                    $field = $tmp[0];
                    $table = $tableName;
            }

            $field = $this->normalizeTableOrColumn("{$table}.{$field}");
            $out .= substr($sql, $offset, $value[1] - $offset).$field;
            $offset = $value[1] + $length;
        }

        $out .= substr($sql, $offset);

        return $out;
    }

    /**
     * 表或者字段格式化（支持别名）.
     *
     * @param string $name
     * @param string $alias
     * @param string $as
     *
     * @return string
     */
    public function normalizeTableOrColumn(string $name, ?string $alias = null, ?string $as = null): string
    {
        $name = str_replace('`', '', $name);

        if (false === strpos($name, '.')) {
            $name = $this->identifierColumn($name);
        } else {
            $tmp = explode('.', $name);

            foreach ($tmp as $offset => $name) {
                if (empty($name)) {
                    unset($tmp[$offset]);
                } else {
                    $tmp[$offset] = $this->identifierColumn($name);
                }
            }

            $name = implode('.', $tmp);
        }

        if ($alias) {
            return "{$name} ".($as ? $as.' ' : '').$this->identifierColumn($alias);
        }

        return $name;
    }

    /**
     * 字段格式化.
     *
     * @param string $key
     * @param string $tableName
     *
     * @return string
     */
    public function normalizeColumn(string $key, string $tableName): string
    {
        return $this->normalizeTableOrColumn("{$tableName}.{$key}");
    }

    /**
     * 字段值格式化.
     *
     * @param mixed $value
     * @param bool  $quotationMark
     *
     * @return mixed
     */
    public function normalizeColumnValue($value, bool $quotationMark = true)
    {
        if (is_int($value)) {
            return $value;
        }

        if (is_bool($value)) {
            return $value;
        }

        if (null === $value) {
            return;
        }

        $value = trim($value);

        // 问号占位符
        if ('[?]' === $value) {
            return '?';
        }

        // [:id] 占位符
        if (preg_match('/^\[:[a-z][a-z0-9_\-\.]*\]$/i', $value, $matches)) {
            return trim($matches[0], '[]');
        }

        if (true === $quotationMark) {
            return "'".addslashes($value)."'";
        }

        return $value;
    }

    /**
     * 分析 sql 类型数据.
     *
     * @param string $sql
     *
     * @return string
     */
    public function normalizeSqlType(string $sql): string
    {
        $sql = trim($sql);

        foreach ([
            'select', 'show', 'call', 'exec',
            'delete', 'insert', 'replace', 'update',
        ] as $value) {
            if (0 === stripos($sql, $value)) {
                if ('show' === $value) {
                    $value = 'select';
                } elseif (in_array($value, ['call', 'exec'], true)) {
                    $value = 'procedure';
                }

                return $value;
            }
        }

        return 'statement';
    }

    /**
     * 分析绑定参数类型数据.
     *
     * @see http://php.net/manual/en/pdo.constants.php
     *
     * @param mixed $value
     *
     * @return int
     */
    public function normalizeBindParamType($value): int
    {
        switch (true) {
            case is_int($value):
                return PDO::PARAM_INT;

                break;
            case is_bool($value):
                return PDO::PARAM_BOOL;

                break;
            case null === $value:
                return PDO::PARAM_NULL;

                break;
            case is_string($value):
                return PDO::PARAM_STR;

                break;
            default:
                return PDO::PARAM_STMT;

                break;
        }
    }

    /**
     * 连接主服务器.
     *
     * @return \PDO
     */
    protected function writeConnect(): PDO
    {
        return $this->connect = $this->commonConnect($this->option['master'], IConnect::MASTER, true);
    }

    /**
     * 连接读服务器.
     *
     * @return \PDO
     */
    protected function readConnect(): PDO
    {
        if (false === $this->option['distributed'] || empty($this->option['slave'])) {
            return $this->writeConnect();
        }

        if (count($this->connects) <= 1) {
            foreach ($this->option['slave'] as $read) {
                $this->commonConnect($read, null);
            }

            if (0 === count($this->connects)) {
                return $this->writeConnect();
            }
        }

        $connects = $this->connects;

        if (true === $this->option['separate'] && isset($connects[IConnect::MASTER])) {
            unset($connects[IConnect::MASTER]);
        }

        if (!$connects) {
            return $this->writeConnect();
        }

        $connects = array_values($connects);

        if (1 === count($connects)) {
            return $connects[0];
        }

        return $this->connect = $connects[floor(mt_rand(0, count($connects) - 1))];
    }

    /**
     * 连接数据库.
     *
     * @param array $option
     * @param int   $linkid
     * @param bool  $throwException
     *
     * @return mixed
     */
    protected function commonConnect(array $option = [], ?int $linkid = null, bool $throwException = false)
    {
        if (null === $linkid) {
            $linkid = count($this->connects);
        }

        if (!empty($this->connects[$linkid])) {
            return $this->connects[$linkid];
        }

        try {
            $connect = $this->connects[$linkid] = new PDO(
                $this->parseDsn($option),
                $option['user'],
                $option['password'],
                $option['options']
            );

            $connect->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

            return $connect;
        } catch (PDOException $e) {
            if (false === $throwException) {
                return false;
            }

            throw $e;
        }
    }

    /**
     * pdo 参数绑定.
     *
     * @param array $bindParams
     */
    protected function bindParams(array $bindParams = []): void
    {
        foreach ($bindParams as $key => $val) {
            $key = is_numeric($key) ? $key + 1 : ':'.$key;

            if (is_array($val)) {
                $param = $val[1];
                $val = $val[0];
            } else {
                $param = PDO::PARAM_STR;
            }

            $this->pdoStatement->bindValue($key, $val, $param);
        }
    }

    /**
     * 获得数据集.
     *
     * @param int   $fetchStyle
     * @param mixed $fetchArgument
     * @param array $ctorArgs
     * @param bool  $procedure
     *
     * @return array
     */
    protected function fetchResult(?int $fetchStyle = null, $fetchArgument = null, array $ctorArgs = [], bool $procedure = false): array
    {
        if ($procedure) {
            return $this->fetchProcedureResult($fetchStyle, $fetchArgument, $ctorArgs);
        }

        if (null === $fetchStyle) {
            $fetchStyle = PDO::FETCH_OBJ;
        }

        $args = [$fetchStyle];

        if ($fetchArgument) {
            $args[] = $fetchArgument;

            if ($ctorArgs) {
                $args[] = $ctorArgs;
            }
        }

        return $this->pdoStatement->fetchAll(...$args);
    }

    /**
     * 获得数据集.
     *
     * @param int   $fetchStyle
     * @param mixed $fetchArgument
     * @param array $ctorArgs
     *
     * @see http://php.net/manual/vote-note.php?id=123030&page=pdostatement.nextrowset&vote=down
     *
     * @return array
     */
    protected function fetchProcedureResult(?int $fetchStyle = null, $fetchArgument = null, array $ctorArgs = []): array
    {
        $result = [];

        do {
            try {
                $result[] = $tim = $this->fetchResult($fetchStyle, $fetchArgument, $ctorArgs);
            } catch (PDOException $e) {
            }
        } while ($this->pdoStatement->nextRowset());

        return $result;
    }

    /**
     * 设置 sql 绑定参数.
     *
     * @param string $sql
     * @param array  $bindParams
     */
    protected function setLastSql(string $sql, array $bindParams = []): void
    {
        $this->sql = $sql;
        $this->bindParams = $bindParams;

        $this->handleDispatch($sql, $bindParams);
    }

    /**
     * 事件派发.
     *
     * @param string $sql
     * @param array  $bindParams
     */
    protected function handleDispatch(string $sql, array $bindParams = []): void
    {
        if ($this->dispatch) {
            $this->dispatch->handle(IConnect::SQL_EVENT, $sql, $bindParams);
        }
    }

    /**
     * 获取部分事务回滚点名字.
     *
     * @return string
     */
    protected function getSavepointName(): string
    {
        return 'trans'.$this->transactionLevel;
    }

    /**
     * 保存部分事务保存点.
     *
     * @param string $savepointName
     */
    protected function createSavepoint(string $savepointName): void
    {
        $this->setLastSql($sql = 'SAVEPOINT '.$savepointName);
        $this->pdo(true)->exec($sql);
    }

    /**
     * 回滚部分事务到保存点.
     *
     * @param string $savepointName
     */
    protected function rollbackSavepoint(string $savepointName)
    {
        $this->setLastSql($sql = 'ROLLBACK TO SAVEPOINT '.$savepointName);
        $this->pdo(true)->exec($sql);
    }

    /**
     * 清除前面定义的部分事务保存点.
     *
     * @param string $savepointName
     */
    protected function releaseSavepoint(string $savepointName): void
    {
        $this->setLastSql($sql = 'RELEASE SAVEPOINT '.$savepointName);
        $this->pdo(true)->exec($sql);
    }

    /**
     * 是否需要重连.
     *
     * @return bool
     */
    protected function needReconnect(PDOException $e): bool
    {
        // errorInfo[1] 表示某个驱动错误码，后期扩展需要优化
        // 可以在驱动重写这个方法
        return in_array($e->errorInfo[1], [2006, 2013], true) &&
            $this->reconnectRetry <= self::RECONNECT_MAX;
    }

    /**
     * PDO 异常处理.
     *
     * @param \PDOException $e
     */
    protected function pdoException(PDOException $e): void
    {
        $message = $e->getMessage();

        // 模拟数据库 replace
        if ('23000' === $e->getCode() &&
            false !== strpos($message, 'Duplicate entry')) {
            throw new ReplaceException($message);
        }

        throw $e;
    }

    /**
     * 初始化查询组件.
     */
    protected function initSelect(): void
    {
        $this->select = new Select($this);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Encryption;

use InvalidArgumentException;
use Throwable;

/**
 * 加密组件.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2017.04.05
 *
 * @version 1.0
 */
class Encryption implements IEncryption
{
    /**
     * 加密 key.
     *
     * @var string
     */
    protected $key;

    /**
     * openssl 加密解密算法.
     *
     * @var string
     */
    protected $cipher;

    /**
     * 安全 RSA 私钥.
     *
     * @var string
     */
    protected $rsaPrivate;

    /**
     * 安全 RSA 公钥.
     *
     * @var string
     */
    protected $rsaPublic;

    /**
     * 构造函数.
     *
     * @param string $key
     * @param string $cipher
     * @param string $rsaPrivate
     * @param string $rsaPublic
     */
    public function __construct(string $key, string $cipher = 'AES-256-CBC', ?string $rsaPrivate = null, ?string $rsaPublic = null)
    {
        $this->validateCipher($cipher);

        $this->key = $key;
        $this->cipher = $cipher;
        $this->rsaPrivate = $rsaPrivate;
        $this->rsaPublic = $rsaPublic;
    }

    /**
     * 加密.
     *
     * @param string $value
     * @param int    $expiry
     *
     * @return string
     */
    public function encrypt(string $value, int $expiry = 0): string
    {
        $iv = $this->createIv();

        $value = $this->packData($value, $expiry, $iv);

        return $this->encryptData($value, $iv);
    }

    /**
     * 解密.
     *
     * @param string $value
     *
     * @return string
     */
    public function decrypt(string $value): string
    {
        if (false === ($value = $this->decryptData($value))) {
            return '';
        }

        list($data, $iv) = $value;

        return $this->validateData($data, $iv);
    }

    /**
     * 打包数据.
     *
     * @param string $value
     * @param int    $expiry
     * @param string $iv
     *
     * @return string
     */
    protected function packData(string $value, int $expiry, string $iv): string
    {
        $data = [
            $this->normalizeExpiry($expiry),
            base64_encode($value),
            base64_encode($iv),
            $this->normalizeSign($value),
        ];

        return implode("\t", $data);
    }

    /**
     * 解包数据.
     *
     * @param string $value
     *
     * @return array|bool
     */
    protected function unpackData(string $value)
    {
        $data = explode("\t", $value);

        if (4 !== count($data)) {
            return false;
        }

        $key = ['expiry', 'value', 'iv', 'sign'];

        return array_combine($key, $data);
    }

    /**
     * 加密数据.
     *
     * @param string $value
     * @param string $iv
     *
     * @return string
     */
    protected function encryptData(string $value, string $iv): string
    {
        $value = openssl_encrypt($value, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);

        if (false === $value) {
            throw new InvalidArgumentException('Encrypt the data failed.'); // @codeCoverageIgnore
        }

        return $this->packDataWithIv($value, $iv);
    }

    /**
     * 解密数据.
     *
     * @param string $value
     *
     * @return array|bool
     */
    protected function decryptData(string $value)
    {
        if (false === ($value = base64_decode($value, true))) {
            return false;
        }

        if (false === ($value = $this->unpackDataWithIv($value))) {
            return false;
        }

        $data = openssl_decrypt(
            $value['value'], $this->cipher, $this->key, OPENSSL_RAW_DATA, $value['iv']
        );

        if (false === $data) {
            throw new InvalidArgumentException('Decrypt the data failed.');
        }

        return [$data, base64_encode($value['iv'])];
    }

    /**
     * 数据加入向量并打包.
     *
     * @param string $value
     * @param string $iv
     *
     * @return string
     */
    protected function packDataWithIv(string $value, string $iv): string
    {
        return base64_encode(base64_encode($value)."\t".base64_encode($iv));
    }

    /**
     * 解包带向量的数据.
     *
     * @param string $value
     *
     * @return array|bool
     */
    protected function unpackDataWithIv(string $value)
    {
        $data = explode("\t", $value);

        if (2 !== count($data)) {
            return false;
        }

        $key = ['value', 'iv'];

        $data[0] = base64_decode($data[0], true);
        $data[1] = base64_decode($data[1], true);

        return array_combine($key, $data);
    }

    /**
     * 格式化过期时间.
     *
     * @param int $expiry
     *
     * @return string
     */
    protected function normalizeExpiry(int $expiry = 0): string
    {
        return sprintf('%010d', $expiry ? $expiry + time() : 0);
    }

    /**
     * 创建初始化向量.
     *
     * @return string
     */
    protected function createIv(): string
    {
        return openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->cipher));
    }

    /**
     * 获取签名.
     *
     * @param string $value
     *
     * @return string
     */
    protected function normalizeSign(string $value): string
    {
        if (!$this->rsaPrivate) {
            return '';
        }

        try {
            $rsaPrivate = openssl_pkey_get_private($this->rsaPrivate);

            if (openssl_sign($value, $sign, $rsaPrivate)) {
                return base64_encode($sign);
            }

            // 在 error_reporting(0) 场景下签名 $rsaPrivate 错误的情况下才会执行
            throw new InvalidArgumentException('Openssl sign failed.');
        } catch (Throwable $e) {
            throw new InvalidArgumentException($e->getMessage());
        }
    }

    /**
     * 校验加密算法.
     *
     * @param string $cipher
     */
    protected function validateCipher(string $cipher): void
    {
        if (!in_array($cipher, openssl_get_cipher_methods(), true)) {
            $e = sprintf('Encrypt cipher `%s` was not found.', $cipher);

            throw new InvalidArgumentException($e);
        }
    }

    /**
     * 校验数据正确性.
     *
     * @param string $data
     * @param string $iv
     *
     * @return string
     */
    protected function validateData(string $data, string $iv): string
    {
        if (false === ($data = $this->unpackData($data))) {
            return '';
        }

        if ($data['iv'] !== $iv ||
            ('0000000000' !== $data['expiry'] && time() > $data['expiry'])) {
            return '';
        }

        $result = base64_decode($data['value'], true) ?: false;

        if (false === $result) {
            return '';
        }

        return $this->validateSign($result, $data['sign']);
    }

    /**
     * 验证签名.
     *
     * @param string $value
     * @param string $sign
     *
     * @return string
     */
    protected function validateSign(string $value, string $sign): string
    {
        if (!$this->rsaPublic) {
            return $value;
        }

        try {
            $rsaPublic = openssl_pkey_get_public($this->rsaPublic);

            if (1 === openssl_verify($value, base64_decode($sign, true), $rsaPublic)) {
                return $value;
            }

            // 在 error_reporting(0) 场景下签名 $rsaPublic 错误的情况下才会执行
            throw new InvalidArgumentException('Openssl verify sign failed.');
        } catch (Throwable $e) {
            throw new InvalidArgumentException($e->getMessage());
        }
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Router\Match;

use Leevel\Http\IRequest;
use Leevel\Router\IRouter;

/**
 * 路由匹配抽象类.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2018.12.25
 *
 * @version 1.0
 */
abstract class Match
{
    /**
     * Router.
     *
     * @var \Leevel\Router\IRouter
     */
    protected $router;

    /**
     * HTTP Request.
     *
     * @var \Leevel\Http\IRequest
     */
    protected $request;

    /**
     * 匹配中间件.
     *
     * @var array
     */
    protected $middlewares = [];

    /**
     * 设置路由和请求.
     *
     * @param \Leevel\Router\IRouter $router
     * @param \Leevel\Http\IRequest  $request
     */
    protected function setRouterAndRequest(IRouter $router, IRequest $request): void
    {
        $this->request = $request;
        $this->router = $router;
    }

    /**
     * 匹配 PathInfo.
     *
     * @return string
     */
    protected function matchePathInfo(): string
    {
        $pathInfo = $this->getPathInfo();

        // 匹配基础路径
        $middlewares = $this->matcheBasePaths($pathInfo);

        // 匹配分组路径
        list($pathInfo, $this->middlewares) = $this->matcheGroupPaths($pathInfo, $middlewares);

        return $pathInfo;
    }

    /**
     * 取得 PathInfo.
     *
     * @return string
     */
    protected function getPathInfo(): string
    {
        return rtrim($this->request->getPathInfo(), '/').'/';
    }

    /**
     * 匹配基础路径.
     *
     * @param string $pathInfo
     *
     * @return array
     */
    protected function matcheBasePaths(string $pathInfo): array
    {
        $middlewares = [];

        foreach ($this->router->getBasePaths() as $item => $option) {
            if ('*' === $item) {
                if (isset($option['middlewares'])) {
                    $middlewares = $option['middlewares'];
                }
            } elseif (preg_match($item, $pathInfo, $matches)) {
                if (isset($option['middlewares'])) {
                    $middlewares = $this->mergeMiddlewares($middlewares, $option['middlewares']);
                }

                break;
            }
        }

        return $middlewares;
    }

    /**
     * 匹配分组路径.
     *
     * @param string $pathInfo
     * @param array  $middlewares
     *
     * @return array
     */
    protected function matcheGroupPaths(string $pathInfo, array $middlewares): array
    {
        foreach ($this->router->getGroupPaths() as $item => $option) {
            if (0 === strpos($pathInfo, $item)) {
                $pathInfo = substr($pathInfo, strlen($item));

                if (isset($option['middlewares'])) {
                    $middlewares = $this->mergeMiddlewares($middlewares, $option['middlewares']);
                }

                break;
            }
        }

        return [$pathInfo, $middlewares];
    }

    /**
     * 合并中间件.
     *
     * @param array $middlewares
     * @param array $newMiddlewares
     *
     * @return array
     */
    protected function mergeMiddlewares(array $middlewares, array $newMiddlewares): array
    {
        return $this->router->mergeMiddlewares($middlewares, $newMiddlewares);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Router;

use Leevel\Di\IContainer;
use Leevel\Http\IRequest;
use Leevel\Http\IResponse;
use Leevel\Http\Response;
use Leevel\Pipeline\Pipeline;

/**
 * 路由解析
 * 2018.04.10 开始进行一次重构系统路由架构.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2017.01.10
 *
 * @version 1.0
 */
class Router implements IRouter
{
    /**
     * IOC Container.
     *
     * @var \Leevel\Di\IContainer
     */
    protected $container;

    /**
     * http 请求
     *
     * @var \Leevel\Http\IRequest
     */
    protected $request;

    /**
     * 路由匹配数据.
     *
     * @var array
     */
    protected $matchedData;

    /**
     * 基础路径.
     *
     * @var array
     */
    protected $basePaths = [];

    /**
     * 分组路径.
     *
     * @var array
     */
    protected $groupPaths = [];

    /**
     * 分组.
     *
     * @var array
     */
    protected $groups = [];

    /**
     * 路由.
     *
     * @var array
     */
    protected $routers = [];

    /**
     * 中间件分组.
     *
     * @var array
     */
    protected $middlewareGroups = [];

    /**
     * 中间件别名.
     *
     * @var array
     */
    protected $middlewareAlias = [];

    /**
     * 控制器相对目录.
     *
     * @var string
     */
    protected $controllerDir = 'App\\Controller';

    /**
     * 设置路由请求预解析结果.
     *
     * @var array
     */
    protected $preRequestMatched = [];

    /**
     * 构造函数.
     *
     * @param \Leevel\Di\IContainer $container
     */
    public function __construct(IContainer $container)
    {
        $this->container = $container;
    }

    /**
     * 分发请求到路由.
     *
     * @param \Leevel\Http\IRequest $request
     *
     * @return \Leevel\Http\IResponse
     */
    public function dispatch(IRequest $request): IResponse
    {
        $this->request = $request;

        return $this->dispatchToRoute($request);
    }

    /**
     * 初始化请求.
     */
    public function initRequest(): void
    {
        $this->matchedData = null;
    }

    /**
     * 设置路由请求预解析结果.
     * 可以用于高性能 Rpc 和 Websocket 预匹配数据.
     *
     * @param \Leevel\Http\IRequest $request
     * @param array                 $matchedData
     */
    public function setPreRequestMatched(IRequest $request, array $matchedData): void
    {
        $this->preRequestMatched[spl_object_id($request)] = $matchedData;
    }

    /**
     * 穿越中间件.
     *
     * @param \Leevel\Http\IRequest $passed
     * @param array                 $passedExtend
     */
    public function throughMiddleware(IRequest $passed, array $passedExtend = []): void
    {
        $method = !$passedExtend ? 'handle' : 'terminate';
        $middlewares = $this->matchedMiddlewares();

        if (empty($middlewares[$method])) {
            return;
        }

        (new Pipeline($this->container))
            ->send([$passed])
            ->send($passedExtend)
            ->through($middlewares[$method])
            ->then();
    }

    /**
     * 设置控制器相对目录.
     *
     * @param string $controllerDir
     */
    public function setControllerDir(string $controllerDir): void
    {
        $controllerDir = str_replace('/', '\\', $controllerDir);

        $this->controllerDir = $controllerDir;
    }

    /**
     * 返回控制器相对目录.
     *
     * @return string
     */
    public function getControllerDir(): string
    {
        return $this->controllerDir;
    }

    /**
     * 设置路由.
     *
     * @param array $routers
     */
    public function setRouters(array $routers): void
    {
        $this->routers = $routers;
    }

    /**
     * 取得当前路由.
     *
     * @return array
     */
    public function getRouters(): array
    {
        return $this->routers;
    }

    /**
     * 设置基础路径.
     *
     * @param array $basePaths
     */
    public function setBasePaths(array $basePaths): void
    {
        $this->basePaths = $basePaths;
    }

    /**
     * 取得基础路径.
     *
     * @return array
     */
    public function getBasePaths(): array
    {
        return $this->basePaths;
    }

    /**
     * 设置分组路径.
     *
     * @param array $groupPaths
     */
    public function setGroupPaths(array $groupPaths): void
    {
        $this->groupPaths = $groupPaths;
    }

    /**
     * 取得分组路径.
     *
     * @return array
     */
    public function getGroupPaths(): array
    {
        return $this->groupPaths;
    }

    /**
     * 设置路由分组.
     *
     * @param array $groups
     */
    public function setGroups(array $groups): void
    {
        $this->groups = $groups;
    }

    /**
     * 取得路由分组.
     *
     * @return array
     */
    public function getGroups(): array
    {
        return $this->groups;
    }

    /**
     * 设置中间件分组.
     *
     * @param array $middlewareGroups
     */
    public function setMiddlewareGroups(array $middlewareGroups): void
    {
        $this->middlewareGroups = $middlewareGroups;
    }

    /**
     * 取得中间件分组.
     *
     * @return array
     */
    public function getMiddlewareGroups(): array
    {
        return $this->middlewareGroups;
    }

    /**
     * 设置中间件别名.
     *
     * @param array $middlewareAlias
     */
    public function setMiddlewareAlias(array $middlewareAlias): void
    {
        $this->middlewareAlias = $middlewareAlias;
    }

    /**
     * 取得中间件别名.
     *
     * @return array
     */
    public function getMiddlewareAlias(): array
    {
        return $this->middlewareAlias;
    }

    /**
     * 合并中间件.
     *
     * @param array $middlewares
     * @param array $newMiddlewares
     *
     * @return array
     */
    public function mergeMiddlewares(array $middlewares, array $newMiddlewares): array
    {
        return [
            'handle'    => array_unique(array_merge(
                $middlewares['handle'] ?? [],
                $newMiddlewares['handle'] ?? []
            )),
            'terminate' => array_unique(array_merge(
                $middlewares['terminate'] ?? [],
                $newMiddlewares['terminate'] ?? []
            )),
        ];
    }

    /**
     * 路由匹配
     * 高效匹配，如果默认 PathInfo 路由能够匹配上则忽略 OpenApi 路由匹配.
     *
     * @return mixed
     */
    protected function matchRouter()
    {
        $this->initRequest();

        $this->resolveMatchedData(
            $dataPathInfo = $this->normalizeMatchedData('PathInfo')
        );

        if (false === ($bind = $this->normalizeRouterBind())) {
            $bind = $this->annotationRouterBind($dataPathInfo);
        }

        return $bind;
    }

    /**
     * 注解路由绑定.
     *
     * @param array $dataPathInfo
     *
     * @return mixed
     */
    protected function annotationRouterBind(array $dataPathInfo)
    {
        $data = $this->normalizeMatchedData('Annotation');

        if (!$data) {
            $data = $dataPathInfo;
        } else {
            $this->initRequest();
        }

        $this->resolveMatchedData($data);

        return $this->findRouterBind();
    }

    /**
     * 完成路由匹配数据.
     *
     * @param array $data
     */
    protected function resolveMatchedData(array $data): void
    {
        $data = $this->mergeMatchedData($data, $this->preRequestMatched[spl_object_id($this->request)] ?? []);

        if (!$data[IRouter::APP]) {
            $data[IRouter::APP] = self::DEFAULT_APP;
        }

        $this->matchedData = $data;
    }

    /**
     * 合并匹配数据.
     *
     * @param array $before
     * @param array $after
     *
     * @return array
     */
    protected function mergeMatchedData(array $before, array $after): array
    {
        $result = [];

        foreach (self::MATCHED as $key) {
            if (self::MIDDLEWARES === $key) {
                $result[$key] = $this->mergeMiddlewares($before[$key] ?? [], $after[$key] ?? []);
            } elseif (in_array($key, [self::PARAMS, self::VARS], true)) {
                $result[$key] = array_merge($before[$key] ?? [], $after[$key] ?? []);
            } else {
                $result[$key] = $after[$key] ?? $before[$key] ?? null;
            }
        }

        return $result;
    }

    /**
     * 解析路由匹配数据.
     *
     * @param string $matche
     *
     * @return array
     */
    protected function normalizeMatchedData(string $matche): array
    {
        $matche = __NAMESPACE__.'\\Match\\'.$matche;

        return (new $matche())->matche($this, $this->request);
    }

    /**
     * 尝试获取路由绑定.
     *
     * @return callable
     */
    protected function findRouterBind()
    {
        if (false === ($bind = $this->normalizeRouterBind())) {
            $this->routerNotFound();
        }

        return $bind;
    }

    /**
     * 解析路由绑定.
     *
     * @return mixed
     */
    protected function normalizeRouterBind()
    {
        $this->completeRequest();

        return $this->parseMatchedBind();
    }

    /**
     * 发送路由并返回响应.
     *
     * @param \Leevel\Http\IRequest $request
     *
     * @return \Leevel\Http\IResponse
     */
    protected function dispatchToRoute(IRequest $request): IResponse
    {
        return $this->runRoute($request, $this->matchRouter());
    }

    /**
     * 运行路由.
     *
     * @param \Leevel\Http\IRequest $request
     * @param callable              $bind
     *
     * @return \Leevel\Http\IResponse
     */
    protected function runRoute(IRequest $request, callable $bind): IResponse
    {
        $this->throughMiddleware($this->request);

        $response = $this->container->call($bind, $this->matchedVars());

        if (!($response instanceof IResponse)) {
            $response = new Response($response);
        }

        return $response;
    }

    /**
     * 路由未找到异常.
     */
    protected function routerNotFound(): void
    {
        $message = sprintf('The router %s was not found.', $this->makeRouterNode());

        throw new RouterNotFoundException($message);
    }

    /**
     * 生成路由节点资源.
     *
     * @return string
     */
    protected function makeRouterNode(): string
    {
        if ($matchedBind = $this->matchedBind()) {
            return $matchedBind;
        }

        return $this->matchedApp().'\\'.
                $this->parseControllerDir().'\\'.
                $this->matchedController().'::'.
                $this->matchedAction().'()';
    }

    /**
     * 取得控制器命名空间目录.
     *
     * @return string
     */
    protected function parseControllerDir(): string
    {
        $result = $this->getControllerDir();

        if ($this->matchedPrefix()) {
            $result .= '\\'.$this->matchedPrefix();
        }

        return $result;
    }

    /**
     * 完成请求.
     */
    protected function completeRequest(): void
    {
        $this->pathinfoRestful();

        $this->container->instance('app_name', $this->matchedApp());

        $this->request->params->add($this->matchedParams());
    }

    /**
     * 智能 restful 解析
     * 路由匹配如果没有匹配上方法则系统会进入 restful 解析.
     */
    protected function pathinfoRestful(): void
    {
        if (!empty($this->matchedData[static::ACTION])) {
            return;
        }

        switch ($this->request->getMethod()) {
            // 跨域请求转发到首页
            // 防止由于 API 请求跨域提示路由不正确
            case 'OPTIONS':
                $this->matchedData[static::CONTROLLER] = static::DEFAULT_CONTROLLER;
                $this->matchedData[static::ACTION] = static::RESTFUL_INDEX;

                break;
            case 'POST':
                $this->matchedData[static::ACTION] = static::RESTFUL_STORE;

                break;
            case 'PUT':
                $this->matchedData[static::ACTION] = static::RESTFUL_UPDATE;

                break;
            case 'DELETE':
                $this->matchedData[static::ACTION] = static::RESTFUL_DESTROY;

                break;
            case 'GET':
            default:
                $params = $this->matchedParams();

                if (isset($params[static::RESTFUL_ID])) {
                    $this->matchedData[static::ACTION] = static::RESTFUL_SHOW;
                } else {
                    $this->matchedData[static::ACTION] = static::RESTFUL_INDEX;
                }

                break;
        }
    }

    /**
     * 分析匹配绑定路由.
     *
     * @return callable|false
     */
    protected function parseMatchedBind()
    {
        if ($matchedBind = $this->matchedBind()) {
            if (false !== strpos($matchedBind, '@')) {
                list($bindClass, $method) = explode('@', $matchedBind);

                if (!class_exists($bindClass)) {
                    return false;
                }

                $controller = $this->container->make($bindClass);
            } else {
                if (!class_exists($matchedBind)) {
                    return false;
                }

                $controller = $this->container->make($matchedBind);
                $method = 'handle';
            }
        } else {
            $matchedApp = $this->matchedApp();
            $matchedController = $this->matchedController();
            $matchedAction = $this->matchedAction();

            // 尝试直接读取方法控制器类
            $controllerClass = $matchedApp.'\\'.$this->parseControllerDir().'\\'.
                $matchedController.'\\'.ucfirst($matchedAction);

            $controllerClass = $this->normalizeForSubdir($controllerClass);

            if (class_exists($controllerClass)) {
                $controller = $this->container->make($controllerClass);
                $method = 'handle';
            }

            // 尝试读取默认控制器
            else {
                $controllerClass = $matchedApp.'\\'.$this->parseControllerDir().'\\'.$matchedController;

                $controllerClass = $this->normalizeForSubdir($controllerClass);

                if (!class_exists($controllerClass)) {
                    return false;
                }

                $controller = $this->container->make($controllerClass);
                $method = $this->normalizeForSubdir($matchedAction, true);
            }
        }

        if (!method_exists($controller, $method)) {
            return false;
        }

        return [
            $controller,
            $method,
        ];
    }

    /**
     * 控制器和方法子目录支持.
     *
     * @param string $className
     * @param bool   $forAction
     *
     * @return string
     */
    protected function normalizeForSubdir(string $className, bool $forAction = false): string
    {
        if (false === strpos($className, ':')) {
            return $className;
        }

        $className = preg_replace_callback(
            '/:([a-zA-Z])/',
            function (array $matches) use ($forAction) {
                return false === $forAction ? '\\'.ucfirst($matches[1]) : ucfirst($matches[1]);
            },
            $className
        );

        return str_replace('\\\\', '\\', $className);
    }

    /**
     * 取回应用名.
     *
     * @return string
     */
    protected function matchedApp(): string
    {
        return ucfirst($this->matchedData[static::APP]);
    }

    /**
     * 取回控制器前缀
     *
     * @return string
     */
    protected function matchedPrefix(): ?string
    {
        $prefix = $this->matchedData[static::PREFIX];

        if (!$prefix || is_scalar($prefix)) {
            return $prefix;
        }

        $prefix = array_map(function ($item) {
            return $this->convertMatched(ucfirst($item));
        }, $prefix);

        return $this->matchedData[static::PREFIX] = implode('\\', $prefix);
    }

    /**
     * 取回控制器名.
     *
     * @return string
     */
    protected function matchedController(): string
    {
        return $this->convertMatched(ucfirst($this->matchedData[static::CONTROLLER]));
    }

    /**
     * 取回方法名.
     *
     * @return string
     */
    protected function matchedAction(): string
    {
        return $this->convertMatched($this->matchedData[static::ACTION]);
    }

    /**
     * 转换匹配资源.
     *
     * @param string $matched
     *
     * @return string
     */
    protected function convertMatched(string $matched): string
    {
        if (false !== strpos($matched, '-')) {
            $matched = str_replace('-', '_', $matched);
        }

        if (false !== strpos($matched, '_')) {
            $matched = '_'.str_replace('_', ' ', $matched);
            $matched = ltrim(str_replace(' ', '', ucwords($matched)), '_');
        }

        return $matched;
    }

    /**
     * 取回绑定资源.
     *
     * @return string
     */
    protected function matchedBind(): ?string
    {
        return $this->matchedData[static::BIND];
    }

    /**
     * 取回匹配参数.
     *
     * @return array
     */
    protected function matchedParams(): array
    {
        return $this->matchedData[static::PARAMS] ?? [];
    }

    /**
     * 取回匹配中间件.
     *
     * @return array
     */
    protected function matchedMiddlewares(): array
    {
        return $this->matchedData[static::MIDDLEWARES] ?? [
            'handle'    => [],
            'terminate' => [],
        ];
    }

    /**
     * 取回匹配变量.
     *
     * @return array
     */
    protected function matchedVars(): array
    {
        return $this->matchedData[static::VARS] ?? [];
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Event;

use Closure;
use InvalidArgumentException;
use Leevel\Di\IContainer;
use SplObjectStorage;
use SplObserver;
use SplSubject;

/**
 * 观察者目标角色 subject.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2017.06.23
 * @see http://php.net/manual/zh/class.splsubject.php
 *
 * @version 1.0
 */
class Subject implements ISubject, SplSubject
{
    /**
     * 容器.
     *
     * @var \Leevel\Di\IContainer
     */
    public $container;

    /**
     * 通知附加参数.
     *
     * @var array
     */
    public $notifyArgs = [];

    /**
     * 观察者角色 observer.
     *
     * @var \SplObjectStorage(\SplObserver)
     */
    protected $observers;

    /**
     * 构造函数.
     *
     * @param \Leevel\Di\IContainer $container
     */
    public function __construct(IContainer $container)
    {
        $this->observers = new SplObjectStorage();
        $this->container = $container;
    }

    /**
     * {@inheritdoc}
     */
    public function attach(SplObserver $observer)
    {
        $this->observers->attach($observer);
    }

    /**
     * {@inheritdoc}
     */
    public function detach(SplObserver $observer)
    {
        $this->observers->detach($observer);
    }

    /**
     * {@inheritdoc}
     */
    public function notify(...$args)
    {
        $this->notifyArgs = $args;

        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     * 添加一个观察者角色.
     *
     * @param \Closure|\SplObserver|string $observer
     */
    public function register($observer): void
    {
        if ($observer instanceof Closure) {
            $observer = new Observer($observer);
        } else {
            if (is_string($observer) &&
                is_string($observer = $this->container->make($observer))) {
                throw new InvalidArgumentException(
                    sprintf('Observer `%s` is invalid.', $observer)
                );
            }

            if (!($observer instanceof SplObserver)) {
                if (!is_callable([$observer, 'handle'])) {
                    throw new InvalidArgumentException(
                        sprintf('Observer `%s` is invalid.', get_class($observer))
                    );
                }

                $observer = new Observer(Closure::fromCallable([$observer, 'handle']));
            }
        }

        $this->attach($observer);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Http;

use DateTime;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;

/**
 * Psr 规范请求转 Leevel
 * This class borrows heavily from the Symfony4 Framework and is part of the symfony package.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 * @author Kévin Dunglas <dunglas@gmail.com>
 *
 * @since 2019.03.11
 *
 * @version 1.0
 *
 * @see Symfony\Bridge\PsrHttpMessage (https://github.com/symfony/psr-http-message-bridge)
 */
class Psr2Leevel implements IPsr2Leevel
{
    /**
     * 从 Psr 请求对象创建 Leevel 请求对象
     *
     * @param \Psr\Http\Message\ServerRequestInterface $psrRequest
     *
     * @return \Leevel\Http\IRequest
     */
    public function createRequest(ServerRequestInterface $psrRequest): IRequest
    {
        $server = [];
        $uri = $psrRequest->getUri();

        if ($uri instanceof UriInterface) {
            $server['SERVER_NAME'] = $uri->getHost();
            $server['SERVER_PORT'] = $uri->getPort();
            $server['REQUEST_URI'] = $uri->getPath();
            $server['QUERY_STRING'] = $uri->getQuery();
        }

        $server['REQUEST_METHOD'] = $psrRequest->getMethod();

        $server = array_replace($server, $psrRequest->getServerParams());

        $parsedBody = $psrRequest->getParsedBody();
        $parsedBody = is_array($parsedBody) ? $parsedBody : [];

        $request = new Request(
            $psrRequest->getQueryParams(),
            $parsedBody,
            $psrRequest->getAttributes(),
            $psrRequest->getCookieParams(),
            $this->getFiles($psrRequest->getUploadedFiles()),
            $server,
            $psrRequest->getBody()->__toString()
        );

        return $request;
    }

    /**
     * 从 Psr 响应对象创建 Leevel 响应对象
     *
     * @param \Psr\Http\Message\ResponseInterface $psrResponse
     *
     * @return \Leevel\Http\IResponse
     */
    public function createResponse(ResponseInterface $psrResponse): IResponse
    {
        $response = new Response(
            $psrResponse->getBody()->__toString(),
            $psrResponse->getStatusCode(),
            $psrResponse->getHeaders()
        );

        $response->setProtocolVersion($psrResponse->getProtocolVersion());

        foreach ($psrResponse->getHeader('Set-Cookie') as $cookie) {
            $response->setCookie(...$this->createCookie($cookie));
        }

        return $response;
    }

    /**
     * 转换上传文件结构.
     *
     * @param array  $uploadedFiles
     * @param string $parent
     *
     * @return array
     */
    protected function getFiles(array $uploadedFiles, string $parent = ''): array
    {
        $files = [];

        foreach ($uploadedFiles as $key => $value) {
            $key = ($parent ? $parent.'\\' : '').$key;

            if ($value instanceof UploadedFileInterface) {
                $files[$key] = $this->createUploadedFile($value);
            } else {
                $files = array_merge($files, $this->getFiles($value, $key));
            }
        }

        return $files;
    }

    /**
     * 创建 Leevel 上传对象.
     *
     * @param \Psr\Http\Message\UploadedFileInterface $psrUploadedFile
     *
     * @return \Leevel\Http\UploadedFile
     */
    protected function createUploadedFile(UploadedFileInterface $psrUploadedFile): UploadedFile
    {
        $temporaryPath = '';
        $clientFileName = '';

        if (UPLOAD_ERR_NO_FILE !== $psrUploadedFile->getError()) {
            $temporaryPath = $this->getTemporaryPath();
            $psrUploadedFile->moveTo($temporaryPath);

            $clientFileName = $psrUploadedFile->getClientFilename();
        }

        return new UploadedFile(
            $temporaryPath,
            $clientFileName ?: '',
            $psrUploadedFile->getClientMediaType(),
            $psrUploadedFile->getError()
        );
    }

    /**
     * 获取上传文件临时目录.
     *
     * @return string
     */
    protected function getTemporaryPath(): string
    {
        return tempnam(sys_get_temp_dir(), uniqid('leevel', true));
    }

    /**
     * 创建 COOKIE 结构.
     *
     * Some snippets have been taken from the Guzzle app: https://github.com/guzzle/guzzle/blob/5.3/src/Cookie/SetCookie.php#L34
     *
     * @param string $cookie
     *
     * @return array
     */
    protected function createCookie(string $cookie): array
    {
        foreach (explode(';', $cookie) as $part) {
            $part = trim($part);

            $data = explode('=', $part, 2);
            $name = $data[0];
            $value = isset($data[1]) ? trim($data[1], " \n\r\t\0\x0B\"") : null;

            if (!isset($cookieName)) {
                $cookieName = $name;
                $cookieValue = $value;

                continue;
            }

            if ('expires' === strtolower($name) && null !== $value) {
                $cookieExpire = new DateTime($value);

                continue;
            }

            if ('path' === strtolower($name) && null !== $value) {
                $cookiePath = $value;

                continue;
            }

            if ('domain' === strtolower($name) && null !== $value) {
                $cookieDomain = $value;

                continue;
            }

            if ('secure' === strtolower($name)) {
                $cookieSecure = true;

                continue;
            }

            if ('httponly' === strtolower($name)) {
                $cookieHttpOnly = true;

                continue;
            }
        }

        if (!isset($cookieName)) {
            throw new InvalidArgumentException('The value of the Set-Cookie header is malformed.');
        }

        return [
            $cookieName,
            $cookieValue,
            [
                'expire'   => $cookieExpire ?? 0,
                'path'     => $cookiePath ?? '/',
                'domain'   => $cookieDomain ?? null,
                'secure'   => isset($cookieSecure),
                'httponly' => isset($cookieHttpOnly),
            ],
        ];
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Database\Ddd;

use Closure;

/**
 * 规约实现.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2018.10.29
 *
 * @version 1.0
 */
class Specification implements ISpecification
{
    /**
     * 闭包规约判断.
     *
     * @var \Closure
     */
    protected $spec;

    /**
     * 闭包规约实现.
     *
     * @var \Closure
     */
    protected $handle;

    /**
     * 构造函数.
     *
     * @param \Closure $spec
     * @param \Closure $handle
     */
    public function __construct(Closure $spec, Closure $handle)
    {
        $this->spec = $spec;
        $this->handle = $handle;
    }

    /**
     * 创建规约.
     *
     * @param \Closure $spec
     * @param \Closure $handle
     *
     * @return static
     */
    public static function make(Closure $spec, Closure $handle): ISpecification
    {
        return new static($spec, $handle);
    }

    /**
     * 是否满足规约.
     *
     * @param \Leevel\Database\Ddd\IEntity $entity
     *
     * @return bool
     */
    public function isSatisfiedBy(IEntity $entity): bool
    {
        return call_user_func($this->spec, $entity);
    }

    /**
     * 规约实现.
     *
     * @param \Leevel\Database\Ddd\Select  $select
     * @param \Leevel\Database\Ddd\IEntity $entity
     */
    public function handle(Select $select, IEntity $entity): void
    {
        call_user_func($this->handle, $select, $entity);
    }

    /**
     * 规约 And 操作.
     *
     * @param \Leevel\Database\Ddd\ISpecification $spec
     *
     * @return \Leevel\Database\Ddd\ISpecification
     */
    public function and(ISpecification $spec): ISpecification
    {
        return new self(function (IEntity $entity) use ($spec): bool {
            return $this->isSatisfiedBy($entity) && $spec->isSatisfiedBy($entity);
        }, function (Select $select, IEntity $entity) use ($spec) {
            $this->handle($select, $entity);
            $spec->handle($select, $entity);
        });
    }

    /**
     * 规约 Or 操作.
     *
     * @param \Leevel\Database\Ddd\ISpecification $spec
     *
     * @return \Leevel\Database\Ddd\ISpecification
     */
    public function or(ISpecification $spec): ISpecification
    {
        return new self(function (IEntity $entity): bool {
            return true;
        }, function (Select $select, IEntity $entity) use ($spec) {
            if ($this->isSatisfiedBy($entity)) {
                $this->handle($select, $entity);
            } elseif ($spec->isSatisfiedBy($entity)) {
                $spec->handle($select, $entity);
            }
        });
    }

    /**
     * 规约 Not 操作.
     *
     * @return \Leevel\Database\Ddd\ISpecification
     */
    public function not(): ISpecification
    {
        return new self(function (IEntity $entity): bool {
            return !$this->isSatisfiedBy($entity);
        }, $this->handle);
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Http;

use DateTime;
use InvalidArgumentException;
use LogicException;
use SplFileInfo;
use SplFileObject;

/**
 * FILE 响应请求
 * This class borrows heavily from the Symfony4 Framework and is part of the symfony package.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2018.02.27
 *
 * @version 1.0
 *
 * @see Symfony\Component\HttpFoundation (https://github.com/symfony/symfony)
 */
class FileResponse extends Response
{
    /**
     * 文件.
     *
     * @var \Leevel\Http\File
     */
    protected $file;

    /**
     * 构造函数.
     *
     * @param \SplFileInfo|\SplFileObject|string $file
     * @param int                                $status
     * @param array                              $headers
     * @param null|string                        $contentDisposition
     * @param bool                               $autoEtag
     * @param bool                               $autoLastModified
     */
    public function __construct($file, int $status = 200, array $headers = [], ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
    {
        parent::__construct(null, $status, $headers);

        $this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified);
    }

    /**
     * 创建一个文件响应.
     *
     * @param \SplFileInfo|\SplFileObject|string $file
     * @param int                                $status
     * @param array                              $headers
     * @param null|string                        $contentDisposition
     * @param bool                               $autoEtag
     * @param bool                               $autoLastModified
     *
     * @return static
     */
    public static function create($file = null, int $status = 200, array $headers = [], ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true): IResponse
    {
        return new static($file, $status, $headers, $contentDisposition, $autoEtag, $autoLastModified);
    }

    /**
     * 设置文件.
     *
     * @param \SplFileInfo|\SplFileObject|string $file
     * @param string                             $contentDisposition
     * @param bool                               $autoEtag
     * @param bool                               $autoLastModified
     *
     * @return $this
     */
    public function setFile($file, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true): IResponse
    {
        if ($this->checkFlowControl()) {
            return $this;
        }

        if (!$file instanceof File) {
            if ($file instanceof SplFileInfo || $file instanceof SplFileObject) {
                $file = new File($file->getPathname());
            } else {
                if (!is_readable($file)) {
                    throw new FileException('File must be readable.');
                }

                $file = new File((string) $file);
            }
        }

        if (!$file->isReadable()) {
            throw new FileException('File must be readable.');
        }

        $this->file = $file;

        if ($autoEtag) {
            $this->setAutoEtag();
        }

        if ($autoLastModified) {
            $this->setAutoLastModified();
        }

        if ($contentDisposition) {
            $this->setContentDisposition($contentDisposition);
        }

        return $this;
    }

    /**
     * 取回文件.
     *
     * @return \Leevel\Http\File
     */
    public function getFile(): File
    {
        return $this->file;
    }

    /**
     * 自动设置最后修改时间.
     *
     * @return $this
     */
    public function setAutoLastModified(): IResponse
    {
        if ($this->checkFlowControl()) {
            return $this;
        }

        $this->setLastModified(DateTime::createFromFormat('U', (string) $this->file->getMTime()));

        return $this;
    }

    /**
     * 自动设置标记.
     *
     * @return $this
     */
    public function setAutoEtag(): IResponse
    {
        if ($this->checkFlowControl()) {
            return $this;
        }

        $etag = base64_encode(hash_file('sha256', $this->file->getPathname(), true));

        $this->setEtag($etag);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function setContent($content): IResponse
    {
        if ($this->checkFlowControl()) {
            return $this;
        }

        if (null !== $content) {
            throw new LogicException('The content cannot be set on a FileResponse instance.');
        }

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getContent()
    {
        return false;
    }

    /**
     * 设置文件下载模式.
     *
     * @param string $disposition
     * @param string $filename
     *
     * @return $this
     */
    public function setContentDisposition(string $disposition, string $filename = ''): IResponse
    {
        if ($this->checkFlowControl()) {
            return $this;
        }

        if ('' === $filename) {
            $filename = $this->file->getFilename();
        }

        if (!in_array($disposition, [
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            ResponseHeaderBag::DISPOSITION_INLINE,
        ], true)) {
            throw new InvalidArgumentException('The disposition type is invalid.');
        }

        $this->headers->set(
            'Content-Disposition',
            sprintf('%s; filename="%s"', $disposition, str_replace('"', '\\"', basename($filename)))
        );

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function sendContent(): IResponse
    {
        if ($this->checkFlowControl()) {
            return $this;
        }

        if (!$this->isSuccessful()) {
            return parent::sendContent();
        }

        $out = fopen('php://output', 'w');
        $file = fopen($this->file->getPathname(), 'r');
        stream_copy_to_stream($file, $out);
        fclose($out);
        fclose($file);

        return $this;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Database\Ddd;

use RuntimeException;

/**
 * 模型实体未找到异常.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2017.07.10
 *
 * @version 1.0
 */
class EntityNotFoundException extends RuntimeException
{
    /**
     * 模型实体名字.
     *
     * @var string
     */
    protected $entity;

    /**
     * 设置模型实体.
     *
     * @param string $entity
     *
     * @return $this
     */
    public function setEntity(string $entity): self
    {
        $this->entity = $entity;
        $this->message = sprintf('Entity `%s` was not found.', $entity);

        return $this;
    }

    /**
     * 取回模型实体.
     *
     * @return string
     */
    public function entity(): string
    {
        return $this->entity;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Session\Middleware;

use Closure;
use Leevel\Http\IRequest;
use Leevel\Http\IResponse;
use Leevel\Session\Manager;

/**
 * Session 中间件.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2017.11.14
 *
 * @version 1.0
 */
class Session
{
    /**
     * session 管理.
     *
     * @var \Leevel\Session\Manager
     */
    protected $manager;

    /**
     * 构造函数.
     *
     * @param \Leevel\Session\Manager $manager
     */
    public function __construct(Manager $manager)
    {
        $this->manager = $manager;
    }

    /**
     * 请求
     *
     * @param \Closure              $next
     * @param \Leevel\Http\IRequest $request
     */
    public function handle(Closure $next, IRequest $request): void
    {
        $this->startSession($request);

        $next($request);
    }

    /**
     * 响应.
     *
     * @param \Closure               $next
     * @param \Leevel\Http\IRequest  $request
     * @param \Leevel\Http\IResponse $response
     */
    public function terminate(Closure $next, IRequest $request, IResponse $response): void
    {
        $this->setPrevUrl($request);
        $this->saveSession();

        if (!$this->getSessionId($request)) {
            $response->setCookie(
                $this->manager->getName(),
                $this->manager->getId(),
                ['expire' => $this->getSessionExpire()]
            );
        }

        $next($request, $response);
    }

    /**
     * 启动 session.
     *
     * @param \Leevel\Http\IRequest $request
     */
    protected function startSession(IRequest $request): void
    {
        $this->manager->start($this->getSessionId($request));
    }

    /**
     * 保存 session.
     */
    protected function saveSession(): void
    {
        $this->manager->save();
    }

    /**
     * 保存当期请求 URL.
     *
     * @param \Leevel\Http\IRequest $request
     */
    protected function setPrevUrl(IRequest $request): void
    {
        $this->manager->setPrevUrl($request->getUri());
    }

    /**
     * 获取 session ID.
     *
     * @param \Leevel\Http\IRequest $request
     *
     * @return null|string
     */
    protected function getSessionId(IRequest $request): ?string
    {
        return $request->cookies->get(
            $this->manager->getName(), null
        );
    }

    /**
     * 获取 session 过期时间.
     *
     * @return int
     */
    protected function getSessionExpire(): int
    {
        return $this->manager->getSessionOption()['expire'] ?? 0;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Validate;

use Leevel\Di\IContainer;
use Leevel\I18n\Helper\gettext;
use function Leevel\I18n\Helper\gettext as __;

/**
 * 验证工厂.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2018.11.26
 *
 * @version 1.0
 */
class Validate implements IValidate
{
    /**
     * IOC 容器.
     *
     * @var \Leevel\Di\IContainer
     */
    protected $container;

    /**
     * 构造函数.
     *
     * @param \Leevel\Di\IContainer $container
     */
    public function __construct(IContainer $container)
    {
        $this->container = $container;

        $this->initMessages();
    }

    /**
     * 创建一个验证器.
     *
     * @param array $datas
     * @param array $rules
     * @param array $names
     * @param array $messages
     *
     * @return \Leevel\Validate\IValidator
     */
    public function make(array $datas = [], array $rules = [], array $names = [], array $messages = []): IValidator
    {
        $validator = new Validator($datas, $rules, $names, $messages);

        $validator->setContainer($this->container);

        return $validator;
    }

    /**
     * 初始化默认验证消息.
     */
    public static function initMessages(): void
    {
        Validator::initMessages(['required'                => __('{field} 不能为空'),
            'number'                                       => __('{field} 必须是数字'),
            'float'                                        => __('{field} 必须是浮点数'),
            'double'                                       => __('{field} 必须是双精度浮点数'),
            'boolean'                                      => __('{field} 必须是布尔值'),
            'array'                                        => __('{field} 必须是数组'),
            'accepted'                                     => __('{field} 必须是 yes、on、true 或者 1'),
            'date'                                         => __('{field} 不是正确的日期格式'),
            'date_format'                                  => __('{field} 必须使用日期格式 {rule}'),
            'timezone'                                     => __('{field} 不是正确的时区'),
            'alpha'                                        => __('{field} 只能是字母'),
            'alpha_upper'                                  => __('{field} 只能是大写字母'),
            'alpha_lower'                                  => __('{field} 只能是小写字母'),
            'alpha_num'                                    => __('{field} 只能是字母和数字'),
            'alpha_dash'                                   => __('{field} 只能是字母、数字、短横线和下划线'),
            'chinese'                                      => __('{field} 只能是汉字'),
            'chinese_alpha_num'                            => __('{field} 只能是汉字、字母、数字'),
            'chinese_alpha_dash'                           => __('{field} 只能是汉字、字母、数字、短横线和下划线'),
            'url'                                          => __('{field} 不是有效的 URL 地址'),
            'active_url'                                   => __('{field} 不是有效的域名或者 IP'),
            'ip'                                           => __('{field} 不是有效的 IP 地址'),
            'ipv4'                                         => __('{field} 不是有效的 IPV4 地址'),
            'ipv6'                                         => __('{field} 不是有效的 IPV6 地址'),
            'in'                                           => __('{field} 必须在 {rule} 范围内'),
            'not_in'                                       => __('{field} 不能在 {rule} 范围内'),
            'between'                                      => __('{field} 只能在 {rule} 和 {rule1} 之间，不包含等于'),
            'not_between'                                  => __('{field} 不在 {rule} 和 {rule1} 之间，不包含等于'),
            'between_equal'                                => __('{field} 只能在 {rule} 和 {rule1} 之间，包含等于'),
            'not_between_equal'                            => __('{field} 不在 {rule} 和 {rule1} 之间，包含等于'),
            'greater_than'                                 => __('{field} 必须大于 {rule}'),
            'equal_greater_than'                           => __('{field} 必须大于等于 {rule}'),
            'less_than'                                    => __('{field} 必须小于 {rule}'),
            'equal_less_than'                              => __('{field} 必须小于等于 {rule}'),
            'equal'                                        => __('{field} 必须等于 {rule}'),
            'not_equal'                                    => __('{field} 不能等于 {rule}'),
            'equal_to'                                     => __('{field} 必须等于字段 {rule}'),
            'different'                                    => __('{field} 不能等于字段 {rule}'),
            'same'                                         => __('{field} 必须完全等于 {rule}'),
            'not_same'                                     => __('{field} 不能完全等于 {rule}'),
            'empty'                                        => __('{field} 必须为空'),
            'not_empty'                                    => __('{field} 不能为空'),
            'null'                                         => __('{field} 必须 null'),
            'not_null'                                     => __('{field} 不能为 null'),
            'strlen'                                       => __('{field} 长度不符合要求 {rule}'),
            'max'                                          => __('{field} 长度不能超过 {rule}'),
            'min'                                          => __('{field} 长度不能小于 {rule}'),
            'digit'                                        => __('{field} 字符串中的字符必须都是数字'),
            'type'                                         => __('{field} 类型不符合要求 {rule}'),
            'lower'                                        => __('{field} 必须全部是小写'),
            'upper'                                        => __('{field} 必须全部是大写'),
            'min_length'                                   => __('{field} 不满足最小长度 {rule}'),
            'max_length'                                   => __('{field} 不满足最大长度 {rule}'),
            'id_card'                                      => __('{field} 必须是有效的中国大陆身份证'),
            'zip_code'                                     => __('{field} 必须是有效的中国邮政编码'),
            'qq'                                           => __('{field} 必须是有效的 QQ 号码'),
            'phone'                                        => __('{field} 必须是有效的电话号码或者手机号'),
            'mobile'                                       => __('{field} 必须是有效的手机号'),
            'telephone'                                    => __('{field} 必须是有效的电话号码'),
            'email'                                        => __('{field} 必须为正确的电子邮件格式'),
            'luhn'                                         => __('{field} 必须为正确的符合 luhn 格式算法银行卡'),
            'after'                                        => __('{field} 日期不能小于 {rule}'),
            'before'                                       => __('{field} 日期不能超过 {rule}'),
            'allowed_ip'                                   => __('{field} 不允许的 IP 访问 {rule}'),
            'deny_ip'                                      => __('{field} 禁止的 IP 访问 {rule}'),
            'json'                                         => __('{field} 不是有效的 JSON'),
            'unique'                                       => __('{field} 不能出现重复值'),
        ]);
    }
}

// import fn.
class_exists(gettext::class);


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Router\Match;

use Leevel\Http\IRequest;
use Leevel\Router\IRouter;

/**
 * pathInfo 路由匹配.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2018.02.15
 *
 * @version 1.0
 */
class PathInfo extends Match implements IMatch
{
    /**
     * 匹配数据项.
     *
     * @param \Leevel\Router\IRouter $router
     * @param \Leevel\Http\IRequest  $request
     *
     * @return array
     */
    public function matche(IRouter $router, IRequest $request): array
    {
        $this->setRouterAndRequest($router, $request);

        return $this->matchMain();
    }

    /**
     * 主匹配.
     *
     * @return array
     */
    protected function matchMain(): array
    {
        // 匹配 PathInfo
        $path = $this->normalizePath($this->matchePathInfo());

        // 应用
        list($result, $path) = $this->matcheApp($path);

        // Middleware
        $result[IRouter::MIDDLEWARES] = $this->middlewares;

        if (!$path) {
            return $result;
        }

        // Mvc
        $result = array_merge($result, $this->matcheMvc($path));

        return $result;
    }

    /**
     * 格式化 PathInfo.
     *
     * @param array $pathInfo
     *
     * @return array
     */
    protected function normalizePath(string $pathInfo): array
    {
        $pathInfo = trim($pathInfo, '/');

        return $pathInfo ? explode('/', $pathInfo) : [];
    }

    /**
     * 匹配路由应用.
     *
     * @param array $path
     *
     * @return array
     */
    protected function matcheApp(array $path): array
    {
        $result = [];

        if ($path && $this->isFindApp($path[0])) {
            $result[IRouter::APP] = substr(array_shift($path), 1);
        }

        if ($restfulResult = $this->matcheRestful($path)) {
            return [array_merge($result, $restfulResult), []];
        }

        if (!$path) {
            $result[IRouter::CONTROLLER] = IRouter::DEFAULT_CONTROLLER;
        }

        return [$result, $path];
    }

    /**
     * 匹配路由 Mvc.
     *
     * @param array $path
     *
     * @return array
     */
    protected function matcheMvc(array $path): array
    {
        $result = [];

        if (1 === count($path)) {
            $result[IRouter::CONTROLLER] = array_pop($path);
        } else {
            if ($path) {
                $result[IRouter::ACTION] = array_pop($path);
            }

            if ($path) {
                $result[IRouter::CONTROLLER] = array_pop($path);
            }

            if ($path) {
                $result[IRouter::PREFIX] = $path;
            }
        }

        return $result;
    }

    /**
     * 是否找到 app.
     *
     * @param string $path
     *
     * @return bool
     */
    protected function isFindApp(string $path): bool
    {
        return 0 === strpos($path, ':');
    }

    /**
     * 匹配路由 Restful.
     *
     * @param array $path
     *
     * @return array
     */
    protected function matcheRestful(array $path): array
    {
        $restfulPath = implode('/', $path);
        $regex = '/^(\S+)\/('.IRouter::RESTFUL_REGEX.')(\/*\S*)$/';

        if (preg_match($regex, $restfulPath, $matches)) {
            $result[IRouter::CONTROLLER] = $matches[1];
            $result[IRouter::PARAMS][IRouter::RESTFUL_ID] = $matches[2];

            if ('' !== $matches[3]) {
                $result[IRouter::ACTION] = substr($matches[3], 1);
            }

            return $result;
        }

        return [];
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of the ************************ package.
 * _____________                           _______________
 *  ______/     \__  _____  ____  ______  / /_  _________
 *   ____/ __   / / / / _ \/ __`\/ / __ \/ __ \/ __ \___
 *    __/ / /  / /_/ /  __/ /  \  / /_/ / / / / /_/ /__
 *      \_\ \_/\____/\___/_/   / / .___/_/ /_/ .___/
 *         \_\                /_/_/         /_/
 *
 * The PHP Framework For Code Poem As Free As Wind. <Query Yet Simple>
 * (c) 2010-2019 http://queryphp.com All rights reserved.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Leevel\Throttler\Provider;

use Leevel\Di\IContainer;
use Leevel\Di\Provider;
use Leevel\Throttler\Throttler;

/**
 * throttler 服务提供者.
 *
 * @author Xiangmin Liu <635750556@qq.com>
 *
 * @since 2017.08.09
 *
 * @version 1.0
 */
class Register extends Provider
{
    /**
     * 注册服务.
     */
    public function register(): void
    {
        $this->throttler();
        $this->middleware();
    }

    /**
     * 可用服务提供者.
     *
     * @return array
     */
    public static function providers(): array
    {
        return [
            'throttler' => [
                'Leevel\\Throttler\\Throttler',
                'Leevel\\Throttler\\IThrottler',
            ],
            'Leevel\\Throttler\\Middleware\\Throttler',
        ];
    }

    /**
     * {@inheritdoc}
     */
    public static function isDeferred(): bool
    {
        return true;
    }

    /**
     * 注册 throttler 服务.
     */
    protected function throttler(): void
    {
        $this->container->singleton('throttler', function (IContainer $container) {
            return (new Throttler($container['caches']->
            connect($container['option']['throttler\\driver'])))->

            setRequest($container['request']);
        });
    }

    /**
     * 注册 middleware 服务.
     */
    protected function middleware(): void
    {
        $this->container->singleton('Leevel\\Throttler\\Middleware\\Throttler');
    }
}