<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Skinny\Core;

use Skinny\Core\Configure\ConfigEngineInterface;
use Skinny\Core\Configure\Engine\PhpConfig;
use Skinny\Core\Exception\Exception;
use Skinny\Utility\Hash;
use RuntimeException;

class Configure
{

    /**
     * Array of values currently stored in Configure.
     *
     * @var array
     */
    protected static $values = [
        'debug' => false
    ];

    /**
     * Configured engine classes, used to load config files from resources
     *
     * @var array
     * @see \Skinny\Core\Configure::load()
     */
    protected static $engines = [];

    /**
     * Flag to track whether or not ini_set exists.
     *
     * @return void
     */
    protected static $hasIniSet = null;

    /**
     * Used to store a dynamic variable in Configure.
     *
     * Usage:
     * ```
     * Configure::write('One.key1', 'value of the Configure::One[key1]');
     * Configure::write(['One.key1' => 'value of the Configure::One[key1]']);
     * Configure::write('One', [
     *     'key1' => 'value of the Configure::One[key1]',
     *     'key2' => 'value of the Configure::One[key2]'
     * ]);
     *
     * Configure::write([
     *     'One.key1' => 'value of the Configure::One[key1]',
     *     'One.key2' => 'value of the Configure::One[key2]'
     * ]);
     * ```
     *
     * @param string|array $config The key to write, can be a dot notation value.
     * Alternatively can be an array containing key(s) and value(s).
     * @param mixed $value Value to set for var.
     *
     * @return bool True if write was successful
     */
    public static function write($config, $value = null)
    {
        if (!is_array($config)) {
            $config = [$config => $value];
        }

        foreach ($config as $name => $value) {
            static::$values = Hash::insert(static::$values, $name, $value);
        }

        if (isset($config['debug'])) {
            if (static::$hasIniSet === null) {
                static::$hasIniSet = function_exists('ini_set');
            }
            if (static::$hasIniSet) {
                ini_set('display_errors', $config['debug'] ? 1 : 0);
            }
        }

        return true;
    }

    /**
     * Used to read information stored in Configure. It's not
     * possible to store `null` values in Configure.
     *
     * Usage:
     * ```
     * Configure::read('Name'); will return all values for Name
     * Configure::read('Name.key'); will return only the value of Configure::Name[key]
     * ```
     *
     * @param string|null $var Variable to obtain. Use '.' to access array elements.
     *
     * @return mixed Value stored in configure, or null.
     */
    public static function read($var = null)
    {
        if ($var === null) {
            return static::$values;
        }

        return Hash::get(static::$values, $var);
    }

    /**
     * Returns true if given variable is set in Configure.
     *
     * @param string $var Variable name to check for.
     *
     * @return bool True if variable is there.
     */
    public static function check($var)
    {
        if (empty($var)) {
            return false;
        }

        return static::read($var) !== null;
    }

    /**
     * Used to get information stored in Configure. It's not
     * possible to store `null` values in Configure.
     *
     * Acts as a wrapper around Configure::read() and Configure::check().
     * The configure key/value pair fetched via this method is expected to exist.
     * In case it does not an exception will be thrown.
     *
     * Usage:
     * ```
     * Configure::readOrFail('Name'); will return all values for Name
     * Configure::readOrFail('Name.key'); will return only the value of Configure::Name[key]
     * ```
     *
     * @param string $var Variable to obtain. Use '.' to access array elements.
     *
     * @return mixed Value stored in configure.
     *
     * @throws \RuntimeException if the requested configuration is not set.
     */
    public static function readOrFail($var)
    {
        if (static::check($var) === false) {
            throw new RuntimeException(sprintf('Expected configuration key "%s" not found.', $var));
        }

        return static::read($var);
    }

    /**
     * Used to delete a variable from Configure.
     *
     * Usage:
     * ```
     * Configure::delete('Name'); will delete the entire Configure::Name
     * Configure::delete('Name.key'); will delete only the Configure::Name[key]
     * ```
     *
     * @param string $var The var to be deleted.
     *
     * @return void
     */
    public static function delete($var)
    {
        static::$values = Hash::remove(static::$values, $var);
    }

    /**
     * Used to read and delete a variable from Configure.
     *
     * @param string $var The key to read and remove.
     *
     * @return array|null
     */
    public static function consume($var)
    {
        if (strpos($var, '.') === false) {
            if (!isset(static::$values[$var])) {
                return null;
            }
            $value = static::$values[$var];
            unset(static::$values[$var]);

            return $value;
        }
        $value = Hash::get(static::$values, $var);
        static::delete($var);

        return $value;
    }

    /**
     * Add a new engine to Configure. Engines allow you to read configuration
     * files in various formats/storage locations. You can also implement your
     * own engine classes in your application.
     *
     * To add a new engine to Configure:
     *
     * ```
     * Configure::config('ini', new IniConfig());
     * ```
     *
     * @param string $name The name of the engine being configured. This alias is used later to
     *   read values from a specific engine.
     * @param \Skinny\Core\Configure\ConfigEngineInterface $engine The engine to append.
     *
     * @return void
     */
    public static function config($name, ConfigEngineInterface $engine)
    {
        static::$engines[$name] = $engine;
    }

    /**
     * Gets the names of the configured Engine objects.
     *
     * @param string|null $name Engine name.
     *
     * @return array Array of the configured Engine objects.
     */
    public static function configured($name = null)
    {
        if ($name !== null) {
            return isset(static::$engines[$name]);
        }

        return array_keys(static::$engines);
    }

    /**
     * Remove a configured engine. This will unset the engine
     * and make any future attempts to use it cause an Exception.
     *
     * @param string $name Name of the engine to drop.
     *
     * @return bool Success
     */
    public static function drop($name)
    {
        if (!isset(static::$engines[$name])) {
            return false;
        }
        unset(static::$engines[$name]);

        return true;
    }

    /**
     * Loads stored configuration information from a resource. You can add
     * config file resource engines with `Configure::config()`.
     *
     * Loaded configuration information will be merged with the current
     * runtime configuration. You can load configuration files from plugins
     * by preceding the filename with the plugin name.
     *
     * `Configure::load('Users.user', 'default')`
     *
     * Would load the 'user' config file using the default config engine. You can load
     * app config files by giving the name of the resource you want loaded.
     *
     * ```
     * Configure::load('setup', 'default');
     * ```
     *
     * If using `default` config and no engine has been configured for it yet,
     * one will be automatically created using PhpConfig.
     *
     * @param string $key Name of configuration resource to load.
     * @param string $config Name of the configured engine to use to read the resource identified by $key.
     * @param bool $merge If config files should be merged instead of simply overridden.
     *
     * @return bool False if file not found, true if load successful.
     */
    public static function load($key, $config = 'default', $merge = true)
    {
        $engine = static::getEngine($config);
        if (!$engine) {
            return false;
        }
        $values = $engine->read($key);

        if ($merge) {
            $values = Hash::merge(static::$values, $values);
        }

        return static::write($values);
    }

    /**
     * Dump data currently in Configure into $key. The serialization format
     * is decided by the config engine attached as $config. For example, if the
     * 'default' adapter is a PhpConfig, the generated file will be a PHP
     * configuration file loadable by the PhpConfig.
     *
     * ### Usage
     *
     * Given that the 'default' engine is an instance of PhpConfig.
     * Save all data in Configure to the file `my_config.php`:
     *
     * ```
     * Configure::dump('my_config', 'default');
     * ```
     *
     * Save only the error handling configuration:
     *
     * ```
     * Configure::dump('error', 'default', ['Error', 'Exception'];
     * ```
     *
     * @param string $key The identifier to create in the config adapter.
     *   This could be a filename or a cache key depending on the adapter being used.
     * @param string $config The name of the configured adapter to dump data with.
     * @param array $keys The name of the top-level keys you want to dump.
     *   This allows you save only some data stored in Configure.
     *
     * @return bool Success
     *
     * @throws \Skinny\Core\Exception\Exception If the adapter does not implement a `dump` method.
     */
    public static function dump($key, $config = 'default', $keys = [])
    {
        $engine = static::getEngine($config);
        if (!$engine) {
            throw new Exception(sprintf('There is no "%s" config engine.', $config));
        }
        $values = static::$values;
        if (!empty($keys) && is_array($keys)) {
            $values = array_intersect_key($values, array_flip($keys));
        }

        return (bool)$engine->dump($key, $values);
    }

    /**
     * Get the configured engine. Internally used by `Configure::load()` and `Configure::dump()`
     * Will create new PhpConfig for default if not configured yet.
     *
     * @param string $config The name of the configured adapter
     *
     * @return \Skinny\Core\Configure\ConfigEngineInterface|false Engine instance or false.
     */
    protected static function getEngine($config)
    {
        if (!isset(static::$engines[$config])) {
            if ($config !== 'default') {
                return false;
            }
            static::config($config, new PhpConfig());
        }

        return static::$engines[$config];
    }

    /**
     * Used to determine the current version of Skinny.
     *
     * Usage
     * ```
     * Configure::version();
     * ```
     *
     * @return string Current version of Skinny.
     */
    public static function version()
    {
        if (!isset(static::$values['Skinny']['version'])) {
            $config = require SKINNY_PATH . 'config' . DS . 'version.php';
            static::write($config);
        }

        return static::$values['Skinny']['version'];
    }

    /**
     * Verifies that the application's token value has been changed from the default value.
     *
     * @return void
     *
     * @throws \RuntimeException When the Discord.token configuration is not set.
     */
    public static function checkTokenKey()
    {
        if (Configure::read('Discord.token') === 'insert-your-token-here') {
            throw new RuntimeException(sprintf('Please change the value of %s in %s to a valid token.',
                '\'Discord.token\'', ROOT . '/config/config.php'));
        }
    }

    /**
     * Clear all values stored in Configure.
     *
     * @return bool success.
     */
    public static function clear()
    {
        static::$values = [];

        return true;
    }
}


<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Skinny\Utility;

use ArrayAccess;
use InvalidArgumentException;
use Skinny\Utility\Text;

