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