<?php
namespace morphos\Russian;

use morphos\S;

/**
 * Rules are from: https://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%BB%D0%BE%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B3%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D1%85_%D0%BD%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B9_%D0%B2_%D1%80%D1%83%D1%81%D1%81%D0%BA%D0%BE%D0%BC_%D1%8F%D0%B7%D1%8B%D0%BA%D0%B5
 */
class GeographicalNamesInflection extends \morphos\BaseInflection implements Cases
{
    use RussianLanguage, CasesHelper;

    protected static $abbreviations = [
        'сша',
        'оаэ',
        'ссср',
        'юар',
    ];

    protected static $delimiters = [
        ' ',
        '-на-',
        '-',
    ];

    protected static $ovAbnormalExceptions = [
        'осташков',
    ];

    protected static $immutableParts = [
        'санкт',
        'йошкар',
        'улан',
    ];

    protected static $runawayVowelsExceptions = [
        'торжо*к',
        'волоче*к',
        'орё*л',
    ];

    protected static $misspellings = [
        'орел' => 'орёл',
    ];

    /**
     * @return array|bool
     */
    protected static function getRunAwayVowelsList()
    {
        $runawayVowelsNormalized = [];
        foreach (static::$runawayVowelsExceptions as $word) {
            $runawayVowelsNormalized[str_replace('*', null, $word)] = S::indexOf($word, '*') - 1;
        }
        return $runawayVowelsNormalized;
    }

    /**
     * Проверяет, склоняемо ли название
     * @param string $name Название
     * @return bool
     */
    public static function isMutable($name)
    {
        $name = S::lower($name);

        // // ends with 'ы' or 'и': plural form
        // if (in_array(S::slice($name, -1), array('и', 'ы')))
        //     return false;

        if (in_array($name, static::$abbreviations, true) || in_array($name, static::$immutableParts, true)) {
            return false;
        }

        if (strpos($name, ' ') !== false) {
            // explode() is not applicable because Geographical unit may have few words
            $first_part = S::slice($name, 0, S::findFirstPosition($name, ' '));
            $last_part = S::slice($name,
                S::findLastPosition($name, ' ') + 1);

            // город N, село N, хутор N, район N, поселок N, округ N, республика N
            // N область, N край
            if (in_array($first_part, ['город', 'село', 'хутор', 'район', 'поселок', 'округ', 'республика'], true)
                || in_array($last_part, ['край', 'область'], true)) {
                return true;
            }

            // пгт N
            if ($first_part === 'пгт')
                return false;
        }

        // ends with 'е' or 'о', but not with 'ово/ёво/ево/ино/ыно'
        if (in_array(S::slice($name, -1), ['е', 'о'], true) && !in_array(S::slice($name, -3, -1), ['ов', 'ёв', 'ев', 'ин', 'ын'], true)) {
            return false;
        }
        return true;
    }

    /**
     * Получение всех форм названия
     * @param string $name
     * @return array
     * @throws \Exception
     */
    public static function getCases($name)
    {
        $name = S::lower($name);

        // Проверка на неизменяемость и сложное название
        if (in_array($name, static::$immutableParts, true)) {
            return array_fill_keys([static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ], S::name($name));
        }

        if (strpos($name, ' ') !== false) {
            $first_part = S::slice($name, 0, S::findFirstPosition($name, ' '));
            // город N, село N, хутор N, пгт N
            if (in_array($first_part, ['город', 'село', 'хутор', 'пгт', 'район', 'поселок', 'округ', 'республика'], true)) {
                if ($first_part !== 'пгт')
                    return static::composeCasesFromWords([
                        $first_part !== 'республика'
                            ? NounDeclension::getCases($first_part)
                            : array_map(['\\morphos\\S', 'name'], NounDeclension::getCases($first_part)),
                        array_fill_keys(static::getAllCases(), S::name(S::slice($name, S::length($first_part) + 1)))
                    ]);
                else
                    return array_fill_keys([static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ], 'пгт '.S::name(S::slice($name, 4)));
            }

            $last_part = S::slice($name,
                S::findLastPosition($name, ' ') + 1);
            // N область, N край
            if (in_array($last_part, ['край', 'область'], true)) {
                return static::composeCasesFromWords([static::getCases(S::slice($name, 0, S::findLastPosition($name, ' '))), NounDeclension::getCases($last_part)]);
            }
        }

        // Сложное название через пробел, '-' или '-на-'
        foreach (static::$delimiters as $delimiter) {
            if (strpos($name, $delimiter) !== false) {
                $parts = explode($delimiter, $name);
                $result = [];
                foreach ($parts as $i => $part) {
                    $result[$i] = static::getCases($part);
                }
                return static::composeCasesFromWords($result, $delimiter);
            }
        }

        // Исправление ошибок
        if (array_key_exists($name, static::$misspellings)) {
            $name = static::$misspellings[$name];
        }

        // Само склонение
        if (!in_array($name, static::$abbreviations, true)) {
            switch (S::slice($name, -2)) {
                // Нижний, Русский
                case 'ий':
                    $prefix = S::name(S::slice($name, 0, -2));
                    return [
                        static::IMENIT => $prefix.'ий',
                        static::RODIT => $prefix.(static::isVelarConsonant(S::slice($name, -3, -2)) ? 'ого' : 'его'),
                        static::DAT => $prefix.(static::isVelarConsonant(S::slice($name, -3, -2)) ? 'ому' : 'ему'),
                        static::VINIT => $prefix.'ий',
                        static::TVORIT => $prefix.'им',
                        static::PREDLOJ => $prefix.(static::chooseEndingBySonority($prefix, 'ем', 'ом')),
                    ];

                // Ростовская
                case 'ая':
                    $prefix = S::name(S::slice($name, 0, -2));
                    return [
                        static::IMENIT => $prefix.'ая',
                        static::RODIT => $prefix.'ой',
                        static::DAT => $prefix.'ой',
                        static::VINIT => $prefix.'ую',
                        static::TVORIT => $prefix.'ой',
                        static::PREDLOJ => $prefix.'ой',
                    ];

                // Россошь
                case 'шь':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix.'ь',
                        static::RODIT => $prefix.'и',
                        static::DAT => $prefix.'и',
                        static::VINIT => $prefix.'ь',
                        static::TVORIT => $prefix.'ью',
                        static::PREDLOJ => $prefix.'и',
                    ];

                // Грозный, Благодарный
                case 'ый':
                    $prefix = S::name(S::slice($name, 0, -2));
                    return [
                        static::IMENIT => $prefix.'ый',
                        static::RODIT => $prefix.'ого',
                        static::DAT => $prefix.'ому',
                        static::VINIT => $prefix.'ый',
                        static::TVORIT => $prefix.'ым',
                        static::PREDLOJ => $prefix.'ом',
                    ];

                // Ставрополь, Ярославль, Электросталь
                case 'ль':
                    $prefix = S::name(S::slice($name, 0, -1));

                    if ($name === 'электросталь')
                        return [
                            static::IMENIT => $prefix.'ь',
                            static::RODIT => $prefix.'и',
                            static::DAT => $prefix.'и',
                            static::VINIT => $prefix.'ь',
                            static::TVORIT => $prefix.'ью',
                            static::PREDLOJ => $prefix.'и',
                        ];

                    return [
                        static::IMENIT => $prefix.'ь',
                        static::RODIT => $prefix.'я',
                        static::DAT => $prefix.'ю',
                        static::VINIT => $prefix.'ь',
                        static::TVORIT => $prefix.'ем',
                        static::PREDLOJ => $prefix.'е',
                    ];

                // Тверь, Анадырь
                case 'рь':
                    $prefix = S::name(S::slice($name, 0, -1));
                    $last_vowel = S::slice($prefix, -2, -1);
                    return [
                        static::IMENIT => $prefix . 'ь',
                        static::RODIT => $prefix . (static::isBinaryVowel($last_vowel) ? 'и' : 'я'),
                        static::DAT => $prefix . (static::isBinaryVowel($last_vowel) ? 'и' : 'ю'),
                        static::VINIT => $prefix . 'ь',
                        static::TVORIT => $prefix . (static::isBinaryVowel($last_vowel) ? 'ью' : 'ем'),
                        static::PREDLOJ => $prefix . (static::isBinaryVowel($last_vowel) ? 'и' : 'е'),
                    ];

                // Березники, Ессентуки
                case 'ки':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix . 'и',
                        static::RODIT => $name == 'луки' ? $prefix : $prefix . 'ов',
                        static::DAT => $prefix . 'ам',
                        static::VINIT => $prefix . 'и',
                        static::TVORIT => $prefix . 'ами',
                        static::PREDLOJ => $prefix . 'ах',
                    ];

                // Пермь, Кемь
                case 'мь':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix . 'ь',
                        static::RODIT => $prefix . 'и',
                        static::DAT => $prefix . 'и',
                        static::VINIT => $prefix . 'ь',
                        static::TVORIT => $prefix . 'ью',
                        static::PREDLOJ => $prefix . 'и',
                    ];

