<?php
/**
 * Parses and verifies the doc comments for functions.
 *
 * PHP version 5
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */

if (class_exists('PEAR_Sniffs_Commenting_FunctionCommentSniff', true) === false) {
    throw new PHP_CodeSniffer_Exception('Class PEAR_Sniffs_Commenting_FunctionCommentSniff not found');
}

/**
 * Parses and verifies the doc comments for functions.
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
 * @version   Release: @package_version@
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
class Ongr_Sniffs_Commenting_FunctionCommentSniff extends PEAR_Sniffs_Commenting_FunctionCommentSniff
{
    /**
     * Processes this test, when one of its tokens is encountered.
     *
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
     * @param int                  $stackPtr  The position of the current token
     *                                        in the stack passed in $tokens.
     *
     * @return void
     */
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();
        $find   = PHP_CodeSniffer_Tokens::$methodPrefixes;
        $find[] = T_WHITESPACE;

        $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
        if ($tokens[$commentEnd]['code'] === T_COMMENT) {
            // Inline comments might just be closing comments for
            // control structures or functions instead of function comments
            // using the wrong comment type. If there is other code on the line,
            // assume they relate to that code.
            $prev = $phpcsFile->findPrevious($find, ($commentEnd - 1), null, true);
            if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) {
                $commentEnd = $prev;
            }
        }

        if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
            && $tokens[$commentEnd]['code'] !== T_COMMENT
        ) {
            $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
            $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'no');
            return;
        } else {
            $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'yes');
        }

        if ($tokens[$commentEnd]['code'] === T_COMMENT) {
            $phpcsFile->addError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle');
            return;
        }

        if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
            $error = 'There must be no blank lines after the function comment';
            $phpcsFile->addError($error, $commentEnd, 'SpacingAfter');
        }

        $commentStart = $tokens[$commentEnd]['comment_opener'];
        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
            if ($tokens[$tag]['content'] === '@see') {
                // Make sure the tag isn't empty.
                $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
                if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
                    $error = 'Content missing for @see tag in function comment';
                    $phpcsFile->addError($error, $tag, 'EmptySees');
                }
            }
            // Ongr checks inheritdoc comment.
            if ($tokens[$tag]['content'] === '@inheritdoc') {
                $error = 'You must use {@inheritdoc}';
                $fix = $phpcsFile->addFixableError($error, $tag, 'WrongInheritDocStyle');
                if ($fix === true) {
                    $phpcsFile->fixer->replaceToken($tag, '{@inheritdoc}');
                }
                return;
            }

        }
        $this->processReturn($phpcsFile, $stackPtr, $commentStart);
        $this->processThrows($phpcsFile, $stackPtr, $commentStart);
        $this->processDeprecated($phpcsFile, $stackPtr, $commentStart);
        $this->processParams($phpcsFile, $stackPtr, $commentStart);
        $this->processComments($phpcsFile, $stackPtr, $commentStart);

    }//end process()

    /**
     * @param PHP_CodeSniffer_File $phpcsFile
     * @param                      $stackPtr
     * @param                      $commentStart
     *
     * @throws PHP_CodeSniffer_Exception
     */
    protected function processComments(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart)
    {
        $tokens = $phpcsFile->getTokens();
        $empty = array(
            T_DOC_COMMENT_WHITESPACE,
            T_DOC_COMMENT_STAR,
        );

        $find = [
            T_DOC_COMMENT_CLOSE_TAG,
            T_DOC_COMMENT_TAG,
        ];

        $commentEnd = $phpcsFile->findNext($find, $commentStart);

        $methodName      = $phpcsFile->getDeclarationName($stackPtr);
        $short = $phpcsFile->findNext($empty, ($commentStart + 1), $commentEnd, true);
        $shortEnd     = $short;
        $shortContent = $tokens[$short]['content'];
        $lastChar = $shortContent[(strlen($shortContent) - 1)];

        // Check for a comment description.
        if (trim($short) === '') {
            // ONGR Comments are not necessary for magic methods.
            $magicMethods = [
                '__call',
                '__callStatic',
                '__clone',
                '__construct',
                '__destruct',
                '__get',
                '__invoke',
                '__isset',
                '__set',
                '__set_state',
                '__sleep',
                '__toString',
                '__unset',
                '__wakeup',
            ];
            $magicRegExp = implode('|', $magicMethods);
            if (preg_match('/^(set|get|has|add|is)[A-Z]|' . $magicRegExp . '/', $methodName) !== 1) {
                $error = 'Missing short description in function doc comment';
                $phpcsFile->addError($error, $commentStart, 'MissingShort');
            }

            return;
        }
        if (preg_match('#^(\p{Lu}|{@inheritdoc})#u', $shortContent) === 0) {
            $error = 'Function comment short description must start with a capital letter';
            $phpcsFile->addError($error, ($commentStart + 1), 'ShortNotCapital');
        }
        if (preg_match('#{@inheritdoc}$#u', $shortContent) === 0 && $lastChar !== '.') {
            $error = 'Function comment short description must end with a full stop';
            $phpcsFile->addError($error, ($commentStart + 1), 'ShortFullStop');
        }

        //Check whitespaces beetween '*' and short comment.
        $foundShort = strlen($tokens[$short-1]['content']);
        if ($foundShort > 1) {
            $phpcsFile->addError(
                "Expected one whitespace before short description. Found {$foundShort}.",
                $short,
                'ExtraWhitespace'
            );
        }

        $long = $phpcsFile->findNext($empty, ($shortEnd + 1), ($commentEnd - 1), true);
        if ($long === false) {
            return;
        }

        //Check whitespaces beetween '*' and long comment.
        $foundLong = strlen($tokens[$long-1]['content']);
        if ($foundLong > 1) {
            $phpcsFile->addError(
                "Expected one whitespace before long description. Found {$foundLong}.",
                $long,
                'ExtraWhitespace'
            );
        }

        if ($tokens[$long]['code'] === T_DOC_COMMENT_STRING) {
            if ($tokens[$long]['line'] !== ($tokens[$shortEnd]['line'] + 2)) {
                $error = 'There must be exactly one blank line between descriptions in a doc comment';
                $phpcsFile->addError($error, $long, 'SpacingBetween');
            }

            if (preg_match('/\p{Lu}|\P{L}/u', $tokens[$long]['content'][0]) === 0) {
                $error = 'Doc comment long description must start with a capital letter';
                $phpcsFile->addError($error, $long, 'LongNotCapital');
            }
        }//end if
    }

    /**
     * Process the return comment of this function comment.
     *
     * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
     * @param int                  $stackPtr     The position of the current token
     *                                           in the stack passed in $tokens.
     * @param int                  $commentStart The position in the stack where the comment started.
     *
     * @return void
     */
    protected function processReturn(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart)
    {
        $tokens = $phpcsFile->getTokens();

        // Skip constructor and destructor.
        $methodName      = $phpcsFile->getDeclarationName($stackPtr);
        $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct');

        $return = null;
        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
            if ($tokens[$tag]['content'] === '@return') {
                if ($return !== null) {
                    $error = 'Only 1 @return tag is allowed in a function comment';
                    $phpcsFile->addError($error, $tag, 'DuplicateReturn');
                    return;
                }

                $return = $tag;
            }
        }

        if ($isSpecialMethod === true) {
            return;
        }

        if ($return !== null) {
            $content = $tokens[($return + 2)]['content'];
            if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) {
                $error = 'Return type missing for @return tag in function comment';
                $phpcsFile->addError($error, $return, 'MissingReturnType');
            } else {
                // Check return type (can be multiple, separated by '|').
                $typeNames      = explode('|', $content);
                $suggestedNames = array();
                foreach ($typeNames as $i => $typeName) {
                    $suggestedName = PHP_CodeSniffer::suggestType($typeName);
                    if ($suggestedName === 'boolean') {
                        $suggestedName = 'bool';
                    } elseif ($suggestedName === 'integer') {
                        $suggestedName = 'int';
                    }
                    if (in_array($suggestedName, $suggestedNames) === false) {
                        $suggestedNames[] = $suggestedName;
                    }
                }
                $suggestedType = implode('|', $suggestedNames);
                if ($content !== $suggestedType) {
                    $error = 'Function return type "%s" is invalid';
                    $data = [$content];
                    $phpcsFile->addError($error, $return, 'InvalidReturn', $data);
                }

                // If the return type is void, make sure there is
                // no return statement in the function.
                if ($content === 'void') {
                    if (isset($tokens[$stackPtr]['scope_closer']) === true) {
                        $endToken = $tokens[$stackPtr]['scope_closer'];
                        for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
                            if ($tokens[$returnToken]['code'] === T_CLOSURE) {
                                $returnToken = $tokens[$returnToken]['scope_closer'];
                                continue;
                            }

                            if ($tokens[$returnToken]['code'] === T_RETURN) {
                                break;
                            }
                        }

                        if ($returnToken !== $endToken) {
                            // If the function is not returning anything, just
                            // exiting, then there is no problem.
                            $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
                            if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
                                $error = 'Function return type is void, but function contains return statement';
                                $phpcsFile->addError($error, $return, 'InvalidReturnVoid');
                            }
                        }
                    }//end if
                } else if ($content !== 'mixed') {
                    // If return type is not void, there needs to be a return statement
                    // somewhere in the function that returns something.
                    if (isset($tokens[$stackPtr]['scope_closer']) === true) {
                        $endToken    = $tokens[$stackPtr]['scope_closer'];
                        $returnToken = $phpcsFile->findNext(T_RETURN, $stackPtr, $endToken);
                        if ($returnToken === false) {
                            $error = 'Function return type is not void, but function has no return statement';
                            $phpcsFile->addError($error, $return, 'InvalidNoReturn');
                        } else {
                            $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
                            if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
                                $error = 'Function return type is not void, but function is returning void here';
                                $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid');
                            }
                        }
                    }
                }//end if

                $comment = null;
                if ($tokens[($return + 2)]['code'] === T_DOC_COMMENT_STRING) {
                    $matches = array();
                    preg_match('/([^\s]+)(?:\s+(.+))?/', $tokens[($return + 2)]['content'], $matches);
                    if (isset($matches[2]) === true) {
                        $comment = $matches[2];
                    }
                }

                //ONGR Validate that return comment begins with capital letter and ends with full stop.
                if ($comment !== null) {
                    if (!preg_match('/^\\p{Lu}/u', $comment)) {
                        $error = 'Return comment must start with a capital letter';
                        $fix = $phpcsFile->addFixableError($error, ($return + 2), 'ReturnCommentNotCapital');

                        if ($fix === true) {
                            $newComment = ucfirst($comment);
                            $tokenLength = strlen($tokens[($return + 2)]['content']);
                            $commentLength = strlen($comment);
                            $tokenWithoutComment
                                = substr($tokens[($return + 2)]['content'], 0, $tokenLength - $commentLength);

                            $phpcsFile->fixer->replaceToken(($return + 2), $tokenWithoutComment . $newComment);
                        }
                    }

                    if (!preg_match('/\\.$/u', $comment)) {
                        $error = 'Return comment must end with a full stop';
                        $fix = $phpcsFile->addFixableError($error, ($return + 2), 'ReturnCommentFullStop');

                        if ($fix === true) {
                            $phpcsFile->fixer->addContent(($return + 2), '.');
                        }
                    }
                }
            }//end if
        } else {
            //ONGR Return is not necessary.
//            $error = 'Missing @return tag in function comment';
//            $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
        }//end if

    }//end processReturn()

    /**
     * Process any throw tags that this function comment has.
     *
     * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
     * @param int                  $stackPtr     The position of the current token
     *                                           in the stack passed in $tokens.
     * @param int                  $commentStart The position in the stack where the comment started.
     *
     * @return void
     */
    protected function processThrows(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart)
    {
        $tokens = $phpcsFile->getTokens();

        $throws = array();
        foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
            if ($tokens[$tag]['content'] !== '@throws') {
                continue;
            }

            $exception = null;
            $comment   = null;
            if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
                $matches = array();
                preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches);
                $exception = $matches[1];
                if (isset($matches[2]) === true) {
                    $comment = $matches[2];
                }
            }

            if ($exception === null) {
                $error = 'Exception type and comment missing for @throws tag in function comment';
                $phpcsFile->addError($error, $tag, 'InvalidThrows');
            } else if ($comment === null) {
                //ONGR We dont write throw comments
//                $error = 'Comment missing for @throws tag in function comment';
//                $phpcsFile->addError($error, $tag, 'EmptyThrows');
            } else {
                // Any strings until the next tag belong to this comment.
                if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
                    $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
                } else {
                    $end = $tokens[$commentStart]['comment_closer'];
                }

                for ($i = ($tag + 3); $i < $end; $i++) {
                    if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
                        $comment .= ' '.$tokens[$i]['content'];
                    }
                }

                // Starts with a capital letter and ends with a fullstop.
                $firstChar = $comment{0};
                if (strtoupper($firstChar) !== $firstChar) {
                    $error = '@throws tag comment must start with a capital letter';
                    $phpcsFile->addError($error, ($tag + 2), 'ThrowsNotCapital');
                }

                $lastChar = substr($comment, -1);
                if ($lastChar !== '.') {
                    $error = '@throws tag comment must end with a full stop';
                    $phpcsFile->addError($error, ($tag + 2), 'ThrowsNoFullStop');
                }
            }//end if
        }//end foreach

    }//end processThrows()

    /**
     * Process any deprecated tags that this function comment has.
     *
     * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
     * @param int                  $stackPtr     The position of the current token
     *                                           in the stack passed in $tokens.
     * @param int                  $commentStart The position in the stack where the comment started.
     *
     * @return void
     */
    protected function processDeprecated(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart)
    {
        $tokens = $phpcsFile->getTokens();
        foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
            if ($tokens[$tag]['content'] !== '@deprecated') {
                continue;
            }

            $content = $tokens[($tag + 2)]['content'];
            if (empty($content) === true || $tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) {
                $error = 'Deprecated type missing for @deprecated tag in function comment';
                $phpcsFile->addError($error, $tag, 'MissingDeprecatedType');
            } else {
                //ONGR Validate that return comment begins with capital letter and ends with full stop.
                if ($content !== null) {
                    $firstChar = $content{0};
                    if (preg_match('|\p{Lu}|u', $firstChar) === 0) {
                        $error = 'Deprecated comment must start with a capital letter';
                        $fix = $phpcsFile->addFixableError($error, ($tag + 2), 'DeprecatedCommentNotCapital');

                        if ($fix === true) {
                            $newComment = ucfirst($content);
                            $tokenLength = strlen($tokens[($tag + 2)]['content']);
                            $commentLength = strlen($content);
                            $tokenWithoutComment
                                = substr($tokens[($tag + 2)]['content'], 0, $tokenLength - $commentLength);

                            $phpcsFile->fixer->replaceToken(($tag + 2), $tokenWithoutComment . $newComment);
                        }
                    }

                    $lastChar = substr($content, -1);
                    if ($lastChar !== '.') {
                        $error = 'Deprecated comment must end with a full stop';
                        $fix = $phpcsFile->addFixableError($error, ($tag + 2), 'DeprecatedCommentFullStop');

                        if ($fix === true) {
                            $phpcsFile->fixer->addContent(($tag + 2), '.');
                        }
                    }
                }
            }
        }
    }

    /**
     * Process the function parameter comments.
     *
     * @param PHP_CodeSniffer_File $phpcsFile    The file being scanned.
     * @param int                  $stackPtr     The position of the current token
     *                                           in the stack passed in $tokens.
     * @param int                  $commentStart The position in the stack where the comment started.
     *
     * @return void
     */
    protected function processParams(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart)
    {
        $tokens = $phpcsFile->getTokens();

        $params  = array();
        $maxType = 0;
        $maxVar  = 0;
        foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
            if ($tokens[$tag]['content'] !== '@param') {
                continue;
            }

            $type         = '';
            $typeSpace    = 0;
            $var          = '';
            $varSpace     = 0;
            $comment      = '';
            $commentLines = array();
            if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
                $matches = array();
                preg_match('/([^$&]+)(?:((?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches);

                $typeLen   = strlen($matches[1]);
                $type      = trim($matches[1]);
                $typeSpace = ($typeLen - strlen($type));
                $typeLen   = strlen($type);
                if ($typeLen > $maxType) {
                    $maxType = $typeLen;
                }

                if (isset($matches[2]) === true) {
                    $var    = $matches[2];
                    $varLen = strlen($var);
                    if ($varLen > $maxVar) {
                        $maxVar = $varLen;
                    }

                    if (isset($matches[4]) === true) {
                        $varSpace       = strlen($matches[3]);
                        $comment        = $matches[4];
                        $commentLines[] = array(
                            'comment' => $comment,
                            'token'   => ($tag + 2),
                            'indent'  => $varSpace,
                        );

                        // Any strings until the next tag belong to this comment.
                        if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
                            $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
                        } else {
                            $end = $tokens[$commentStart]['comment_closer'];
                        }

                        for ($i = ($tag + 3); $i < $end; $i++) {
                            if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
                                $indent = 0;
                                if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
                                    $indent = strlen($tokens[($i - 1)]['content']);
                                }

                                $comment       .= ' '.$tokens[$i]['content'];
                                $commentLines[] = array(
                                    'comment' => $tokens[$i]['content'],
                                    'token'   => $i,
                                    'indent'  => $indent,
                                );
                            }
                        }
                    } else {
//                        $error = 'Missing parameter comment';
//                        $phpcsFile->addError($error, $tag, 'MissingParamComment');
//                        $commentLines[] = array('comment' => '');
                    }//end if
                } else {
                    $error = 'Missing parameter name';
                    $phpcsFile->addError($error, $tag, 'MissingParamName');
                }//end if
            } else {
                $error = 'Missing parameter type';
                $phpcsFile->addError($error, $tag, 'MissingParamType');
            }//end if

            $params[] = array(
                'tag'          => $tag,
                'type'         => $type,
                'var'          => $var,
                'comment'      => $comment,
                'commentLines' => $commentLines,
                'type_space'   => $typeSpace,
                'var_space'    => $varSpace,
            );
        }//end foreach

        $realParams  = $phpcsFile->getMethodParameters($stackPtr);
        $foundParams = array();

        foreach ($params as $pos => $param) {
            // If the type is empty, the whole line is empty.
            if ($param['type'] === '') {
                continue;
            }

            // Check the param type value.
            $typeNames = explode('|', $param['type']);
            foreach ($typeNames as $typeName) {
                $suggestedName = PHP_CodeSniffer::suggestType($typeName);
                if ($suggestedName === 'boolean') {
                    $suggestedName = 'bool';
                } elseif ($suggestedName === 'integer') {
                    $suggestedName = 'int';
                }
                if ($typeName !== $suggestedName) {
                    $error = 'Expected "%s" but found "%s" for parameter type';
                    $data  = array(
                        $suggestedName,
                        $typeName,
                    );

                    $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data);
                    if ($fix === true) {
                        $content  = $suggestedName;
                        $content .= str_repeat(' ', $param['type_space']);
                        $content .= $param['var'];
                        $content .= str_repeat(' ', $param['var_space']);
                        if (isset($param['commentLines'][0]) === true) {
                            $content .= $param['commentLines'][0]['comment'];
                        }

                        $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
                    }
                } else if (count($typeNames) === 1) {
                    // Check type hint for array and custom type.
                    $suggestedTypeHint = '';
                    if (strpos($suggestedName, 'array') !== false || strpos($suggestedName, '[]') !== false) {
                        $suggestedTypeHint = 'array';
                    } else
                    if (strpos($suggestedName, 'callable') !== false) {
                        $suggestedTypeHint = 'callable';
                    } else if (in_array($typeName, PHP_CodeSniffer::$allowedTypes) === false) {
                        $suggestedTypeHint = $suggestedName;
                    }

                    if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) {
                        $typeHint = $realParams[$pos]['type_hint'];
                        if ($typeHint !== '' && $typeHint !== substr($suggestedTypeHint, (strlen($typeHint) * -1))) {
                            $error = 'Expected type hint "%s"; found "%s" for %s';
                            $data  = array(
                                $suggestedTypeHint,
                                $typeHint,
                                $param['var'],
                            );
                            $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data);
                        }
                    } else if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) {
                        $typeHint = $realParams[$pos]['type_hint'];
                        if ($typeHint !== '') {
                            $error = 'Unknown type hint "%s" found for %s';
                            $data  = array(
                                $typeHint,
                                $param['var'],
                            );
                            $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data);
                        }
                    }//end if
                }//end if
            }//end foreach

            if ($param['var'] === '') {
                continue;
            }

            $foundParams[] = $param['var'];

            // Check number of spaces after the type.
            $spaces = ($maxType - strlen($param['type']) + 1);
            if ($param['type_space'] !== $spaces) {
                $error = 'Expected %s spaces after parameter type; %s found';
                $data  = array(
                    $spaces,
                    $param['type_space'],
                );

                $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
                if ($fix === true) {
                    $phpcsFile->fixer->beginChangeset();

                    $content  = $param['type'];
                    $content .= str_repeat(' ', $spaces);
                    $content .= $param['var'];
                    $content .= str_repeat(' ', $param['var_space']);
                    $content .= $param['commentLines'][0]['comment'];
                    $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);

                    // Fix up the indent of additional comment lines.
                    foreach ($param['commentLines'] as $lineNum => $line) {
                        if ($lineNum === 0
                            || $param['commentLines'][$lineNum]['indent'] === 0
                        ) {
                            continue;
                        }

                        $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space']);
                        $phpcsFile->fixer->replaceToken(
                            ($param['commentLines'][$lineNum]['token'] - 1),
                            str_repeat(' ', $newIndent)
                        );
                    }

                    $phpcsFile->fixer->endChangeset();
                }//end if
            }//end if

            // Make sure the param name is correct.
            if (isset($realParams[$pos]) === true) {
                $realName = $realParams[$pos]['name'];
                if ($realName !== $param['var']) {
                    $code = 'ParamNameNoMatch';
                    $data = array(
                        $param['var'],
                        $realName,
                    );

                    $error = 'Doc comment for parameter %s does not match ';
                    if (strtolower($param['var']) === strtolower($realName)) {
                        $error .= 'case of ';
                        $code   = 'ParamNameNoCaseMatch';
                    }

                    $error .= 'actual variable name %s';

                    $phpcsFile->addError($error, $param['tag'], $code, $data);
                }
            } else if (substr($param['var'], -4) !== ',...') {
                // We must have an extra parameter comment.
                $error = 'Superfluous parameter comment';
                $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment');
            }//end if

            if ($param['comment'] === '') {
                continue;
            }

            // Check number of spaces after the var name.
            $spaces = ($maxVar - strlen($param['var']) + 1);
            if ($param['var_space'] !== $spaces) {
                $error = 'Expected %s spaces after parameter name; %s found';
                $data  = array(
                    $spaces,
                    $param['var_space'],
                );

                $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data);
                if ($fix === true) {
                    $phpcsFile->fixer->beginChangeset();

                    $content  = $param['type'];
                    $content .= str_repeat(' ', $param['type_space']);
                    $content .= $param['var'];
                    $content .= str_repeat(' ', $spaces);
                    $content .= $param['commentLines'][0]['comment'];
                    $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);

                    // Fix up the indent of additional comment lines.
                    foreach ($param['commentLines'] as $lineNum => $line) {
                        if ($lineNum === 0
                            || $param['commentLines'][$lineNum]['indent'] === 0
                        ) {
                            continue;
                        }

                        $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['var_space']);
                        $phpcsFile->fixer->replaceToken(
                            ($param['commentLines'][$lineNum]['token'] - 1),
                            str_repeat(' ', $newIndent)
                        );
                    }

                    $phpcsFile->fixer->endChangeset();
                }//end if
            }//end if

            // Param comments must start with a capital letter and end with the full stop.
            $firstChar = $param['comment']{0};
            if (preg_match('|\p{Lu}|u', $firstChar) === 0) {
                $error = 'Parameter comment must start with a capital letter';
                $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital');
            }

            $lastChar = substr($param['comment'], -1);
            if ($lastChar !== '.') {
                $error = 'Parameter comment must end with a full stop';
                $phpcsFile->addError($error, $param['tag'], 'ParamCommentFullStop');
            }
        }//end foreach

        //ONGR Ignore doc tag rules if it contains only {@inheritdoc}.
        $inheritDocMatches = false;
        for ($i = $commentStart; $i < $tokens[$commentStart]['comment_closer']; $i++) {
            if ($tokens[$i]['content'] === '{@inheritdoc}') {
                $inheritDocMatches = true;
            }
        }
        // Report missing comments.
        if (!$inheritDocMatches) {
            $realNames = array();
            foreach ($realParams as $realParam) {
                $realNames[] = $realParam['name'];
            }

            $diff = array_diff($realNames, $foundParams);
            foreach ($diff as $neededParam) {
                $error = 'Doc comment for parameter "%s" missing';
                $data  = array($neededParam);
                $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data);
            }
        }

    }//end processParams()

}//end class


