Test project for media files management.
<?php
namespace Illuminate\Foundation\Exceptions\Renderer\Mappers;
use Illuminate\Contracts\View\Factory;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\View\Compilers\BladeCompiler;
use Illuminate\View\ViewException;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Throwable;
/*
* This file contains parts of https://github.com/spatie/laravel-ignition.
*
* (c) Spatie <info@spatie.be>
*
* For the full copyright and license information, please review its LICENSE:
*
* The MIT License (MIT)
*
* Copyright (c) Spatie <info@spatie.be>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
class BladeMapper
{
/**
* The view factory instance.
*
* @var \Illuminate\Contracts\View\Factory
*/
protected $factory;
/**
* The Blade compiler instance.
*
* @var \Illuminate\View\Compilers\BladeCompiler
*/
protected $bladeCompiler;
/**
* Create a new Blade mapper instance.
*
* @param \Illuminate\Contracts\View\Factory $factory
* @param \Illuminate\View\Compilers\BladeCompiler $bladeCompiler
* @return void
*/
public function __construct(Factory $factory, BladeCompiler $bladeCompiler)
{
$this->factory = $factory;
$this->bladeCompiler = $bladeCompiler;
}
/**
* Map cached view paths to their original paths.
*
* @param \Symfony\Component\ErrorHandler\Exception\FlattenException $exception
* @return \Symfony\Component\ErrorHandler\Exception\FlattenException
*/
public function map(FlattenException $exception)
{
while ($exception->getClass() === ViewException::class) {
if (($previous = $exception->getPrevious()) === null) {
break;
}
$exception = $previous;
}
$trace = Collection::make($exception->getTrace())
->map(function ($frame) {
if ($originalPath = $this->findCompiledView((string) Arr::get($frame, 'file', ''))) {
$frame['file'] = $originalPath;
$frame['line'] = $this->detectLineNumber($frame['file'], $frame['line']);
}
return $frame;
})->toArray();
return tap($exception, fn () => (fn () => $this->trace = $trace)->call($exception));
}
/**
* Find the compiled view file for the given compiled path.
*
* @param string $compiledPath
* @return string|null
*/
protected function findCompiledView(string $compiledPath)
{
return once(fn () => $this->getKnownPaths())[$compiledPath] ?? null;
}
/**
* Get the list of known paths from the compiler engine.
*
* @return array<string, string>
*/
protected function getKnownPaths()
{
$compilerEngineReflection = new ReflectionClass(
$bladeCompilerEngine = $this->factory->getEngineResolver()->resolve('blade'),
);
if (! $compilerEngineReflection->hasProperty('lastCompiled') && $compilerEngineReflection->hasProperty('engine')) {
$compilerEngine = $compilerEngineReflection->getProperty('engine');
$compilerEngine->setAccessible(true);
$compilerEngine = $compilerEngine->getValue($bladeCompilerEngine);
$lastCompiled = new ReflectionProperty($compilerEngine, 'lastCompiled');
$lastCompiled->setAccessible(true);
$lastCompiled = $lastCompiled->getValue($compilerEngine);
} else {
$lastCompiled = $compilerEngineReflection->getProperty('lastCompiled');
$lastCompiled->setAccessible(true);
$lastCompiled = $lastCompiled->getValue($bladeCompilerEngine);
}
$knownPaths = [];
foreach ($lastCompiled as $lastCompiledPath) {
$compiledPath = $bladeCompilerEngine->getCompiler()->getCompiledPath($lastCompiledPath);
$knownPaths[realpath($compiledPath ?? $lastCompiledPath)] = realpath($lastCompiledPath);
}
return $knownPaths;
}
/**
* Filter out the view data that should not be shown in the exception report.
*
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function filterViewData(array $data)
{
return array_filter($data, function ($value, $key) {
if ($key === 'app') {
return ! $value instanceof Application;
}
return $key !== '__env';
}, ARRAY_FILTER_USE_BOTH);
}
/**
* Detect the line number in the original blade file.
*
* @param string $filename
* @param int $compiledLineNumber
* @return int
*/
protected function detectLineNumber(string $filename, int $compiledLineNumber)
{
$map = $this->compileSourcemap((string) file_get_contents($filename));
return $this->findClosestLineNumberMapping($map, $compiledLineNumber);
}
/**
* Compile the source map for the given blade file.
*
* @param string $value
* @return string
*/
protected function compileSourcemap(string $value)
{
try {
$value = $this->addEchoLineNumbers($value);
$value = $this->addStatementLineNumbers($value);
$value = $this->addBladeComponentLineNumbers($value);
$value = $this->bladeCompiler->compileString($value);
return $this->trimEmptyLines($value);
} catch (Throwable $e) {
report($e);
return $value;
}
}
/**
* Add line numbers to echo statements.
*
* @param string $value
* @return string
*/
protected function addEchoLineNumbers(string $value)
{
$echoPairs = [['{{', '}}'], ['{{{', '}}}'], ['{!!', '!!}']];
foreach ($echoPairs as $pair) {
// Matches {{ $value }}, {!! $value !!} and {{{ $value }}} depending on $pair
$pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $pair[0], $pair[1]);
if (preg_match_all($pattern, $value, $matches, PREG_OFFSET_CAPTURE)) {
foreach (array_reverse($matches[0]) as $match) {
$position = mb_strlen(substr($value, 0, $match[1]));
$value = $this->insertLineNumberAtPosition($position, $value);
}
}
}
return $value;
}
/**
* Add line numbers to blade statements.
*
* @param string $value
* @return string
*/
protected function addStatementLineNumbers(string $value)
{
$shouldInsertLineNumbers = preg_match_all(
'/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x',
$value,
$matches,
PREG_OFFSET_CAPTURE
);
if ($shouldInsertLineNumbers) {
foreach (array_reverse($matches[0]) as $match) {
$position = mb_strlen(substr($value, 0, $match[1]));
$value = $this->insertLineNumberAtPosition($position, $value);
}
}
return $value;
}
/**
* Add line numbers to blade components.
*
* @param string $value
* @return string
*/
protected function addBladeComponentLineNumbers(string $value)
{
$shouldInsertLineNumbers = preg_match_all(
'/<\s*x[-:]([\w\-:.]*)/mx',
$value,
$matches,
PREG_OFFSET_CAPTURE
);
if ($shouldInsertLineNumbers) {
foreach (array_reverse($matches[0]) as $match) {
$position = mb_strlen(substr($value, 0, $match[1]));
$value = $this->insertLineNumberAtPosition($position, $value);
}
}
return $value;
}
/**
* Insert a line number at the given position.
*
* @param int $position
* @param string $value
* @return string
*/
protected function insertLineNumberAtPosition(int $position, string $value)
{
$before = mb_substr($value, 0, $position);
$lineNumber = count(explode("\n", $before));
return mb_substr($value, 0, $position)."|---LINE:{$lineNumber}---|".mb_substr($value, $position);
}
/**
* Trim empty lines from the given value.
*
* @param string $value
* @return string
*/
protected function trimEmptyLines(string $value)
{
$value = preg_replace('/^\|---LINE:([0-9]+)---\|$/m', '', $value);
return ltrim((string) $value, PHP_EOL);
}
/**
* Find the closest line number mapping in the given source map.
*
* @param string $map
* @param int $compiledLineNumber
* @return int
*/
protected function findClosestLineNumberMapping(string $map, int $compiledLineNumber)
{
$map = explode("\n", $map);
$maxDistance = 20;
$pattern = '/\|---LINE:(?P<line>[0-9]+)---\|/m';
$lineNumberToCheck = $compiledLineNumber - 1;
while (true) {
if ($lineNumberToCheck < $compiledLineNumber - $maxDistance) {
return min($compiledLineNumber, count($map));
}
if (preg_match($pattern, $map[$lineNumberToCheck] ?? '', $matches)) {
return (int) $matches['line'];
}
$lineNumberToCheck--;
}
}
}