Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | 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
 
14
use Psy\Exception\RuntimeException;
15
use Psy\Exception\UnexpectedTargetException;
16
use Psy\Formatter\CodeFormatter;
17
use Psy\Formatter\SignatureFormatter;
18
use Psy\Input\CodeArgument;
19
use Symfony\Component\Console\Formatter\OutputFormatter;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\InputOption;
22
use Symfony\Component\Console\Output\OutputInterface;
23
 
24
/**
25
 * Show the code for an object, class, constant, method or property.
26
 */
27
class ShowCommand extends ReflectingCommand
28
{
29
    private $lastException;
30
    private $lastExceptionIndex;
31
 
32
    /**
33
     * @param string|null $colorMode (deprecated and ignored)
34
     */
35
    public function __construct($colorMode = null)
36
    {
37
        parent::__construct();
38
    }
39
 
40
    /**
41
     * {@inheritdoc}
42
     */
43
    protected function configure()
44
    {
45
        $this
46
            ->setName('show')
47
            ->setDefinition([
48
                new CodeArgument('target', CodeArgument::OPTIONAL, 'Function, class, instance, constant, method or property to show.'),
49
                new InputOption('ex', null, InputOption::VALUE_OPTIONAL, 'Show last exception context. Optionally specify a stack index.', 1),
50
            ])
51
            ->setDescription('Show the code for an object, class, constant, method or property.')
52
            ->setHelp(
53
                <<<HELP
54
Show the code for an object, class, constant, method or property, or the context
55
of the last exception.
56
 
57
<return>cat --ex</return> defaults to showing the lines surrounding the location of the last
58
exception. Invoking it more than once travels up the exception's stack trace,
59
and providing a number shows the context of the given index of the trace.
60
 
61
e.g.
62
<return>>>> show \$myObject</return>
63
<return>>>> show Psy\Shell::debug</return>
64
<return>>>> show --ex</return>
65
<return>>>> show --ex 3</return>
66
HELP
67
            );
68
    }
69
 
70
    /**
71
     * {@inheritdoc}
72
     *
73
     * @return int 0 if everything went fine, or an exit code
74
     */
75
    protected function execute(InputInterface $input, OutputInterface $output)
76
    {
77
        // n.b. As far as I can tell, InputInterface doesn't want to tell me
78
        // whether an option with an optional value was actually passed. If you
79
        // call `$input->getOption('ex')`, it will return the default, both when
80
        // `--ex` is specified with no value, and when `--ex` isn't specified at
81
        // all.
82
        //
83
        // So we're doing something sneaky here. If we call `getOptions`, it'll
84
        // return the default value when `--ex` is not present, and `null` if
85
        // `--ex` is passed with no value. /shrug
86
        $opts = $input->getOptions();
87
 
88
        // Strict comparison to `1` (the default value) here, because `--ex 1`
89
        // will come in as `"1"`. Now we can tell the difference between
90
        // "no --ex present", because it's the integer 1, "--ex with no value",
91
        // because it's `null`, and "--ex 1", because it's the string "1".
92
        if ($opts['ex'] !== 1) {
93
            if ($input->getArgument('target')) {
94
                throw new \InvalidArgumentException('Too many arguments (supply either "target" or "--ex")');
95
            }
96
 
97
            $this->writeExceptionContext($input, $output);
98
 
99
            return 0;
100
        }
101
 
102
        if ($input->getArgument('target')) {
103
            $this->writeCodeContext($input, $output);
104
 
105
            return 0;
106
        }
107
 
108
        throw new RuntimeException('Not enough arguments (missing: "target")');
109
    }
110
 
111
    private function writeCodeContext(InputInterface $input, OutputInterface $output)
112
    {
113
        try {
114
            list($target, $reflector) = $this->getTargetAndReflector($input->getArgument('target'));
115
        } catch (UnexpectedTargetException $e) {
116
            // If we didn't get a target and Reflector, maybe we got a filename?
117
            $target = $e->getTarget();
118
            if (\is_string($target) && \is_file($target) && $code = @\file_get_contents($target)) {
119
                $file = \realpath($target);
120
                if ($file !== $this->context->get('__file')) {
121
                    $this->context->setCommandScopeVariables([
122
                        '__file' => $file,
123
                        '__dir'  => \dirname($file),
124
                    ]);
125
                }
126
 
127
                $output->page(CodeFormatter::formatCode($code));
128
 
129
                return;
130
            } else {
131
                throw $e;
132
            }
133
        }
134
 
135
        // Set some magic local variables
136
        $this->setCommandScopeVariables($reflector);
137
 
138
        try {
139
            $output->page(CodeFormatter::format($reflector));
140
        } catch (RuntimeException $e) {
141
            $output->writeln(SignatureFormatter::format($reflector));
142
            throw $e;
143
        }
144
    }
145
 
146
    private function writeExceptionContext(InputInterface $input, OutputInterface $output)
147
    {
148
        $exception = $this->context->getLastException();
149
        if ($exception !== $this->lastException) {
150
            $this->lastException = null;
151
            $this->lastExceptionIndex = null;
152
        }
153
 
154
        $opts = $input->getOptions();
155
        if ($opts['ex'] === null) {
156
            if ($this->lastException && $this->lastExceptionIndex !== null) {
157
                $index = $this->lastExceptionIndex + 1;
158
            } else {
159
                $index = 0;
160
            }
161
        } else {
162
            $index = \max(0, (int) $input->getOption('ex') - 1);
163
        }
164
 
165
        $trace = $exception->getTrace();
166
        \array_unshift($trace, [
167
            'file' => $exception->getFile(),
168
            'line' => $exception->getLine(),
169
        ]);
170
 
171
        if ($index >= \count($trace)) {
172
            $index = 0;
173
        }
174
 
175
        $this->lastException = $exception;
176
        $this->lastExceptionIndex = $index;
177
 
178
        $output->writeln($this->getApplication()->formatException($exception));
179
        $output->writeln('--');
180
        $this->writeTraceLine($output, $trace, $index);
181
        $this->writeTraceCodeSnippet($output, $trace, $index);
182
 
183
        $this->setCommandScopeVariablesFromContext($trace[$index]);
184
    }
185
 
186
    private function writeTraceLine(OutputInterface $output, array $trace, $index)
187
    {
188
        $file = isset($trace[$index]['file']) ? $this->replaceCwd($trace[$index]['file']) : 'n/a';
189
        $line = isset($trace[$index]['line']) ? $trace[$index]['line'] : 'n/a';
190
 
191
        $output->writeln(\sprintf(
192
            'From <info>%s:%d</info> at <strong>level %d</strong> of backtrace (of %d):',
193
            OutputFormatter::escape($file),
194
            OutputFormatter::escape($line),
195
            $index + 1,
196
            \count($trace)
197
        ));
198
    }
199
 
200
    private function replaceCwd(string $file): string
201
    {
202
        if ($cwd = \getcwd()) {
203
            $cwd = \rtrim($cwd, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
204
        }
205
 
206
        if ($cwd === false) {
207
            return $file;
208
        } else {
209
            return \preg_replace('/^'.\preg_quote($cwd, '/').'/', '', $file);
210
        }
211
    }
212
 
213
    private function writeTraceCodeSnippet(OutputInterface $output, array $trace, $index)
214
    {
215
        if (!isset($trace[$index]['file'])) {
216
            return;
217
        }
218
 
219
        $file = $trace[$index]['file'];
220
        if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
221
            list($file, $line) = $fileAndLine;
222
        } else {
223
            if (!isset($trace[$index]['line'])) {
224
                return;
225
            }
226
 
227
            $line = $trace[$index]['line'];
228
        }
229
 
230
        if (\is_file($file)) {
231
            $code = @\file_get_contents($file);
232
        }
233
 
234
        if (empty($code)) {
235
            return;
236
        }
237
 
238
        $startLine = \max($line - 5, 0);
239
        $endLine = $line + 5;
240
 
241
        $output->write(CodeFormatter::formatCode($code, $startLine, $endLine, $line), false);
242
    }
243
 
244
    private function setCommandScopeVariablesFromContext(array $context)
245
    {
246
        $vars = [];
247
 
248
        if (isset($context['class'])) {
249
            $vars['__class'] = $context['class'];
250
            if (isset($context['function'])) {
251
                $vars['__method'] = $context['function'];
252
            }
253
 
254
            try {
255
                $refl = new \ReflectionClass($context['class']);
256
                if ($namespace = $refl->getNamespaceName()) {
257
                    $vars['__namespace'] = $namespace;
258
                }
259
            } catch (\Throwable $e) {
260
                // oh well
261
            }
262
        } elseif (isset($context['function'])) {
263
            $vars['__function'] = $context['function'];
264
 
265
            try {
266
                $refl = new \ReflectionFunction($context['function']);
267
                if ($namespace = $refl->getNamespaceName()) {
268
                    $vars['__namespace'] = $namespace;
269
                }
270
            } catch (\Throwable $e) {
271
                // oh well
272
            }
273
        }
274
 
275
        if (isset($context['file'])) {
276
            $file = $context['file'];
277
            if ($fileAndLine = $this->extractEvalFileAndLine($file)) {
278
                list($file, $line) = $fileAndLine;
279
            } elseif (isset($context['line'])) {
280
                $line = $context['line'];
281
            }
282
 
283
            if (\is_file($file)) {
284
                $vars['__file'] = $file;
285
                if (isset($line)) {
286
                    $vars['__line'] = $line;
287
                }
288
                $vars['__dir'] = \dirname($file);
289
            }
290
        }
291
 
292
        $this->context->setCommandScopeVariables($vars);
293
    }
294
 
295
    private function extractEvalFileAndLine(string $file)
296
    {
297
        if (\preg_match('/(.*)\\((\\d+)\\) : eval\\(\\)\'d code$/', $file, $matches)) {
298
            return [$matches[1], $matches[2]];
299
        }
300
    }
301
}