class Hash
{
    /**
     * Insert $values into an array with the given $path. You can use
     * `{n}` and `{s}` elements to insert $data multiple times.
     *
     * @param array $data The data to insert into.
     * @param string $path The path to insert at.
     * @param array|null $values The values to insert.
     *
     * @return array The data with $values inserted.
     */
    public static function insert(array $data, $path, $values = null)
    {
        $noTokens = strpos($path, '[') === false;
        if ($noTokens && strpos($path, '.') === false) {
            $data[$path] = $values;

            return $data;
        }

        if ($noTokens) {
            $tokens = explode('.', $path);
        } else {
            $tokens = Text::tokenize($path, '.', '[', ']');
        }

        if ($noTokens && strpos($path, '{') === false) {
            return static::simpleOp('insert', $data, $tokens, $values);
        }

        $token = array_shift($tokens);
        $nextPath = implode('.', $tokens);

        list($token, $conditions) = static::splitConditions($token);

        foreach ($data as $k => $v) {
            if (static::matchToken($k, $token)) {
                if (!$conditions || static::matches($v, $conditions)) {
                    $data[$k] = $nextPath
                        ? static::insert($v, $nextPath, $values)
                        : array_merge($v, (array)$values);
                }
            }
        }

        return $data;
    }

    /**
     * Get a single value specified by $path out of $data.
     * Does not support the full dot notation feature set,
     * but is faster for simple read operations.
     *
     * @param array|\ArrayAccess $data Array of data or object implementing
     *   \ArrayAccess interface to operate on.
     * @param string|array $path The path being searched for. Either a dot
     *   separated string, or an array of path segments.
     * @param mixed $default The return value when the path does not exist
     *
     * @throws \InvalidArgumentException
     *
     * @return mixed The value fetched from the array, or null.
     */
    public static function get($data, $path, $default = null)
    {
        if (!(is_array($data) || $data instanceof ArrayAccess)) {
            throw new InvalidArgumentException(
                'Invalid data type, must be an array or \ArrayAccess instance.'
            );
        }

        if (empty($data) || $path === null) {
            return $default;
        }

        if (is_string($path) || is_numeric($path)) {
            $parts = explode('.', $path);
        } else {
            if (!is_array($path)) {
                throw new InvalidArgumentException(sprintf(
                    'Invalid Parameter %s, should be dot separated path or array.',
                    $path
                ));
            }

            $parts = $path;
        }

        switch (count($parts)) {
            case 1:
                return isset($data[$parts[0]]) ? $data[$parts[0]] : $default;
            case 2:
                return isset($data[$parts[0]][$parts[1]]) ? $data[$parts[0]][$parts[1]] : $default;
            case 3:
                return isset($data[$parts[0]][$parts[1]][$parts[2]]) ?
                    $data[$parts[0]][$parts[1]][$parts[2]] : $default;
            default:
                foreach ($parts as $key) {
                    if ((is_array($data) || $data instanceof ArrayAccess) && isset($data[$key])) {
                        $data = $data[$key];
                    } else {
                        return $default;
                    }
                }
        }

        return $data;
    }

    /**
     * Remove data matching $path from the $data array.
     * You can use `{n}` and `{s}` to remove multiple elements
     * from $data.
     *
     * @param array $data The data to operate on
     * @param string $path A path expression to use to remove.
     *
     * @return array The modified array.
     */
    public static function remove(array $data, $path)
    {
        $noTokens = strpos($path, '[') === false;
        $noExpansion = strpos($path, '{') === false;

        if ($noExpansion && $noTokens && strpos($path, '.') === false) {
            unset($data[$path]);

            return $data;
        }

        $tokens = $noTokens ? explode('.', $path) : Text::tokenize($path, '.', '[', ']');

        if ($noExpansion && $noTokens) {
            return static::simpleOp('remove', $data, $tokens);
        }

        $token = array_shift($tokens);
        $nextPath = implode('.', $tokens);

        list($token, $conditions) = self::splitConditions($token);

        foreach ($data as $k => $v) {
            $match = static::matchToken($k, $token);
            if ($match && is_array($v)) {
                if ($conditions) {
                    if (static::matches($v, $conditions)) {
                        if ($nextPath) {
                            $data[$k] = static::remove($v, $nextPath);
                        } else {
                            unset($data[$k]);
                        }
                    }
                } else {
                    $data[$k] = static::remove($v, $nextPath);
                }
                if (empty($data[$k])) {
                    unset($data[$k]);
                }
            } elseif ($match && empty($nextPath)) {
                unset($data[$k]);
            }
        }

        return $data;
    }

    /**
     * Expands a flat array to a nested array.
     *
     * For example, unflattens an array that was collapsed with `Hash::flatten()`
     * into a multi-dimensional array. So, `['0.Foo.Bar' => 'Far']` becomes
     * `[['Foo' => ['Bar' => 'Far']]]`.
     *
     * @param array $data Flattened array.
     * @param string $separator The delimiter used.
     *
     * @return array
     */
    public static function expand(array $data, $separator = '.')
    {
        $result = [];
        foreach ($data as $flat => $value) {
            $keys = explode($separator, $flat);
            $keys = array_reverse($keys);
            $child = [
            $keys[0] => $value
            ];
            array_shift($keys);
            foreach ($keys as $k) {
                $child = [
                $k => $child
                ];
            }

            $stack = [[$child, &$result]];
            static::mergeHelper($stack, $result);
        }

        return $result;
    }

    /**
     * Collapses a multi-dimensional array into a single dimension, using a delimited array path for
     * each array element's key, i.e. [['Foo' => ['Bar' => 'Far']]] becomes
     * ['0.Foo.Bar' => 'Far'].)
     *
     * @param array $data Array to flatten
     * @param string $separator String used to separate array key elements in a path, defaults to '.'
     *
     * @return array
     */
    public static function flatten(array $data, $separator = '.')
    {
        $result = [];
        $stack = [];
        $path = null;

        reset($data);
        while (!empty($data)) {
            $key = key($data);
            $element = $data[$key];
            unset($data[$key]);

            if (is_array($element) && !empty($element)) {
                if (!empty($data)) {
                    $stack[] = [$data, $path];
                }
                $data = $element;
                reset($data);
                $path .= $key . $separator;
            } else {
                $result[$path . $key] = $element;
            }

            if (empty($data) && !empty($stack)) {
                list($data, $path) = array_pop($stack);
                reset($data);
            }
        }

        return $result;
    }

    /**
     * This function can be thought of as a hybrid between PHP's `array_merge` and `array_merge_recursive`.
     *
     * The difference between this method and the built-in ones, is that if an array key contains another array, then
     * Hash::merge() will behave in a recursive fashion (unlike `array_merge`). But it will not act recursively for
     * keys that contain scalar values (unlike `array_merge_recursive`).
     *
     * Note: This function will work with an unlimited amount of arguments
     * and typecasts non-array parameters into arrays.
     *
     * @param array $data Array to be merged.
     * @param mixed $merge Array to merge with. The argument and all trailing arguments will be array cast when merged.
     *
     * @return array Merged array
     */
    public static function merge(array $data, $merge)
    {
        $args = array_slice(func_get_args(), 1);
        $return = $data;

        foreach ($args as &$curArg) {
            $stack[] = [(array)$curArg, &$return];
        }
        unset($curArg);
        static::mergeHelper($stack, $return);

        return $return;
    }

    /**
     * Split token conditions.
     *
     * @param string $token the token being splitted.
     *
     * @return array [token, conditions] with token splitted
     */
    protected static function splitConditions($token)
    {
        $conditions = false;
        $position = strpos($token, '[');
        if ($position !== false) {
            $conditions = substr($token, $position);
            $token = substr($token, 0, $position);
        }

        return [$token, $conditions];
    }

    /**
     * Check a key against a token.
     *
     * @param string $key The key in the array being searched.
     * @param string $token The token being matched.
     *
     * @return bool
     */
    protected static function matchToken($key, $token)
    {
        if ($token === '{n}') {
            return is_numeric($key);
        }
        if ($token === '{s}') {
            return is_string($key);
        }
        if (is_numeric($token)) {
            return ($key == $token);
        }

        return ($key === $token);
    }