                // Рязань, Назрань
                case 'нь':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix . 'ь',
                        static::RODIT => $prefix . 'и',
                        static::DAT => $prefix . 'и',
                        static::VINIT => $prefix . 'ь',
                        static::TVORIT => $prefix . 'ью',
                        static::PREDLOJ => $prefix . 'и',
                    ];

                // Набережные
                case 'ые':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix . 'е',
                        static::RODIT => $prefix . 'х',
                        static::DAT => $prefix . 'м',
                        static::VINIT => $prefix . 'е',
                        static::TVORIT => $prefix . 'ми',
                        static::PREDLOJ => $prefix . 'х',
                    ];

                // Челны
                case 'ны':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix . 'ы',
                        static::RODIT => $prefix . 'ов',
                        static::DAT => $prefix . 'ам',
                        static::VINIT => $prefix . 'ы',
                        static::TVORIT => $prefix . 'ами',
                        static::PREDLOJ => $prefix . 'ах',
                    ];

                // Великие
                case 'ие':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix.'е',
                        static::RODIT => $prefix.'х',
                        static::DAT => $prefix.'м',
                        static::VINIT => $prefix.'е',
                        static::TVORIT => $prefix.'ми',
                        static::PREDLOJ => $prefix.'х',
                    ];

                // Керчь
                case 'чь':
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix.'ь',
                        static::RODIT => $prefix.'и',
                        static::DAT => $prefix.'и',
                        static::VINIT => $prefix.'ь',
                        static::TVORIT => $prefix.'ью',
                        static::PREDLOJ => $prefix.'и',
                    ];
            }

            switch (S::slice($name, -1)) {
                case 'р':
                    // Бор
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix.'р',
                        static::RODIT => $prefix.'ра',
                        static::DAT => $prefix.'ру',
                        static::VINIT => $prefix.'р',
                        static::TVORIT => $prefix.'ром',
                        static::PREDLOJ => $prefix.'ре',
                    ];

                case 'ы':
                    // Чебоксары, Шахты
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix.'ы',
                        static::RODIT => $prefix,
                        static::DAT => $prefix.'ам',
                        static::VINIT => $prefix.'ы',
                        static::TVORIT => $prefix.'ами',
                        static::PREDLOJ => $prefix.'ах',
                    ];

                case 'я':
                    // Азия
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => S::name($name),
                        static::RODIT => $prefix.'и',
                        static::DAT => $prefix.'и',
                        static::VINIT => $prefix.'ю',
                        static::TVORIT => $prefix.'ей',
                        static::PREDLOJ => $prefix.'и',
                    ];

                case 'а':
                    // Москва, Рига
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix.'а',
                        static::RODIT => $prefix.(static::isVelarConsonant(S::slice($name, -2, -1)) ? 'и' : 'ы'),
                        static::DAT => $prefix.'е',
                        static::VINIT => $prefix.'у',
                        static::TVORIT => $prefix.'ой',
                        static::PREDLOJ => $prefix.'е',
                    ];

                case 'й':
                    // Ишимбай
                    $prefix = S::name(S::slice($name, 0, -1));
                    return [
                        static::IMENIT => $prefix . 'й',
                        static::RODIT => $prefix . 'я',
                        static::DAT => $prefix . 'ю',
                        static::VINIT => $prefix . 'й',
                        static::TVORIT => $prefix . 'ем',
                        static::PREDLOJ => $prefix . 'е',
                    ];
            }

            if (static::isConsonant(S::slice($name,  -1)) && !in_array($name, static::$ovAbnormalExceptions, true)) {
                $runaway_vowels_list = static::getRunAwayVowelsList();

                // if run-away vowel in name
                if (isset($runaway_vowels_list[$name])) {
                    $runaway_vowel_offset = $runaway_vowels_list[$name];
                    $prefix = S::name(S::slice($name, 0, $runaway_vowel_offset) . S::slice($name, $runaway_vowel_offset + 1));
                } else {
                    $prefix = S::name($name);
                }

                // Париж, Валаам, Киев
                return [
                    static::IMENIT => S::name($name),
                    static::RODIT => $prefix . 'а',
                    static::DAT => $prefix . 'у',
                    static::VINIT => S::name($name),
                    static::TVORIT => $prefix . (static::isVelarConsonant(S::slice($name, -2, -1)) ? 'ем' : 'ом'),
                    static::PREDLOJ => $prefix . 'е',
                ];
            }

            // ов, ово, ёв, ёво, ев, ево, ...
            $suffixes = ['ов', 'ёв', 'ев', 'ин', 'ын'];
            if ((in_array(S::slice($name, -1), ['е', 'о'], true) && in_array(S::slice($name, -3, -1), $suffixes, true)) || in_array(S::slice($name, -2), $suffixes, true)) {
                // ово, ёво, ...
                if (in_array(S::slice($name, -3, -1), $suffixes, true)) {
                    $prefix = S::name(S::slice($name, 0, -1));
                }
                // ов, её, ...
                elseif (in_array(S::slice($name, -2), $suffixes, true)) {
                    $prefix = S::name($name);
                }
                return [
                    static::IMENIT => S::name($name),
                    static::RODIT => $prefix.'а',
                    static::DAT => $prefix.'у',
                    static::VINIT => S::name($name),
                    static::TVORIT => $prefix.'ым',
                    static::PREDLOJ => $prefix.'е',
                ];
            }
        }

        // if no rules matches or name is immutable
        $name = in_array($name, static::$abbreviations, true) ? S::upper($name) : S::name($name);
        return array_fill_keys([static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ], $name);
    }

    /**
     * Получение одной формы (падежа) названия.
     * @param string $name  Название
     * @param integer $case Падеж. Одна из констант \morphos\Russian\Cases или \morphos\Cases.
     * @see \morphos\Russian\Cases
     * @return string
     * @throws \Exception
     */
    public static function getCase($name, $case)
    {
        $case = static::canonizeCase($case);
        $forms = static::getCases($name);
        return $forms[$case];
    }
}


<?php
namespace morphos\Russian;

use morphos\NumeralGenerator;
use morphos\S;

/**
 * Rules are from http://www.fio.ru/pravila/grammatika/sklonenie-imen-chislitelnykh/
 *            and http://school-collection.edu.ru/dlrstore-wrapper/ebbc76cf-46b6-4d51-ad92-d9a84db35cfa/[I-RUS_06-05]_[TE_16-SU]/[I-RUS_06-05]_[TE_16-SU].html
 */
class OrdinalNumeralGenerator extends NumeralGenerator implements Cases
{
    use RussianLanguage, CasesHelper;

    protected static $words = [
        1 => 'первый',
        2 => 'второй',
        3 => 'третий',
        4 => 'четвертый',
        5 => 'пятый',
        6 => 'шестой',
        7 => 'седьмой',
        8 => 'восьмой',
        9 => 'девятый',
        10 => 'десятый',
        11 => 'одиннадцатый',
        12 => 'двенадцатый',
        13 => 'тринадцатый',
        14 => 'четырнадцатый',
        15 => 'пятнадцатый',
        16 => 'шестнадцатый',
        17 => 'семнадцатый',
        18 => 'восемнадцатый',
        19 => 'девятнадцатый',
        20 => 'двадцатый',
        30 => 'тридцатый',
        40 => 'сороковой',
        50 => 'пятьдесятый',
        60 => 'шестьдесятый',
        70 => 'семьдесятый',
        80 => 'восемьдесятый',
        90 => 'девяностый',
        100 => 'сотый',
        200 => 'двухсотый',
        300 => 'трехсотый',
        400 => 'четырехсотый',
        500 => 'пятисотый',
        600 => 'шестисотый',
        700 => 'семисотый',
        800 => 'восемисотый',
        900 => 'девятисотый',
    ];

    protected static $exponents = [
        '1000' => 'тысячный',
        '1000000' => 'миллионный',
        '1000000000' => 'миллиардный',
        '1000000000000' => 'триллионный',
    ];

    protected static $multipliers = [
        2 => 'двух',
        3 => 'трех',
        4 => 'четырех',
        5 => 'пяти',
        6 => 'шести',
        7 => 'седьми',
        8 => 'восьми',
        9 => 'девяти',
        10 => 'десяти',
        11 => 'одиннадцати',
        12 => 'двенадцати',
        13 => 'тринадцати',
        14 => 'четырнадцати',
        15 => 'пятнадцати',
        16 => 'шестнадцати',
        17 => 'семнадцати',
        18 => 'восемнадцати',
        19 => 'девятнадцати',
        20 => 'двадцати',
        30 => 'тридцати',
        40 => 'сорока',
        50 => 'пятьдесяти',
        60 => 'шестьдесяти',
        70 => 'семьдесяти',
        80 => 'восемьдесяти',
        90 => 'девяности',
        100 => 'сто',
        200 => 'двухста',
        300 => 'трехста',
        400 => 'четырехста',
        500 => 'пятиста',
        600 => 'шестиста',
        700 => 'семиста',
        800 => 'восемиста',
        900 => 'девятиста',
    ];

    /**
     * @param        $number
     * @param string $gender
     *
     * @return array
     * @throws \Exception
     */
    public static function getCases($number, $gender = self::MALE)
    {
        // simple numeral
        if (isset(static::$words[$number]) || isset(static::$exponents[$number])) {
            $word = isset(static::$words[$number]) ? static::$words[$number] : static::$exponents[$number];
            // special rules for 3
            if ($number == 3) {
                $prefix = S::slice($word, 0, -2);
                return [
                    static::IMENIT => $prefix.($gender == static::MALE ? 'ий' : ($gender == static::FEMALE ? 'ья' : 'ье')),
                    static::RODIT => $prefix.($gender == static::FEMALE ? 'ьей' : 'ьего'),
                    static::DAT => $prefix.($gender == static::FEMALE ? 'ьей' : 'ьему'),
                    static::VINIT => $prefix.($gender == static::FEMALE ? 'ью' : 'ьего'),
                    static::TVORIT => $prefix.($gender == static::FEMALE ? 'ьей' : 'ьим'),
                    static::PREDLOJ => $prefix.($gender == static::FEMALE ? 'ьей' : 'ьем'),
                ];
            } else {
                switch ($gender) {
                    case static::MALE:
                        $prefix = S::slice($word, 0, $number == 40 ? -1 : -2);
                        return [
                            static::IMENIT => $word,
                            static::RODIT => $prefix.'ого',
                            static::DAT => $prefix.'ому',
                            static::VINIT => $word,
                            static::TVORIT => $prefix.'ым',
                            static::PREDLOJ => $prefix.'ом',
                        ];

                    case static::FEMALE:
                        $prefix = S::slice($word, 0, $number == 40 ? -1 : -2);
                        return [
                            static::IMENIT => $prefix.'ая',
                            static::RODIT => $prefix.'ой',
                            static::DAT => $prefix.'ой',
                            static::VINIT => $prefix.'ую',
                            static::TVORIT => $prefix.'ой',
                            static::PREDLOJ => $prefix.'ой',
                        ];

                    case static::NEUTER:
                        $prefix = S::slice($word, 0, $number == 40 ? -1 : -2);
                        return [
                            static::IMENIT => $prefix.'ое',
                            static::RODIT => $prefix.'ого',
                            static::DAT => $prefix.'ому',
                            static::VINIT => $prefix.'ое',
                            static::TVORIT => $prefix.'ым',
                            static::PREDLOJ => $prefix.'ом',
                        ];
                }
            }
        }
        // compound numeral
        else {
            $ordinal_part = null;
            $ordinal_prefix = null;
            $result = [];

            // test for exponents. If smaller summand of number is an exponent, declinate it
            foreach (array_reverse(static::$exponents, true) as $word_number => $word) {
                if ($number >= $word_number && ($number % $word_number) == 0) {
                    $count = floor($number / $word_number) % 1000;
                    $number -= ($count * $word_number);
                    foreach (array_reverse(static::$multipliers, true) as $multiplier => $multipliers_word) {
                        if ($count >= $multiplier) {
                            $ordinal_prefix .= $multipliers_word;
                            $count -= $multiplier;
                        }
                    }
                    $ordinal_part = static::getCases($word_number, $gender);
                    foreach ($ordinal_part as $case => $ordinal_word) {
                        $ordinal_part[$case] = $ordinal_prefix.$ordinal_part[$case];
                    }

                    break;
                }
            }

            // otherwise, test if smaller summand is just a number with it's own name
            if (empty($ordinal_part)) {
                // get the smallest number with it's own name
                foreach (static::$words as $word_number => $word) {
                    if ($number >= $word_number) {
                        if ($word_number <= 9) {
                            if ($number % 10 == 0) {
                                continue;
                            }
                            // check for case when word_number smaller than should be used (e.g. 1,2,3 when it can be 4 (number: 344))
                            if (($number % 10) > $word_number) {
                                continue;
                            }
                            // check that there is no two-digits number with it's own name (e.g. 13 for 113)
                            if (isset(static::$words[$number % 100]) && $number % 100 > $word_number) {
                                continue;
                            }
                        } elseif ($word_number <= 90) {
                            // check for case when word_number smaller than should be used (e.g. 10, 11, 12 when it can be 13)
                            if (($number % 100) > $word_number) {
                                continue;
                            }
                        }
                        $ordinal_part = static::getCases($word_number, $gender);
                        $number -= $word_number;
                        break;
                    }
                }
            }

            // if number has second summand, get cardinal form of it
            if ($number > 0) {
                $cardinal_part = CardinalNumeralGenerator::getCase($number, static::IMENIT, $gender);

                // make one array with cases and delete 'o/об' prepositional from all parts except the last one
                foreach ([static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ] as $case) {
                    $result[$case] = $cardinal_part.' '.$ordinal_part[$case];
                }
            } else {
                $result = $ordinal_part;
            }

            return $result;
        }
    }

