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