<?php

namespace beacon;

/**
 * Created by PhpStorm.
 * User: wj008
 * Date: 2017/12/11
 * Time: 20:40
 */
abstract class Controller
{

    /**
     * 获取视图
     * @return View|\sdopx\Sdopx
     */
    protected function view()
    {
        return View::instance();
    }

    /**
     * 注册变量
     * @param $key
     * @param null $val
     */
    protected function assign($key, $val = null)
    {
        $this->view()->assign($key, $val);
    }

    /**
     * 获取已注册参数
     * @return array|mixed|null
     */
    protected function getAssign()
    {
        $data = $this->view()->getAssign();
        unset($data['this']);
        return $data;
    }

    /**
     * 输出显示
     * @param string $tplName
     * @param string|null $parent
     */
    protected function display(string $tplName, string $parent = null)
    {
        $this->view()->context($this);
        $this->setContentType('html');
        if ($parent !== null) {
            return $this->view()->display($this, 'extends:' . $parent . '|' . $tplName);
        }
        return $this->view()->display($tplName);
    }

    /**
     * 获取内容
     * @param string $tplname
     * @param string|null $parent
     * @return mixed|string|void
     */
    protected function fetch(string $tplname, string $parent = null)
    {
        $this->view()->context($this);
        if ($parent !== null) {
            return $this->view()->fetch('extends:' . $parent . '|' . $tplname);
        }
        return $this->view()->fetch($tplname);
    }