    /**
     * @param $number
     * @param $case
     * @param string $gender
     * @return mixed
     * @throws \Exception
     */
    public static function getCase($number, $case, $gender = self::MALE)
    {
        $case = static::canonizeCase($case);
        $forms = static::getCases($number, $gender);
        return $forms[$case];
    }
}


<?php
namespace morphos;

/**
 * Multibyte string helper
 */
class S
{
    /** @var string Encoding used for string manipulations */
    static protected $encoding;

    static protected $cyrillicAlphabet = [
        ['Ё', 'Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж', 'Э', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю'],
        ['ё', 'й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю'],
    ];

    /**
     * Sets encoding for all operations
     * @param string $encoding
     * @return bool
     */
    public static function setEncoding($encoding)
    {
        static::$encoding = $encoding;
    }

    /**
     * Returns encoding used for all operations
     * @return string
     */
    public static function getEncoding()
    {
        return static::$encoding ?: 'utf-8';
    }

    /**
     * Calculates count of characters in string.
     * @param $string
     * @return bool|int
     */
    public static function length($string)
    {
        if (function_exists('mb_strlen')) {
            return mb_strlen($string, static::getEncoding());
        }

        if (function_exists('iconv_strlen')) {
            return iconv_strlen($string, static::getEncoding());
        }

        return false;
    }

    /**
     * Slices string like python.
     * @param string $string
     * @param int $start
     * @param int|null $end
     * @return bool|string
     */
    public static function slice($string, $start, $end = null)
    {
        if ($end !== null) {
            $end -= $start;
        }

        if (function_exists('mb_substr')) {
            return mb_substr($string, $start, $end, static::getEncoding());
        }

        if (function_exists('iconv_substr')) {
            return iconv_substr($string, $start, $end ?: iconv_strlen($string), static::getEncoding());
        }

        return false;
    }

    /**
     * Lower case.
     * @param $string
     * @return bool|string
     */
    public static function lower($string)
    {
        if (function_exists('mb_strtolower')) {
            return mb_strtolower($string, static::getEncoding());
        }

        return static::replaceByMap($string, static::$cyrillicAlphabet[0], static::$cyrillicAlphabet[1]);
    }

    /**
     * Upper case.
     * @param $string
     * @return bool|string
     */
    public static function upper($string)
    {
        if (function_exists('mb_strtoupper')) {
            return mb_strtoupper($string, static::getEncoding());
        }

        return static::replaceByMap($string, static::$cyrillicAlphabet[1], static::$cyrillicAlphabet[0]);
    }

    /**
     * Name case. (ex: Thomas, Lewis). Works properly with separated by '-' words
     * @param $string
     * @return bool|string
     */
    public static function name($string)
    {
        if (strpos($string, '-') !== false) {
            return implode('-', array_map([__CLASS__, __FUNCTION__], explode('-', $string)));
        }

        return static::upper(static::slice($string, 0, 1)).static::lower(static::slice($string, 1));
    }

    /**
     * multiple substr_count().
     * @param $string
     * @param array $chars
     * @return bool|int
     */
    public static function countChars($string, array $chars)
    {
        if (function_exists('mb_split')) {
            return count(mb_split('('.implode('|', $chars).')', $string)) - 1;
        }

        $counter = 0;
        foreach ($chars as $char) {
            $counter += substr_count($string, $char);
        }
        return $counter;
    }

    /**
     * @param string $string
     * @param string $char
     * @return bool|string
     */
    public static function findFirstPosition($string, $char)
    {
        if (function_exists('mb_strpos')) {
            return mb_strpos($string, $char, 0, static::getEncoding());
        }

        return strpos($string, $char);
    }

    /**
     * @param string $string
     * @param string $char
     * @return bool|string
     */
    public static function findLastPosition($string, $char)
    {
        if (function_exists('mb_strrpos')) {
            return mb_strrpos($string, $char, 0, static::getEncoding());
        }

        return strrpos($string, $char);
    }

    /**
     * @param $string
     * @param array $chars
     * @return bool|string
     */
    public static function findLastPositionForOneOfChars($string, array $chars)
    {
        if (function_exists('mb_strrpos')) {
            $last_position = false;
            foreach ($chars as $char) {
                if (($pos = mb_strrpos($string, $char, 0, static::getEncoding())) !== false) {
                    if ($pos > $last_position) {
                        $last_position = $pos;
                    }
                }
            }
            if ($last_position !== false) {
                return mb_substr($string, $last_position, null, static::getEncoding());
            }
            return false;
        }

        return false;
    }

    /**
     * @param $string
     * @param $substring
     * @param bool $caseSensetive
     * @param int $startOffset
     * @return string|false
     */
    public static function indexOf($string, $substring, $caseSensetive = false, $startOffset = 0)
    {
        if (function_exists('mb_stripos')) {
            return $caseSensetive
                ? mb_strpos($string, $substring, $startOffset, static::getEncoding())
                : mb_stripos($string, $substring, $startOffset, static::getEncoding());
        }

        return false;
    }

    /**
     * @param $string
     * @param $fromMap
     * @param $toMap
     * @return string
     */
    private static function replaceByMap($string, $fromMap, $toMap)
    {
        $encoding = static::getEncoding();
        if ($encoding !== 'utf-8')
            $string = iconv($encoding, 'utf-8', $string);

        $string = strtr($string, array_combine($fromMap, $toMap));

        if ($encoding !== 'utf-8')
            $string = iconv('utf-8', $encoding, $string);

        return $string;
    }
}


<?php
namespace morphos\Russian;

use morphos\Gender;
use morphos\S;

/**
 * Rules are from http://morpher.ru/Russian/Noun.aspx
 */
class NounDeclension extends \morphos\BaseInflection implements Cases, Gender
{
    use RussianLanguage, CasesHelper;

    const FIRST_DECLENSION = 1;
    const SECOND_DECLENSION = 2;
    const THIRD_DECLENSION = 3;

    /**
     * These words has 2 declension type.
     */
    protected static $abnormalExceptions = [
        'бремя',
        'вымя',
        'темя',
        'пламя',
        'стремя',
        'пламя',
        'время',
        'знамя',
        'имя',
        'племя',
        'семя',
        'путь' => ['путь', 'пути', 'пути', 'путь', 'путем', 'пути'],
        'дитя' => ['дитя', 'дитяти', 'дитяти', 'дитя', 'дитятей', 'дитяти'],
    ];

    protected static $masculineWithSoft = [
        'ячмень',
        'путь',
        'шкворень',
        'пельмень',
        'табель',
        'рояль',
        'шампунь',
        'гвоздь',
        'рубль',
        'дождь',
        'зверь',
        'юань',
        'олень',
        'конь',
        'конь',
        'лось',
        'тюлень',
        'выхухоль',
        'медведь',
        'председатель',
        'руководитель',
        'заместитель',
    ];

    protected static $masculineWithSoftAndRunAwayVowels = [
        'день',
        'пень',
        'парень',
        'камень',
        'корень',
        'трутень',
    ];

    protected static $immutableWords = [
        // валюты
        'евро', 'пенни', 'песо', 'сентаво',

        // на а
        'боа', 'бра', 'фейхоа', 'амплуа', 'буржуа',
        // на о
        'манго', 'какао', 'кино', 'трюмо', 'пальто', 'бюро', 'танго', 'вето', 'бунгало', 'сабо', 'авокадо', 'депо',
        // на у
        'зебу', 'кенгуру', 'рагу', 'какаду', 'шоу',
        // на е
        'шимпанзе', 'конферансье', 'атташе', 'колье', 'резюме', 'пенсне', 'кашне', 'протеже', 'коммюнике', 'драже', 'суфле', 'пюре', 'купе', 'фойе', 'шоссе',
        // на и
        'такси', 'жалюзи', 'шасси', 'алиби', 'киви', 'иваси', 'регби', 'конфетти', 'колибри', 'жюри', 'пенальти', 'рефери', 'кольраби',
        // на э
        'каноэ', 'алоэ',
        // на ю
        'меню', 'парвеню', 'авеню', 'дежавю', 'инженю', 'барбекю', 'интервью',
    ];

    /**
     * Проверка, изменяемое ли слово.
     * @param string $word Слово для проверки
     * @param bool $animateness Признак одушевленности
     * @return bool
     */
    public static function isMutable($word, $animateness = false)
    {
        $word = S::lower($word);
        if (in_array(S::slice($word, -1), ['у', 'и', 'е', 'о', 'ю'], true) || in_array($word, static::$immutableWords, true)) {
            return false;
        }
        return true;
    }

