| 148 |
lars |
1 |
<?php
|
|
|
2 |
|
|
|
3 |
namespace Spatie\LaravelIgnition\Views;
|
|
|
4 |
|
|
|
5 |
use Exception;
|
|
|
6 |
use Illuminate\Contracts\View\Engine;
|
|
|
7 |
use Illuminate\Foundation\Application;
|
|
|
8 |
use Illuminate\Support\Arr;
|
|
|
9 |
use Illuminate\Support\Collection;
|
|
|
10 |
use Illuminate\View\Engines\PhpEngine;
|
|
|
11 |
use Illuminate\View\ViewException;
|
|
|
12 |
use ReflectionClass;
|
|
|
13 |
use ReflectionProperty;
|
|
|
14 |
use Spatie\Ignition\Contracts\ProvidesSolution;
|
|
|
15 |
use Spatie\LaravelIgnition\Exceptions\ViewException as IgnitionViewException;
|
|
|
16 |
use Spatie\LaravelIgnition\Exceptions\ViewExceptionWithSolution;
|
|
|
17 |
use Throwable;
|
|
|
18 |
|
|
|
19 |
class ViewExceptionMapper
|
|
|
20 |
{
|
|
|
21 |
protected Engine $compilerEngine;
|
|
|
22 |
protected BladeSourceMapCompiler $bladeSourceMapCompiler;
|
|
|
23 |
protected array $knownPaths;
|
|
|
24 |
|
|
|
25 |
public function __construct(BladeSourceMapCompiler $bladeSourceMapCompiler)
|
|
|
26 |
{
|
|
|
27 |
$resolver = app('view.engine.resolver');
|
|
|
28 |
|
|
|
29 |
$this->compilerEngine = $resolver->resolve('blade');
|
|
|
30 |
|
|
|
31 |
$this->bladeSourceMapCompiler = $bladeSourceMapCompiler;
|
|
|
32 |
}
|
|
|
33 |
|
|
|
34 |
public function map(ViewException $viewException): IgnitionViewException
|
|
|
35 |
{
|
|
|
36 |
$baseException = $this->getRealException($viewException);
|
|
|
37 |
|
|
|
38 |
if ($baseException instanceof IgnitionViewException) {
|
|
|
39 |
return $baseException;
|
|
|
40 |
}
|
|
|
41 |
|
|
|
42 |
preg_match('/\(View: (?P<path>.*?)\)/', $viewException->getMessage(), $matches);
|
|
|
43 |
|
|
|
44 |
$compiledViewPath = $matches['path'];
|
|
|
45 |
|
|
|
46 |
$exception = $this->createException($baseException);
|
|
|
47 |
|
|
|
48 |
if ($baseException instanceof ProvidesSolution) {
|
|
|
49 |
/** @var ViewExceptionWithSolution $exception */
|
|
|
50 |
$exception->setSolution($baseException->getSolution());
|
|
|
51 |
}
|
|
|
52 |
|
|
|
53 |
$this->modifyViewsInTrace($exception);
|
|
|
54 |
|
|
|
55 |
$exception->setView($compiledViewPath);
|
|
|
56 |
$exception->setViewData($this->getViewData($exception));
|
|
|
57 |
|
|
|
58 |
return $exception;
|
|
|
59 |
}
|
|
|
60 |
|
|
|
61 |
protected function createException(Throwable $baseException): IgnitionViewException
|
|
|
62 |
{
|
|
|
63 |
$viewExceptionClass = $baseException instanceof ProvidesSolution
|
|
|
64 |
? ViewExceptionWithSolution::class
|
|
|
65 |
: IgnitionViewException::class;
|
|
|
66 |
|
|
|
67 |
$viewFile = $this->findCompiledView($baseException->getFile());
|
|
|
68 |
$file = $viewFile ?? $baseException->getFile();
|
|
|
69 |
$line = $viewFile ? $this->getBladeLineNumber($file, $baseException->getLine()) : $baseException->getLine();
|
|
|
70 |
|
|
|
71 |
return new $viewExceptionClass(
|
|
|
72 |
$baseException->getMessage(),
|
|
|
73 |
0,
|
|
|
74 |
1,
|
|
|
75 |
$file,
|
|
|
76 |
$line,
|
|
|
77 |
$baseException
|
|
|
78 |
);
|
|
|
79 |
}
|
|
|
80 |
|
|
|
81 |
protected function modifyViewsInTrace(IgnitionViewException $exception): void
|
|
|
82 |
{
|
|
|
83 |
$trace = Collection::make($exception->getPrevious()->getTrace())
|
|
|
84 |
->map(function ($trace) {
|
|
|
85 |
if ($originalPath = $this->findCompiledView(Arr::get($trace, 'file', ''))) {
|
|
|
86 |
$trace['file'] = $originalPath;
|
|
|
87 |
$trace['line'] = $this->getBladeLineNumber($trace['file'], $trace['line']);
|
|
|
88 |
}
|
|
|
89 |
|
|
|
90 |
return $trace;
|
|
|
91 |
})->toArray();
|
|
|
92 |
|
|
|
93 |
$traceProperty = new ReflectionProperty('Exception', 'trace');
|
|
|
94 |
$traceProperty->setAccessible(true);
|
|
|
95 |
$traceProperty->setValue($exception, $trace);
|
|
|
96 |
}
|
|
|
97 |
|
|
|
98 |
/**
|
|
|
99 |
* Look at the previous exceptions to find the original exception.
|
|
|
100 |
* This is usually the first Exception that is not a ViewException.
|
|
|
101 |
*/
|
|
|
102 |
protected function getRealException(Throwable $exception): Throwable
|
|
|
103 |
{
|
|
|
104 |
$rootException = $exception->getPrevious() ?? $exception;
|
|
|
105 |
|
|
|
106 |
while ($rootException instanceof ViewException && $rootException->getPrevious()) {
|
|
|
107 |
$rootException = $rootException->getPrevious();
|
|
|
108 |
}
|
|
|
109 |
|
|
|
110 |
return $rootException;
|
|
|
111 |
}
|
|
|
112 |
|
|
|
113 |
protected function findCompiledView(string $compiledPath): ?string
|
|
|
114 |
{
|
|
|
115 |
$this->knownPaths ??= $this->getKnownPaths();
|
|
|
116 |
|
|
|
117 |
return $this->knownPaths[$compiledPath] ?? null;
|
|
|
118 |
}
|
|
|
119 |
|
|
|
120 |
protected function getKnownPaths(): array
|
|
|
121 |
{
|
|
|
122 |
$compilerEngineReflection = new ReflectionClass($this->compilerEngine);
|
|
|
123 |
|
|
|
124 |
if (! $compilerEngineReflection->hasProperty('lastCompiled') && $compilerEngineReflection->hasProperty('engine')) {
|
|
|
125 |
$compilerEngine = $compilerEngineReflection->getProperty('engine');
|
|
|
126 |
$compilerEngine->setAccessible(true);
|
|
|
127 |
$compilerEngine = $compilerEngine->getValue($this->compilerEngine);
|
|
|
128 |
$lastCompiled = new ReflectionProperty($compilerEngine, 'lastCompiled');
|
|
|
129 |
$lastCompiled->setAccessible(true);
|
|
|
130 |
$lastCompiled = $lastCompiled->getValue($compilerEngine);
|
|
|
131 |
} else {
|
|
|
132 |
$lastCompiled = $compilerEngineReflection->getProperty('lastCompiled');
|
|
|
133 |
$lastCompiled->setAccessible(true);
|
|
|
134 |
$lastCompiled = $lastCompiled->getValue($this->compilerEngine);
|
|
|
135 |
}
|
|
|
136 |
|
|
|
137 |
$knownPaths = [];
|
|
|
138 |
foreach ($lastCompiled as $lastCompiledPath) {
|
|
|
139 |
$compiledPath = $this->compilerEngine->getCompiler()->getCompiledPath($lastCompiledPath);
|
|
|
140 |
|
|
|
141 |
$knownPaths[realpath($compiledPath ?? $lastCompiledPath)] = realpath($lastCompiledPath);
|
|
|
142 |
}
|
|
|
143 |
|
|
|
144 |
return $knownPaths;
|
|
|
145 |
}
|
|
|
146 |
|
|
|
147 |
protected function getBladeLineNumber(string $view, int $compiledLineNumber): int
|
|
|
148 |
{
|
|
|
149 |
return $this->bladeSourceMapCompiler->detectLineNumber($view, $compiledLineNumber);
|
|
|
150 |
}
|
|
|
151 |
|
|
|
152 |
protected function getViewData(Throwable $exception): array
|
|
|
153 |
{
|
|
|
154 |
foreach ($exception->getTrace() as $frame) {
|
|
|
155 |
if (Arr::get($frame, 'class') === PhpEngine::class) {
|
|
|
156 |
$data = Arr::get($frame, 'args.1', []);
|
|
|
157 |
|
|
|
158 |
return $this->filterViewData($data);
|
|
|
159 |
}
|
|
|
160 |
}
|
|
|
161 |
|
|
|
162 |
return [];
|
|
|
163 |
}
|
|
|
164 |
|
|
|
165 |
protected function filterViewData(array $data): array
|
|
|
166 |
{
|
|
|
167 |
// By default, Laravel views get two data keys:
|
|
|
168 |
// __env and app. We try to filter them out.
|
|
|
169 |
return array_filter($data, function ($value, $key) {
|
|
|
170 |
if ($key === 'app') {
|
|
|
171 |
return ! $value instanceof Application;
|
|
|
172 |
}
|
|
|
173 |
|
|
|
174 |
return $key !== '__env';
|
|
|
175 |
}, ARRAY_FILTER_USE_BOTH);
|
|
|
176 |
}
|
|
|
177 |
}
|