    /**
     * 设置跳转
     * @param string $url
     * @param array $query
     */
    protected function redirect(string $url, array $query = [])
    {
        $url = empty($url) ? '/' : $url;
        $url = Route::url($url, $query);
        Request::setHeader('Location', $url);
        exit;
    }

    /**
     * @param $error string|array
     * @param $option array :data code back template 等
     * 如 ['data'=>$mydata,'back'=>'/index','code'=>33,'template'=>'myerror.tpl']
     */
    public function error($error, array $option = [])
    {
        $option['status'] = false;
        if (is_array($error)) {
            $option['formError'] = $error;
            reset($error);
            $option['msg'] = current($error);
            $option['msg'] = $option['msg'] == null ? '错误' : $option['msg'];
        } else {
            $option['msg'] = $error;
        }
        if ($this->isAjax() || $this->getContentType() == 'application/json' || $this->getContentType() == 'text/json') {
            echo json_encode($option, JSON_UNESCAPED_UNICODE);
            exit;
        }
        if (empty($option['back'])) {
            $option['back'] = $this->getReferrer();
        }
        $this->assign('info', $option);
        if (!empty($option['template'])) {
            $this->display($option['template']);
        } else {
            $template = Config::get('beacon.error_template', '@error.tpl');
            $this->display($template);
        }
        exit;
    }

    /**
     * 显示正确信息
     * @param null $message
     * @param array $option :data code back template 等
     * 如 ['data'=>$mydata,'back'=>'/index','code'=>33,'template'=>'myerror.tpl']
     */
    public function success($message = null, array $option = [])
    {
        $option['status'] = true;
        if (!empty($message)) {
            $option['msg'] = $message;
        }
        if ($this->isAjax() || $this->getContentType() == 'application/json' || $this->getContentType() == 'text/json') {
            echo json_encode($option, JSON_UNESCAPED_UNICODE);
            exit;
        }
        if (empty($option['back'])) {
            $back = $this->param('__BACK__');
            if (empty($back)) {
                $back = $this->getReferrer();
            }
            $option['back'] = $back;
        }
        $this->assign('info', $option);
        if (!empty($option['template'])) {
            $this->display($option['template']);
        } else {
            $template = Config::get('beacon.success_template', '@success.tpl');
            $this->display($template);
        }
        exit;
    }

    /**
     * get 获取数据 相当于 $_GET
     * @param string|null $name 键名 也可以指定 返回类型 如  get('name:s')  get('age:i')  支持 a:array  i:int  f:float b:bool
     * @param null $def 默认值
     * @return array|mixed|null
     */
    public function get(string $name = null, $def = null)
    {
        return Request::get($name, $def);
    }

    /**
     * post 获取数据 相当于 $_POST
     * @param string|null $name 键名 也可以指定 返回类型 如  post('name:s')  post('age:i')  支持 a:array  i:int  f:float b:bool
     * @param null $def 默认值
     * @return array|mixed|null
     */
    public function post(string $name = null, $def = null)
    {
        return Request::post($name, $def);
    }

    /**
     * param 获取数据 相当于 $_REQUEST
     * @param string|null $name
     * @param null $def
     * @return array|mixed|null
     */
    public function param(string $name = null, $def = null)
    {
        return Request::param($name, $def);
    }

    /**
     * 获取 session 相当于 $_SESSION[$name]
     * @param string|null $name
     * @param null $def
     * @return null
     */
    public function getSession(string $name = null, $def = null)
    {
        return Request::getSession($name, $def);
    }