    /**
     * Определение рода существительного.
     * @param string $word
     * @return string
     */
    public static function detectGender($word)
    {
    	$word = S::lower($word);
    	$last = S::slice($word, -1);
		// пытаемся угадать род объекта, хотя бы примерно, чтобы правильно склонять
		if (S::slice($word, -2) == 'мя' || in_array($last, ['о', 'е', 'и', 'у'], true))
			return static::NEUTER;

		if (in_array($last, ['а', 'я'], true) ||
			($last == 'ь' && !in_array($word, static::$masculineWithSoft, true) && !in_array($word, static::$masculineWithSoftAndRunAwayVowels, true)))
			return static::FEMALE;

		return static::MALE;
    }

    /**
     * Определение склонения (по школьной программе) существительного.
     * @param $word
     * @param bool $animateness
     * @return int
     */
    public static function getDeclension($word, $animateness = false)
    {
        $word = S::lower($word);
        $last = S::slice($word, -1);
        if (isset(static::$abnormalExceptions[$word]) || in_array($word, static::$abnormalExceptions, true)) {
            return 2;
        }

        if (in_array($last, ['а', 'я'], true) && S::slice($word, -2) != 'мя') {
            return 1;
        } elseif (static::isConsonant($last) || in_array($last, ['о', 'е', 'ё'], true)
            || ($last == 'ь' && static::isConsonant(S::slice($word, -2, -1)) && !static::isHissingConsonant(S::slice($word, -2, -1))
                && (in_array($word, static::$masculineWithSoft, true)) || in_array($word, static::$masculineWithSoftAndRunAwayVowels, true))) {
            return 2;
        } else {
            return 3;
        }
    }

    /**
     * Получение слова во всех 6 падежах.
     * @param string $word
     * @param bool $animateness Признак одушевлённости
     * @return array
     */
    public static function getCases($word, $animateness = false)
    {
        $word = S::lower($word);

        // Адъективное склонение (Сущ, образованные от прилагательных и причастий) - прохожий, существительное
        if (static::isAdjectiveNoun($word)) {
            return static::declinateAdjective($word, $animateness);
        }

        // Субстантивное склонение (существительные)
        if (in_array($word, static::$immutableWords, true)) {
            return [
                static::IMENIT => $word,
                static::RODIT => $word,
                static::DAT => $word,
                static::VINIT => $word,
                static::TVORIT => $word,
                static::PREDLOJ => $word,
            ];
        } elseif (isset(static::$abnormalExceptions[$word])) {
            return array_combine([static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ], static::$abnormalExceptions[$word]);
        } elseif (in_array($word, static::$abnormalExceptions, true)) {
            $prefix = S::slice($word, 0, -1);
            return [
                static::IMENIT => $word,
                static::RODIT => $prefix.'ени',
                static::DAT => $prefix.'ени',
                static::VINIT => $word,
                static::TVORIT => $prefix.'енем',
                static::PREDLOJ => $prefix.'ени',
            ];
        }

        switch (static::getDeclension($word)) {
            case static::FIRST_DECLENSION:
                return static::declinateFirstDeclension($word);
            case static::SECOND_DECLENSION:
                return static::declinateSecondDeclension($word, $animateness);
            case static::THIRD_DECLENSION:
                return static::declinateThirdDeclension($word);
        }
    }

    /**
     * Получение всех форм слова первого склонения.
     * @param $word
     * @return array
     */
    public static function declinateFirstDeclension($word)
    {
        $word = S::lower($word);
        $prefix = S::slice($word, 0, -1);
        $last = S::slice($word, -1);
        $soft_last = static::checkLastConsonantSoftness($word);
        $forms =  [
            Cases::IMENIT => $word,
        ];

        // RODIT
        $forms[Cases::RODIT] = static::chooseVowelAfterConsonant($last, $soft_last || (in_array(S::slice($word, -2, -1), ['г', 'к', 'х'], true)), $prefix.'и', $prefix.'ы');

        // DAT
        $forms[Cases::DAT] = static::getPredCaseOf12Declensions($word, $last, $prefix);

        // VINIT
        $forms[Cases::VINIT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ю', $prefix.'у');

        // TVORIT
        if ($last == 'ь') {
            $forms[Cases::TVORIT] = $prefix.'ой';
        } else {
            $forms[Cases::TVORIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'ей', $prefix.'ой');
        }

        // 	if ($last == 'й' || (static::isConsonant($last) && !static::isHissingConsonant($last)) || static::checkLastConsonantSoftness($word))
        // 	$forms[Cases::TVORIT] = $prefix.'ей';
        // else
        // 	$forms[Cases::TVORIT] = $prefix.'ой'; # http://morpher.ru/Russian/Spelling.aspx#sibilant

        // PREDLOJ the same as DAT
        $forms[Cases::PREDLOJ] = $forms[Cases::DAT];
        return $forms;
    }

    /**
     * Получение всех форм слова второго склонения.
     * @param $word
     * @param bool $animateness
     * @return array
     */
    public static function declinateSecondDeclension($word, $animateness = false)
    {
        $word = S::lower($word);
        $last = S::slice($word, -1);
        $soft_last = $last == 'й' || (in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true)
            && ((
                static::isConsonant(S::slice($word, -2, -1)) && !static::isHissingConsonant(S::slice($word, -2, -1)))
                    || S::slice($word, -2, -1) == 'и'));
        $prefix = static::getPrefixOfSecondDeclension($word, $last);
        $forms =  [
            Cases::IMENIT => $word,
        ];

        // RODIT
        $forms[Cases::RODIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'я', $prefix.'а');

        // DAT
        $forms[Cases::DAT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'ю', $prefix.'у');

        // VINIT
        if (in_array($last, ['о', 'е', 'ё'], true)) {
            $forms[Cases::VINIT] = $word;
        } else {
            $forms[Cases::VINIT] = static::getVinitCaseByAnimateness($forms, $animateness);
        }

        // TVORIT
        // if ($last == 'ь')
        // 	$forms[Cases::TVORIT] = $prefix.'ом';
        // else if ($last == 'й' || (static::isConsonant($last) && !static::isHissingConsonant($last)))
        // 	$forms[Cases::TVORIT] = $prefix.'ем';
        // else
        // 	$forms[Cases::TVORIT] = $prefix.'ом'; # http://morpher.ru/Russian/Spelling.aspx#sibilant
        if (static::isHissingConsonant($last) || (in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true) && static::isHissingConsonant(S::slice($word, -2, -1))) || $last == 'ц') {
            $forms[Cases::TVORIT] = $prefix.'ем';
        } elseif (in_array($last, ['й'/*, 'ч', 'щ'*/], true) || $soft_last) {
            $forms[Cases::TVORIT] = $prefix.'ем';
        } else {
            $forms[Cases::TVORIT] = $prefix.'ом';
        }

        // PREDLOJ
        $forms[Cases::PREDLOJ] = static::getPredCaseOf12Declensions($word, $last, $prefix);

        return $forms;
    }

    /**
     * Получение всех форм слова третьего склонения.
     * @param $word
     * @return array
     */
    public static function declinateThirdDeclension($word)
    {
        $word = S::lower($word);
        $prefix = S::slice($word, 0, -1);
        return [
            Cases::IMENIT => $word,
            Cases::RODIT => $prefix.'и',
            Cases::DAT => $prefix.'и',
            Cases::VINIT => $word,
            Cases::TVORIT => $prefix.'ью',
            Cases::PREDLOJ => $prefix.'и',
        ];
    }

    /**
     * Склонение существительных, образованных от прилагательных и причастий.
     * Rules are from http://rusgram.narod.ru/1216-1231.html
     * @param $word
     * @param $animateness
     * @return array
     */
    public static function declinateAdjective($word, $animateness)
    {
        $prefix = S::slice($word, 0, -2);

        switch (S::slice($word, -2)) {
            // Male adjectives
            case 'ой':
            case 'ый':
                return [
                    Cases::IMENIT => $word,
                    Cases::RODIT => $prefix.'ого',
                    Cases::DAT => $prefix.'ому',
                    Cases::VINIT => $word,
                    Cases::TVORIT => $prefix.'ым',
                    Cases::PREDLOJ => $prefix.'ом',
                ];

            case 'ий':
                return [
                    Cases::IMENIT => $word,
                    Cases::RODIT => $prefix.'его',
                    Cases::DAT => $prefix.'ему',
                    Cases::VINIT => $prefix.'его',
                    Cases::TVORIT => $prefix.'им',
                    Cases::PREDLOJ => $prefix.'ем',
                ];

            // Neuter adjectives
            case 'ое':
            case 'ее':
                $prefix = S::slice($word, 0, -1);
                return [
                    Cases::IMENIT => $word,
                    Cases::RODIT => $prefix.'го',
                    Cases::DAT => $prefix.'му',
                    Cases::VINIT => $word,
                    Cases::TVORIT => S::slice($word, 0, -2).(S::slice($word, -2, -1) == 'о' ? 'ы' : 'и').'м',
                    Cases::PREDLOJ => $prefix.'м',
                ];

            // Female adjectives
            case 'ая':
                $ending = static::isHissingConsonant(S::slice($prefix, -1)) ? 'ей' : 'ой';
                return [
                    Cases::IMENIT => $word,
                    Cases::RODIT => $prefix.$ending,
                    Cases::DAT => $prefix.$ending,
                    Cases::VINIT => $prefix.'ую',
                    Cases::TVORIT => $prefix.$ending,
                    Cases::PREDLOJ => $prefix.$ending,
                ];
        }
    }

    /**
     * Получение одной формы слова (падежа).
     * @param string $word Слово
     * @param integer $case Падеж
     * @param bool $animateness Признак одушевленности
     * @return string
     * @throws \Exception
     */
    public static function getCase($word, $case, $animateness = false)
    {
        $case = static::canonizeCase($case);
        $forms = static::getCases($word, $animateness);
        return $forms[$case];
    }

    /**
     * @param $word
     * @param $last
     * @return bool
     */
    public static function getPrefixOfSecondDeclension($word, $last)
    {
        // слова с бегающей гласной в корне
        if (in_array($word, static::$masculineWithSoftAndRunAwayVowels, true)) {
            $prefix = S::slice($word, 0, -3).S::slice($word, -2, -1);
        } elseif (in_array($last, ['о', 'е', 'ё', 'ь', 'й'], true)) {
            $prefix = S::slice($word, 0, -1);
        }
        // уменьшительные формы слов (котенок) и слова с суффиксом ок
        elseif (S::slice($word, -2) == 'ок' && S::length($word) > 3) {
            $prefix = S::slice($word, 0, -2).'к';
        } else {
            $prefix = $word;
        }
        return $prefix;
    }

    /**
     * @param array $forms
     * @param $animate
     * @return mixed
     */
    public static function getVinitCaseByAnimateness(array $forms, $animate)
    {
        if ($animate) {
            return $forms[Cases::RODIT];
        } else {
            return $forms[Cases::IMENIT];
        }
    }

