1<?php
2
3namespace dokuwiki\Remote\OpenApiDoc;
4
5class ClassResolver
6{
7    /** @var ClassResolver */
8    private static $instance;
9
10    protected $classUses = [];
11    protected $classDocs = [];
12
13    /**
14     * Get a singleton instance
15     *
16     * Constructor is public for testing purposes
17     * @return ClassResolver
18     */
19    public static function getInstance()
20    {
21        if (self::$instance === null) {
22            self::$instance = new self();
23        }
24        return self::$instance;
25    }
26
27    /**
28     * Resolve a class name to a fully qualified class name
29     *
30     * Results are cached in the instance for reuse
31     *
32     * @param string $classalias The class name to resolve
33     * @param string $context The classname in which context in which the class is used
34     * @return string No guarantee that the class exists! No leading backslash!
35     */
36    public function resolve($classalias, $context)
37    {
38        if ($classalias[0] === '\\') {
39            // Fully qualified class name given
40            return ltrim($classalias, '\\');
41        }
42        $classinfo = $this->getClassUses($context);
43
44        return $classinfo['uses'][$classalias] ?? $classinfo['ownNS'] . '\\' . $classalias;
45    }
46
47    /**
48     * Resolve a class name to a fully qualified class name and return a DocBlockClass for it
49     *
50     * Results are cached in the instance for reuse
51     *
52     * @param string $classalias The class name to resolve
53     * @param string $context The classname in which context in which the class is used
54     * @return DocBlockClass|null
55     */
56    public function document($classalias, $context)
57    {
58        $class = $this->resolve($classalias, $context);
59        if (!class_exists($class)) return null;
60
61        if (isset($this->classDocs[$class])) {
62            $reflector = new \ReflectionClass($class);
63            $this->classDocs[$class] = new DocBlockClass($reflector);
64        }
65
66        return $this->classDocs[$class];
67    }
68
69    /**
70     * Cached fetching of all defined class aliases
71     *
72     * @param string $class The class to parse
73     * @return array
74     */
75    public function getClassUses($class)
76    {
77        if (!isset($this->classUses[$class])) {
78            $reflector = new \ReflectionClass($class);
79            $source = $this->readSource($reflector->getFileName(), $reflector->getStartLine());
80            $this->classUses[$class] = [
81                'ownNS' => $reflector->getNamespaceName(),
82                'uses' => $this->tokenizeSource($source)
83            ];
84        }
85        return $this->classUses[$class];
86    }
87
88    /**
89     * Parse the use statements from the given source code
90     *
91     * This is a simplified version of the code by @jasondmoss - we do not support multiple
92     * classed within one file
93     *
94     * @link https://gist.github.com/jasondmoss/6200807
95     * @param string $source
96     * @return array
97     */
98    private function tokenizeSource($source)
99    {
100
101        $tokens = token_get_all($source);
102
103        $useStatements = [];
104        $record = false;
105        $currentUse = [
106            'class' => '',
107            'as' => ''
108        ];
109
110        foreach ($tokens as $token) {
111            if (!is_array($token)) {
112                // statement ended
113                if ($record) {
114                    $useStatements[] = $currentUse;
115                    $record = false;
116                    $currentUse = [
117                        'class' => '',
118                        'as' => ''
119                    ];
120                }
121                continue;
122            }
123            $tokenname = token_name($token[0]);
124
125            if ($token[0] === T_CLASS) {
126                break;  // we reached the class itself, no need to parse further
127            }
128
129            if ($token[0] === T_USE) {
130                $record = 'class';
131                continue;
132            }
133
134            if ($token[0] === T_AS) {
135                $record = 'as';
136                continue;
137            }
138
139            if ($record) {
140                switch ($token[0]) {
141                    case T_STRING:
142                    case T_NS_SEPARATOR:
143                    case defined('T_NAME_QUALIFIED') ? T_NAME_QUALIFIED : -1: // PHP 7.4 compatibility
144                        $currentUse[$record] .= $token[1];
145                        break;
146                }
147            }
148        }
149
150        // Return a lookup table alias to FQCN
151        $table = [];
152        foreach ($useStatements as $useStatement) {
153            $class = $useStatement['class'];
154            $alias = $useStatement['as'] ?: substr($class, strrpos($class, '\\') + 1);
155            $table[$alias] = $class;
156        }
157
158        return $table;
159    }
160
161
162    /**
163     * Read file source up to the line where our class is defined.
164     *
165     * @return string
166     */
167    protected function readSource($file, $startline)
168    {
169        $file = fopen($file, 'r');
170        $line = 0;
171        $source = '';
172
173        while (!feof($file)) {
174            ++$line;
175
176            if ($line >= $startline) {
177                break;
178            }
179
180            $source .= fgets($file);
181        }
182        fclose($file);
183
184        return $source;
185    }
186}
187