    /**
     * 设置 session
     * @param string $name
     * @param $value
     */
    protected function setSession(string $name, $value)
    {
        return Request::setSession($name, $value);
    }

    /**
     * 清空session
     */
    protected function delSession()
    {
        return Request::delSession();
    }

    /**
     * 获取cookie
     * @param string $name
     * @param null $def
     * @return null
     */
    public function getCookie(string $name, $def = null)
    {
        return Request::getCookie($name, $def);
    }

    /**
     * 设置cookie
     * @param string $name
     * @param $value
     * @param $options
     * @return bool
     */
    protected function setCookie(string $name, $value, $options)
    {
        return Request::setCookie($name, $value, $options);
    }

    /**
     * 获取文件
     * @param string|null $name
     * @return null
     */
    protected function file(string $name = null)
    {
        return Request::file($name);
    }

    /**
     * 获取路由
     * @param string|null $name 支持 ctl:控制器名  act:方法名  app:应用名
     * @param null $def
     * @return null
     */
    public function route(string $name = null, $def = null)
    {
        return Request::route($name, $def);
    }

    /**
     * 获取请求头
     * @param string|null $name
     * @return array|mixed|null|string
     */
    public function getHeader(string $name = null)
    {
        return Request::getHeader($name);
    }

    /**
     * 设置请求头
     * @param string $name
     * @param string $value
     * @param bool $replace
     * @param null $http_response_code
     */
    protected function setHeader(string $name, string $value, bool $replace = true, $http_response_code = null)
    {
        return Request::setHeader($name, $value, $replace, $http_response_code);
    }

    /**
     * 获取IP
     * @param bool $proxy
     * @param bool $forward
     * @return array|mixed|null|string
     */
    protected function getIP(bool $proxy = false, bool $forward = false)
    {
        return Request::getIP($proxy, $forward);
    }

    /**
     * 获取 内容类型
     * @param bool $whole
     * @return array|mixed|null|string
     */
    protected function getContentType(bool $whole = false)
    {
        return Request::getContentType($whole);
    }

    /**
     * 设置内容类型
     * @param string $type
     * @param string $encoding
     */
    protected function setContentType(string $type, string $encoding = 'utf-8')
    {
        return Request::setContentType($type, $encoding);
    }

    /**
     * 获取配置项
     * @param string $name
     * @param null $def
     * @return mixed|string
     */
    protected function config(string $name, $def = null)
    {
        return Request::config($name, $def);
    }

    /**
     * 根据模板补丁修正输出数据
     * @param string $tplName
     * @param array|null $list
     * @param array $origFields
     * @param string $assign
     * @return array
     */
    protected function hook(string $tplName, array $list = null, array $origFields = [], string $assign = 'rs')
    {
        if ($list == null) {
            $data = $this->getAssign();
            $list = (isset($data['list']) && is_array($data['list'])) ? $data['list'] : [];
        }
        if (!isset($list[0])) {
            return $list;
        }
        $this->fetch($tplName);
        $retList = [];
        $view = $this->view();
        $hookFunc = $view->getHook();
        foreach ($list as $item) {
            $column = [];
            foreach ($origFields as $key) {
                if (isset($item[$key])) {
                    $column[$key] = isset($item[$key]) ? $item[$key] : null;
                }
            }
            foreach ($hookFunc as $key => $func) {
                $column[$key] = call_user_func($func, [$assign => $item]);
            }
            $retList[] = $column;
        }
        return $retList;
    }

    /**
     * 是否get请求
     * @return bool
     */
    protected function isGet()
    {
        return Request::isGet();
    }

    /**
     * 判断请求方式
     * @param string $method
     * @return bool
     */
    protected function isMethod(string $method)
    {
        return Request::isMethod($method);
    }

    /**
     * 获取请求方式
     * @return string
     */
    public function getMethod()
    {
        return Request::getMethod();
    }

    /**
     * 是否post 请求
     * @return bool
     */
    public function isPost()
    {
        return Request::isPost();
    }

    /**
     * 是否ajax
     * @return bool
     */
    public function isAjax()
    {
        return Request::isAjax();
    }

    /**
     * 获取来源页
     * @return array|mixed|null|string
     */
    public function getReferrer()
    {
        return Request::getReferrer();
    }
}


<?php

namespace beacon;

/**
 * Created by PhpStorm.
 * User: wj008
 * Date: 2017/12/11
 * Time: 1:43
 */
if (!defined('ROOT_DIR')) {
    die('未定义ROOT_DIR目录');
}
set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
    if (0 === error_reporting()) {
        return false;
    }
    throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});

defined('DEV_DEBUG') or define('DEV_DEBUG', false);
defined('DEBUG_LOG') or define('DEBUG_LOG', false);
defined('IS_CGI') or define('IS_CGI', substr(PHP_SAPI, 0, 3) == 'cgi' ? true : false);
defined('IS_CLI') or define('IS_CLI', PHP_SAPI == 'cli' ? true : false);
defined('IS_WIN') or define('IS_WIN', strstr(PHP_OS, 'WIN') ? true : false);

class RouteError extends \Exception
{
}

class Route
{

    private static $cacheUris = null;
    private static $routeMap = [];
    private static $routePath = null;
    private static $cachePath = null;
    private static $route = null;

    private static $ctlPrefix = [];
    private static $rethrowFunc = null;

    /**
     * 设置路由配置文件路径
     * @param string $path
     */
    public static function setRoutePath(string $path)
    {
        self::$routePath = Utils::path($path);
    }

    /**
     * 注册路由规则
     * @param string $name
     * @param array|bool $route
     */
    public static function register(string $name, $route = null)
    {
        if (empty($name)) {
            return;
        }
        //默认数据
        $map = [
            'path' => 'app/' . $name,
            'namespace' => 'app\\' . $name,
            'base' => '/' . ($name == 'home' ? '' : $name),
            'rules' => [
                '@^/(\w+)/(\w+)/(\d+)$@i' => [
                    'ctl' => '$1',
                    'act' => '$2',
                    'id' => '$3',
                ],
                '@^/(\w+)/(\d+)$@i' => [
                    'ctl' => '$1',
                    'act' => 'index',
                    'id' => '$2',
                ],
                '@^/(\w+)/(\w+)$@i' => [
                    'ctl' => '$1',
                    'act' => '$2',
                ],
                '@^/(\w+)/?$@i' => [
                    'ctl' => '$1',
                    'act' => 'index',
                ],
                '@^/$@' => [
                    'ctl' => 'index',
                    'act' => 'index',
                ],
            ],
            'resolve' => function ($ctl, $act, $keys = []) {
                $url = '/{ctl}';
                if (!empty($act) && $act != 'index') {
                    $url .= '/{act}';
                }
                if (isset($keys['id'])) {
                    $url .= '/{id}';
                }
                return $url;
            }
        ];

        if ($route === null) {
            if (empty(self::$routePath)) {
                self::$routePath = Utils::path(ROOT_DIR, 'config');
            }
            $filePath = Utils::path(self::$routePath, $name . '.route.php');
            if (file_exists($filePath)) {
                $map = require $filePath;
            }
        } else if (is_array($route)) {
            $map = array_merge($map, $route);
        }
        $map['name'] = $name;
        $map['base'] = rtrim(empty($map['base']) ? '' : $map['base'], '/');
        $map['base_match'] = '@^' . preg_quote($map['base'], '@') . '(/.*)?$@i';
        $map['path'] = Utils::trimPath($map['path']);
        self::$routeMap[$name] = $map;
    }

    /**
     * 提取uri
     * @param string $url
     * @return mixed|null
     */
    private static function matchUrl(string $url)
    {
        uasort(self::$routeMap, function ($a, $b) {
            if (strlen($a['base']) == strlen($b['base'])) {
                return 0;
            }
            return strlen($a['base']) > strlen($b['base']) ? -1 : 1;
        });
        foreach (self::$routeMap as $name => $item) {
            if (preg_match($item['base_match'], $url, $m)) {
                $item['uri'] = empty($m[1]) ? '' : $m[1];
                $item['uri'] = preg_replace('@^/index\.php@i', '/', $item['uri']);
                return $item;
            }
        }
        return null;
    }

    /**
     * 获取路由解析数据
     * @param null $name
     * @return null
     */
    public static function get($name = null)
    {
        if (self::$route == null) {
            return null;
        }
        if ($name == null) {
            return self::$route;
        }
        if (isset(self::$route[$name])) {
            return self::$route[$name];
        }
        return null;
    }

    /**
     * 解析URL路径
     * @param string $url
     * @return array|null
     */
    private static function parseUrl(string $url)
    {
        if ('/favicon.ico' == $url) {
            exit;
        }
        $url_temp = parse_url($url);
        $url = $url_temp['path'];
        if (preg_match('@\.json$@i', $url)) {
            $_SERVER['REQUEST_AJAX'] = true;
            $url = preg_replace('@\.json$@i', '', $url);
        }
        $iData = self::matchUrl($url);

        if ($iData == null) {
            return null;
        }
        //路由路径
        $uri = empty($iData['uri']) ? '/' : $iData['uri'];
        $name = $iData['name'];
        $arg = [
            'app' => $name,
            'base' => $iData['base'],
            'ctl' => '',
            'act' => '',
        ];
        if (!isset($iData['rules']) || !is_array($iData['rules'])) {
            return null;
        }
        foreach ($iData['rules'] as $preg => $item) {
            if (preg_match($preg, $uri, $m)) {
                if (!is_array($item)) {
                    continue;
                }
                foreach ($item as $key => $val) {
                    $temp = null;
                    if (is_string($val)) {
                        $temp = preg_replace_callback('@\$(\d+)@', function ($m2) use ($m) {
                            return isset($m[$m2[1]]) ? $m[$m2[1]] : '';
                        }, $val);
                    } elseif (is_array($val)) {
                        $temp = preg_replace_callback('@\$(\d+)@', function ($m2) use ($m, $val) {
                            return isset($m[$m2[1]]) ? $m[$m2[1]] : $val['def'];
                        }, $val['map']);
                    } else {
                        continue;
                    }
                    $arg[$key] = $temp;
                }
                break;
            }
        }
        if (empty($arg['ctl'])) {
            return null;
        }
        $arg['ctl'] = strtolower($arg['ctl']);
        $arg['act'] = strtolower($arg['act']);
        foreach ($arg as $key => $val) {
            if (in_array($key, ['act', 'ctl', 'base', 'app'])) {
                continue;
            }
            if (!isset($_GET[$key])) {
                $_GET[$key] = $val;
            }
            if (!isset($_REQUEST[$key])) {
                $_REQUEST[$key] = $val;
            }
        }
        self::$route = $arg;
        return $arg;
    }