    /**
     * @param $word
     * @param $last
     * @param $prefix
     * @return string
     */
    public static function getPredCaseOf12Declensions($word, $last, $prefix)
    {
        if (in_array(S::slice($word, -2), ['ий', 'ие'], true)) {
            if ($last == 'ё') {
                return $prefix.'е';
            } else {
                return $prefix.'и';
            }
        } else {
            return $prefix.'е';
        }
    }
}


<?php
namespace morphos\Russian;

use morphos\S;

/**
 * Rules are from http://morpher.ru/Russian/Noun.aspx
 */
class NounPluralization extends \morphos\NounPluralization implements Cases
{
    use RussianLanguage, CasesHelper;

    const ONE = 1;
    const TWO_FOUR = 2;
    const FIVE_OTHER = 3;

    protected static $abnormalExceptions = [
        'человек' => ['люди', 'человек', 'людям', 'людей', 'людьми', 'людях'],
    ];

    protected static $neuterExceptions = [
        'поле',
        'море',
    ];

    protected static $genitiveExceptions = [
        'письмо' => 'писем',
        'пятно' => 'пятен',
        'кресло' => 'кресел',
        'коромысло' => 'коромысел',
        'ядро' => 'ядер',
        'блюдце' => 'блюдец',
        'полотенце' => 'полотенец',
        'гривна' => 'гривен',
        'год' => 'лет',
    ];

    protected static $immutableWords = [
        'евро',
        'пенни',
    ];

    protected static $runawayVowelsExceptions = [
        'писе*ц',
        'песе*ц',
        'глото*к',
    ];

    /**
     * @return array|bool
     */
    protected static function getRunAwayVowelsList()
    {
        $runawayVowelsNormalized = [];
        foreach (static::$runawayVowelsExceptions as $word) {
            $runawayVowelsNormalized[str_replace('*', null, $word)] = S::indexOf($word, '*') - 1;
        }
        return $runawayVowelsNormalized;
    }

    /**
     * Склонение существительного для сочетания с числом (кол-вом предметов).
     *
     * @param string|int $word        Название предмета
     * @param int|string $count       Количество предметов
     * @param bool       $animateness Признак одушевленности
     * @param string     $case        Род существительного
     *
     * @return string
     * @throws \Exception
     */
    public static function pluralize($word, $count = 2, $animateness = false, $case = null)
    {
        // меняем местами аргументы, если они переданы в старом формате
        if (is_string($count) && is_numeric($word)) {
            list($count, $word) = [$word, $count];
        }

        if ($case !== null)
            $case = static::canonizeCase($case);

        // для адъективных существительных правила склонения проще:
        // только две формы
        if (static::isAdjectiveNoun($word)) {
            if (static::getNumeralForm($count) == static::ONE)
                return $word;
            else
                return NounPluralization::getCase($word,
                    $case !== null
                        ? $case
                        : static::RODIT, $animateness);
        }

        if ($case === null) {
            switch (static::getNumeralForm($count)) {
                case static::ONE:
                    return $word;
                case static::TWO_FOUR:
                    return NounDeclension::getCase($word, static::RODIT, $animateness);
                case static::FIVE_OTHER:
                    return NounPluralization::getCase($word, static::RODIT, $animateness);
            }
        }

        if (static::getNumeralForm($count) == static::ONE)
            return NounDeclension::getCase($word, $case, $animateness);
        else
            return NounPluralization::getCase($word, $case, $animateness);
    }

    /**
     * @param $count
     * @return int
     */
    public static function getNumeralForm($count)
    {
        if ($count > 100) {
            $count %= 100;
        }
        $ending = $count % 10;

        if (($count > 20 && $ending == 1) || $count == 1) {
            return static::ONE;
        } elseif (($count > 20 && in_array($ending, range(2, 4))) || in_array($count, range(2, 4))) {
            return static::TWO_FOUR;
        } else {
            return static::FIVE_OTHER;
        }
    }

    /**
     * @param $word
     * @param $case
     * @param bool $animateness
     * @return string
     * @throws \Exception
     */
    public static function getCase($word, $case, $animateness = false)
    {
        $case = static::canonizeCase($case);
        $forms = static::getCases($word, $animateness);
        return $forms[$case];
    }

    /**
     * @param $word
     * @param bool $animateness
     * @return array
     */
    public static function getCases($word, $animateness = false)
    {
        $word = S::lower($word);

        if (in_array($word, static::$immutableWords, true)) {
            return [
                static::IMENIT => $word,
                static::RODIT => $word,
                static::DAT => $word,
                static::VINIT => $word,
                static::TVORIT => $word,
                static::PREDLOJ => $word,
            ];
        }

        if (isset(static::$abnormalExceptions[$word])) {
            return array_combine(
                [static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ],
                static::$abnormalExceptions[$word]);
        }

        // Адъективное склонение (Сущ, образованные от прилагательных и причастий)
        // Пример: прохожий, существительное
        if (static::isAdjectiveNoun($word)) {
            return static::declinateAdjective($word, $animateness);
        }

        // Субстантивное склонение (существительные)
        return static::declinateSubstative($word, $animateness);
    }

    /**
     * Склонение обычных существительных.
     * @param $word
     * @param $animateness
     * @return array
     */
    protected static function declinateSubstative($word, $animateness)
    {
        $prefix = S::slice($word, 0, -1);
        $last = S::slice($word, -1);

        $runaway_vowels_list = static::getRunAwayVowelsList();
        if (isset($runaway_vowels_list[$word])) {
            $vowel_offset = $runaway_vowels_list[$word];
            $word = S::slice($word, 0, $vowel_offset) . S::slice($word, $vowel_offset + 1);
        }

        if (($declension = NounDeclension::getDeclension($word)) == NounDeclension::SECOND_DECLENSION) {
            $soft_last = $last == 'й' || (in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true)
                    && ((
                        static::isConsonant(S::slice($word, -2, -1)) && !static::isHissingConsonant(S::slice($word, -2, -1)))
                        || S::slice($word, -2, -1) == 'и'));
            $prefix = NounDeclension::getPrefixOfSecondDeclension($word, $last);
        } elseif ($declension == NounDeclension::FIRST_DECLENSION) {
            $soft_last = static::checkLastConsonantSoftness($word);
        } else {
            $soft_last = in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь'], true);
        }

        $forms = [];

        if (in_array($last, ['ч', 'г'], false) || in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь'], true)
            || (static::isVowel($last) && in_array(S::slice($word, -2, -1), ['ч', 'к'], true))) { // before ч, чь, сь, ч+vowel, к+vowel
            $forms[Cases::IMENIT] = $prefix.'и';
        } elseif (in_array($last, ['н', 'ц', 'р', 'т'], true)) {
            $forms[Cases::IMENIT] = $prefix.'ы';
        } else {
            $forms[Cases::IMENIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'я', $prefix.'а');
        }

        // RODIT
        if (isset(static::$genitiveExceptions[$word])) {
            $forms[Cases::RODIT] = static::$genitiveExceptions[$word];
        } elseif (in_array($last, ['о', 'е'], true)) {
            // exceptions
            if (in_array($word, static::$neuterExceptions, true)) {
                $forms[Cases::RODIT] = $prefix.'ей';
            } elseif (S::slice($word, -2, -1) == 'и') {
                $forms[Cases::RODIT] = $prefix.'й';
            } else {
                $forms[Cases::RODIT] = $prefix;
            }
        } elseif (S::slice($word, -2) == 'ка' && S::slice($word, -3, -2) !== 'и') { // words ending with -ка: чашка, вилка, ложка, тарелка, копейка, батарейка
            if (S::slice($word, -3, -2) == 'л') {
                $forms[Cases::RODIT] = S::slice($word, 0, -2).'ок';
            } elseif (S::slice($word, -3, -2) == 'й') {
                $forms[Cases::RODIT] = S::slice($word, 0, -3).'ек';
            } else {
                $forms[Cases::RODIT] = S::slice($word, 0, -2).'ек';
            }
        } elseif (in_array($last, ['а'], true)) { // обида, ябеда
            $forms[Cases::RODIT] = $prefix;
        } elseif (in_array($last, ['я'], true)) { // молния
            $forms[Cases::RODIT] = $prefix.'й';
        } elseif (RussianLanguage::isHissingConsonant($last) || ($soft_last && $last != 'й') || in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь'], true)) {
            $forms[Cases::RODIT] = $prefix.'ей';
        } elseif ($last == 'й' || S::slice($word, -2) == 'яц') { // месяц
            $forms[Cases::RODIT] = $prefix.'ев';
        } else { // (static::isConsonant($last) && !RussianLanguage::isHissingConsonant($last))
            $forms[Cases::RODIT] = $prefix.'ов';
        }

        // DAT
        $forms[Cases::DAT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ям', $prefix.'ам');

        // VINIT
        $forms[Cases::VINIT] = NounDeclension::getVinitCaseByAnimateness($forms, $animateness);

        // TVORIT
        // my personal rule
        if ($last == 'ь' && $declension == NounDeclension::THIRD_DECLENSION && !in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь'], true)) {
            $forms[Cases::TVORIT] = $prefix.'ми';
        } else {
            $forms[Cases::TVORIT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ями', $prefix.'ами');
        }

        // PREDLOJ
        $forms[Cases::PREDLOJ] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ях', $prefix.'ах');
        return $forms;
    }

    /**
     * Склонение существительных, образованных от прилагательных и причастий.
     * Rules are from http://rusgram.narod.ru/1216-1231.html
     * @param $word
     * @param $animateness
     * @return array
     */
    protected static function declinateAdjective($word, $animateness)
    {
        $prefix = S::slice($word, 0, -2);
        $vowel = static::isHissingConsonant(S::slice($prefix, -1)) ? 'и' : 'ы';
        return [
            Cases::IMENIT => $prefix.$vowel.'е',
            Cases::RODIT => $prefix.$vowel.'х',
            Cases::DAT => $prefix.$vowel.'м',
            Cases::VINIT => $prefix.$vowel.($animateness ? 'х' : 'е'),
            Cases::TVORIT => $prefix.$vowel.'ми',
            Cases::PREDLOJ => $prefix.$vowel.'х',
        ];
    }
}


<?php
namespace morphos;

use InvalidArgumentException;

trait CasesHelper
{
    /**
     * @param $case
     * @return string
     * @throws InvalidArgumentException If passed case is invalid.
     */
    public static function canonizeCase($case)
    {
        $case = S::lower($case);
        switch ($case) {
            case Cases::NOMINATIVE:
            case 'nominativus':
            case 'n':
                return Cases::NOMINATIVE;

            case Cases::GENITIVE:
            case Cases::GENETIVE:
            case 'genetivus':
            case 'g':
                return Cases::GENITIVE;

            case Cases::DATIVE:
            case 'dativus':
            case 'd':
                return Cases::DATIVE;

            case Cases::ACCUSATIVE:
                return Cases::ACCUSATIVE;

            case Cases::ABLATIVE:
            case 'ablativus':
            case 'a':
                return Cases::ABLATIVE;

            case Cases::PREPOSITIONAL:
            case 'praepositionalis':
            case 'p':
                return Cases::PREPOSITIONAL;

            default:
                throw new InvalidArgumentException('Invalid case: '.$case);
        }
    }