    /**
     * Checks whether or not $data matches the attribute patterns.
     *
     * @param array $data Array of data to match.
     * @param string $selector The patterns to match.
     *
     * @return bool Fitness of expression.
     */
    protected static function matches(array $data, $selector)
    {
        preg_match_all(
            '/(\[ (?P<attr>[^=><!]+?) (\s* (?P<op>[><!]?[=]|[><]) \s* (?P<val>(?:\/.*?\/ | [^\]]+)) )? \])/x',
            $selector,
            $conditions,
            PREG_SET_ORDER
        );

        foreach ($conditions as $cond) {
            $attr = $cond['attr'];
            $op = isset($cond['op']) ? $cond['op'] : null;
            $val = isset($cond['val']) ? $cond['val'] : null;

            // Presence test.
            if (empty($op) && empty($val) && !isset($data[$attr])) {
                return false;
            }

            // Empty attribute = fail.
            if (!(isset($data[$attr]) || array_key_exists($attr, $data))) {
                return false;
            }

            $prop = null;
            if (isset($data[$attr])) {
                $prop = $data[$attr];
            }
            $isBool = is_bool($prop);
            if ($isBool && is_numeric($val)) {
                $prop = $prop ? '1' : '0';
            } elseif ($isBool) {
                $prop = $prop ? 'true' : 'false';
            }

            // Pattern matches and other operators.
            if ($op === '=' && $val && $val[0] === '/') {
                if (!preg_match($val, $prop)) {
                    return false;
                }
            } elseif (($op === '=' && $prop != $val) ||
            ($op === '!=' && $prop == $val) ||
            ($op === '>' && $prop <= $val) ||
            ($op === '<' && $prop >= $val) ||
            ($op === '>=' && $prop < $val) ||
            ($op === '<=' && $prop > $val)
            ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Perform a simple insert/remove operation.
     *
     * @param string $op The operation to do.
     * @param array $data The data to operate on.
     * @param array $path The path to work on.
     * @param mixed $values The values to insert when doing inserts.
     *
     * @return array data.
     */
    protected static function simpleOp($op, $data, $path, $values = null)
    {
        $_list =& $data;

        $count = count($path);
        $last = $count - 1;
        foreach ($path as $i => $key) {
            if ((is_numeric($key) && (int)($key) > 0 || $key === '0') &&
            strpos($key, '0') !== 0
            ) {
                $key = (int)$key;
            }
            if ($op === 'insert') {
                if ($i === $last) {
                    $_list[$key] = $values;

                    return $data;
                }
                if (!isset($_list[$key])) {
                    $_list[$key] = [];
                }
                $_list =& $_list[$key];
                if (!is_array($_list)) {
                    $_list = [];
                }
            } elseif ($op === 'remove') {
                if ($i === $last) {
                    unset($_list[$key]);

                    return $data;
                }
                if (!isset($_list[$key])) {
                    return $data;
                }
                $_list =& $_list[$key];
            }
        }
    }

    /**
     * Merge helper function to reduce duplicated code between merge() and expand().
     *
     * @param array $stack The stack of operations to work with.
     * @param array $return The return value to operate on.
     *
     * @return void
     */
    protected static function mergeHelper($stack, &$return)
    {
        while (!empty($stack)) {
            foreach ($stack as $curKey => &$curMerge) {
                foreach ($curMerge[0] as $key => &$val) {
                    if (!empty($curMerge[1][$key]) &&
                        (array)$curMerge[1][$key] === $curMerge[1][$key] &&
                        (array)$val === $val) {
                        $stack[] = [&$val, &$curMerge[1][$key]];
                    } elseif ((int)$key === $key && isset($curMerge[1][$key])) {
                        $curMerge[1][] = $val;
                    } else {
                        $curMerge[1][$key] = $val;
                    }
                }
                unset($stack[$curKey]);
            }
            unset($curMerge);
        }
    }
}


<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Skinny\Utility;

class Inflector
{
    /**
     * Method cache array.
     *
     * @var array
     */
    protected static $cache = [];

    /**
     * Cache inflected values, and return if already available.
     *
     * @param string $type Inflection type.
     * @param string $key Original value.
     * @param string|bool $value Inflected value.
     *
     * @return string|bool Inflected value on cache hit or false on cache miss.
     */
    protected static function cache($type, $key, $value = false)
    {
        $key = '_' . $key;
        $type = '_' . $type;
        if ($value !== false) {
            static::$cache[$type][$key] = $value;

            return $value;
        }
        if (!isset(static::$cache[$type][$key])) {
            return false;
        }

        return static::$cache[$type][$key];
    }

    /**
     * Returns the input lower_case_delimited_string as a CamelCasedString.
     *
     * @param string $string String to camelize.
     * @param string $delimiter The delimiter in the input string.
     *
     * @return string CamelizedStringLikeThis.
     */
    public static function camelize($string, $delimiter = '_')
    {
        $cacheKey = __FUNCTION__ . $delimiter;
        $result = static::cache($cacheKey, $string);
        if ($result === false) {
            $result = str_replace(' ', '', static::humanize($string, $delimiter));
            static::cache(__FUNCTION__, $string, $result);
        }

        return $result;
    }

    /**
     * Returns the input lower_case_delimited_string as 'A Human Readable String'.
     * (Underscores are replaced by spaces and capitalized following words.)
     *
     * @param string $string String to be humanized.
     * @param string $delimiter The character to replace with a space.
     *
     * @return string Human-readable string.
     */
    public static function humanize($string, $delimiter = '_')
    {
        $cacheKey = __FUNCTION__ . $delimiter;
        $result = static::cache($cacheKey, $string);
        if ($result === false) {
            $result = explode(' ', str_replace($delimiter, ' ', $string));
            foreach ($result as &$word) {
                $word = mb_strtoupper(mb_substr($word, 0, 1)) . mb_substr($word, 1);
            }
            $result = implode(' ', $result);
            static::cache($cacheKey, $string, $result);
        }

        return $result;
    }
}


<?php
namespace Skinny\Network;

use Skinny\Singleton\Singleton;

/**
 * This class is a wrapper to separate all the Discord classes into variables
 * for a better accessibility and clarity when developing modules.
 */
class Wrapper extends Singleton
{
    /**
     * The ModuleManager instance.
     *
     * @var \Skinny\Module\ModuleManager
     */
    public $ModuleManager;

    /**
     * The Message instance.
     *
     * @var \Discord\Parts\Channel\Message
     */
    public $Message;

    /**
     * The ModuleManager instance.
     *
     * @var \Discord\Parts\Channel\Channel
     */
    public $Channel;

    /**
     * The ModuleManager instance.
     *
     * @var \Discord\Parts\Guild\Guild
     */
    public $Guild;

    /**
     * The ModuleManager instance.
     *
     * @var \Discord\Repository\Guild\MemberRepository
     */
    public $Members;

    /**
     * Set the instances to the Wrapper.
     *
     * @param \Discord\Parts\Channel\Message $message The messages object.
     * @param \Skinny\Module\ModuleManager $moduleManager The ModuleManager object.
     *
     * @return object Return this Wrapper.
     */
    public function setInstances($message, $moduleManager)
    {
        $this->ModuleManager = $moduleManager;
        $this->Message = $message;
        $this->Channel = $message->channel;

        if (isset($message->channel->guild) && is_object($message->channel->guild)) {
            $this->Guild = $message->channel->guild;
        }

        if (isset($message->channel->guild->members) && is_object($message->channel->guild->members)) {
            $this->Members = $message->channel->guild->members;
        }

        return $this;
    }
}


<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Skinny\Error;

use Skinny\Utility\Hash;

class Debugger
{
    /**
     * A list of errors generated by the application.
     *
     * @var array
     */
    public $errors = [];

    /**
     * The current output format.
     *
     * @var string
     */
    protected $outputFormat = 'array';

    /**
     * Holds current output data when outputFormat is false.
     *
     * @var string
     */
    protected $data = [];

    /**
     * Returns a reference to the Debugger singleton object instance.
     *
     * @param string|null $class Class name.
     * @return object
     */
    public static function getInstance($class = null)
    {
        static $instance = [];
        //@codingStandardsIgnoreStart
        if (!empty($class)) {
            if (!$instance || strtolower($class) != strtolower(get_class($instance[0]))) {
                $instance[0] = new $class();
            }
        }
        //@codingStandardsIgnoreEnd
        if (!$instance) {
            $instance[0] = new Debugger();
        }

        return $instance[0];
    }

    /**
     * Outputs a stack trace based on the supplied options.
     *
     * ### Options
     *
     * - `depth` - The number of stack frames to return. Defaults to 999
     * - `format` - The format you want the return. Defaults to the currently selected format. If
     * format is 'array' or 'points' the return will be an array.
     * - `args` - Should arguments for functions be shown? If true, the arguments for each method call
     * will be displayed.
     * - `start` - The stack frame to start generating a trace from. Defaults to 0
     *
     * @param array $options Format for outputting stack trace.
     *
     * @return mixed Formatted stack trace.
     */
    public static function trace(array $options = [])
    {
        $self = Debugger::getInstance();
        $defaults = [
            'depth' => 999,
            'format' => $self->outputFormat,
            'args' => false,
            'start' => 0,
            'scope' => null,
            'exclude' => ['call_user_func_array', 'trigger_error']
        ];
        $options = Hash::merge($defaults, $options);
        $backtrace = debug_backtrace();
        $count = count($backtrace);
        $back = [];
        $_trace = [
            'line' => '??',
            'file' => '[internal]',
            'class' => null,
            'function' => '[main]'
        ];
        for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) {
            $trace = $backtrace[$i] + ['file' => '[internal]', 'line' => '??'];
            $signature = $reference = '[main]';

            if (isset($backtrace[$i + 1])) {
                $next = $backtrace[$i + 1] + $_trace;
                $signature = $reference = $next['function'];

                if (!empty($next['class'])) {
                    $signature = $next['class'] . '::' . $next['function'];
                    $reference = $signature . '(';

                    if ($options['args'] && isset($next['args'])) {
                        $args = [];

                        foreach ($next['args'] as $arg) {
                            $args[] = Debugger::exportVar($arg);
                        }

                        $reference .= implode(', ', $args);
                    }

                    $reference .= ')';
                }
            }

            if (in_array($signature, $options['exclude'])) {
                continue;
            }

            if ($options['format'] === 'points' && $trace['file'] !== '[internal]') {
                $back[] = ['file' => $trace['file'], 'line' => $trace['line']];
            } else {
                $back[] = $trace;
            }
        }

        if ($options['format'] === 'array' || $options['format'] === 'points') {
            return $back;
        }

        return implode("\n", $back);
    }

    /**
     * Converts a variable to a string for debug output.
     *
     * @param string $var Variable to convert
     * @param int $depth The depth to output to. Defaults to 3.
     *
     * @return string Variable as a formatted string.
     */
    public static function exportVar($var, $depth = 3)
    {
        return static::export($var, $depth, 0);
    }

    /**
     * Protected export function used to keep track of indentation and recursion.
     *
     * @param mixed $var The variable to dump.
     * @param int $depth The remaining depth.
     * @param int $indent The current indentation level.
     *
     * @return string The dumped variable.
     */
    protected static function export($var, $depth, $indent)
    {
        switch (static::getType($var)) {
            case 'boolean':
                return ($var) ? 'true' : 'false';
            case 'integer':
                return '(int) ' . $var;
            case 'float':
                return '(float) ' . $var;
            case 'string':
                if (trim($var) === '') {
                    return "''";
                }

                return "'" . $var . "'";
            case 'array':
                return static::arrayExport($var, $depth - 1, $indent + 1);
            case 'resource':
                return strtolower(gettype($var));
            case 'null':
                return 'null';
            case 'unknown':
                return 'unknown';
            default:
                return static::object($var, $depth - 1, $indent + 1);
        }
    }

    /**
     * Handles object to string conversion.
     *
     * @param string $var Object to convert
     * @param int $depth The current depth, used for tracking recursion.
     * @param int $indent The current indentation level.
     *
     * @return string
     */
    protected static function object($var, $depth, $indent)
    {
        $out = '';
        $props = [];

        $className = get_class($var);
        $out .= 'object(' . $className . ') {';
        $break = "\n" . str_repeat("\t", $indent);
        $end = "\n" . str_repeat("\t", $indent - 1);

        if ($depth > 0 && method_exists($var, '__debugInfo')) {
            try {
                return $out . "\n" .
                substr(static::arrayExport($var->__debugInfo(), $depth - 1, $indent), 1, -1) .
                $end . '}';
            } catch (Exception $e) {
                return $out . "\n(unable to export object)\n }";
            }
        }

        if ($depth > 0) {
            $objectVars = get_object_vars($var);
            foreach ($objectVars as $key => $value) {
                $value = static::export($value, $depth - 1, $indent);
                $props[] = "$key => " . $value;
            }

            $ref = new \ReflectionObject($var);

            $filters = [
                \ReflectionProperty::IS_PROTECTED => 'protected',
                \ReflectionProperty::IS_PRIVATE => 'private',
            ];

            foreach ($filters as $filter => $visibility) {
                $reflectionProperties = $ref->getProperties($filter);

                foreach ($reflectionProperties as $reflectionProperty) {
                    $reflectionProperty->setAccessible(true);
                    $property = $reflectionProperty->getValue($var);
                    $value = static::export($property, $depth - 1, $indent);
                    $key = $reflectionProperty->name;
                    $props[] = sprintf('[%s] %s => %s', $visibility, $key, $value);
                }
            }

            $out .= $break . implode($break, $props) . $end;
        }
        $out .= '}';

        return $out;
    }

    /**
     * Export an array type object. Filters out keys used in datasource configuration.
     *
     * @param array $var The array to export.
     * @param int $depth The current depth, used for recursion tracking.
     * @param int $indent The current indentation level.
     *
     * @return string Exported array.
     */
    protected static function arrayExport(array $var, $depth, $indent)
    {
        $out = "[";
        $break = $end = null;
        if (!empty($var)) {
            $break = "\n" . str_repeat("\t", $indent);
            $end = "\n" . str_repeat("\t", $indent - 1);
        }
        $vars = [];
        if ($depth >= 0) {
            foreach ($var as $key => $val) {
                // Sniff for globals as !== explodes in < 5.4
                if ($key === 'GLOBALS' && is_array($val) && isset($val['GLOBALS'])) {
                    $val = '[recursion]';
                } elseif ($val !== $var) {
                    $val = static::export($val, $depth, $indent);
                }
                $vars[] = $break . static::exportVar($key) .
                ' => ' .
                $val;
            }
        } else {
            $vars[] = $break . '[maximum depth reached]';
        }

        return $out . implode(',', $vars) . $end . ']';
    }

    /**
     * Get the type of the given variable. Will return the class name
     * for objects.
     *
     * @param mixed $var The variable to get the type of.
     *
     * @return string The type of variable.
     */
    public static function getType($var)
    {
        if (is_object($var)) {
            return get_class($var);
        }
        if ($var === null) {
            return 'null';
        }
        if (is_string($var)) {
            return 'string';
        }
        if (is_array($var)) {
            return 'array';
        }
        if (is_int($var)) {
            return 'integer';
        }
        if (is_bool($var)) {
            return 'boolean';
        }
        if (is_float($var)) {
            return 'float';
        }
        if (is_resource($var)) {
            return 'resource';
        }

        return 'unknown';
    }
}


<?php
namespace Skinny\Network;

use Discord\Discord;
use Skinny\Core\Configure;
use Skinny\Message\Message;
use Skinny\Module\ModuleManager;
use Skinny\Network\Wrapper;
use Skinny\Utility\Command;
use Skinny\Utility\User;

class Server
{
    /**
     * The Discord instance.
     *
     * @var \Discord\Discord
     */
    public $Discord;

    /**
     * The Module Manager instance.
     *
     * @var \Skinny\Module\ModuleManager
     */
    public $ModuleManager;

    /**
     * Initialize the Bot and and the ModuleManager.
     */
    public function __construct()
    {
        Configure::checkTokenKey();
        $this->Discord = new Discord(Configure::read('Discord'));

        //Initialize the ModuleManager.
        $modulesPriorities = [];
        if (Configure::check('Modules.priority')) {
            $modulesPriorities = Configure::read('Modules.priority');
        }

        $this->ModuleManager = new ModuleManager($modulesPriorities);
    }

    /**
     * Handle the events.
     *
     * @return void
     */
    public function listen()
    {
        $this->Discord->on('ready', function ($discord) {
            $discord->on('message', function ($message) {
                if ($this->Discord->id === $message->author->id) {
                    return;
                }

                $content = Message::parse($message->content);
                $wrapper = Wrapper::getInstance()->setInstances($message, $this->ModuleManager);

                //Handle the type of the message.
                //Note : The order is very important !
                if ($wrapper->Channel->is_private === true) {
                    $this->ModuleManager->onPrivateMessage($wrapper, $content);
                } elseif ($content['commandCode'] === Configure::read('Command.prefix') &&
                            isset(Configure::read('Commands')[$content['command']])) {
                    $command = Configure::read('Commands')[$content['command']];

                    if ((isset($command['admin']) && $command['admin'] === true) &&
                            !User::hasPermission($wrapper->Message->author->id, Configure::read('Bot.admins'))) {
                        $wrapper->Message->reply('You are not administrator of the bot.');

                        return;
                    }

                    if (count($content['arguments']) < $command['params']) {
                        $wrapper->Message->reply(Command::syntax($content));

                        return;
                    }

                    $this->ModuleManager->onCommandMessage($wrapper, $content);
                } else {
                    $this->ModuleManager->onChannelMessage($wrapper, $content);
                }
            });
        });
    }

    /**
     * Run the bot.
     *
     * @return void
     */
    public function startup()
    {
        $this->listen();

        $this->Discord->run();
    }
}


<?php
namespace Skinny\Message;

/**
 * This class is used to parse a raw message from Discord.
 */
class Message
{
    use StaticMessageTrait;

    /**
     * Parse the message and return an array with informations about the message.
     *
     * ### Usage
     *```
     * Message::parse('!say hi');
     *```
     * Wil return this array:
     *```
     * [
     *     'raw' => '!say hi',
     *     'parts' => [
     *          (int) 0 => '!say',
     *          (int) 1 => 'hi'
     *     ],
     *     'command' => 'say',
     *     'message' => 'hi',
     *     'commandCode' => '!',
     *     'arguments' => [
     *         (int) 0 => 'hi'
     *     ]
     * ]
     *
     *```
     *
     * @param string $message The message to parse.
     *
     * @return array The data.
     */
    public static function parse($message)
    {
        static::resetConfig();

        $chr32 = chr(32);
        $config = [];

        if (empty($message) || !is_string($message)) {
            return static::read();
        }

        $config += [
            'raw' => $message,
            'parts' => explode($chr32, trim($message), 2)
        ];

        $config += [
            'commandCode' => substr($config['parts'][0], 0, 1),
            'command' => strtolower(substr($config['parts'][0], 1))
        ];

        if (count($config['parts']) > 1) {
            $config += [
                'message' => $config['parts'][1],
                'arguments' => array_map(
                    'strtolower',
                    explode(
                        $chr32,
                        $config['parts'][1]
                    )
                )
            ];
        }
        static::config($config);

        return static::read();
    }
}


<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Skinny\Core\Configure;

use Skinny\Core\Exception\Exception;
use Skinny\Core\Plugin;

trait FileConfigTrait
{

    /**
     * The path this engine finds files on.
     *
     * @var string
     */
    protected $path = '';

    /**
     * Get file path
     *
     * @param string $key The identifier to write to. If the key has a . it will be treated
     *  as a plugin prefix.
     * @param bool $checkExists Whether to check if file exists. Defaults to false.
     *
     * @return string Full file path
     *
     * @throws \Skinny\Core\Exception\Exception When files don't exist or when
     *  files contain '..' as this could lead to abusive reads.
     */
    protected function getFilePath($key, $checkExists = false)
    {
        if (strpos($key, '..') !== false) {
            throw new Exception('Cannot load/dump configuration files with ../ in them.');
        }

        list($plugin, $key) = pluginSplit($key);

        if ($plugin) {
            $file = Plugin::configPath($plugin) . $key;
        } else {
            $file = $this->path . $key;
        }

        $file .= $this->extension;

        if (!$checkExists || is_file($file)) {
            return $file;
        }

        if (is_file(realpath($file))) {
            return realpath($file);
        }

        throw new Exception(sprintf('Could not load configuration file: %s', $file));
    }
}


<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Skinny\Core;

use DirectoryIterator;
use Skinny\Core\Configure;
use Skinny\Core\Exception\MissingPluginException;

class Plugin
{

    /**
     * Holds a list of all loaded plugins and their configuration.
     *
     * @var array
     */
    protected static $plugins = [];

    /**
     * Loads a plugin and optionally loads bootstrap files.
     *
     * ### Examples:
     *
     * `Plugin::load('Basic')`
     *
     * Will load the Basic plugin and will not load the bootstrap file.
     *
     * `Plugin::load('Basic', ['bootstrap' => true])`
     *
     * Will load the bootstrap.php file.
     *
     * It is also possible to load multiple plugins at once. Examples:
     *
     * `Plugin::load(['Basic', 'Developer'])`
     *
     * Will load the Basic and Developer plugins.
     *
     * `Plugin::load(['Basic', 'Developer'], ['bootstrap' => true])`
     *
     * Will load bootstrap file for both plugins
     *
     *
     * ### Configuration options
     *
     * - `bootstrap` - array - Whether or not you want the $plugin/config/bootstrap.php file loaded.
     * - `ignoreMissing` - boolean - Set to true to ignore missing bootstrap files.
     * - `path` - string - The path the plugin can be found on. If empty the default plugin path will be used.
     * - `classBase` - string - The path relative to `path` which contains the folders with class files.
     * Defaults to "src".
     *
     * @param string|array $plugin Name of the plugin to be loaded in CamelCase format or array or plugins to load.
     * @param array $config Configuration options for the plugin.
     *
     * @throws \Skinny\Core\Exception\MissingPluginException If the folder for the plugin to be loaded is not found.
     *
     * @return void
     */
    public static function load($plugin, array $config = [])
    {
        if (is_array($plugin)) {
            foreach ($plugin as $name => $conf) {
                list($name, $conf) = (is_numeric($name)) ? [$conf, $config] : [$name, $conf];
                static::load($name, $conf);
            }

            return;
        }

        static::loadConfig();

        $config += [
            'bootstrap' => false,
            'classBase' => 'src',
            'ignoreMissing' => false
        ];

        if (!isset($config['path'])) {
            $config['path'] = Configure::read('plugins.' . $plugin);
        }

        if (empty($config['path'])) {
            $paths = (array)Configure::read('App.paths.plugins');
            $pluginPath = str_replace('/', DIRECTORY_SEPARATOR, $plugin);
            foreach ($paths as $path) {
                if (is_dir($path . $pluginPath)) {
                    $config['path'] = $path . $pluginPath . DIRECTORY_SEPARATOR;
                    break;
                }
            }
        }

        if (empty($config['path'])) {
            throw new MissingPluginException(['plugin' => $plugin]);
        }

        $config['classPath'] = $config['path'] . $config['classBase'] . DIRECTORY_SEPARATOR;
        if (!isset($config['configPath'])) {
            $config['configPath'] = $config['path'] . 'config' . DIRECTORY_SEPARATOR;
        }

        static::$plugins[$plugin] = $config;

        if ($config['bootstrap'] === true) {
            static::bootstrap($plugin);
        }
    }

    /**
     * Load the plugin path configuration file.
     *
     * @return void
     */
    protected static function loadConfig()
    {
        if (Configure::check('plugins')) {
            return;
        }
        $vendorFile = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR . 'skinny-plugins.php';
        if (!file_exists($vendorFile)) {
            $vendorFile = dirname(dirname(dirname(dirname(__DIR__)))) . DIRECTORY_SEPARATOR . 'skinny-plugins.php';
            if (!file_exists($vendorFile)) {
                Configure::write(['plugins' => []]);

                return;
            }
        }

        $config = require $vendorFile;
        Configure::write($config);
    }

    /**
     * Will load all the plugins located in the default plugin folder.
     *
     * If passed an options array, it will be used as a common default for all plugins to be loaded
     * It is possible to set specific defaults for each plugins in the options array. Examples:
     *
     * ```
     *  Plugin::loadAll([
     *      ['bootstrap' => true],
     *      'Basic' => ['bootstrap' => false],
     *  ]);
     * ```
     *
     * The above example will load the bootstrap file for all plugins,
     * but for Basic it will not load the bootstrap file.
     *
     * If a plugin has been loaded already, it will not be reloaded by loadAll().
     *
     * @param array $options Options.
     *
     * @return void
     */
    public static function loadAll(array $options = [])
    {
        static::loadConfig();
        $plugins = [];
        foreach ((array)Configure::read('App.paths.plugins') as $path) {
            if (!is_dir($path)) {
                continue;
            }
            $dir = new DirectoryIterator($path);
            foreach ($dir as $path) {
                if ($path->isDir() && !$path->isDot()) {
                    $plugins[] = $path->getBasename();
                }
            }
        }
        if (Configure::check('plugins')) {
            $plugins = array_merge($plugins, array_keys(Configure::read('plugins')));
            $plugins = array_unique($plugins);
        }

        foreach ($plugins as $p) {
            $opts = isset($options[$p]) ? $options[$p] : null;
            if ($opts === null && isset($options[0])) {
                $opts = $options[0];
            }
            if (isset(static::$plugins[$p])) {
                continue;
            }
            static::load($p, (array)$opts);
        }
    }

    /**
     * Returns the filesystem path for a plugin.
     *
     * @param string $plugin Name of the plugin in CamelCase format.
     *
     * @return string Path to the plugin folder.
     *
     * @throws \Skinny\Core\Exception\MissingPluginException if the folder for plugin was not found or
     * plugin has not been loaded
     */
    public static function path($plugin)
    {
        if (empty(static::$plugins[$plugin])) {
            throw new MissingPluginException(['plugin' => $plugin]);
        }

        return static::$plugins[$plugin]['path'];
    }

    /**
     * Returns the filesystem path for plugin's folder containing class folders.
     *
     * @param string $plugin name of the plugin in CamelCase format.
     *
     * @return string Path to the plugin folder container class folders.
     *
     * @throws \Skinny\Core\Exception\MissingPluginException If plugin has not been loaded.
     */
    public static function classPath($plugin)
    {
        if (empty(static::$plugins[$plugin])) {
            throw new MissingPluginException(['plugin' => $plugin]);
        }

        return static::$plugins[$plugin]['classPath'];
    }

    /**
     * Returns the filesystem path for plugin's folder containing config files.
     *
     * @param string $plugin name of the plugin in CamelCase format.
     *
     * @return string Path to the plugin folder container config files.
     *
     * @throws \Skinny\Core\Exception\MissingPluginException If plugin has not been loaded.
     */
    public static function configPath($plugin)
    {
        if (empty(static::$plugins[$plugin])) {
            throw new MissingPluginException(['plugin' => $plugin]);
        }

        return static::$plugins[$plugin]['configPath'];
    }

    /**
     * Loads the bootstrapping files for a plugin, or calls the initialization setup in the configuration.
     *
     * @param string $plugin Name of the plugin.
     *
     * @return mixed
     *
     * @see \Skinny\Core\Plugin::load() for examples of bootstrap configuration
     */
    public static function bootstrap($plugin)
    {
        $config = static::$plugins[$plugin];
        if ($config['bootstrap'] === false) {
            return false;
        }
        if ($config['bootstrap'] === true) {
            return static::includeFile(
                $config['configPath'] . 'bootstrap.php',
                $config['ignoreMissing']
            );
        }
    }

    /**
     * Returns true if the plugin $plugin is already loaded
     * If plugin is null, it will return a list of all loaded plugins
     *
     * @param string|null $plugin Plugin name.
     *
     * @return bool|array Boolean true if $plugin is already loaded.
     *   If $plugin is null, returns a list of plugins that have been loaded.
     */
    public static function loaded($plugin = null)
    {
        if ($plugin !== null) {
            return isset(static::$plugins[$plugin]);
        }
        $return = array_keys(static::$plugins);
        sort($return);

        return $return;
    }

    /**
     * Returns the plugin configuration if the plugin $plugin is already loaded.If plugin is null,
     * it will return a list of all loaded plugins with their configuration.
     *
     * @param string|null $plugin Plugin name.
     *
     * @return array The $plugin configuration if $plugin is already loaded. If $plugin is null, returns
     *   a list of plugins that have been loaded with their configuration.
     */
    public static function loadedWithValues($plugin = null)
    {
        if ($plugin !== null && isset(static::$plugins[$plugin])) {
            return static::$plugins[$plugin];
        }

        return static::$plugins;
    }

    /**
     * Forgets a loaded plugin or all of them if first parameter is null.
     *
     * @param string|null $plugin Name of the plugin to forget.
     *
     * @return void
     */
    public static function unload($plugin = null)
    {
        if ($plugin === null) {
            static::$plugins = [];
        } else {
            unset(static::$plugins[$plugin]);
        }
    }

    /**
     * Include file, ignoring include error if needed if file is missing.
     *
     * @param string $file File to include.
     * @param bool $ignoreMissing Whether to ignore include error for missing files.
     *
     * @return mixed
     */
    protected static function includeFile($file, $ignoreMissing = false)
    {
        if ($ignoreMissing && !is_file($file)) {
            return false;
        }

        return include $file;
    }
}


<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace Skinny\Core\Configure\Engine;

use Skinny\Core\Configure\ConfigEngineInterface;
use Skinny\Core\Configure\FileConfigTrait;
use Skinny\Core\Exception\Exception;

class PhpConfig implements ConfigEngineInterface
{
    use FileConfigTrait;

    /**
     * File extension.
     *
     * @var string
     */
    protected $extension = '.php';

    /**
     * Constructor for PHP Config file reading.
     *
     * @param string|null $path The path to read config files from. Defaults to CONFIG.
     */
    public function __construct($path = null)
    {
        if ($path === null) {
            $path = CONFIG;
        }
        $this->path = $path;
    }

    /**
     * Read a config file and return its contents.
     *
     * Files with `.` in the name will be treated as values in plugins. Instead of
     * reading from the initialized path, plugin keys will be located using Plugin::path().
     *
     * @param string $key The identifier to read from. If the key has a . it will be treated
     *  as a plugin prefix.
     *
     * @return array Parsed configuration values.
     *
     * @throws \Skinny\Core\Exception\Exception when files don't exist or they don't contain `$config`.
     *  Or when files contain '..' as this could lead to abusive reads.
     */
    public function read($key)
    {
        $file = $this->getFilePath($key, true);

        $return = include $file;
        if (!is_array($return)) {
            throw new Exception(sprintf('Config file "%s" did not return an array', $key . '.php'));
        }

        return $return;
    }

    /**
     * Converts the provided $data into a string of PHP code that can
     * be used saved into a file and loaded later.
     *
     * @param string $key The identifier to write to. If the key has a . it will be treated
     *  as a plugin prefix.
     * @param array $data Data to dump.
     *
     * @return bool Success.
     */
    public function dump($key, array $data)
    {
        $contents = '<?php' . "\n" . 'return ' . var_export($data, true) . ';';

        $filename = $this->getFilePath($key);

        return file_put_contents($filename, $contents) > 0;
    }
}


<?php
namespace Skinny\Message;

use InvalidArgumentException;

/**
 * A trait that provides a set of static methods to manage Message class.
 */
trait StaticMessageTrait
{

    /**
     * The default configuration.
     *
     * @var array
     */
    protected static $defaultConfig = [
        'raw' => '',
        'parts' => [],
        'command' => '',
        'message' => '',
        'commandCode' => '',
        'arguments' => []
    ];

    /**
     * Configuration sets.
     *
     * @var array
     */
    protected static $config = [];

    /**
     * This method can be used to define configuration.
     *
     * ### Usage
     *
     * Writing config data:
     *
     * ```
     * Message::config('message', 'hi');
     * ```
     *
     * Writing multiple config data at once:
     *
     * ```
     * Message::config(['message' => 'hi', 'commandCode' => '!']);
     * ```
     *
     * @param string|array $key The name of the configuration to read, or an array of multiple configs.
     *
     * @return null
     *
     * @throws \InvalidArgumentException If the key is not a string or an array.
     */
    public static function config($key, $value = null)
    {
        if (is_string($key)) {
            static::$config[$key] = $value;

            return;
        }

        if (!is_array($key)) {
            throw new InvalidArgumentException('Only string and array can be passed to config.');
        }

        foreach ($key as $name => $value) {
            static::$config[$name] = $value;
        }

        return;
    }

    /**
     * Read a config variable or all the config.
     *
     * ### Usage
     *
     * Reading config with key:
     *
     * ```
     * Message::read('message');
     * ```
     *
     * Reading all the config:
     *
     * ```
     * Message::read();
     * ```
     *
     * @param string|null The key to read the configuration.
     *
     * @return mixed
     */
    public static function read($key = null)
    {
        if ($key === null) {
            return static::$config;
        }

        return isset(static::$config[$key]) ? static::$config[$key] : null;
    }

    /**
     * Reset the config. If default is true, reset to the default values.
     *
     * @param bool $default True to reset the config to the default value.
     *
     * @return void|null Null when reseting the config tot he default values.
     */
    public static function resetConfig($default = true)
    {
        if ($default) {
            static::$config = static::$defaultConfig;

            return;
        }

        static::$config = [];
    }
}


<?php
namespace Skinny\Module;

use ArrayAccess;
use Countable;
use DirectoryIterator;
use Exception;
use RuntimeException;
use Skinny\Core\Configure;
use Skinny\Core\Plugin;
use Skinny\Module\Exception\NotImplementedException;
use Skinny\Utility\Inflector;

class ModuleManager implements ArrayAccess, Countable
{
    /**
     * Constant that can be returned by modules to indicate to halt noticing further modules.
     *
     * @var int
     */
    const STOP = -1;

    /**
     * Modules that have priority over other modules.
     *
     * @var array
     */
    protected $priorityList = [];

    /**
     * Loaded modules.
     *
     * @var array
     */
    protected $loadedModules = [];

    /**
     * An argument that should be passed every call.
     *
     * @var array
     */
    protected $prefixArgument = null;

    /**
     * Constructor, loads all Modules in the Module directory.
     *
     * @param array $priorities The modules to load in priority.
     */
    public function __construct(array $priorities = [])
    {
        $this->priorityList = $priorities;
        $files = new DirectoryIterator(MODULE_DIR);
        $this->loadModules($files);

        $plugins = Plugin::loaded();
        if (!empty((array)$plugins)) {
            foreach ($plugins as $plugin) {
                $path = Plugin::classPath($plugin) . 'Module' . DS . 'Modules';
                $files = new DirectoryIterator($path);

                $this->loadModules($files, ['pathDir' => $path, 'plugin' => $plugin]);
            }
        }
    }

    /**
     * Call destructors to unload all modules directly.
     */
    public function __destruct()
    {
        $this->loadedModules = [];
    }

    /**
     * Calls a given method on all modules with the arguments passed to this method.
     * The loop will halt when the method returns the STOP constant, indicating all
     * work is done.
     *
     * @param string $method The method to check.
     * @param array $arguments The arguments to pass to the function.
     *
     * @return bool
     */
    public function __call($method, array $arguments)
    {
        if (!is_null($this->prefixArgument)) {
            array_unshift($arguments, $this->prefixArgument);
        }

        foreach ($this->loadedModules as $module) {
            if (!method_exists($module['object'], $method)) {
                continue;
            }

            if (call_user_func_array([$module['object'], $method], $arguments) === self::STOP) {
                break;
            }
        }

        return true;
    }

    /**
     * Argument that should be passed into Modules.
     *
     * @param array $argument The arguments to add.
     *
     * @return void
     */
    public function addPrefixArgument($argument)
    {
        $this->prefixArgument = $argument;
    }

    /**
     * Loads the list of modules in the DirectoryIterator.
     *
     * Options :
     * - pathDir : The full path of the Modules directory. Default to MODULE_DIR.
     * - plugin : The plugin name or false if no plugin. Default to false.
     *
     * @param \DirectoryIterator $files The DirectoryIterator instance.
     * @param array $config The configuration options.
     *
     * @throws \RuntimeException When the class doesn't implement the ModuleInterface.
     *
     * @return string
     */
    protected function loadModules(DirectoryIterator $files, array $config = [])
    {
        foreach ($files as $file) {
            $filename = $file->getFilename();
            if ($file->isDot() || $file->isDir() || $filename[0] == '.') {
                continue;
            } elseif ($file->isFile() && substr($filename, -4) != '.php') {
                continue;
            } else {
                try {
                    $this->load(substr($filename, 0, -4), $config);
                } catch (Exception $e) {
                    throw new RuntimeException(sprintf('Error while loading module : %s', $e->getMessage()));
                }
            }
        }
    }

    /**
     * Check if the module exist in the plugin list. Primary used when we need to load a module and
     * the module doesn't exists in the application itself.
     *
     * @param string $module The module to check.
     *
     * @return array The config needed for the ModuleManager::load() function.
     */
    protected function checkPlugins($module)
    {
        $loadedPlugins = Configure::read('plugins');
        $arr = [];

        if (empty($loadedPlugins) || !is_array($loadedPlugins)) {
            return $arr;
        }

        foreach ($loadedPlugins as $plugin => $pluginPath) {
            $pluginPath = Plugin::classPath($plugin) . 'Module' . DS . 'Modules';
            $files = new DirectoryIterator($pluginPath);

            foreach ($files as $file) {
                $filename = $file->getFilename();

                if ($file->isDot() || $file->isDir() || $filename[0] == '.') {
                    continue;
                } elseif ($file->isFile() && substr($filename, -4) != '.php') {
                    continue;
                } elseif (Inflector::camelize(substr($filename, 0, -4)) !== $module) {
                    continue;
                } else {
                    $arr = ['pathDir' => $pluginPath, 'plugin' => $plugin];

                    return $arr;
                }
            }
        }

        return $arr;
    }

    /**
     * Loads a module into the Framework and prioritize it according to our priority list.
     *
     * Options :
     * - pathDir : The full path of the Modules directory. Default to MODULE_DIR.
     * - plugin : The plugin name or false if no plugin. Default to false.
     *
     * @param string $module Filename of the module we want to load.
     * @param array $config The configuration options.
     *
     * @throws \RuntimeException When the class doesn't implement the ModuleInterface.
     *
     * @return string
     */
    public function load($module, array $config = [])
    {
        $module = Inflector::camelize($module);

        $config += [
            'pathDir' => MODULE_DIR,
            'plugin' => false
        ];

        if (isset($this->loadedModules[$module])) {
            //Return the message AlreadyLoaded.
            return 'AL';
        }

        if ($config['plugin'] != false) {
            $config['pathDir'] = Plugin::classPath($config['plugin']) . 'Module' . DS . 'Modules';
        }

        //Check if the module exist in the plugin list.
        $config = array_merge($config, $this->checkPlugins($module));

        if (!file_exists($config['pathDir'] . DS . $module . '.php')) {
            //Return NotFound.
            return 'NF';
        }

        $path = $config['pathDir'] . DS . $module . '.php';
        $className = Configure::read('App.namespace') . DS . 'Module' . DS . 'Modules' . DS . $module;

        if (Configure::read('debug') === false) {
            require_once $path;
        } else {
            $newClass = $module . '_' . md5(mt_rand() . time());
            $contents = preg_replace(
                '/(class[\s]+?)' . $module . '([\s]+?implements[\s]+?ModuleInterface[\s]+?{)/',
                '\\1' . $newClass . '\\2',
                file_get_contents($path)
            );

            $name = tempnam(TMP_MODULE_DIR, $module . '_');
            file_put_contents($name, $contents);

            $namespace = ($config['plugin'] !== false) ? $config['plugin'] : Configure::read('App.namespace');
            $className = $namespace . DS . 'Module' . DS . 'Modules' . DS . $newClass;

            if (!preg_match('/([\s]+?implements[\s]+?ModuleInterface)/', $contents)) {
                throw new NotImplementedException(['function' => 'ModuleManager::load()', 'class' => $className]);
            }

            require_once $name;
            unlink($name);
        }

        $className = str_replace('/', '\\', rtrim($className, '\\'));
        $objectModule = new $className();

        if (!$objectModule instanceof ModuleInterface) {
            throw new NotImplementedException(['function' => 'ModuleManager::load()', 'class' => $className]);
        }

        $new = [
            'object' => $objectModule,
            'loaded' => time(),
            'plugin' => ($config['plugin'] !== false) ? true : false,
            'pluginName' => ($config['plugin'] !== false) ? $config['plugin'] : '',
            'pluginPath' => $config['pathDir'],
            'name' => $className,
            'modified' => isset($contents) ? true : false
        ];

        if (in_array($module, $this->priorityList)) {
            $temp = array_reverse($this->loadedModules);
            $temp[$module] = $new;
            $this->loadedModules = array_reverse($temp);
        } else {
            $temp = [];
            $temp[$module] = $new;
            $this->loadedModules = array_merge($this->loadedModules, $temp);
        }

        //Return the message Loaded.
        return 'L';
    }

    /**
     * Unload a module from the Framework.
     *
     * @param string $module Module to unload.
     *
     * @return string
     */
    public function unload($module)
    {
        $module = Inflector::camelize($module);

        if (!isset($this->loadedModules[$module])) {
            //Return the message AlreadyUnloaded.
            return 'AU';
        }

        //Remove this module, also calling the __destruct method of it.
        $object = $this->loadedModules[$module]['object'];
        unset($object);
        unset($this->loadedModules[$module]);

        //Return the message Unloaded.
        return 'U';
    }

    /**
     * Reloads a module by first calling unload and then load.
     *
     * @param string $module The module to reload.
     *
     * @return string
     */
    public function reload($module)
    {
        $module = Inflector::camelize($module);
        $config = [];

        if (isset($this->loadedModules[$module]) && $this->loadedModules[$module]['plugin'] === true) {
            $config += [
                'pathDir' => $this->loadedModules[$module]['pluginPath'],
                'plugin' => $this->loadedModules[$module]['pluginName']
            ];
        }

        $unload = $this->unload($module);

        if ($unload != "U") {
            return $unload;
        }

        return $this->load($module, $config);
    }

    /**
     * Returns the time when a module was loaded or false if we don't have it.
     *
     * @param string $module The module to check the time.
     *
     * @return false|int
     */
    public function timeLoaded($module)
    {
        $module = Inflector::camelize($module);

        if (!isset($this->loadedModules[$module])) {
            return false;
        }

        return $this->loadedModules[$module]['loaded'];
    }

    /**
     * Returns if a module has been modified or -1 if we do not have it
     *
     * @param string $module The module to check.
     *
     * @return bool|int
     */
    public function isModified($module)
    {
        $module = Inflector::camelize($module);

        if (!isset($this->loadedModules[$module])) {
            return -1;
        }

        return $this->loadedModules[$module]['modified'];
    }

    /**
     * Returns an array with names of all loaded modules, sorted on their priority.
     *
     * @return array
     */
    public function getLoadedModules()
    {
        return array_keys($this->loadedModules);
    }

    /**
     * Returns the numbers of modules loaded.
     *
     * @return int
     */
    public function count()
    {
        return count($this->loadedModules);
    }

    /**
     * Returns the module if we have it, or false if we don't have it.
     *
     * @param string $module The module to get.
     *
     * @return bool|object
     */
    public function offsetGet($module)
    {
        $module = Inflector::camelize($module);

        if (!isset($this->loadedModules[$module])) {
            return false;
        }

        return $this->loadedModules[$module];
    }

    /**
     * Check if we have loaded a certain module.
     *
     * @param string $module The module to check.
     *
     * @return bool
     */
    public function offsetExists($module)
    {
        $module = Inflector::camelize($module);

        return isset($this->loadedModules[$module]);
    }

    /**
     * Creates a new Module in our list.
     *
     * @param string $offset The offset of the moddule.
     * @param object $module The module to create.
     *
     * @throws \RuntimeException
     *
     * @return void
     */
    public function offsetSet($offset, $module)
    {
        if (!$module instanceof ModuleInterface) {
            throw new RuntimeException(
                sprintf('ModuleManager::offsetSet() expects "%s" to be an instance of ModuleInterface.', $module)
            );
        }

        $newModule = [
            'object' => $module,
            'loaded' => time(),
            'plugin' => false,
            'pluginName' => '',
            'pluginPath' => MODULE_DIR,
            'name' => get_class($module),
            'modified' => false
        ];

        if (in_array($offset, $this->priorityList)) {
            $temp = array_reverse($this->loadedModules, true);
            $temp[$offset] = $newModule;
            $this->loadedModules = array_reverse($temp, true);
        } else {
            $this->loadedModules[$offset] = $newModule;
        }
    }

    /**
     * Unload a Module, this is basically the same as unload().
     *
     * @param string $module The module to unlod.
     *
     * @return void
     */
    public function offsetUnset($module)
    {
        $module = Inflector::camelize($module);

        if (isset($this->loadedModules[$module])) {
            unset($this->loadedModules[$module]);
        }
    }
}


<?php
/**
 * This file is part of Swoft.
 *
 * @link     https://swoft.org
 * @document https://doc.swoft.org
 * @contact  group@swoft.org
 * @license  https://github.com/swoft-cloud/swoft/blob/master/LICENSE
 */

namespace Swoft\Redis;

use Psr\SimpleCache\CacheInterface;
use Swoft\App;
use Swoft\Cache\CacheCoResult;
use Swoft\Cache\CacheDataResult;
use Swoft\Core\ResultInterface;
use Swoft\Pool\ConnectionInterface;
use Swoft\Pool\PoolInterface;
use Swoft\Redis\Pool\RedisPool;

/**
 * Psr 16 implement by Redis
 * @method string getLastError()
 * @method bool clearLastError()
 * key and string
 * @method int append($key, $value)
 * @method int decr($key)
 * @method int decrBy($key, $value)
 * @method string getRange($key, $start, $end)
 * @method int incr($key)
 * @method int incrBy($key, $value)
 * @method float incrByFloat($key, $increment)
 * @method int strlen($key)
 * @method bool mset( array $array )
 * hash
 * @method int hSet($key, $hashKey, $value)
 * @method bool hSetNx($key, $hashKey, $value)
 * @method string hGet($key, $hashKey)
 * @method int hLen($key)
 * @method int hDel($key, $hashKey1, $hashKey2 = null, $hashKeyN = null)
 * @method array hKeys($key)
 * @method array hVals($key)
 * @method array hGetAll($key)
 * @method bool hExists($key, $hashKey)
 * @method bool hIncrBy($key, $hashKey, $value)
 * @method bool hIncrByFloat($key, $field, $increment)
 * @method bool hMset($key, $hashKeys)
 * list
 * @method array brPop(array $keys, $timeout)
 * @method array blPop(array $keys, $timeout)
 * @method int lLen($key)
 * @method int lPush($key, $value1, $value2 = null, $valueN = null)
 * @method string lPop($key)
 * @method array lRange($key, $start, $end)
 * @method int lRem($key, $value, $count)
 * @method bool lSet($key, $index, $value)
 * @method int rPush($key, $value1, $value2 = null, $valueN = null)
 * @method string rPop($key)
 * set
 * @method int sAdd($key, $value1, $value2 = null, $valueN = null)
 * @method array|bool scan(&$iterator, $pattern = null, $count = 0)
 * @method int sCard($key)
 * @method array sDiff($key1, $key2, $keyN = null)
 * @method array sInter($key1, $key2, $keyN = null)
 * @method int sInterStore($dstKey, $key1, $key2, $keyN = null)
 * @method int sDiffStore($dstKey, $key1, $key2, $keyN = null)
 * @method array sMembers($key)
 * @method bool sMove($srcKey, $dstKey, $member)
 * @method bool sPop($key)
 * @method string|array sRandMember($key, $count = null)
 * @method int sRem($key, $member1, $member2 = null, $memberN = null)
 * @method array sUnion($key1, $key2, $keyN = null)
 * @method int sUnionStore($dstKey, $key1, $key2, $keyN = null)
 * sort
 * @method int zAdd($key, $score1, $value1, $score2 = null, $value2 = null, $scoreN = null, $valueN = null)
 * @method array zRange($key, $start, $end, $withscores = null)
 * @method int zRem($key, $member1, $member2 = null, $memberN = null)
 * @method array zRevRange($key, $start, $end, $withscore = null)
 * @method array zRangeByScore($key, $start, $end, array $options = array())
 * @method array zRangeByLex($key, $min, $max, $offset = null, $limit = null)
 * @method int zCount($key, $start, $end)
 * @method int zRemRangeByScore($key, $start, $end)
 * @method int zRemRangeByRank($key, $start, $end)
 * @method int zCard($key)
 * @method float zScore($key, $member)
 * @method int zRank($key, $member)
 * @method float zIncrBy($key, $value, $member)
 * @method int zUnion($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM')
 * @method int zInter($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM')
 * pub/sub
 * @method int publish($channel, $message)
 * @method string|array psubscribe($patterns, $callback)
 * @method string|array subscribe($channels, $callback)
 * @method array|int pubsub($keyword, $argument)
 * script
 * @method mixed eval($script, $args = array(), $numKeys = 0)
 * @method mixed evalSha($scriptSha, $args = array(), $numKeys = 0)
 * @method mixed script($command, $script)
 */
class Redis implements CacheInterface
{
    /**
     * @var string
     */
    protected $poolName = RedisPool::class;

    /**
     * Get the value related to the specified key
     *
     * @param string $key
     * @param mixed  $default
     *
     * @return string|bool
     */
    public function get($key, $default = null)
    {
        $result = $this->call('get', [$key]);
        if ($result === false || $result === null) {
            return $default;
        }

        return $result;
    }

    /**
     * Set the string value in argument as value of the key.
     *
     * @param string $key
     * @param mixed  $value
     * @param int    $ttl unit is seconds
     *
     * @return bool
     */
    public function set($key, $value, $ttl = null): bool
    {
        $ttl    = $this->getTtl($ttl);
        $params = ($ttl <= 0) ? [$key, $value] : [$key, $value, $ttl];

        return $this->call('set', $params);
    }

    /**
     * Remove specified keys.
     *
     * @param string $key
     *
     * @return bool
     */
    public function delete($key): bool
    {
        return (bool)$this->call('del', [$key]);
    }

    /**
     * Removes all entries from the current database.
     *
     * @return  bool  Always TRUE.
     */
    public function clear(): bool
    {
        return $this->call('flushDB', []);
    }

    /**
     * Returns the values of all specified keys.
     * For every key that does not hold a string value or does not exist,
     * the special value false is returned. Because of this, the operation never fails.
     *
     * @param iterable $keys
     * @param mixed    $default
     *
     * @return array|mixed
     */
    public function getMultiple($keys, $default = null)
    {
        $mgetResult = $this->call('mget', [$keys]);
        if ($mgetResult === false) {
            return $default;
        }
        $result = [];
        foreach ($mgetResult ?? [] as $key => $value) {
            $result[$keys[$key]] = $value;
        }

        return $result;
    }

    /**
     * Sets multiple key-value pairs in one atomic command.
     *
     * @param iterable $values
     * @param int      $ttl
     *
     * @return bool TRUE in case of success, FALSE in case of failure.
     */
    public function setMultiple($values, $ttl = null): bool
    {
        $result = $this->call('mset', [$values]);

        return $result;
    }

    /**
     * Remove specified keys.
     *
     * @param iterable $keys
     *
     * @return bool
     */
    public function deleteMultiple($keys): bool
    {
        return (bool)$this->call('del', [$keys]);
    }

    /**
     * Verify if the specified key exists.
     *
     * @param string $key
     *
     * @return  bool  If the key exists, return TRUE, otherwise return FALSE.
     */
    public function has($key): bool
    {
        return $this->call('exists', [$key]);
    }

    /**
     * defer call
     *
     * @param string $method
     * @param array  $params
     *
     * @return ResultInterface
     */
    public function deferCall(string $method, array $params)
    {
        $connectPool = App::getPool($this->poolName);

        /* @var $client RedisConnection */
        $client = $connectPool->getConnection();
        $client->setDefer();
        $result = $client->$method(...$params);

        return $this->getResult($client, $result);
    }

    /**
     * @param array $keys
     *
     * @return array|mixed
     */
    public function mget(array $keys)
    {
        return $this->getMultiple($keys, false);
    }

    /**
     * magic method
     *
     * @param string $method
     * @param array  $arguments
     *
     * @return mixed
     */
    public function __call($method, $arguments)
    {
        return $this->call($method, $arguments);
    }

    /**
     * call method by redis client
     *
     * @param string $method
     * @param array  $params
     *
     * @return mixed
     */
    public function call(string $method, array $params)
    {
        /* @var PoolInterface $connectPool */
        $connectPool = App::getPool($this->poolName);
        /* @var ConnectionInterface $client */
        $connection = $connectPool->getConnection();
        $result     = $connection->$method(...$params);
        $connection->release(true);

        return $result;
    }

    /**
     * @param ConnectionInterface $connection
     * @param mixed               $result
     *
     * @return ResultInterface
     */
    private function getResult(ConnectionInterface $connection, $result)
    {
        if (App::isCoContext()) {
            return new CacheCoResult($result, $connection);
        }

        return new CacheDataResult($result, $connection);
    }

    /**
     * the ttl
     *
     * @param $ttl
     *
     * @return int
     */
    private function getTtl($ttl): int
    {
        return ($ttl === null) ? 0 : (int)$ttl;
    }
}


<?php

namespace Swoft\Redis\Aspect;

use Swoft\Aop\JoinPoint;
use Swoft\Bean\Annotation\AfterReturning;
use Swoft\Bean\Annotation\Aspect;
use Swoft\Bean\Annotation\Before;
use Swoft\Bean\Annotation\PointBean;
use Swoft\Log\Log;

/**
 * the aspect of redis
 *
 * @Aspect()
 * @PointBean({
 *     RedisCache::class
 * })
 *
 * @uses      RedisAspect
 * @version   2018年01月03日
 * @author    stelin <phpcrazy@126.com>
 * @copyright Copyright 2010-2016 swoft software
 * @license   PHP Version 7.x {@link http://www.php.net/license/3_0.txt}
 */
class RedisAspect
{
    /**
     * the prefix of profile
     */
    const PROFILE_PREFIX = 'redis';

    /**
     * before of method
     *
     * @Before()
     * @param JoinPoint $joinPoint
     */
    public function before(JoinPoint $joinPoint)
    {
        $profileKey = $this->getProfileKey($joinPoint);
        Log::profileStart($profileKey);
    }

    /**
     * afterReturning of method
     *
     * @AfterReturning()
     * @param JoinPoint $joinPoint
     *
     * @return mixed
     */
    public function afterReturning(JoinPoint $joinPoint)
    {
        $profileKey = $this->getProfileKey($joinPoint);
        Log::profileEnd($profileKey);

        return $joinPoint->getReturn();
    }

    /**
     * the key of profile
     *
     * @param JoinPoint $joinPoint
     *
     * @return string
     */
    private function getProfileKey(JoinPoint $joinPoint)
    {
        $method = $joinPoint->getMethod();
        if ($method == '__call') {
            list($method) = $joinPoint->getArgs();
        }

        return self::PROFILE_PREFIX . '.' . $method;
    }
}


<?php

namespace Swoft\Redis\Operator\Hashes;

use Swoft\Redis\Operator\Command;

class HashGetMultiple extends Command
{
    /**
     * [Hash] hMGet
     *
     * @return string
     */
    public function getId()
    {
        return 'hMGet';
    }

    /**
     * {@inheritdoc}
     */
    public function parseResponse($data)
    {
        if ($data === false) {
            return false;
        }

        $result = [];
        $hashKeys = $this->getArgument(1);
        foreach ($data as $key => $value) {
            if (!isset($hashKeys[$key])) {
                continue;
            }

            $value = ($value === null) ? false : $value;
            $result[$hashKeys[$key]] = $value;
        }

        return $result;
    }

}


<?php
/**
 * This file is part of Swoft.
 *
 * @link     https://swoft.org
 * @document https://doc.swoft.org
 * @contact  group@swoft.org
 * @license  https://github.com/swoft-cloud/swoft/blob/master/LICENSE
 */

namespace Swoft\Redis;

use Swoft\App;
use Swoft\Redis\Exception\RedisException;
use Swoft\Redis\Profile\RedisCommandProvider;
use Swoole\Coroutine\Redis as CoRedis;
use Swoft\Redis\Pool\Config\RedisPoolConfig;

/**
 * Redis connection
 *
 * @method bool select($dbindex)
 */
class RedisConnection extends AbstractRedisConnection
{

    /**
     * Create connection
     *
     * @throws RedisException
     */
    public function createConnection()
    {
        $redis            = $this->initRedis();
        $this->connection = $redis;

        /** @var RedisPoolConfig $config */
        $config = $this->getPool()->getPoolConfig();
        $redis->select($config->getDb());
    }

    /**
     * @return mixed
     */
    public function receive()
    {
        $result = $this->connection->recv();
        $this->connection->setDefer(false);
        $this->recv = true;

        return $result;
    }

    /**
     * 设置延迟收包
     *
     * @param bool $defer
     */
    public function setDefer($defer = true)
    {
        $this->recv = false;
        $this->connection->setDefer($defer);
    }

    /**
     * @param string $method
     * @param array  $arguments
     *
     * @return mixed
     * @throws RedisException
     */
    public function __call($method, $arguments)
    {
        /* @var RedisCommandProvider $commandProvider */
        $commandProvider = App::getBean(RedisCommandProvider::class);
        $commandProvider->setPrefix($this->pool->getPoolConfig()->getPrefix());
        $command         = $commandProvider->createCommand($method, $arguments);
        $arguments       = $command->getArguments();
        $method          = $command->getId();

        $data = parent::__call($method, $arguments);
        return $command->parseResponse($data);
    }

    /**
     * @return CoRedis
     * @throws RedisException
     */
    protected function getConnectRedis(string $host, int $port, float $timeout): CoRedis
    {
        /* @var RedisPoolConfig $poolConfig */
        $poolConfig = $this->pool->getPoolConfig();
        $serialize  = $poolConfig->getSerialize();
        $serialize  = ((int)$serialize == 0) ? false : true;
        $redis      = new CoRedis(['timeout' => $timeout]);
        $result     = $redis->connect($host, $port, $serialize);
        if ($result === false) {
            $error = sprintf('Redis connection failure host=%s port=%d', $host, $port);
            throw new RedisException($error);
        }

        return $redis;
    }
}


<?php

namespace Swoft\Redis\Pool;

use Swoft\App;
use Swoft\Bean\Annotation\Inject;
use Swoft\Bean\Annotation\Pool;
use Swoft\Pool\ConnectionInterface;
use Swoft\Pool\ConnectionPool;
use Swoft\Redis\RedisConnection;
use Swoft\Redis\SyncRedisConnection;
use Swoft\Redis\Pool\Config\RedisPoolConfig;

/**
 * Redis pool
 *
 * @Pool()
 */
class RedisPool extends ConnectionPool
{
    /**
     * Config
     *
     * @Inject()
     * @var RedisPoolConfig
     */
    protected $poolConfig;

    /**
     * Create connection
     *
     * @return ConnectionInterface
     */
    public function createConnection(): ConnectionInterface
    {
        if (App::isCoContext()) {
            $redis = new RedisConnection($this);
        } else {
            $redis = new SyncRedisConnection($this);
        }

        return $redis;
    }
}


<?php

namespace Swoft\Redis\Operator\Strings;

use Swoft\Redis\Operator\Command;

class StringSetMultiple extends Command
{
    /**
     * [String] mSet
     *
     * @return string
     */
    public function getId()
    {
        return 'mSet';
    }

    /**
     * {@inheritdoc}
     */
    protected function filterArguments(array $arguments)
    {
        if (count($arguments) === 1 && is_array($arguments[0])) {
            $flattenedKVs = array();
            $args = $arguments[0];

            foreach ($args as $k => $v) {
                $flattenedKVs[$k] = $v;
            }

            return $flattenedKVs;
        }

        return $arguments;
    }
}


<?php
/**
 * This file is part of Swoft.
 *
 * @link     https://swoft.org
 * @document https://doc.swoft.org
 * @contact  group@swoft.org
 * @license  https://github.com/swoft-cloud/swoft/blob/master/LICENSE
 */

namespace Swoft\Redis;

use Swoft\Helper\PhpHelper;
use Swoft\Pool\AbstractConnection;
use Swoft\Redis\Exception\RedisException;

/**
 * Class AbstractRedisConnection
 *
 * @link https://wiki.swoole.com/wiki/page/590.html
 * @link https://github.com/phpredis/phpredis
 * @package Swoft\Redis
 *
 * set
 * @method int sAdd($key, $value1, $value2 = null, $valueN = null)
 * @method array|bool scan(&$iterator, $pattern = null, $count = 0)
 * @method int sCard($key)
 * @method array sDiff($key1, $key2, $keyN = null)
 * @method array sInter($key1, $key2, $keyN = null)
 * @method int sInterStore($dstKey, $key1, $key2, $keyN = null)
 * @method int sDiffStore($dstKey, $key1, $key2, $keyN = null)
 * @method array sMembers($key)
 * @method bool sMove($srcKey, $dstKey, $member)
 * @method bool sPop($key)
 * @method string|array sRandMember($key, $count = null)
 * @method int sRem($key, $member1, $member2 = null, $memberN = null)
 * @method array sUnion($key1, $key2, $keyN = null)
 * @method int sUnionStore($dstKey, $key1, $key2, $keyN = null)
 * sort
 * @method int zAdd($key, $score1, $value1, $score2 = null, $value2 = null, $scoreN = null, $valueN = null)
 * @method array zRange($key, $start, $end, $withscores = null)
 * @method int zRem($key, $member1, $member2 = null, $memberN = null)
 * @method array zRevRange($key, $start, $end, $withscore = null)
 * @method array zRangeByScore($key, $start, $end, array $options = array())
 * @method array zRangeByLex($key, $min, $max, $offset = null, $limit = null)
 * @method int zCount($key, $start, $end)
 * @method int zRemRangeByScore($key, $start, $end)
 * @method int zRemRangeByRank($key, $start, $end)
 * @method int zCard($key)
 * @method float zScore($key, $member)
 * @method int zRank($key, $member)
 * @method float zIncrBy($key, $value, $member)
 * @method int zUnion($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM')
 * @method int zInter($Output, $ZSetKeys, array $Weights = null, $aggregateFunction = 'SUM')
 * pub/sub
 * @method int publish($channel, $message)
 * @method string|array psubscribe($patterns, $callback)
 * @method string|array subscribe($channels, $callback)
 * @method array|int pubsub($keyword, $argument)
 * script
 * @method mixed eval($script, $args = array(), $numKeys = 0)
 * @method mixed evalSha($scriptSha, $args = array(), $numKeys = 0)
 * @method mixed script($command, $script)
 * @method string getLastError()
 * @method bool clearLastError()
 */
abstract class AbstractRedisConnection extends AbstractConnection
{
    /**
     * @var Redis
     */
    protected $connection;

    /**
     * @return \Redis|Redis
     * @throws RedisException
     */
    protected function initRedis()
    {
        $timeout = $this->pool->getTimeout();
        $address = $this->pool->getConnectionAddress();
        $config = $this->parseUri($address);

        $host = $config['host'];
        $port = (int)$config['port'];
        $redis = $this->getConnectRedis($host, $port, $timeout);
        if (isset($config['auth']) && false === $redis->auth($config['auth'])) {
            $error = sprintf('Redis connection authentication failed host=%s port=%d auth=%s', $host, (int)$port, (string)$config['auth']);
            throw new RedisException($error);
        }

        return $redis;
    }

    /**
     * @return $this
     */
    public function reconnect()
    {
        $this->createConnection();

        return $this;
    }

    /**
     * @return bool
     */
    public function check(): bool
    {
        try {
            if (false === $this->connection->ping()) {
                throw new \RuntimeException('Connection lost');
            }
            $connected = true;
        } catch (\Throwable $throwable) {
            $connected = false;
        }

        return $connected;
    }

    /**
     * @param string $uri
     *
     * @return array
     * @throws RedisException
     */
    protected function parseUri(string $uri): array
    {
        $parseAry = parse_url($uri);
        if (!isset($parseAry['host']) || !isset($parseAry['port'])) {
            $error = sprintf('Redis Connection format is incorrect uri=%s, eg:tcp://127.0.0.1:6379/1?auth=password', $uri);
            throw new RedisException($error);
        }
        isset($parseAry['path']) && $parseAry['database'] = str_replace('/', '', $parseAry['path']);
        $query = $parseAry['query'] ?? '';
        parse_str($query, $options);
        $configs = array_merge($parseAry, $options);
        unset($configs['path']);
        unset($configs['query']);

        return $configs;
    }

    /**
     * @param string $host
     * @param int $port
     * @param int $timeout
     *
     * @return Redis | \Redis
     */
    abstract protected function getConnectRedis(string $host, int $port, float $timeout);

    /**
     * @param string $method
     * @param array $arguments
     *
     * @return mixed
     */
    public function __call($method, $arguments)
    {
        return PhpHelper::call([$this->connection, $method], $arguments);
    }
}