    /**
     * 解析URL
     * @param null $url
     * @return null|string
     */
    public static function parse($url = null)
    {
        $request_uri = null;
        if ($url === null) {
            if (IS_CLI) {
                if (isset($_SERVER['argv']) && !empty($_SERVER['argv'][1])) {
                    $request_uri = $_SERVER['REQUEST_URI'] = $_SERVER['argv'][1];
                    self::parseUrl($_SERVER['argv'][1]);
                    $data = parse_url($_SERVER['REQUEST_URI']);
                    if (isset($data['query'])) {
                        parse_str($data['query'], $args);
                        foreach ($args as $key => $val) {
                            $_GET[$key] = $val;
                            $_REQUEST[$key] = $val;
                        }
                    }
                } else {
                    $request_uri = $_SERVER['REQUEST_URI'] = '/';
                    self::parseUrl('/');
                }
            } else {
                if (isset($_SERVER['PATH_INFO'])) {
                    $request_uri = $_SERVER['PATH_INFO'];
                } else {
                    $request_uri = $_SERVER['REQUEST_URI'];
                }
                if (empty($request_uri)) {
                    $request_uri = '/';
                }
                self::parseUrl($request_uri);
            }
        } else {
            $request_uri = $url;
            self::parseUrl($url);
        }

        return $request_uri;
    }

    /**
     * 获取当前应用目录
     * @param string|null $app
     * @return null|string
     */
    public static function getPath(string $app = null)
    {
        if ($app == null) {
            $app = self::get('app');
        }
        if (empty($app)) {
            return null;
        }
        $data = isset(self::$routeMap[$app]) ? self::$routeMap[$app] : [];
        if (empty($data['path'])) {
            return null;
        }
        $path = Utils::path(ROOT_DIR, $data['path']);
        return $path;
    }

    /**
     * 获取当前应用命名空间
     * @param string|null $app
     * @return mixed|null|string
     */
    public static function getNamespace(string $app = null)
    {
        if ($app == null) {
            $app = self::get('app');
        }
        if (empty($app)) {
            return null;
        }
        $data = isset(self::$routeMap[$app]) ? self::$routeMap[$app] : [];
        if (empty($data['path']) && empty($data['namespace'])) {
            return null;
        }
        $namespace = isset($data['namespace']) ? $data['namespace'] : $data['path'];
        $namespace = trim(str_replace(['/', '\\'], '\\', $namespace), '\\');
        return $namespace;
    }

    /**
     * 反解析URL
     * @param string $app
     * @param string $pathname
     * @param array $query
     * @return mixed|string
     */
    public static function resolve(string $app, string $pathname = '', array $query = [])
    {

        if (empty($app)) {
            return '';
        }
        if (!empty($query)) {
            $temp = [];
            foreach ($query as $key => $val) {
                array_push($temp, $key . '={' . $key . '}');
            }
            $hash = $pathname . '?' . join('&', $temp);
        } else {
            $hash = $pathname;
        }
        $hash = isset($hash[80]) ? md5($hash) : $hash;
        if (empty(self::$cachePath)) {
            self::$cachePath = Utils::path(ROOT_DIR, 'runtime');
        }
        $filepath = Utils::path(self::$cachePath, 'route.' . $app . '.cache.php');
        if (self::$cacheUris == null) {
            self::$cacheUris = [];
        }
        if (!isset(self::$cacheUris[$app])) {
            if (file_exists($filepath)) {
                self::$cacheUris[$app] = require $filepath;
            } else {
                self::$cacheUris[$app] = [];
            }
        }
        //使用了缓存
        if (isset(self::$cacheUris[$app][$hash])) {
            $temp_url = self::$cacheUris[$app][$hash];
            $temp_url = preg_replace_callback('@\{(\w+)\}@', function ($m) use ($query) {
                $key = $m[1];
                return isset($query[$key]) ? urlencode($query[$key]) : '';
            }, $temp_url);
            return $temp_url;
        }

        $idata = isset(self::$routeMap[$app]) ? self::$routeMap[$app] : null;
        if ($idata == null) {
            return '';
        }
        $ctl = '';
        $act = '';
        if (!empty($pathname)) {
            if (preg_match('@^\/?(\w+)(?:\/(\w+))?@', $pathname, $mth)) {
                $ctl = Utils::toUnder($mth[1]);
                if (isset($mth[2])) {
                    $act = Utils::toUnder($mth[2]);
                }
            }
        }
        $args = [];
        foreach ($query as $key => $val) {
            $args[$key] = '{' . $key . '}';
        }
        $base = rtrim(empty($idata['base']) ? '' : $idata['base'], '/');
        if (!isset($idata['resolve']) && !is_callable($idata['resolve'])) {
            return '';
        }
        $out_url = '';
        $info = $idata['resolve']($ctl, $act, $args);
        if (is_string($info)) {
            $out_url = preg_replace_callback('@\{(ctl|act)\}@', function ($m) use ($ctl, $act) {
                if ($m[1] == 'ctl') {
                    return $ctl;
                }
                if ($m[1] == 'act') {
                    return $act;
                }
            }, $info);
            if (preg_match_all('@\{(\w+)\}@', $out_url, $mts)) {
                foreach ($mts[1] as $mt) {
                    $key = $mt;
                    unset($args[$key]);
                }
            }
        } elseif (is_array($info)) {
            $out_url = preg_replace_callback('@\{(ctl|act)\}@', function ($m) use ($ctl, $act) {
                if ($m[1] == 'ctl') {
                    return $ctl;
                }
                if ($m[1] == 'act') {
                    return $act;
                }
            }, $info[0]);
            $args = $info[1];
            if (!isset($info[2]) || $info[2] == false) {
                if (preg_match_all('@\{(\w+)\}@', $out_url, $mts)) {
                    foreach ($mts[1] as $mt) {
                        $key = $mt;
                        unset($args[$key]);
                    }
                }
            }
        }
        $queryStr = [];
        foreach ($args as $key => $val) {
            array_push($queryStr, $key . '={' . $key . '}');
        }
        $temp_url = $base . $out_url;
        if (count($queryStr) > 0) {
            $temp_url .= '?' . join('&', $queryStr);
        }
        self::$cacheUris[$app][$hash] = $temp_url;
        @file_put_contents($filepath, '<?php return ' . var_export(self::$cacheUris[$app], true) . ';');
        // echo '创建缓存';
        $temp_url = preg_replace_callback('@\{(\w+)\}@', function ($m) use ($query) {
            $key = $m[1];
            return isset($query[$key]) ? urlencode($query[$key]) : '';
        }, $temp_url);
        return $temp_url;
    }

    /**
     * 获取生成的URL
     * ~/ctl/act
     * ^/admin/ctl/act
     * @param string $url
     * @param array $query
     * @return bool|mixed|null|string
     */
    public static function url($url = null, array $query = [])
    {
        if (is_array($url)) {
            $url['app'] = isset($url['app']) ? $url['app'] : self::get('app');
            $url['ctl'] = isset($url['ctl']) ? $url['ctl'] : self::get('ctl');
            $url['act'] = isset($url['act']) ? $url['act'] : self::get('act');
            $temp = '^/' . $url['app'] . '/' . $url['ctl'] . '/' . $url['act'];
            unset($url['app']);
            unset($url['ctl']);
            unset($url['act']);
            if (!empty($url) && is_array($query)) {
                $query = array_merge($url, $query);
            }
            $url = $temp;
        }
        if (!is_string($url)) {
            return $url;
        }
        $innerUri = (isset($url[1]) && ($url[0] == '~' || $url[0] == '^') && $url[1] == '/');
        if (!$innerUri) {
            if ($query == null || count($query) == 0) {
                return $url;
            }
        }
        $info = parse_url($url);
        $path = isset($info['path']) ? $info['path'] : '';
        $str_query = isset($info['query']) ? $info['query'] : '';
        $query = is_array($query) ? $query : [];
        //合并参数
        if (!empty($str_query)) {
            $temp = [];
            parse_str($str_query, $temp);
            $query = array_merge($temp, $query);
        }
        if (!$innerUri) {
            $str_query = http_build_query($query);
            if (!empty($str_query)) {
                return $path . '?' . $str_query;
            }
            return $path;
        }
        if ($url[0] == '~') {
            $app = self::get('app');
            $path = substr($path, 1);
            return self::resolve($app, $path, $query);
        }
        if (!preg_match('@^\^/(\w+)((?:/\w+){1,2})?$@', $path, $data)) {
            return $url;
        }
        $app = isset($data[1]) ? $data[1] : self::get('app');
        $path = isset($data[2]) ? $data[2] : self::get('path');
        $path = empty($path) ? '/' : $path;
        return self::resolve($app, $path, $query);
    }

