xref: /dokuwiki/inc/Remote/OpenApiDoc/ClassResolver.php (revision ebae58f7ac1327a9d017e725ec882f642355326b)
18ddd9b69SAndreas Gohr<?php
28ddd9b69SAndreas Gohr
38ddd9b69SAndreas Gohrnamespace dokuwiki\Remote\OpenApiDoc;
48ddd9b69SAndreas Gohr
58ddd9b69SAndreas Gohr
68ddd9b69SAndreas Gohrclass ClassResolver
78ddd9b69SAndreas Gohr{
88ddd9b69SAndreas Gohr
98ddd9b69SAndreas Gohr    /** @var ClassResolver */
108ddd9b69SAndreas Gohr    private static $instance;
118ddd9b69SAndreas Gohr
128ddd9b69SAndreas Gohr    protected $classUses = [];
138ddd9b69SAndreas Gohr    protected $classDocs = [];
148ddd9b69SAndreas Gohr
158ddd9b69SAndreas Gohr    /**
168ddd9b69SAndreas Gohr     * @internal Use ClassResolver::getInstance() instead
178ddd9b69SAndreas Gohr     */
188ddd9b69SAndreas Gohr    public function __construct()
198ddd9b69SAndreas Gohr    {
208ddd9b69SAndreas Gohr    }
218ddd9b69SAndreas Gohr
228ddd9b69SAndreas Gohr    /**
238ddd9b69SAndreas Gohr     * Get a singleton instance
248ddd9b69SAndreas Gohr     *
258ddd9b69SAndreas Gohr     * Constructor is public for testing purposes
268ddd9b69SAndreas Gohr     * @return ClassResolver
278ddd9b69SAndreas Gohr     */
288ddd9b69SAndreas Gohr    public static function getInstance()
298ddd9b69SAndreas Gohr    {
308ddd9b69SAndreas Gohr        if (self::$instance === null) {
318ddd9b69SAndreas Gohr            self::$instance = new self();
328ddd9b69SAndreas Gohr        }
338ddd9b69SAndreas Gohr        return self::$instance;
348ddd9b69SAndreas Gohr    }
358ddd9b69SAndreas Gohr
368ddd9b69SAndreas Gohr    /**
378ddd9b69SAndreas Gohr     * Resolve a class name to a fully qualified class name
388ddd9b69SAndreas Gohr     *
398ddd9b69SAndreas Gohr     * Results are cached in the instance for reuse
408ddd9b69SAndreas Gohr     *
418ddd9b69SAndreas Gohr     * @param string $classalias The class name to resolve
428ddd9b69SAndreas Gohr     * @param string $context The classname in which context in which the class is used
438ddd9b69SAndreas Gohr     * @return string No guarantee that the class exists! No leading backslash!
448ddd9b69SAndreas Gohr     */
458ddd9b69SAndreas Gohr    public function resolve($classalias, $context)
468ddd9b69SAndreas Gohr    {
478ddd9b69SAndreas Gohr        if ($classalias[0] === '\\') {
488ddd9b69SAndreas Gohr            // Fully qualified class name given
498ddd9b69SAndreas Gohr            return ltrim($classalias, '\\');
508ddd9b69SAndreas Gohr        }
518ddd9b69SAndreas Gohr        $classinfo = $this->getClassUses($context);
528ddd9b69SAndreas Gohr
538ddd9b69SAndreas Gohr        if (isset($classinfo['uses'][$classalias])) {
548ddd9b69SAndreas Gohr            return $classinfo['uses'][$classalias];
558ddd9b69SAndreas Gohr        }
568ddd9b69SAndreas Gohr
578ddd9b69SAndreas Gohr        return $classinfo['ownNS'] . '\\' . $classalias;
588ddd9b69SAndreas Gohr    }
598ddd9b69SAndreas Gohr
608ddd9b69SAndreas Gohr    /**
618ddd9b69SAndreas Gohr     * Resolve a class name to a fully qualified class name and return a DocBlockClass for it
628ddd9b69SAndreas Gohr     *
638ddd9b69SAndreas Gohr     * Results are cached in the instance for reuse
648ddd9b69SAndreas Gohr     *
658ddd9b69SAndreas Gohr     * @param string $classalias The class name to resolve
668ddd9b69SAndreas Gohr     * @param string $context The classname in which context in which the class is used
678ddd9b69SAndreas Gohr     * @return DocBlockClass|null
688ddd9b69SAndreas Gohr     */
698ddd9b69SAndreas Gohr    public function document($classalias, $context)
708ddd9b69SAndreas Gohr    {
718ddd9b69SAndreas Gohr        $class = $this->resolve($classalias, $context);
728ddd9b69SAndreas Gohr        if(!class_exists($class)) return null;
738ddd9b69SAndreas Gohr
748ddd9b69SAndreas Gohr        if(isset($this->classDocs[$class])) {
758ddd9b69SAndreas Gohr            $reflector = new \ReflectionClass($class);
768ddd9b69SAndreas Gohr            $this->classDocs[$class] = new DocBlockClass($reflector);
778ddd9b69SAndreas Gohr        }
788ddd9b69SAndreas Gohr
798ddd9b69SAndreas Gohr        return $this->classDocs[$class];
808ddd9b69SAndreas Gohr    }
818ddd9b69SAndreas Gohr
828ddd9b69SAndreas Gohr    /**
838ddd9b69SAndreas Gohr     * Cached fetching of all defined class aliases
848ddd9b69SAndreas Gohr     *
858ddd9b69SAndreas Gohr     * @param string $class The class to parse
868ddd9b69SAndreas Gohr     * @return array
878ddd9b69SAndreas Gohr     */
888ddd9b69SAndreas Gohr    public function getClassUses($class)
898ddd9b69SAndreas Gohr    {
908ddd9b69SAndreas Gohr        if (!isset($this->classUses[$class])) {
918ddd9b69SAndreas Gohr            $reflector = new \ReflectionClass($class);
928ddd9b69SAndreas Gohr            $source = $this->readSource($reflector->getFileName(), $reflector->getStartLine());
938ddd9b69SAndreas Gohr            $this->classUses[$class] = [
948ddd9b69SAndreas Gohr                'ownNS' => $reflector->getNamespaceName(),
958ddd9b69SAndreas Gohr                'uses' => $this->tokenizeSource($source)
968ddd9b69SAndreas Gohr            ];
978ddd9b69SAndreas Gohr        }
988ddd9b69SAndreas Gohr        return $this->classUses[$class];
998ddd9b69SAndreas Gohr    }
1008ddd9b69SAndreas Gohr
1018ddd9b69SAndreas Gohr    /**
1028ddd9b69SAndreas Gohr     * Parse the use statements from the given source code
1038ddd9b69SAndreas Gohr     *
1048ddd9b69SAndreas Gohr     * This is a simplified version of the code by @jasondmoss - we do not support multiple
1058ddd9b69SAndreas Gohr     * classed within one file
1068ddd9b69SAndreas Gohr     *
1078ddd9b69SAndreas Gohr     * @link https://gist.github.com/jasondmoss/6200807
1088ddd9b69SAndreas Gohr     * @param string $source
1098ddd9b69SAndreas Gohr     * @return array
1108ddd9b69SAndreas Gohr     */
1118ddd9b69SAndreas Gohr    private function tokenizeSource($source)
1128ddd9b69SAndreas Gohr    {
1138ddd9b69SAndreas Gohr
1148ddd9b69SAndreas Gohr        $tokens = token_get_all($source);
1158ddd9b69SAndreas Gohr
1168ddd9b69SAndreas Gohr        $useStatements = [];
1178ddd9b69SAndreas Gohr        $record = false;
1188ddd9b69SAndreas Gohr        $currentUse = [
1198ddd9b69SAndreas Gohr            'class' => '',
1208ddd9b69SAndreas Gohr            'as' => ''
1218ddd9b69SAndreas Gohr        ];
1228ddd9b69SAndreas Gohr
1238ddd9b69SAndreas Gohr        foreach ($tokens as $token) {
1248ddd9b69SAndreas Gohr            if (!is_array($token)) {
1258ddd9b69SAndreas Gohr                // statement ended
1268ddd9b69SAndreas Gohr                if ($record) {
1278ddd9b69SAndreas Gohr                    $useStatements[] = $currentUse;
1288ddd9b69SAndreas Gohr                    $record = false;
1298ddd9b69SAndreas Gohr                    $currentUse = [
1308ddd9b69SAndreas Gohr                        'class' => '',
1318ddd9b69SAndreas Gohr                        'as' => ''
1328ddd9b69SAndreas Gohr                    ];
1338ddd9b69SAndreas Gohr                }
1348ddd9b69SAndreas Gohr                continue;
1358ddd9b69SAndreas Gohr            }
1368ddd9b69SAndreas Gohr            $tokenname = token_name($token[0]);
1378ddd9b69SAndreas Gohr
1388ddd9b69SAndreas Gohr            if ($token[0] === T_CLASS) {
1398ddd9b69SAndreas Gohr                break;  // we reached the class itself, no need to parse further
1408ddd9b69SAndreas Gohr            }
1418ddd9b69SAndreas Gohr
1428ddd9b69SAndreas Gohr            if ($token[0] === T_USE) {
1438ddd9b69SAndreas Gohr                $record = 'class';
1448ddd9b69SAndreas Gohr                continue;
1458ddd9b69SAndreas Gohr            }
1468ddd9b69SAndreas Gohr
1478ddd9b69SAndreas Gohr            if ($token[0] === T_AS) {
1488ddd9b69SAndreas Gohr                $record = 'as';
1498ddd9b69SAndreas Gohr                continue;
1508ddd9b69SAndreas Gohr            }
1518ddd9b69SAndreas Gohr
1528ddd9b69SAndreas Gohr            if ($record) {
1538ddd9b69SAndreas Gohr                switch ($token[0]) {
1548ddd9b69SAndreas Gohr                    case T_STRING:
1558ddd9b69SAndreas Gohr                    case T_NS_SEPARATOR:
156*ebae58f7SAndreas Gohr                    case defined('T_NAME_QUALIFIED') ? T_NAME_QUALIFIED : -1: // PHP 7.4 compatibility
1578ddd9b69SAndreas Gohr                        $currentUse[$record] .= $token[1];
1588ddd9b69SAndreas Gohr                        break;
1598ddd9b69SAndreas Gohr                }
1608ddd9b69SAndreas Gohr            }
1618ddd9b69SAndreas Gohr        }
1628ddd9b69SAndreas Gohr
1638ddd9b69SAndreas Gohr        // Return a lookup table alias to FQCN
1648ddd9b69SAndreas Gohr        $table = [];
1658ddd9b69SAndreas Gohr        foreach ($useStatements as $useStatement) {
1668ddd9b69SAndreas Gohr            $class = $useStatement['class'];
1678ddd9b69SAndreas Gohr            $alias = $useStatement['as'] ?: substr($class, strrpos($class, '\\') + 1);
1688ddd9b69SAndreas Gohr            $table[$alias] = $class;
1698ddd9b69SAndreas Gohr        }
1708ddd9b69SAndreas Gohr
1718ddd9b69SAndreas Gohr        return $table;
1728ddd9b69SAndreas Gohr    }
1738ddd9b69SAndreas Gohr
1748ddd9b69SAndreas Gohr
1758ddd9b69SAndreas Gohr    /**
1768ddd9b69SAndreas Gohr     * Read file source up to the line where our class is defined.
1778ddd9b69SAndreas Gohr     *
1788ddd9b69SAndreas Gohr     * @return string
1798ddd9b69SAndreas Gohr     */
1808ddd9b69SAndreas Gohr    protected function readSource($file, $startline)
1818ddd9b69SAndreas Gohr    {
1828ddd9b69SAndreas Gohr        $file = fopen($file, 'r');
1838ddd9b69SAndreas Gohr        $line = 0;
1848ddd9b69SAndreas Gohr        $source = '';
1858ddd9b69SAndreas Gohr
1868ddd9b69SAndreas Gohr        while (!feof($file)) {
1878ddd9b69SAndreas Gohr            ++$line;
1888ddd9b69SAndreas Gohr
1898ddd9b69SAndreas Gohr            if ($line >= $startline) {
1908ddd9b69SAndreas Gohr                break;
1918ddd9b69SAndreas Gohr            }
1928ddd9b69SAndreas Gohr
1938ddd9b69SAndreas Gohr            $source .= fgets($file);
1948ddd9b69SAndreas Gohr        }
1958ddd9b69SAndreas Gohr        fclose($file);
1968ddd9b69SAndreas Gohr
1978ddd9b69SAndreas Gohr        return $source;
1988ddd9b69SAndreas Gohr    }
1998ddd9b69SAndreas Gohr
2008ddd9b69SAndreas Gohr}
201