    /**
     * @return array
     */
    public static function getAllCases()
    {
        return [
            Cases::NOMINATIVE,
            Cases::GENITIVE,
            Cases::GENETIVE,
            Cases::DATIVE,
            Cases::ACCUSATIVE,
            Cases::ABLATIVE,
            Cases::PREPOSITIONAL,
        ];
    }

    /**
     * Составляет один массив с падежами из нескольких массивов падежей разных слов
     * @param array $words Двумерный массив слов и их падежей
     * @param string $delimiter Разделитель между падежами слов
     * @return array Одномерный массив падежей
     */
    public static function composeCasesFromWords(array $words, $delimiter = ' ') {
        $cases = [];
        foreach (CasesHelper::getAllCases() as $case) {
            $composed_case = [];
            foreach ($words as $wordCases) {
                $composed_case[] = $wordCases[$case];
            }
            $cases[$case] = implode($delimiter, $composed_case);
        }
        return $cases;
    }
}


<?php
namespace morphos\Russian;

use InvalidArgumentException;

class TimeSpeller extends \morphos\TimeSpeller
{
    protected static $units = [
        self::YEAR => 'год',
        self::MONTH => 'месяц',
        self::DAY => 'день',
        self::HOUR => 'час',
        self::MINUTE => 'минута',
        self::SECOND => 'секунда',
    ];

    const AGO = 'назад';
    const IN = 'через';

    const AND_WORD = 'и';

    const JUST_NOW = 'только что';

    /**
     * @param $count
     * @param $unit
     *
     * @return string
     * @throws \Exception
     */
    public static function spellUnit($count, $unit)
    {
        if (!isset(static::$units[$unit])) {
            throw new InvalidArgumentException('Unknown time unit: '.$unit);
        }

        return pluralize($count, static::$units[$unit]);
    }
}


<?php
namespace morphos\Russian;

use morphos\Gender;
use morphos\S;

trait RussianLanguage
{
    /**
     * @var array Все гласные
     */
    public static $vowels = [
        'а',
        'е',
        'ё',
        'и',
        'о',
        'у',
        'ы',
        'э',
        'ю',
        'я',
    ];

    /**
     * @var array Все согласные
     */
    public static $consonants = [
        'б',
        'в',
        'г',
        'д',
        'ж',
        'з',
        'й',
        'к',
        'л',
        'м',
        'н',
        'п',
        'р',
        'с',
        'т',
        'ф',
        'х',
        'ц',
        'ч',
        'ш',
        'щ',
    ];

    /**
     * @var array Пары согласных
     */
    public static $pairs = [
        'б' => 'п',
        'в' => 'ф',
        'г' => 'к',
        'д' => 'т',
        'ж' => 'ш',
        'з' => 'с',
    ];

    /**
     * @var array Звонкие согласные
     */
    public static $sonorousConsonants = ['б', 'в', 'г', 'д', 'з', 'ж', 'л', 'м', 'н', 'р'];
    /**
     * @var array Глухие согласные
     */
    public static $deafConsonants = ['п', 'ф', 'к', 'т', 'с', 'ш', 'х', 'ч', 'щ'];

    /**
     * Проверка гласной
     * @param $char
     * @return bool
     */
    public static function isVowel($char)
    {
        return in_array($char, static::$vowels, true);
    }

    /**
     * Проверка согласной
     * @param $char
     * @return bool
     */
    public static function isConsonant($char)
    {
        return in_array($char, static::$consonants, true);
    }

    /**
     * Проверка звонкости согласной
     */
    public static function isSonorousConsonant($char)
    {
        return in_array($char, static::$sonorousConsonants, true);
    }

    /**
     * Проверка глухости согласной
     * @param $char
     * @return bool
     */
    public static function isDeafConsonant($char)
    {
        return in_array($char, static::$deafConsonants, true);
    }

    /**
     * Щипящая ли согласная
     * @param $consonant
     * @return bool
     */
    public static function isHissingConsonant($consonant)
    {
        return in_array(S::lower($consonant), ['ж', 'ш', 'ч', 'щ'], true);
    }

    /**
     * Проверка на велярность согласной
     * @param string[1] $consonant
     * @return bool
     */
    protected static function isVelarConsonant($consonant)
    {
        return in_array(S::lower($consonant), ['г', 'к', 'х'], true);
    }

    /**
     * Подсчет слогов
     * @param $string
     * @return bool|int
     */
    public static function countSyllables($string)
    {
        return S::countChars($string, static::$vowels);
    }

    /**
     * Проверка парности согласной
     *
     * @param $consonant
     * @return bool
     */
    public static function isPaired($consonant)
    {
        $consonant = S::lower($consonant);
        return array_key_exists($consonant, static::$pairs) || (array_search($consonant, static::$pairs) !== false);
    }

    /**
     * Проверка мягкости последней согласной
     * @param $word
     * @return bool
     */
    public static function checkLastConsonantSoftness($word)
    {
        if (($substring = S::findLastPositionForOneOfChars(S::lower($word), static::$consonants)) !== false) {
            if (in_array(S::slice($substring, 0, 1), ['й', 'ч', 'щ'], true)) { // always soft consonants
                return true;
            } elseif (S::length($substring) > 1 && in_array(S::slice($substring, 1, 2), ['е', 'ё', 'и', 'ю', 'я', 'ь'], true)) { // consonants are soft if they are trailed with these vowels
                return true;
            }
        }
        return false;
    }

    /**
     * Проверяет, что гласная образует два звука в словах
     * @param $vowel
     * @return bool
     */
    public static function isBinaryVowel($vowel)
    {
        return in_array(S::lower($vowel), ['е', 'ё', 'ю', 'я'], true);
    }

    /**
     * Выбор предлога по первой букве
     */
    public static function choosePrepositionByFirstLetter($word, $prepositionWithVowel, $preposition)
    {
        if (in_array(S::lower(S::slice($word, 0, 1)), ['а', 'о', 'и', 'у', 'э'], true)) {
            return $prepositionWithVowel;
        } else {
            return $preposition;
        }
    }

    /**
     * Выбор окончания в зависимости от мягкости
     *
     * @param $last
     * @param $softLast
     * @param $afterSoft
     * @param $afterHard
     *
     * @return mixed
     */
    public static function chooseVowelAfterConsonant($last, $softLast, $afterSoft, $afterHard)
    {
        if ((RussianLanguage::isHissingConsonant($last) && !in_array($last, ['ж', 'ч'], true)) || /*static::isVelarConsonant($last) ||*/ $softLast) {
            return $afterSoft;
        } else {
            return $afterHard;
        }
    }

    /**
     * @param string $verb Verb to modify if gender is female
     * @param string $gender If not `m`, verb will be modified
     * @return string Correct verb
     */
    public static function verb($verb, $gender)
    {
        $verb = S::lower($verb);
        // возвратный глагол
        if (S::slice($verb, -2) == 'ся') {

            return ($gender == Gender::MALE
                ? $verb
                : S::slice($verb, 0, -2).(S::slice($verb, -3, -2) === 'л' ? null : 'л').'ась');
        }

        // обычный глагол
        return ($gender == Gender::MALE
            ? $verb
            : $verb.(S::slice($verb, -1) === 'л' ? null : 'л').'а');
    }

    /**
     * Add 'в' or 'во' prepositional before the word
     * @param string $word
     * @return string
     */
    public static function in($word)
    {
        $normalized = trim(S::lower($word));
        if (in_array(S::slice($normalized, 0, 1), ['в', 'ф'], true))
            return 'во '.$word;
        return 'в '.$word;
    }

    /**
     * Add 'с' or 'со' prepositional before the word
     * @param string $word
     * @return string
     */
    public static function with($word)
    {
        $normalized = trim(S::lower($word));
        if (in_array(S::slice($normalized, 0, 1), ['c', 'з', 'ш', 'ж'], true) && static::isConsonant(S::slice($normalized, 1, 2)) || S::slice($normalized, 0, 1) == 'щ')
            return 'со '.$word;
        return 'с '.$word;
    }

    /**
     * Add 'о' or 'об' or 'обо' prepositional before the word
     * @param string $word
     * @return string
     */
    public static function about($word)
    {
        $normalized = trim(S::lower($word));
        if (static::isVowel(S::slice($normalized, 0, 1)) && !in_array(S::slice($normalized, 0, 1), ['е', 'ё', 'ю', 'я'], true))
            return 'об '.$word;

        if (in_array(S::slice($normalized, 0, 3), ['все', 'всё', 'всю', 'что', 'мне'], true))
            return 'обо '.$word;

        return 'о '.$word;
    }

    /**
     * Выбирает первое или второе окончание в зависимости от звонкости/глухости в конце слова.
     * @param string $word Слово (или префикс), на основе звонкости которого нужно выбрать окончание
     * @param string $ifSonorous Окончание, если слово оканчивается на звонкую согласную
     * @param string $ifDeaf Окончание, если слово оканчивается на глухую согласную
     * @return string Первое или второе окончание
     * @throws \Exception
     */
    public static function chooseEndingBySonority($word, $ifSonorous, $ifDeaf)
    {
        $last = S::slice($word, -1);
        if (static::isSonorousConsonant($last))
            return $ifSonorous;
        if (static::isDeafConsonant($last))
            return $ifDeaf;

        throw new \Exception('Not implemented');
    }