    /**
     * 运行实例的方法
     * @param string $class
     * @param string $method
     * @throws RouteError
     * @throws \ReflectionException
     */
    public static function runMethod(string $class, string $method)
    {
        $oReflectionClass = new \ReflectionClass($class);
        //获取方法信息
        $method = $oReflectionClass->getMethod($method);
        if (!$method->isPublic()) {
            throw new RouteError('未公开方法:' . $method);
        }
        //获取方法参数
        $params = $method->getParameters();
        $args = [];
        if (count($params) > 0) {
            foreach ($params as $param) {
                //获取变量名称
                $name = $param->getName();
                $type = 'any';
                //如果可以获取类型
                if (is_callable([$param, 'hasType'])) {
                    if ($param->hasType()) {
                        $refType = $param->getType();
                        if ($refType != null) {
                            if (is_callable([$refType, 'getName'])) {
                                $type = $refType->getName();
                            } else {
                                $type = strval($refType);
                            }
                            $type = empty($type) ? 'any' : $type;
                        }
                    }
                }
                //类型获取不到
                if ($type == 'any') {
                    if (is_callable([$param, 'getClass'])) {
                        $refType = $param->getClass();
                        if ($refType != null) {
                            if (is_callable([$refType, 'getName'])) {
                                $type = $refType->getName();
                            } else {
                                $type = strval($refType);
                            }
                            $type = empty($type) ? 'any' : $type;
                        }
                    }
                }
                //默认值
                $def = null;
                //如果有默认值,从默认值中获取类型
                if ($param->isOptional()) {
                    $def = $param->getDefaultValue();
                    if ($type == 'any') {
                        $type = gettype($def);
                    }
                }
                switch ($type) {
                    case 'bool':
                    case 'boolean':
                        $args[] = Request::param($name . ':b', $def);
                        break;
                    case 'int':
                    case 'integer':
                        $val = Request::param($name . ':s', $def);
                        //如果默认值是整数,但是传递的值是浮点数
                        if (preg_match('@[+-]?\d*\.\d+@', $val)) {
                            $args[] = Request::param($name . ':f', $def);
                        } else {
                            $args[] = Request::param($name . ':i', $def);
                        }
                        break;
                    case 'double':
                    case 'float':
                        $args[] = Request::param($name . ':f', $def);
                        break;
                    case 'string':
                        $args[] = Request::param($name . ':s', $def);
                        break;
                    case 'array':
                        $args[] = Request::param($name . ':a', $def);
                        break;
                    default :
                        $args[] = Request::param($name, $def);
                        break;
                }
            }
        }
        $example = new $class();
        //如果有初始化方法
        if (method_exists($example, 'initialize')) {
            $example->initialize();
        }
        //调用方法
        $out = $method->invokeArgs($example, $args);
        if (Request::getContentType() == 'application/json' || Request::getContentType() == 'text/json') {
            die(json_encode($out, JSON_UNESCAPED_UNICODE));
        } else if (is_array($out)) {
            Request::setContentType('json');
            die(json_encode($out, JSON_UNESCAPED_UNICODE));
        } else if (!empty($out)) {
            Request::setContentType('html');
            echo $out;
        }
    }

    /**
     * 获取映射信息
     * @param string|null $url
     * @return array
     * @throws RouteError
     */
    public static function getMapping(string $url = null)
    {
        $url = self::parse($url);
        if (self::$route == null) {
            throw new RouteError('未初始化路由参数,url:' . $url);
        }
        if (empty(self::$route['app'])) {
            throw new RouteError('路由应用名称 app 为空,url:' . $url);
        }
        if (empty(self::$route['ctl'])) {
            throw new RouteError('路由控制器名称 ctl 为空,url:' . $url);
        }
        if (empty(self::$route['act'])) {
            throw new RouteError('路由方法名称 act 为空,url:' . $url);
        }

        $ctl = Utils::toCamel(self::$route['ctl']);
        $act = Utils::toCamel(self::$route['act']);
        $act = lcfirst($act);
        $appPath = self::getPath();
        if (empty($appPath)) {
            throw new RouteError('没有设置应用目录,url:' . $url);
        }
        //设置当前应用下的配置文件
        $config = Utils::path($appPath, 'config.php');
        if (file_exists($config)) {
            $cfgData = Config::loadFile($config);
            foreach ($cfgData as $key => $val) {
                Config::set($key, $val);
            }
        }
        //开始进入入口---------------------
        $namespace = self::getNamespace();
        return [
            'namespace' => $namespace,
            'classFullName' => $namespace . '\\controller\\' . $ctl,
            'className' => $ctl,
            'method' => $act . 'Action'
        ];
    }

    /**
     * 添加控制器路由前缀
     * @param string $app
     * @param string $prefix
     */
    public static function addCtlPrefix(string $app, string $prefix)
    {
        if (!isset(self::$ctlPrefix[$app])) {
            self::$ctlPrefix[$app] = [];
        }
        self::$ctlPrefix[$app][] = ltrim($prefix, '\\');
    }

    /**
     * 记录运行时间
     */
    public static function recordRunTime()
    {
        if (defined('DEBUG_LOG') && DEBUG_LOG) {
            error_reporting(E_ALL);
            //程序计时---
            if (isset($_SERVER['REQUEST_URI'])) {
                $t1 = microtime(true);
                register_shutdown_function(function () use ($t1) {
                    $t2 = microtime(true);
                    Logger::info('URL:', $_SERVER['REQUEST_URI'], '耗时' . round($t2 - $t1, 3) . '秒');
                });
            }
        }
    }

    /**
     * 运行
     * @param string|null $url
     */
    public static function run(string $url = null)
    {
        self::recordRunTime();
        try {
            $data = self::getMapping($url);
            $app = self::$route['app'];
            if (!class_exists($data['classFullName']) && isset(self::$ctlPrefix[$app])) {
                foreach (self::$ctlPrefix[$app] as $prefix) {
                    $classFullName = $data['namespace'] . '\\' . $prefix . $data['className'];
                    if (class_exists($classFullName)) {
                        $data['classFullName'] = $classFullName;
                        break;
                    }
                }
            }
            self::runMethod($data['classFullName'], $data['method']);
        } catch (RouteError $exception) {
            self::rethrow($exception);
        } catch (\Exception $exception) {
            self::rethrow($exception);
        } catch (\Error $error) {
            self::rethrow($error);
        }
    }

    /**
     * 抛出错误
     * @param \Throwable $exception
     */
    public static function rethrow(\Throwable $exception)
    {

        if (self::$rethrowFunc && is_callable(self::$rethrowFunc)) {
            $ret = call_user_func(self::$rethrowFunc, $exception);
            if ($ret === false) {
                return;
            }
        }
        $out = [];
        $out['status'] = false;
        $out['_code'] = 500;
        //如果开启调试,打印更详细的栈信息.
        if ((defined('DEV_DEBUG') && DEV_DEBUG)) {
            $code = [];
            $code[] = get_class($exception) . ": {$exception->getMessage()}";
            $code[] = $exception->getTraceAsString();
            if (is_callable([$exception, 'getDetail'])) {
                $code[] = "----------------------------------------------------------------------------------------------------------";
                $code[] = $exception->getDetail();
            }
            //开启日志
            if ((defined('DEBUG_LOG') && DEBUG_LOG)) {
                Logger::error(join("\n", $code));
            }
            $out['_stack'] = explode("\n", join("\n", $code));
            if ($exception instanceof RouteError) {
                $out['msg'] = '404 页面没有找到:' . $exception->getMessage();
            } else {
                $out['msg'] = '数据出现异常:' . $exception->getMessage();
            }
        } else {
            if ($exception instanceof RouteError) {
                $out['msg'] = '404 页面没有找到!';
            } else {
                $out['msg'] = '数据出现异常,请稍后再试.';
            }
        }
        if (Request::isAjax()) {
            Request::setContentType('json');
            die(json_encode($out, JSON_UNESCAPED_UNICODE));
        }
        //输出错误页面--------
        Request::setContentType('html');
        $view = new View();
        $view->assign('info', $out);
        $template = Config::get('beacon.exception_template', '@exception.tpl');
        $view->display($template);
    }

    /**
     * @param \Closure $func
     */
    public static function extendRethrow(\Closure $func)
    {
        self::$rethrowFunc = $func;
    }
}


<?php
/**
 * This file is part of the Koriym.DevPdoStatement
 *
 * @license http://opensource.org/licenses/MIT MIT
 */
declare(strict_types=1);

namespace Koriym\DevPdoStatement;

class Logger implements LoggerInterface
{
    /**
     * EXPLAIN
     *
     * @var array
     */
    public $explain;

    /**
     * SHOW WARNINGS
     *
     * @var array
     */
    public $warnings;

    /**
     * {@inheritdoc}
     */
    public function logQuery($query, $time, array $explain, array $warnings)
    {
        $log = sprintf('time:%s query: %s', $time, $query);
        error_log($log);
        $this->explain = $explain;
        $this->warnings = $warnings;
        if ($warnings) {
            error_log('warnings:' . (string) json_encode($warnings, JSON_PRETTY_PRINT));
            error_log('explain :' . (string) json_encode($explain, JSON_PRETTY_PRINT));
        }
    }
}


<?php

// http://php.net/manual/en/function.debug-backtrace.php

namespace CarbonPHP\Error;

use CarbonPHP\CarbonPHP;
use CarbonPHP\Database;

/**
 * Class ErrorCatcher
 * @package CarbonPHP\Error
 *
 * Provide a global error and exception handler.
 *
 */
class ErrorCatcher
{
    /**
     * @var string TODO - re-setup logs saving to files
     */
    public static $defaultLocation;
    /**
     * @var bool $printToScreen determine if a generated error log should be shown on the browser.
     * This value can be set using the ["ERROR"]["SHOW"] configuration option
     */
    public static $printToScreen;
    /**
     * @var bool
     */
    public static $fullReports;
    /**
     * @var bool
     */
    public static $storeReport;

    /**
     * @var int to be used with error_reporting()
     * @link http://php.net/manual/en/function.error-reporting.php
     */
    public static $level;

    /**
     * ErrorCatcher constructor.
     */
    public static function start(): void     // TODO - not this.
    {
        ini_set('display_errors', 1);
        ini_set('track_errors', 1);
        error_reporting(self::$level);

        if (TEST) {
            return;
        }

        /**
         * if you return true from here it will continue script execution from the point of the error..
         * this is not wanted. Die and Exit are equivalent in execution. I ran across a post once that
         * explained how die should signify a complete error with no resolution, while exit has resolution
         * returning 1 rather than 0 in both cases will signify an error occurred
         * @param array $argv
         * @internal param TYPE_NAME $closure
         */
        $closure = function (...$argv) {
            static $count;

            if (empty($count)) {
                $count = 0;
            }
            $count++;

            self::generateLog($argv);

            if (!SOCKET && !APP_LOCAL && CarbonPHP::$setupComplete) {     // TODO - do we really want to reset?
                if ($count > 1) {
                    print 'A recursive error has occurred in (or at least affecting) your $app->defaultRoute();';
                    die(1);
                }
                startApplication(true);
                exit(1);
            }

            /** @noinspection ForgottenDebugOutputInspection
             * TODO - fix this? */
            print_r($argv);
            print "\n\nCarbonPHP Caught This Error.\n\n";
            die(1);

        };
        set_error_handler($closure);
        set_exception_handler($closure);
    }

