Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 200 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of Psy Shell.
5
 *
200 lars 6
 * (c) 2012-2023 Justin Hileman
148 lars 7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Psy\Command;
13
 
1294 lars 14
use PhpParser\NodeTraverser;
15
use PhpParser\PrettyPrinter\Standard as Printer;
148 lars 16
use Psy\CodeCleaner\NoReturnValue;
17
use Psy\Context;
18
use Psy\ContextAware;
19
use Psy\Exception\ErrorException;
20
use Psy\Exception\RuntimeException;
21
use Psy\Exception\UnexpectedTargetException;
1294 lars 22
use Psy\ParserFactory;
148 lars 23
use Psy\Reflection\ReflectionClassConstant;
24
use Psy\Reflection\ReflectionConstant_;
1294 lars 25
use Psy\Sudo\SudoVisitor;
148 lars 26
use Psy\Util\Mirror;
27
 
28
/**
29
 * An abstract command with helpers for inspecting the current context.
30
 */
31
abstract class ReflectingCommand extends Command implements ContextAware
32
{
33
    const CLASS_OR_FUNC = '/^[\\\\\w]+$/';
34
    const CLASS_MEMBER = '/^([\\\\\w]+)::(\w+)$/';
35
    const CLASS_STATIC = '/^([\\\\\w]+)::\$(\w+)$/';
36
    const INSTANCE_MEMBER = '/^(\$\w+)(::|->)(\w+)$/';
37
 
38
    /**
39
     * Context instance (for ContextAware interface).
40
     *
41
     * @var Context
42
     */
43
    protected $context;
44
 
1294 lars 45
    private $parser;
46
    private $traverser;
47
    private $printer;
48
 
148 lars 49
    /**
1294 lars 50
     * {@inheritdoc}
51
     */
52
    public function __construct($name = null)
53
    {
54
        $parserFactory = new ParserFactory();
55
        $this->parser = $parserFactory->createParser();
56
 
57
        $this->traverser = new NodeTraverser();
58
        $this->traverser->addVisitor(new SudoVisitor());
59
 
60
        $this->printer = new Printer();
61
 
62
        parent::__construct($name);
63
    }
64
 
65
    /**
148 lars 66
     * ContextAware interface.
67
     *
68
     * @param Context $context
69
     */
70
    public function setContext(Context $context)
71
    {
72
        $this->context = $context;
73
    }
74
 
75
    /**
76
     * Get the target for a value.
77
     *
78
     * @throws \InvalidArgumentException when the value specified can't be resolved
79
     *
80
     * @param string $valueName Function, class, variable, constant, method or property name
81
     *
82
     * @return array (class or instance name, member name, kind)
83
     */
84
    protected function getTarget(string $valueName): array
85
    {
86
        $valueName = \trim($valueName);
87
        $matches = [];
88
        switch (true) {
89
            case \preg_match(self::CLASS_OR_FUNC, $valueName, $matches):
90
                return [$this->resolveName($matches[0], true), null, 0];
91
 
92
            case \preg_match(self::CLASS_MEMBER, $valueName, $matches):
93
                return [$this->resolveName($matches[1]), $matches[2], Mirror::CONSTANT | Mirror::METHOD];
94
 
95
            case \preg_match(self::CLASS_STATIC, $valueName, $matches):
96
                return [$this->resolveName($matches[1]), $matches[2], Mirror::STATIC_PROPERTY | Mirror::PROPERTY];
97
 
98
            case \preg_match(self::INSTANCE_MEMBER, $valueName, $matches):
99
                if ($matches[2] === '->') {
100
                    $kind = Mirror::METHOD | Mirror::PROPERTY;
101
                } else {
102
                    $kind = Mirror::CONSTANT | Mirror::METHOD;
103
                }
104
 
105
                return [$this->resolveObject($matches[1]), $matches[3], $kind];
106
 
107
            default:
108
                return [$this->resolveObject($valueName), null, 0];
109
        }
110
    }
111
 
112
    /**
113
     * Resolve a class or function name (with the current shell namespace).
114
     *
115
     * @throws ErrorException when `self` or `static` is used in a non-class scope
116
     *
117
     * @param string $name
118
     * @param bool   $includeFunctions (default: false)
119
     */
120
    protected function resolveName(string $name, bool $includeFunctions = false): string
121
    {
122
        $shell = $this->getApplication();
123
 
124
        // While not *technically* 100% accurate, let's treat `self` and `static` as equivalent.
125
        if (\in_array(\strtolower($name), ['self', 'static'])) {
126
            if ($boundClass = $shell->getBoundClass()) {
127
                return $boundClass;
128
            }
129
 
130
            if ($boundObject = $shell->getBoundObject()) {
131
                return \get_class($boundObject);
132
            }
133
 
134
            $msg = \sprintf('Cannot use "%s" when no class scope is active', \strtolower($name));
135
            throw new ErrorException($msg, 0, \E_USER_ERROR, "eval()'d code", 1);
136
        }
137
 
138
        if (\substr($name, 0, 1) === '\\') {
139
            return $name;
140
        }
141
 
142
        // Check $name against the current namespace and use statements.
143
        if (self::couldBeClassName($name)) {
144
            try {
145
                $name = $this->resolveCode($name.'::class');
146
            } catch (RuntimeException $e) {
147
                // /shrug
148
            }
149
        }
150
 
151
        if ($namespace = $shell->getNamespace()) {
152
            $fullName = $namespace.'\\'.$name;
153
 
154
            if (\class_exists($fullName) || \interface_exists($fullName) || ($includeFunctions && \function_exists($fullName))) {
155
                return $fullName;
156
            }
157
        }
158
 
159
        return $name;
160
    }
161
 
162
    /**
163
     * Check whether a given name could be a class name.
164
     */
165
    protected function couldBeClassName(string $name): bool
166
    {
167
        // Regex based on https://www.php.net/manual/en/language.oop5.basic.php#language.oop5.basic.class
168
        return \preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*(\\\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)*$/', $name) === 1;
169
    }
170
 
171
    /**
172
     * Get a Reflector and documentation for a function, class or instance, constant, method or property.
173
     *
174
     * @param string $valueName Function, class, variable, constant, method or property name
175
     *
176
     * @return array (value, Reflector)
177
     */
178
    protected function getTargetAndReflector(string $valueName): array
179
    {
180
        list($value, $member, $kind) = $this->getTarget($valueName);
181
 
182
        return [$value, Mirror::get($value, $member, $kind)];
183
    }
184
 
185
    /**
186
     * Resolve code to a value in the current scope.
187
     *
188
     * @throws RuntimeException when the code does not return a value in the current scope
189
     *
190
     * @param string $code
191
     *
192
     * @return mixed Variable value
193
     */
194
    protected function resolveCode(string $code)
195
    {
196
        try {
1294 lars 197
            // Add an implicit `sudo` to target resolution.
198
            $nodes = $this->traverser->traverse($this->parse($code));
199
            $sudoCode = $this->printer->prettyPrint($nodes);
200
            $value = $this->getApplication()->execute($sudoCode, true);
148 lars 201
        } catch (\Throwable $e) {
202
            // Swallow all exceptions?
203
        }
204
 
205
        if (!isset($value) || $value instanceof NoReturnValue) {
206
            throw new RuntimeException('Unknown target: '.$code);
207
        }
208
 
209
        return $value;
210
    }
211
 
212
    /**
1294 lars 213
     * Lex and parse a string of code into statements.
214
     *
215
     * @param string $code
216
     *
217
     * @return array Statements
218
     */
219
    private function parse($code)
220
    {
221
        $code = '<?php '.$code;
222
 
223
        try {
224
            return $this->parser->parse($code);
225
        } catch (\PhpParser\Error $e) {
226
            if (\strpos($e->getMessage(), 'unexpected EOF') === false) {
227
                throw $e;
228
            }
229
 
230
            // If we got an unexpected EOF, let's try it again with a semicolon.
231
            return $this->parser->parse($code.';');
232
        }
233
    }
234
 
235
    /**
148 lars 236
     * Resolve code to an object in the current scope.
237
     *
238
     * @throws UnexpectedTargetException when the code resolves to a non-object value
239
     *
240
     * @param string $code
241
     *
242
     * @return object Variable instance
243
     */
244
    private function resolveObject(string $code)
245
    {
246
        $value = $this->resolveCode($code);
247
 
248
        if (!\is_object($value)) {
249
            throw new UnexpectedTargetException($value, 'Unable to inspect a non-object');
250
        }
251
 
252
        return $value;
253
    }
254
 
255
    /**
256
     * @deprecated Use `resolveCode` instead
257
     *
258
     * @param string $name
259
     *
260
     * @return mixed Variable instance
261
     */
262
    protected function resolveInstance(string $name)
263
    {
264
        @\trigger_error('`resolveInstance` is deprecated; use `resolveCode` instead.', \E_USER_DEPRECATED);
265
 
266
        return $this->resolveCode($name);
267
    }
268
 
269
    /**
270
     * Get a variable from the current shell scope.
271
     *
272
     * @param string $name
273
     *
274
     * @return mixed
275
     */
276
    protected function getScopeVariable(string $name)
277
    {
278
        return $this->context->get($name);
279
    }
280
 
281
    /**
282
     * Get all scope variables from the current shell scope.
283
     *
284
     * @return array
285
     */
286
    protected function getScopeVariables(): array
287
    {
288
        return $this->context->getAll();
289
    }
290
 
291
    /**
292
     * Given a Reflector instance, set command-scope variables in the shell
293
     * execution context. This is used to inject magic $__class, $__method and
294
     * $__file variables (as well as a handful of others).
295
     *
296
     * @param \Reflector $reflector
297
     */
298
    protected function setCommandScopeVariables(\Reflector $reflector)
299
    {
300
        $vars = [];
301
 
302
        switch (\get_class($reflector)) {
303
            case \ReflectionClass::class:
304
            case \ReflectionObject::class:
305
                $vars['__class'] = $reflector->name;
306
                if ($reflector->inNamespace()) {
307
                    $vars['__namespace'] = $reflector->getNamespaceName();
308
                }
309
                break;
310
 
311
            case \ReflectionMethod::class:
312
                $vars['__method'] = \sprintf('%s::%s', $reflector->class, $reflector->name);
313
                $vars['__class'] = $reflector->class;
314
                $classReflector = $reflector->getDeclaringClass();
315
                if ($classReflector->inNamespace()) {
316
                    $vars['__namespace'] = $classReflector->getNamespaceName();
317
                }
318
                break;
319
 
320
            case \ReflectionFunction::class:
321
                $vars['__function'] = $reflector->name;
322
                if ($reflector->inNamespace()) {
323
                    $vars['__namespace'] = $reflector->getNamespaceName();
324
                }
325
                break;
326
 
327
            case \ReflectionGenerator::class:
328
                $funcReflector = $reflector->getFunction();
329
                $vars['__function'] = $funcReflector->name;
330
                if ($funcReflector->inNamespace()) {
331
                    $vars['__namespace'] = $funcReflector->getNamespaceName();
332
                }
333
                if ($fileName = $reflector->getExecutingFile()) {
334
                    $vars['__file'] = $fileName;
335
                    $vars['__line'] = $reflector->getExecutingLine();
336
                    $vars['__dir'] = \dirname($fileName);
337
                }
338
                break;
339
 
340
            case \ReflectionProperty::class:
341
            case \ReflectionClassConstant::class:
342
            case ReflectionClassConstant::class:
343
                $classReflector = $reflector->getDeclaringClass();
344
                $vars['__class'] = $classReflector->name;
345
                if ($classReflector->inNamespace()) {
346
                    $vars['__namespace'] = $classReflector->getNamespaceName();
347
                }
348
                // no line for these, but this'll do
349
                if ($fileName = $reflector->getDeclaringClass()->getFileName()) {
350
                    $vars['__file'] = $fileName;
351
                    $vars['__dir'] = \dirname($fileName);
352
                }
353
                break;
354
 
355
            case ReflectionConstant_::class:
356
                if ($reflector->inNamespace()) {
357
                    $vars['__namespace'] = $reflector->getNamespaceName();
358
                }
359
                break;
360
        }
361
 
362
        if ($reflector instanceof \ReflectionClass || $reflector instanceof \ReflectionFunctionAbstract) {
363
            if ($fileName = $reflector->getFileName()) {
364
                $vars['__file'] = $fileName;
365
                $vars['__line'] = $reflector->getStartLine();
366
                $vars['__dir'] = \dirname($fileName);
367
            }
368
        }
369
 
370
        $this->context->setCommandScopeVariables($vars);
371
    }
372
}