    /**
     * Проверяет, является ли существительно адъективным существительным
     * @param string $noun Существительное
     * @return bool
     */
    public static function isAdjectiveNoun($noun)
    {
        return in_array(S::slice($noun, -2), ['ой', 'ий', 'ый', 'ая', 'ое', 'ее'])
            && !in_array($noun, ['гений', 'комментарий']);
    }
}


<?php

namespace phpmock\phpunit;

use phpmock\generator\MockFunctionGenerator;
use PHPUnit\Framework\MockObject\Invocation;
use PHPUnit\Framework\MockObject\Matcher\Invocation as InvocationInterface;

/**
 * Removes default arguments from the invocation.
 *
 * @author Markus Malkusch <markus@malkusch.de>
 * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations
 * @license http://www.wtfpl.net/txt/copying/ WTFPL
 * @internal
 */
class DefaultArgumentRemoverReturnTypes implements InvocationInterface
{

    /**
     * @SuppressWarnings(PHPMD)
     */
    public function invoked(Invocation $invocation)
    {
    }

    /**
     * @SuppressWarnings(PHPMD)
     */
    public function matches(Invocation $invocation) : bool
    {
        if ($invocation instanceof Invocation\StaticInvocation) {
            $this->removeDefaultArguments($invocation);
        } else {
            MockFunctionGenerator::removeDefaultArguments($invocation->parameters);
        }

        return false;
    }

    public function verify() : void
    {
    }

    /**
     * This method is not defined in the interface, but used in
     * PHPUnit_Framework_MockObject_InvocationMocker::hasMatchers().
     *
     * @return boolean
     * @see \PHPUnit_Framework_MockObject_InvocationMocker::hasMatchers()
     */
    public function hasMatchers()
    {
        return false;
    }

    public function toString() : string
    {
        return __CLASS__;
    }

    /**
     * Remove default arguments from StaticInvocation or its children (hack)
     *
     * @SuppressWarnings(PHPMD)
     */
    private function removeDefaultArguments(Invocation\StaticInvocation $invocation)
    {
        $remover = function () {
            MockFunctionGenerator::removeDefaultArguments($this->parameters);
        };

        $remover->bindTo($invocation, Invocation\StaticInvocation::class)();
    }
}


<?php

namespace phpmock\phpunit;

use phpmock\integration\MockDelegateFunctionBuilder;
use phpmock\MockBuilder;
use phpmock\Deactivatable;
use PHPUnit\Framework\MockObject\MockObject;

/**
 * Adds building a function mock functionality into \PHPUnit\Framework\TestCase.
 *
 * Use this trait in your \PHPUnit\Framework\TestCase:
 * <code>
 * <?php
 *
 * namespace foo;
 *
 * class FooTest extends \PHPUnit\Framework\TestCase
 * {
 *
 *     use \phpmock\phpunit\PHPMock;
 *
 *     public function testBar()
 *     {
 *         $time = $this->getFunctionMock(__NAMESPACE__, "time");
 *         $time->expects($this->once())->willReturn(3);
 *         $this->assertEquals(3, time());
 *     }
 * }
 * </code>
 *
 * @author Markus Malkusch <markus@malkusch.de>
 * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations
 * @license http://www.wtfpl.net/txt/copying/ WTFPL
 */
trait PHPMock
{
    /**
     * Returns the enabled function mock.
     *
     * This mock will be disabled automatically after the test run.
     *
     * @param string $namespace The function namespace.
     * @param string $name      The function name.
     *
     * @return MockObject The PHPUnit mock.
     */
    public function getFunctionMock($namespace, $name)
    {
        $delegateBuilder = new MockDelegateFunctionBuilder();
        $delegateBuilder->build($name);

        $mock = $this->getMockBuilder($delegateBuilder->getFullyQualifiedClassName())->getMockForAbstractClass();

        $mock->__phpunit_getInvocationMocker()->addMatcher(new DefaultArgumentRemover());

        $functionMockBuilder = new MockBuilder();
        $functionMockBuilder->setNamespace($namespace)
                            ->setName($name)
                            ->setFunctionProvider($mock);

        $functionMock = $functionMockBuilder->build();
        $functionMock->enable();

        $this->registerForTearDown($functionMock);

        $proxy = new MockObjectProxy($mock);
        return $proxy;
    }

    /**
     * Automatically disable function mocks after the test was run.
     *
     * If you use getFunctionMock() you don't need this method. This method
     * is meant for a Deactivatable (e.g. a MockEnvironment) which was
     * directly created with PHPMock's API.
     *
     * @param Deactivatable $deactivatable The function mocks.
     */
    public function registerForTearDown(Deactivatable $deactivatable)
    {
        $result = $this->getTestResultObject();
        $result->addListener(new MockDisabler($deactivatable));
    }

    /**
     * Defines the mocked function in the given namespace.
     *
     * In most cases you don't have to call this method. {@link getFunctionMock()}
     * is doing this for you. But if the mock is defined after the first call in the
     * tested class, the tested class doesn't resolve to the mock. This is
     * documented in Bug #68541. You therefore have to define the namespaced
     * function before the first call (e.g. with the beforeClass annotation).
     *
     * Defining the function has no side effects. If the function was
     * already defined this method does nothing.
     *
     * @see getFunctionMock()
     * @link https://bugs.php.net/bug.php?id=68541 Bug #68541
     *
     * @param string $namespace The function namespace.
     * @param string $name      The function name.
     */
    public static function defineFunctionMock($namespace, $name)
    {
        $functionMockBuilder = new MockBuilder();
        $functionMockBuilder->setNamespace($namespace)
                            ->setName($name)
                            ->setFunction(function () {
                            })
                            ->build()
                            ->define();
    }
}


<?php
/**
 * @link https://github.com/consik/yii2-websocket
 * @category yii2-extension
 * @package consik\yii2websocket
 *
 * @author Sergey Poltaranin <consigliere.kz@gmail.com>
 * @copyright Copyright (c) 2016
 */

namespace consik\yii2websocket;

use consik\yii2websocket\events\ExceptionEvent;
use consik\yii2websocket\events\WSClientCommandEvent;
use consik\yii2websocket\events\WSClientErrorEvent;
use consik\yii2websocket\events\WSClientEvent;
use consik\yii2websocket\events\WSClientMessageEvent;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServer;
use Ratchet\MessageComponentInterface;
use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use yii\base\Component;

class WebSocketServer extends Component implements MessageComponentInterface
{
    /**
     * @event yii\base\Event Triggered when binding is successfully completed
     */
    const EVENT_WEBSOCKET_OPEN = 'ws_open';
    /**
     * @event yii\base\Event Triggered when socket listening is closed
     */
    const EVENT_WEBSOCKET_CLOSE = 'ws_close';
    /**
     * @event ExceptionEvent Triggered when throwed Exception on binding socket
     */
    const EVENT_WEBSOCKET_OPEN_ERROR = 'ws_open_error';

    /**
     * @event WSClientEvent Triggered when client connected to the server
     */
    const EVENT_CLIENT_CONNECTED = 'ws_client_connected';
    /**
     * @event WSClientErrorEvent Triggered when an error occurs on a Connection
     */
    const EVENT_CLIENT_ERROR = 'ws_client_error';
    /**
     * @event WSClientEvent Triggered when client close connection with server
     */
    const EVENT_CLIENT_DISCONNECTED = 'ws_client_disconnected';
    /**
     * @event WSClientMessageEvent Triggered when message recieved from client
     */
    const EVENT_CLIENT_MESSAGE = 'ws_client_message';
    /**
     * @event WSClientCommandEvent Triggered when controller starts user's command
     */
    const EVENT_CLIENT_RUN_COMMAND = 'ws_client_run_command';
    /**
     * @event WSClientCommandEvent Triggered when controller finished user's command
     */
    const EVENT_CLIENT_END_COMMAND = 'ws_client_end_command';

    /**
     * @var int $port
     */
    public $port = 8080;

    /**
     * @var bool $closeConnectionOnError
     */
    protected $closeConnectionOnError = true;

    /**
     * @var bool $runMessageCommands
     */
    protected $runClientCommands = true;

    /**
     * @var IoServer|null $server
     */
    protected $server = null;

    /**
     * @var null|\SplObjectStorage $clients
     */
    protected $clients = null;

    /**
     * @return bool
     *
     * @event yii\base\Event EVENT_WEBSOCKET_OPEN
     * @event ExceptionEvent EVENT_WEBSOCKET_OPEN_ERROR
     */
    public function start()
    {
        try {
            $this->server = IoServer::factory(
                new HttpServer(
                    new WsServer(
                        $this
                    )
                ),
                $this->port
            );
            $this->trigger(self::EVENT_WEBSOCKET_OPEN);
            $this->clients = new \SplObjectStorage();
            $this->server->run();

            return true;

        } catch (\Exception $e) {
            $errorEvent = new ExceptionEvent([
                'exception' => $e
            ]);
            $this->trigger(self::EVENT_WEBSOCKET_OPEN_ERROR, $errorEvent);
            return false;
        }
    }

    /**
     * @return void
     *
     * @event yii\base\Event EVENT_WEBSOCKET_CLOSE
     */
    public function stop()
    {
        $this->server->socket->shutdown();
        $this->trigger(self::EVENT_WEBSOCKET_CLOSE);
    }

    /**
     * @param ConnectionInterface $conn
     *
     * @event WSClientEvent EVENT_CLIENT_CONNECTED
     */
    function onOpen(ConnectionInterface $conn)
    {
        $this->trigger(self::EVENT_CLIENT_CONNECTED, new WSClientEvent([
            'client' => $conn
        ]));

        $this->clients->attach($conn);
    }

    /**
     * @param ConnectionInterface $conn
     *
     * @event WSClientEvent EVENT_CLIENT_DISCONNECTED
     */
    function onClose(ConnectionInterface $conn)
    {
        $this->trigger(self::EVENT_CLIENT_DISCONNECTED, new WSClientEvent([
            'client' => $conn
        ]));

        $this->clients->detach($conn);
    }

    /**
     * @param ConnectionInterface $conn
     * @param \Exception $e
     *
     * @event WSClientErrorEvent EVENT_CLIENT_ERROR
     */
    function onError(ConnectionInterface $conn, \Exception $e)
    {
        $this->trigger(self::EVENT_CLIENT_ERROR, new WSClientErrorEvent([
            'client' => $conn,
            'exception' => $e
        ]));

        if ($this->closeConnectionOnError) {
            $conn->close();
        }
    }

