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