Lullabot/mpx-php

View on GitHub
src/DataService/CachingContextFactory.php

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
<?php

namespace Lullabot\Mpx\DataService;

use phpDocumentor\Reflection\Types\Context;

/**
 * Creates a context for docblocks, storing cached results for already parsed files.
 *
 * @see \phpDocumentor\Reflection\Types\ContextFactory
 */
final class CachingContextFactory
{
    /** The literal used at the end of a use statement. */
    public const T_LITERAL_END_OF_USE = ';';

    /** The literal used between sets of use statements */
    public const T_LITERAL_USE_SEPARATOR = ',';

    /**
     * Build a Context given a Class Reflection.
     *
     * @see Context for more information on Contexts.
     *
     * @return Context
     */
    public function createFromReflector(\Reflector $reflector)
    {
        if (method_exists($reflector, 'getDeclaringClass')) {
            $reflector = $reflector->getDeclaringClass();
        }

        $fileName = $reflector->getFileName();
        $namespace = $reflector->getNamespaceName();

        if (file_exists($fileName)) {
            return $this->createForNamespace($namespace, file_get_contents($fileName));
        }

        return new Context($namespace, []);
    }

    /**
     * Build a Context for a namespace in the provided file contents.
     *
     * @param string $namespace    It does not matter if a `\` precedes the namespace name, this method first normalizes.
     * @param string $fileContents the file's contents to retrieve the aliases from with the given namespace.
     *
     * @see Context for more information on Contexts.
     *
     * @return Context
     */
    public function createForNamespace($namespace, $fileContents)
    {
        static $cache = [];
        $key = md5($fileContents);

        if (!isset($cache[$namespace][$key])) {
            $namespace = trim($namespace, '\\');
            $useStatements = [];
            $currentNamespace = '';
            $tokens = new \ArrayIterator(\PhpToken::tokenize($fileContents));

            while ($tokens->valid()) {
                $token = $tokens->current();
                \assert($token instanceof \PhpToken);
                switch ($token->id) {
                    case \T_NAMESPACE:
                        $currentNamespace = $this->parseNamespace($tokens);
                        break;
                    case \T_CLASS:
                        // Fast-forward the iterator through the class so that any
                        // T_USE tokens found within are skipped - these are not
                        // valid namespace use statements so should be ignored.
                        $braceLevel = 0;
                        $firstBraceFound = false;
                        while ($tokens->valid() && ($braceLevel > 0 || !$firstBraceFound)) {
                            if ('{' === $token
                                || \T_CURLY_OPEN === $token->id
                                || \T_DOLLAR_OPEN_CURLY_BRACES === $token->id) {
                                if (!$firstBraceFound) {
                                    $firstBraceFound = true;
                                }
                                ++$braceLevel;
                            }

                            if ('}' === $token) {
                                --$braceLevel;
                            }
                            $tokens->next();
                        }
                        break;
                    case \T_USE:
                        if ($currentNamespace === $namespace) {
                            $useStatements = array_merge($useStatements, $this->parseUseStatement($tokens));
                        }
                        break;
                }
                $tokens->next();
            }

            $cache[$namespace][$key] = new Context($namespace, $useStatements);
        }

        return $cache[$namespace][$key];
    }

    /**
     * Deduce the name from tokens when we are at the T_NAMESPACE token.
     *
     * @return string
     */
    private function parseNamespace(\ArrayIterator $tokens)
    {
        // skip to the first string or namespace separator
        $this->skipToNextStringOrNamespaceSeparator($tokens);

        $name = '';
        while ($tokens->valid() && (\T_STRING === $tokens->current()->id || \T_NS_SEPARATOR === $tokens->current()->id)
        ) {
            $name .= $tokens->current()->text;
            $tokens->next();
        }

        return $name;
    }

    /**
     * Deduce the names of all imports when we are at the T_USE token.
     *
     * @return string[]
     */
    private function parseUseStatement(\ArrayIterator $tokens)
    {
        $uses = [];
        $continue = true;

        while ($continue) {
            $this->skipToNextStringOrNamespaceSeparator($tokens);

            [$alias, $fqnn] = str_split($this->extractUseStatement($tokens));
            $uses[$alias] = $fqnn;
            if (self::T_LITERAL_END_OF_USE === $tokens->current()->id) {
                $continue = false;
            }
        }

        return $uses;
    }

    /**
     * Fast-forwards the iterator as longs as we don't encounter a T_STRING or T_NS_SEPARATOR token.
     */
    private function skipToNextStringOrNamespaceSeparator(\ArrayIterator $tokens)
    {
        while ($tokens->valid() && (\T_STRING !== $tokens->current()->id) && (\T_NS_SEPARATOR !== $tokens->current()->id)) {
            $tokens->next();
        }
    }

    /**
     * Deduce the namespace name and alias of an import when we are at the T_USE token or have not reached the end of
     * a USE statement yet.
     *
     * @return string
     */
    private function extractUseStatement(\ArrayIterator $tokens)
    {
        $result = [''];
        while ($tokens->valid()
            && (self::T_LITERAL_USE_SEPARATOR !== $tokens->current()->id)
            && (self::T_LITERAL_END_OF_USE !== $tokens->current()->id)
        ) {
            if (\T_AS === $tokens->current()->id) {
                $result[] = '';
            }
            if (\T_STRING === $tokens->current()->id || \T_NS_SEPARATOR === $tokens->current()->id) {
                $result[\count($result) - 1] .= $tokens->current()->text;
            }
            $tokens->next();
        }

        if (1 == \count($result)) {
            $backslashPos = strrpos($result[0], '\\');

            if (false !== $backslashPos) {
                $result[] = substr($result[0], $backslashPos + 1);
            } else {
                $result[] = $result[0];
            }
        }

        return array_reverse($result);
    }
}