    /**
     * @param ConnectionInterface $from
     * @param string $msg
     *
     * @event WSClientMessageEvent EVENT_CLIENT_MESSAGE
     * @event WSClientCommandEvent EVENT_CLIENT_RUN_COMMAND
     * @event WSClientCommandEvent EVENT_CLIENT_END_COMMAND
     */
    function onMessage(ConnectionInterface $from, $msg)
    {
        $this->trigger(self::EVENT_CLIENT_MESSAGE, new WSClientMessageEvent([
            'client' => $from,
            'message' => $msg
        ]));

        if ($this->runClientCommands) {
            $command = $this->getCommand($from, $msg);

            if ($command && method_exists($this, 'command' . ucfirst($command))) {
                $this->trigger(self::EVENT_CLIENT_RUN_COMMAND, new WSClientCommandEvent([
                    'client' => $from,
                    'command' => $command
                ]));

                $result = call_user_func([$this, 'command' . ucfirst($command)], $from, $msg);

                $this->trigger(self::EVENT_CLIENT_END_COMMAND, new WSClientCommandEvent([
                    'client' => $from,
                    'command' => $command,
                    'result' => $result
                ]));
            }
        }
    }

    /**
     * @param ConnectionInterface $from
     * @param $msg
     * @return null|string - _NAME_ of command that implemented in class method command_NAME_()
     */
    protected function getCommand(ConnectionInterface $from, $msg)
    {
        return null;
    }
}


<?php

namespace Akaunting\Language;

class Language
{
    /**
     * The Laravel application instance.
     *
     * @var \Illuminate\Foundation\Application
     */
    protected $app;

    /**
     * Normalized Laravel Version.
     *
     * @var string
     */
    protected $version;

    /**
     * True when this is a Lumen application.
     *
     * @var bool
     */
    protected $is_lumen = false;

    /**
     * @param Application $app
     */
    public function __construct($app = null)
    {
        if (!$app) {
            $app = app();   //Fallback when $app is not given
        }

        $this->app = $app;
        $this->version = $app->version();
        $this->is_lumen = str_contains($this->version, 'Lumen');
    }

    /**
     * Get single flags view.
     *
     * @param string $code
     *
     * @return mixed
     **/
    public static function flag($code = 'default')
    {
        if ($code == 'default') {
            $code = app()->getLocale();
        }

        $name = self::getName($code);
        $code = self::country($code);

        return view('vendor.language.flag', compact('code', 'name'));
    }

    /**
     * Get country code based on locale.
     *
     * @param string $locale
     *
     * @return mixed
     **/
    public static function country($locale = 'default')
    {
        if ($locale == 'default') {
            $locale = app()->getLocale();
        }

        if (config('language.mode.code', 'short') == 'short') {
            $code = strtolower(substr(self::getLongCode($locale), 3));
        } else {
            $code = strtolower(substr($locale, 3));
        }

        return $code;
    }

    /**
     * Get all flags view.
     *
     * @return mixed
     **/
    public static function flags()
    {
        return view('vendor.language.flags');
    }

    /**
     * Return true if $code is an allowed lang.
     * Get all allowed languages.
     *
     * @param string $locale
     *
     * @return bool|array
     **/
    public static function allowed($locale = null)
    {
        if ($locale) {
            return in_array($locale, array_keys(self::allowed()));
        }

        if (config('language.allowed')) {
            return self::names(array_merge(config('language.allowed'), [config('app.locale')]));
        } else {
            return self::names([config('app.locale')]);
        }
    }

    /**
     * Add names to an array of language codes as [$code => $language].
     *
     * @param array $codes
     *
     * @return array
     **/
    public static function names($codes)
    {
        // Get mode
        $mode = config('language.mode');

        // Get languages from config
        $languages = config('language.all');

        $array = [];

        // Generate an array with $code as key and $code language as value
        foreach ($codes as $code) {
            $lang_name = 'Unknown';

            foreach ($languages as $language) {
                if ($language[$mode['code']] == $code) {
                    $lang_name = $language[$mode['name']];
                }
            }

            $array[$code] = $lang_name;
        }

        return $array;
    }

    /**
     * Add names to an array of language codes as [$language => $code].
     *
     * @param array $langs
     *
     * @return array
     **/
    public static function codes($langs)
    {
        // Get mode
        $mode = config('language.mode');

        // Get languages from config
        $languages = config('language.all');

        $array = [];

        // Generate an array with $lang as key and $lang code as value
        foreach ($langs as $lang) {
            $lang_code = 'unk';

            foreach ($languages as $language) {
                if ($language[$mode['name']] == $lang) {
                    $lang_code = $language[$mode['code']];
                }
            }

            $array[$lang] = $lang_code;
        }

        return $array;
    }

    /**
     * Returns the url to set up language and return back.
     *
     * @param string $code
     *
     * @return string
     **/
    public static function back($code)
    {
        return route('language::back', ['locale' => $code]);
    }

    /**
     * Returns the url to set up language and return to url('/').
     *
     * @param string $code
     *
     * @return string
     **/
    public static function home($code)
    {
        return route('language::home', ['locale' => $code]);
    }

    /**
     * Returns the language code.
     *
     * @param string $name
     *
     * @return string
     **/
    public static function getCode($name = 'default')
    {
        if ($name == 'default') {
            $name = self::getName();
        }

        return self::codes([$name])[$name];
    }

    /**
     * Returns the language long code.
     *
     * @param string $short
     *
     * @return string
     **/
    public static function getLongCode($short = 'default')
    {
        if ($short == 'default') {
            $short = app()->getLocale();
        }

        $long = 'en-GB';

        // Get languages from config
        $languages = config('language.all');

        foreach ($languages as $language) {
            if ($language['short'] != $short) {
                continue;
            }

            $long = $language['long'];
        }

        return $long;
    }

    /**
     * Returns the language short code.
     *
     * @param string $long
     *
     * @return string
     **/
    public static function getShortCode($long = 'default')
    {
        if ($long == 'default') {
            $long = app()->getLocale();
        }

        $short = 'en';

        // Get languages from config
        $languages = config('language.all');

        foreach ($languages as $language) {
            if ($language['long'] != $long) {
                continue;
            }

            $short = $language['short'];
        }

        return $short;
    }

    /**
     * Returns the language name.
     *
     * @param string $code
     *
     * @return string
     **/
    public static function getName($code = 'default')
    {
        if ($code == 'default') {
            $code = app()->getLocale();
        }

        return self::names([$code])[$code];
    }
}


<?php

namespace Akaunting\Language;

use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Unicodeveloper\Identify\IdentifyServiceProvider;

class Provider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @param Router $router
     *
     * @return void
     */
    public function boot(Router $router)
    {
        if (!$this->app->routesAreCached()) {
            require __DIR__ . '/Routes/web.php';
        }

        $this->publishes([
            __DIR__ . '/Config/language.php'                                  => config_path('language.php'),
            __DIR__ . '/Migrations/2020_01_01_000000_add_locale_column.php'   => database_path('migrations/2020_01_01_000000_add_locale_column.php'),
            __DIR__ . '/Resources/views/flag.blade.php'                       => resource_path('views/vendor/language/flag.blade.php'),
            __DIR__ . '/Resources/views/flags.blade.php'                      => resource_path('views/vendor/language/flags.blade.php'),
        ], 'language');

        $router->aliasMiddleware('language', config('language.middleware'));

        $this->app->register(IdentifyServiceProvider::class);

        $this->app->singleton('language', function ($app) {
            return new Language($app);
        });
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->mergeConfigFrom(__DIR__ . '/Config/language.php', 'language');
    }
}


<?php

namespace Akaunting\Language\Controllers;

use Auth;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class Language extends Controller
{
    /**
     * Set locale if it's allowed.
     *
     * @param string                   $locale
     * @param \Illuminate\Http\Request $request
     **/
    private function setLocale($locale, $request)
    {
        // Check if is allowed and set default locale if not
        if (!language()->allowed($locale)) {
            $locale = config('app.locale');
        }

        if (Auth::check()) {
            Auth::user()->setAttribute('locale', $locale)->save();
        } else {
            $request->session()->put('locale', $locale);
        }
    }

    /**
     * Set locale and return home url.
     *
     * @param string                   $locale
     * @param \Illuminate\Http\Request $request
     *
     * @return string
     **/
    public function home($locale, Request $request)
    {
        $this->setLocale($locale, $request);

        $url = config('language.url') ? url('/' . $locale) : url('/');

        return redirect($url);
    }

    /**
     * Set locale and return back.
     *
     * @param string                   $locale
     * @param \Illuminate\Http\Request $request
     *
     * @return string
     **/
    public function back($locale, Request $request)
    {
        $this->setLocale($locale, $request);

        $session = $request->session();

        if (config('language.url')) {
            $previous_url = substr(str_replace(env('APP_URL'), '', $session->previousUrl()), 7);

            if (strlen($previous_url) == 3) {
                $previous_url = substr($previous_url, 3);
            } else {
                $previous_url = substr($previous_url, strrpos($previous_url, '/') + 1);
            }

            $url = rtrim(env('APP_URL'), '/') . '/' . $locale . '/' . ltrim($previous_url, '/');

            $session->setPreviousUrl($url);
        }

        return redirect($session->previousUrl());
    }
}


<?php

namespace Akaunting\Language\Middleware;

use Closure;
use Unicodeveloper\Identify\Facades\IdentityFacade as Identify;

class SetLocale
{
    /**
     * This function checks if language to set is an allowed lang of config.
     *
     * @param string $locale
     **/
    private function setLocale($locale)
    {
        // Check if is allowed and set default locale if not
        if (!language()->allowed($locale)) {
            $locale = config('app.locale');
        }

        // Set app language
        \App::setLocale($locale);

        // Set carbon language
        if (config('language.carbon')) {
            // Carbon uses only language code
            if (config('language.mode.code') == 'long') {
                $locale = explode('-', $locale)[0];
            }

            \Carbon\Carbon::setLocale($locale);
        }

        // Set date language
        if (config('language.date')) {
            // Date uses only language code
            if (config('language.mode.code') == 'long') {
                $locale = explode('-', $locale)[0];
            }

            \Date::setLocale($locale);
        }
    }

    public function setDefaultLocale()
    {
        if (config('language.auto')) {
            $this->setLocale(Identify::lang()->getLanguage());
        } else {
            $this->setLocale(config('app.locale'));
        }
    }

    public function setUserLocale()
    {
        $user = auth()->user();

        if ($user->locale) {
            $this->setLocale($user->locale);
        } else {
            $this->setDefaultLocale();
        }
    }

    public function setSystemLocale($request)
    {
        if ($request->session()->has('locale')) {
            $this->setLocale(session('locale'));
        } else {
            $this->setDefaultLocale();
        }
    }

    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure                 $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->has('lang')) {
            $this->setLocale($request->get('lang'));
        } elseif (auth()->check()) {
            $this->setUserLocale();
        } else {
            $this->setSystemLocale($request);
        }

        return $next($request);
    }
}