    /** Generate a full error log consisting of a
     * @param \Throwable|array|null $e
     * @param string $level
     * @return string
     * @internal param $argv
     */
    public static function generateLog($e = null, string $level = 'log'): string
    {
        if (ob_get_status()) {
            // Attempt to remove any previous in-progress output buffers
            ob_end_clean();
        }

        ob_start(null, null, PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_FLUSHABLE | PHP_OUTPUT_HANDLER_REMOVABLE);     // start a new buffer for saving errors

        if ($e instanceof \Throwable) {
            $trace = self::generateCallTrace($e);
            if (!$e instanceof PublicAlert) {
                print '(set_error_handler || set_exception_handler) caught this error. #Bubbled up#' . PHP_EOL;
            } else {
                print 'Public Alert Thrown!' . PHP_EOL;
            }
            print PHP_EOL . $e->getMessage() . PHP_EOL;

        } else {
            $trace = self::generateCallTrace();

            if (\is_array($e) && \count($e) >= 4) {
                print PHP_EOL . 'Carbon caught this Message: ' . $e[1] . PHP_EOL . 'line: ' . $e[2] . '(' . $e[3] . ')' . PHP_EOL;
            }
        }

        print PHP_EOL . $trace . PHP_EOL;

        $output = ob_get_contents();

        ob_end_clean();

        /** @noinspection ForgottenDebugOutputInspection */
        error_log($output);

        if (self::$printToScreen) {
            print "
You have the print to screen Error Catching option turned on!

 Turn it off to suppress this reporting.

$output
";
        }

        if (self::$storeReport === true || self::$storeReport === 'file') {       // TODO - store to file?
            if (!is_dir(REPORTS) && !mkdir($concurrentDirectory = REPORTS) && !is_dir($concurrentDirectory)) {
                PublicAlert::danger('Failed Storing Log');
            } else {
                $file = fopen(REPORTS . '/Log_' . time() . '.log', 'ab');

                if (!\is_resource($file) || !fwrite($file, $output)) {
                    PublicAlert::danger('Failed Storing Log');
                }
                fclose($file);
            }
        }

        if (self::$storeReport === true || self::$storeReport === 'database') {       // TODO - store to file?
            $sql = 'INSERT INTO carbon_reports (log_level, report, call_trace) VALUES (?, ?, ?)';
            $sql = Database::database()->prepare($sql);
            if (!$sql->execute([$level, $output, $trace])) {
                print 'Failed to store error log, nothing works... Why does nothing work?' and die(1);
            }
        }

        return $output;
    }

    /** A simplified back trace for quickly identifying route.
     * reverse array to make steps line up chronologically
     * @link http://php.net/manual/en/function.debug-backtrace.php
     * @param \Throwable $e
     * @return string
     */
    public static function generateCallTrace(\Throwable $e = null): string
    {
        ob_start();
        if (null === $e) {
            $e = new \Exception();
            $trace = explode("\n", $e->getTraceAsString());
            $args = array_reverse($e->getTrace());
            $trace = array_reverse($trace);
            array_shift($trace); // remove {main}
            array_pop($args); // remove call to this method
            array_pop($args); // remove call to this method
            array_pop($trace); // remove call to this method
            array_pop($trace); // remove call to this method
        } else {
            $trace = explode("\n", $e->getTraceAsString());
            $args = array_reverse($e->getTrace());
            $trace = array_reverse($trace);
            array_shift($trace); // remove {main}
        }

        $length = \count($trace);
        $result = array();
        for ($i = 0; $i < $length; $i++) {
            $result[] = ($i + 1) . ') ' . implode(' (', explode('(', substr($trace[$i], strpos($trace[$i], ' ')))) . PHP_EOL . "\t\t\t\t" . json_encode($args[$i]['args']) . PHP_EOL;
        }
        print "\n\t" . implode("\n\t", $result);

        $output = ob_get_contents();
        ob_end_clean();
        return $output;
    }

}


<?php
/**
 * Created by PhpStorm.
 * User: wj008
 * Date: 2018/2/25
 * Time: 5:54
 */

namespace beacon;

class SqlItem
{
    public $sql = '';
    public $args = null;

    public function __construct(string $sql, $args = null)
    {
        $this->sql = trim($sql);
        if ($args === null || (is_array($args) && count($args) == 0)) {
            return;
        }
        if (!is_array($args)) {
            $this->args = [$args];
        } else {
            $this->args = $args;
        }
    }

    public function add(string $sql, $args = null)
    {
        $this->sql .= ' ' . $sql;
        if ($args === null || (is_array($args) && count($args) == 0)) {
            return $this;
        }
        if (!is_array($args)) {
            $args = [$args];
        }
        if ($this->args == null) {
            $this->args = $args;
        } else {
            $this->args = array_merge($this->args, $args);
        }
        return $this;
    }

}

class SqlCondition
{
    const WITHOUT_EMPTY = 0;
    const WITHOUT_NULL = 1;
    const WITHOUT_ZERO_LENGTH = 2;
    const WITHOUT_ZERO = 3;
    /**
     * @var SqlItem[]
     */
    private $items = [];
    public $type = 'and';

    public function __construct(string $type = 'and')
    {
        $this->type = 'and';
    }

    /**
     * 查询条件
     * @param null $sql
     * @param null $args
     * @return $this
     */
    public function where($sql = null, $args = null)
    {
        if ($sql === null) {
            return $this;
        }
        if ($sql instanceof SqlCondition) {
            $frame = $sql->getFrame();
            if (!empty($frame['sql'])) {
                if (preg_match('@^(AND|OR)\s+@i', $frame['sql'])) {
                    $frame['sql'] = preg_replace('@^(AND|OR)\s+@i', '', $frame['sql']);
                }
                if ($frame['type'] !== '') {
                    $this->items[] = new SqlItem($frame['type'] . ' (' . $frame['sql'] . ')', $frame['args']);
                } else {
                    $this->items[] = new SqlItem('(' . $frame['sql'] . ')', $frame['args']);
                }
            }
            return $this;
        } elseif (is_array($sql) && $args === null) {
            foreach ($sql as $key => $value) {
                if (strpos($key, '?') === false) {
                    $this->where($key . '=?', $value);
                } else {
                    $this->where($key, $value);
                }
            }
            return $this;
        }
        if (!is_string($sql)) {
            return $this;
        }
        $sql = trim($sql);
        if (!isset($sql[0])) {
            return $this;
        }
        $item = new SqlItem($sql, $args);
        $this->items[] = $item;
        return $this;
    }

    /**
     * 检索条件,如果值为空 不加入筛选
     * @param string $sql
     * @param $value
     * @param int $type
     * @param string|null $format
     * @return $this
     */
    public function search(string $sql, $value, $type = self::WITHOUT_EMPTY, string $format = null)
    {
        switch ($type) {
            case self::WITHOUT_EMPTY:
                if (empty($value)) {
                    return $this;
                }
                break;
            case self::WITHOUT_NULL:
                if ($value === null) {
                    return $this;
                }
                break;
            case self::WITHOUT_ZERO_LENGTH:
                if ($value === null || strval($value) === '') {
                    return $this;
                }
                break;
            case self::WITHOUT_ZERO:
                if ($value === '0' || (is_numeric($value) && floatval($value) == 0) || $value === 0 || $value === false || $value === null) {
                    return $this;
                }
                break;
            default:
                break;
        }
        if ($format !== null) {
            $value = preg_replace('@\{0\}@', $value);
        }
        $maxCount = substr_count($sql, '?');
        if ($maxCount > 1) {
            $temp = [];
            for ($i = 0; $i < $maxCount; $i++) {
                $temp[] = $value;
            }
            $value = $temp;
        }
        $this->where($sql, $value);
        return $this;
    }

    /**
     * 获取代码帧
     * @return array
     */
    public function getFrame()
    {
        $sqlItems = [];
        $argItems = [];
        foreach ($this->items as $item) {
            $tempSql = $item->sql;
            $tempArgs = $item->args;
            if (preg_match('@^or\s+@i', $tempSql)) {
                if (count($sqlItems) == 0) {
                    $tempSql = preg_replace('@^or\s+@i', '', $tempSql);
                }
            } else if (preg_match('@^and\s+@i', $tempSql)) {
                if (count($sqlItems) == 0) {
                    $tempSql = preg_replace('@^and\s+@i', '', $tempSql);
                }
            } else {
                if (count($sqlItems) >= 0) {
                    $tempSql = 'and ' . $tempSql;
                }
            }
            $sqlItems[] = $tempSql;
            if (is_array($tempArgs)) {
                $argItems = array_merge($argItems, $tempArgs);
            } else if ($tempArgs !== null) {
                $argItems[] = $tempArgs;
            }

        }
        return ['sql' => join(' ', $sqlItems), 'args' => $argItems, 'type' => $this->type];
    }

    public function empty()
    {
        $this->items = [];
    }

}


<?php

namespace CarbonPHP;

class C6 extends Application
{

    /**
     *  constructor.
     * @param null $structure
     * @throws \CarbonPHP\Error\PublicAlert the only
     * way this will throw an error is if you do
     * not define a url using sockets.
     */
    public function __construct($structure = null)
    {
        parent::__construct($structure);

        #if ($_SESSION['id']) {
        ##View::$wrapper = SERVER_ROOT . APP_VIEW . 'Layout/logged-in-layout.php';
        #}

    }

    public function defaultRoute()  // Sockets will not execute this
    {
        View::$forceWrapper = true; // this will hard refresh the wrapper
        return $this->wrap()('Documentation/Home.php');  // don't change how wrap works, I know it looks funny

        /*
        if (!$_SESSION['id']):
        else:
            return MVC('user', 'profile');
        endif;
        */
    }

    /** we dont use this return value for anything
     * @param null $uri
     * @return bool
     * @throws \CarbonPHP\Error\PublicAlert
     */
    public function startApplication($uri = null): bool
    {

        \defined('TEMPLATE') or \define('TEMPLATE', 'node_modules/admin-lte/');

        $this->structure($this->wrap());

        #################################### CarbonPHP Doc
        if ($this->match('Home/', 'Documentation/Home.php')() ||
            $this->match('CarbonPHP', 'Documentation/Introduction.php')() ||
            $this->match('Installation', 'Documentation/Installation.php')() ||
            $this->match('Implementations', 'Documentation/Implementations.php')() ||
            $this->match('Dependencies', 'Documentation/Dependencies.php')() ||
            $this->match('FileStructure', 'Documentation/QuickStart/FileStructure.php')() ||
            $this->match('Environment', 'Documentation/QuickStart/Environment.php')() ||
            $this->match('Options', 'Documentation/QuickStart/Options.php')() ||
            $this->match('Bootstrap', 'Documentation/QuickStart/Bootstrap.php')() ||
            $this->match('Wrapper', 'Documentation/QuickStart/Wrapper.php')() ||
            $this->match('Parallel', 'Documentation/QuickStart/ParallelProcessing.php')() ||
            $this->match('Overview', 'Documentation/PHP/Overview.php')() ||
            $this->match('Entities', 'Documentation/PHP/Entities.php')() ||
            $this->match('Request', 'Documentation/PHP/Request.php')() ||
            $this->match('Route', 'Documentation/PHP/Route.php')() ||
            $this->match('Server', 'Documentation/PHP/Server.php')() ||
            $this->match('Session', 'Documentation/PHP/Session.php')() ||
            $this->match('Singleton', 'Documentation/PHP/Singleton.php')() ||
            $this->match('View', 'Documentation/PHP/View.php')() ||
            $this->match('OSSupport', 'Documentation/PlatformSupport.php')() ||
            $this->match('UpgradeGuide', 'Documentation/PlatformSupport.php')() ||
            $this->match('Support', 'Documentation/Support.php')() ||
            $this->match('License', 'Documentation/License.php')() ||
            $this->match('AdminLTE', 'Documentation/AdminLTE.php')() ||
            $this->match('N00B', 'Documentation/N00B.php')()) {
            return true;
        }

        ###################################### AdminLTE DOC
        if ($this->match('UIElements/{AdminLTE?}', function ($AdminLTE) use ($uri) {

            $AdminLTE = (new Request())->set($AdminLTE)->word();  // must be validated

            if (!$AdminLTE) {
                View::$forceWrapper = true;
                $AdminLTE = 'widgets';
            }

            if (file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . $AdminLTE . '.php') ||
                file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . 'Charts' . DS . $AdminLTE . '.php') ||
                file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . 'Examples' . DS . $AdminLTE . '.php') ||
                file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . 'Forms' . DS . $AdminLTE . '.php') ||
                file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . 'Layout' . DS . $AdminLTE . '.php') ||
                file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . 'Mailbox' . DS . $AdminLTE . '.php') ||
                file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . 'Tables' . DS . $AdminLTE . '.php') ||
                file_exists(SERVER_ROOT . APP_VIEW . $path = 'AdminLTE' . DS . 'UI' . DS . $AdminLTE . '.php')) {

                View::$wrapper = SERVER_ROOT . APP_VIEW . 'AdminLTE/wrapper.php';

                $this->wrap()($path);
            }
        })()) {
            return true;
        }

        return false;
    }
}


<?php
/**
 * Created by IntelliJ IDEA.
 * User: Miles
 * Date: 1/17/17
 * Time: 11:48 AM
 */

namespace CarbonPHP;

use CarbonPHP\Error\PublicAlert;

abstract class Route
{
    use Singleton;                // We use the add method function to bind the closure to the class

    /**
     * @var array $uri will hold an exploded array with the
     * back slash '\' as our delimiter
     */
    public $uri;
    /**
     * @var int $uriLength will hold the current number of fields
     * separated by backslashes
     */
    public $uriLength;
    /**
     * @var bool|string $matched will equal "true" if the
     * current state has not executed a lambda function in response.
     * If a function has been executed the value of matched will be
     * true;
     */
    public $matched = false;             // a bool
    /**
     * @var callable $closure will hold the function to execute if
     * the match function should accept a given path-to-match. Arguments
     * can be specified in the variable length parameter field $argv.
     * See Route.Match(...)
     */
    protected $closure;           // The MVC pattern is currently passes

    /**
     * This function must be implemented by the user to use the Route Class.
     * If no url is provided in $_SERVER['REQUEST_URI'], or $matched is
     * false when this class destructs the default route will be executed.
     * @return mixed
     */
    abstract public function defaultRoute();

    /**
     * Will be run when the active object is destroyed. If the
     * $matched variable is false then our default route will be
     * executed. If matched is set to "true", indicating that
     * no callable has been run, no errors or warnings will be printed.
     * This is by design.
     */
    public function __destruct()
    {
        if ($this->matched) {
            return;
        }
        if (SOCKET) {
            print 'Socket Left Route Class Un-Matched' . PHP_EOL;
            exit(1);
        }

        /*
        if (AJAX) {
            print 'AJAX Left Route Class Un-Matched' . PHP_EOL;
            exit(1);
        }
        */

        $this->matched = true;
        $this->defaultRoute();
    }

    /**
     * Route constructor. If the url is / or null then our
     * default route will be invoked.
     * @param callable|null $structure
     * @throws PublicAlert
     */
    public function __construct(callable $structure = null)
    {
        $this->closure = $structure;
        // This check allows Route to be independent of Carbon/Application, but benefit if we've already initiated
        if (\defined('URI') && !SOCKET) {
            $this->uri = explode('/', trim(URI, '/'));
            $this->uriLength = \count($this->uri);
        } else {
            $this->uri = explode('/', $this->uriLength = trim(urldecode(parse_url(trim(preg_replace('/\s+/', ' ', $_SERVER['REQUEST_URI'])), PHP_URL_PATH)), ' /'));
            $this->uriLength = \substr_count($this->uriLength, '/') + 1; // I need the exploded string
        }

    }

    /**
     * @param string $uri
     */
    public function changeURI(string $uri): void
    {
        $this->uri = explode('/', $uri = trim($uri, '/'));
        $this->uriLength = substr_count($uri, '/') + 1;
        $this->matched = false;
    }

    /** This will return the current value of $this->matched.
     * Added to allow us to quickly check the status using the
     * (string) type cast
     * @return bool
     */
    public function __invoke()
    {
        return (bool)$this->matched;
    }

    /** This sets a default route to execute immediately when
     * our match function is successful. Useful when multiple routes
     * rely on the same algorithm. Parameters used in the closure
     * may be specified using the match function.
     * @param callable|null $struct
     * @return Route
     */
    public function structure(callable $struct = null): Route
    {
        $this->closure = $struct;
        return $this;
    }

    /** // TODO - comment this
     * @param string $pathToMatch
     * @param array ...$argv
     * @return Route
     * @throws PublicAlert
     */
    public function match(string $pathToMatch, ...$argv): self
    {
        $this->storage = $argv;  // This is for home route function (singleton)

        $uri = $this->uri;

        $arrayToMatch = explode('/', trim($pathToMatch, '/'));

        $pathLength = \count($arrayToMatch);

        $pathLength === 0 and $pathToMatch = '*';   // shorthand if stmt

        // The order of the following
        if ($pathLength < $this->uriLength && substr($pathToMatch, -1) !== '*') {
            return $this;
        }

        $required = true;       // variables can be made optional by `?`
        $variables = array();

        for ($i = 0; $i <= $pathLength; $i++) {

            // set up our ending condition
            if ($pathLength === $i || $arrayToMatch[$i] === null) {
                $arrayToMatch[$i] = '*';
            }

            switch ($arrayToMatch[$i][0]) {
                case  '*':
                    $this->matched = true;
                    $referenceVariables = [];

                    foreach ($variables as $key => $value) {
                        $GLOBALS[$key] = $value;                    // this must be done in two lines
                        $referenceVariables[] = &$GLOBALS[$key];
                    }

                    // Variables captured in the path to match will passed to the closure
                    if (\is_callable($argv[0])) {
                        $this->addMethod('routeMatched', $argv[0]);
                        if (\call_user_func_array($this->methods['routeMatched'], $referenceVariables) === false) {
                            throw new PublicAlert('Bad Closure Passed to Route::match()');
                        }
                        return $this;
                    }

                    // If variables were captured in our path to match, they will be merged with our variable list provided with $argv
                    if (\is_callable($this->closure)) {
                        $argv[] = &$referenceVariables;
                        $this->addMethod('routeMatched', $this->closure);
                        \call_user_func_array($this->methods['routeMatched'], $argv);
                        return $this;
                    }
                    return $this;

                case '{': // this is going to indicate the start of a variable name

                    if (substr($arrayToMatch[$i], -1) !== '}') {
                        throw new PublicAlert('Variable declaration must be rapped in brackets. ie `/{var}/`');
                    }

                    $variable = rtrim(ltrim($arrayToMatch[$i], '{'), '}');

                    if (substr($variable, -1) === '?' && $variable = rtrim($variable, '?')) {
                        $required = false;
                    }

                    if (empty($variable)) {
                        throw new PublicAlert('Variable must have a name association. ie  `/{var}/`');
                    }

                    $value = null;

                    if (array_key_exists($i, $uri)) {
                        $value = $uri[$i];
                    }
                    if ($required === true && $value === null) {
                        return $this;
                    }
                    $variables[$variable] = $value;
                    break;
                default:
                    if (!array_key_exists($i, $uri)) {
                        return $this;
                    }
                    if (strtolower($arrayToMatch[$i]) !== strtolower($uri[$i])) {
                        return $this;
                    }
            }
        }
        return $this;
    }
}