Build modified streamline images

The official image from streamline does not currently work for the arm64
platform. As a temporary measure, the source code and docker build scripts
have been lifted from the official images and are used to build locally.

Some additional modifications are made to reduce overall image size, these
are documented in docker/README.md
This commit is contained in:
2024-03-10 16:17:50 -07:00
parent 2ffc3c408a
commit a424394109
13506 changed files with 1860172 additions and 6 deletions
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Bart van den Burg <bart@burgov.nl>
*
* @final
*/
class AjaxDataCollector extends DataCollector
{
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
// all collecting is done client side
}
public function reset(): void
{
// all collecting is done client side
}
public function getName(): string
{
return 'ajax';
}
}
@@ -0,0 +1,259 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class ConfigDataCollector extends DataCollector implements LateDataCollectorInterface
{
private KernelInterface $kernel;
/**
* Sets the Kernel associated with this Request.
*/
public function setKernel(?KernelInterface $kernel = null): void
{
if (1 > \func_num_args()) {
trigger_deprecation('symfony/http-kernel', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
}
$this->kernel = $kernel;
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE);
$eol = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE);
$this->data = [
'token' => $response->headers->get('X-Debug-Token'),
'symfony_version' => Kernel::VERSION,
'symfony_minor_version' => sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION),
'symfony_lts' => 4 === Kernel::MINOR_VERSION,
'symfony_state' => $this->determineSymfonyState(),
'symfony_eom' => $eom->format('F Y'),
'symfony_eol' => $eol->format('F Y'),
'env' => isset($this->kernel) ? $this->kernel->getEnvironment() : 'n/a',
'debug' => isset($this->kernel) ? $this->kernel->isDebug() : 'n/a',
'php_version' => \PHP_VERSION,
'php_architecture' => \PHP_INT_SIZE * 8,
'php_intl_locale' => class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a',
'php_timezone' => date_default_timezone_get(),
'xdebug_enabled' => \extension_loaded('xdebug'),
'apcu_enabled' => \extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL),
'zend_opcache_enabled' => \extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL),
'bundles' => [],
'sapi_name' => \PHP_SAPI,
];
if (isset($this->kernel)) {
foreach ($this->kernel->getBundles() as $name => $bundle) {
$this->data['bundles'][$name] = new ClassStub($bundle::class);
}
}
if (preg_match('~^(\d+(?:\.\d+)*)(.+)?$~', $this->data['php_version'], $matches) && isset($matches[2])) {
$this->data['php_version'] = $matches[1];
$this->data['php_version_extra'] = $matches[2];
}
}
public function lateCollect(): void
{
$this->data = $this->cloneVar($this->data);
}
/**
* Gets the token.
*/
public function getToken(): ?string
{
return $this->data['token'];
}
/**
* Gets the Symfony version.
*/
public function getSymfonyVersion(): string
{
return $this->data['symfony_version'];
}
/**
* Returns the state of the current Symfony release
* as one of: unknown, dev, stable, eom, eol.
*/
public function getSymfonyState(): string
{
return $this->data['symfony_state'];
}
/**
* Returns the minor Symfony version used (without patch numbers of extra
* suffix like "RC", "beta", etc.).
*/
public function getSymfonyMinorVersion(): string
{
return $this->data['symfony_minor_version'];
}
public function isSymfonyLts(): bool
{
return $this->data['symfony_lts'];
}
/**
* Returns the human readable date when this Symfony version ends its
* maintenance period.
*/
public function getSymfonyEom(): string
{
return $this->data['symfony_eom'];
}
/**
* Returns the human readable date when this Symfony version reaches its
* "end of life" and won't receive bugs or security fixes.
*/
public function getSymfonyEol(): string
{
return $this->data['symfony_eol'];
}
/**
* Gets the PHP version.
*/
public function getPhpVersion(): string
{
return $this->data['php_version'];
}
/**
* Gets the PHP version extra part.
*/
public function getPhpVersionExtra(): ?string
{
return $this->data['php_version_extra'] ?? null;
}
public function getPhpArchitecture(): int
{
return $this->data['php_architecture'];
}
public function getPhpIntlLocale(): string
{
return $this->data['php_intl_locale'];
}
public function getPhpTimezone(): string
{
return $this->data['php_timezone'];
}
/**
* Gets the environment.
*/
public function getEnv(): string
{
return $this->data['env'];
}
/**
* Returns true if the debug is enabled.
*
* @return bool|string true if debug is enabled, false otherwise or a string if no kernel was set
*/
public function isDebug(): bool|string
{
return $this->data['debug'];
}
/**
* Returns true if the Xdebug is enabled.
*/
public function hasXdebug(): bool
{
return $this->data['xdebug_enabled'];
}
/**
* Returns true if the function xdebug_info is available.
*/
public function hasXdebugInfo(): bool
{
return \function_exists('xdebug_info');
}
/**
* Returns true if APCu is enabled.
*/
public function hasApcu(): bool
{
return $this->data['apcu_enabled'];
}
/**
* Returns true if Zend OPcache is enabled.
*/
public function hasZendOpcache(): bool
{
return $this->data['zend_opcache_enabled'];
}
public function getBundles(): array|Data
{
return $this->data['bundles'];
}
/**
* Gets the PHP SAPI name.
*/
public function getSapiName(): string
{
return $this->data['sapi_name'];
}
public function getName(): string
{
return 'config';
}
private function determineSymfonyState(): string
{
$now = new \DateTimeImmutable();
$eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->modify('last day of this month');
$eol = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->modify('last day of this month');
if ($now > $eol) {
$versionState = 'eol';
} elseif ($now > $eom) {
$versionState = 'eom';
} elseif ('' !== Kernel::EXTRA_VERSION) {
$versionState = 'dev';
} else {
$versionState = 'stable';
}
return $versionState;
}
}
@@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\VarDumper\Caster\CutStub;
use Symfony\Component\VarDumper\Caster\ReflectionCaster;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\Stub;
use Symfony\Component\VarDumper\Cloner\VarCloner;
/**
* DataCollector.
*
* Children of this class must store the collected data in the data property.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@symfony.com>
*/
abstract class DataCollector implements DataCollectorInterface
{
/**
* @var array|Data
*/
protected $data = [];
private ClonerInterface $cloner;
/**
* Converts the variable into a serializable Data instance.
*
* This array can be displayed in the template using
* the VarDumper component.
*/
protected function cloneVar(mixed $var): Data
{
if ($var instanceof Data) {
return $var;
}
if (!isset($this->cloner)) {
$this->cloner = new VarCloner();
$this->cloner->setMaxItems(-1);
$this->cloner->addCasters($this->getCasters());
}
return $this->cloner->cloneVar($var);
}
/**
* @return callable[] The casters to add to the cloner
*/
protected function getCasters()
{
$casters = [
'*' => function ($v, array $a, Stub $s, $isNested) {
if (!$v instanceof Stub) {
foreach ($a as $k => $v) {
if (\is_object($v) && !$v instanceof \DateTimeInterface && !$v instanceof Stub) {
$a[$k] = new CutStub($v);
}
}
}
return $a;
},
] + ReflectionCaster::UNSET_CLOSURE_FILE_INFO;
return $casters;
}
public function __sleep(): array
{
return ['data'];
}
/**
* @return void
*/
public function __wakeup()
{
}
/**
* @internal to prevent implementing \Serializable
*/
final protected function serialize(): void
{
}
/**
* @internal to prevent implementing \Serializable
*/
final protected function unserialize(string $data): void
{
}
/**
* @return void
*/
public function reset()
{
$this->data = [];
}
}
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Service\ResetInterface;
/**
* DataCollectorInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface DataCollectorInterface extends ResetInterface
{
/**
* Collects data for the given Request and Response.
*
* @return void
*/
public function collect(Request $request, Response $response, ?\Throwable $exception = null);
/**
* Returns the name of the collector.
*
* @return string
*/
public function getName();
}
@@ -0,0 +1,287 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
use Symfony\Component\VarDumper\Dumper\DataDumperInterface;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Symfony\Component\VarDumper\Server\Connection;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @final
*/
class DumpDataCollector extends DataCollector implements DataDumperInterface
{
private ?Stopwatch $stopwatch = null;
private string|FileLinkFormatter|false $fileLinkFormat;
private int $dataCount = 0;
private bool $isCollected = true;
private int $clonesCount = 0;
private int $clonesIndex = 0;
private array $rootRefs;
private string $charset;
private ?RequestStack $requestStack;
private DataDumperInterface|Connection|null $dumper;
private mixed $sourceContextProvider;
private bool $webMode;
public function __construct(?Stopwatch $stopwatch = null, string|FileLinkFormatter|null $fileLinkFormat = null, ?string $charset = null, ?RequestStack $requestStack = null, DataDumperInterface|Connection|null $dumper = null, ?bool $webMode = null)
{
$fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
$this->stopwatch = $stopwatch;
$this->fileLinkFormat = $fileLinkFormat instanceof FileLinkFormatter && false === $fileLinkFormat->format('', 0) ? false : $fileLinkFormat;
$this->charset = $charset ?: \ini_get('php.output_encoding') ?: \ini_get('default_charset') ?: 'UTF-8';
$this->requestStack = $requestStack;
$this->dumper = $dumper;
$this->webMode = $webMode ?? !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true);
// All clones share these properties by reference:
$this->rootRefs = [
&$this->data,
&$this->dataCount,
&$this->isCollected,
&$this->clonesCount,
];
$this->sourceContextProvider = $dumper instanceof Connection && isset($dumper->getContextProviders()['source']) ? $dumper->getContextProviders()['source'] : new SourceContextProvider($this->charset);
}
public function __clone()
{
$this->clonesIndex = ++$this->clonesCount;
}
public function dump(Data $data): ?string
{
$this->stopwatch?->start('dump');
['name' => $name, 'file' => $file, 'line' => $line, 'file_excerpt' => $fileExcerpt] = $this->sourceContextProvider->getContext();
if (!$this->dumper || $this->dumper instanceof Connection && !$this->dumper->write($data)) {
$this->isCollected = false;
}
$context = $data->getContext();
$label = $context['label'] ?? '';
unset($context['label']);
$data = $data->withContext($context);
if ($this->dumper && !$this->dumper instanceof Connection) {
$this->doDump($this->dumper, $data, $name, $file, $line, $label);
}
if (!$this->dataCount) {
$this->data = [];
}
$this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt', 'label');
++$this->dataCount;
$this->stopwatch?->stop('dump');
return null;
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
if (!$this->dataCount) {
$this->data = [];
}
// Sub-requests and programmatic calls stay in the collected profile.
if ($this->dumper || ($this->requestStack && $this->requestStack->getMainRequest() !== $request) || $request->isXmlHttpRequest() || $request->headers->has('Origin')) {
return;
}
// In all other conditions that remove the web debug toolbar, dumps are written on the output.
if (!$this->requestStack
|| !$response->headers->has('X-Debug-Token')
|| $response->isRedirection()
|| ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type') ?? '', 'html'))
|| 'html' !== $request->getRequestFormat()
|| false === strripos($response->getContent(), '</body>')
) {
if ($response->headers->has('Content-Type') && str_contains($response->headers->get('Content-Type') ?? '', 'html')) {
$dumper = new HtmlDumper('php://output', $this->charset);
$dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]);
} else {
$dumper = new CliDumper('php://output', $this->charset);
$dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]);
}
foreach ($this->data as $dump) {
$this->doDump($dumper, $dump['data'], $dump['name'], $dump['file'], $dump['line'], $dump['label'] ?? '');
}
}
}
public function reset(): void
{
$this->stopwatch?->reset();
parent::reset();
$this->dataCount = 0;
$this->isCollected = true;
$this->clonesCount = 0;
$this->clonesIndex = 0;
}
/**
* @internal
*/
public function __sleep(): array
{
if (!$this->dataCount) {
$this->data = [];
}
if ($this->clonesCount !== $this->clonesIndex) {
return [];
}
$this->data[] = $this->fileLinkFormat;
$this->data[] = $this->charset;
$this->dataCount = 0;
$this->isCollected = true;
return parent::__sleep();
}
/**
* @internal
*/
public function __wakeup(): void
{
parent::__wakeup();
$charset = array_pop($this->data);
$fileLinkFormat = array_pop($this->data);
$this->dataCount = \count($this->data);
foreach ($this->data as $dump) {
if (!\is_string($dump['name']) || !\is_string($dump['file']) || !\is_int($dump['line'])) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
}
self::__construct($this->stopwatch, \is_string($fileLinkFormat) || $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : null, \is_string($charset) ? $charset : null);
}
public function getDumpsCount(): int
{
return $this->dataCount;
}
public function getDumps(string $format, int $maxDepthLimit = -1, int $maxItemsPerDepth = -1): array
{
$data = fopen('php://memory', 'r+');
if ('html' === $format) {
$dumper = new HtmlDumper($data, $this->charset);
$dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]);
} else {
throw new \InvalidArgumentException(sprintf('Invalid dump format: "%s".', $format));
}
$dumps = [];
if (!$this->dataCount) {
return $this->data = [];
}
foreach ($this->data as $dump) {
$dumper->dump($dump['data']->withMaxDepth($maxDepthLimit)->withMaxItemsPerDepth($maxItemsPerDepth));
$dump['data'] = stream_get_contents($data, -1, 0);
ftruncate($data, 0);
rewind($data);
$dumps[] = $dump;
}
return $dumps;
}
public function getName(): string
{
return 'dump';
}
public function __destruct()
{
if (0 === $this->clonesCount-- && !$this->isCollected && $this->dataCount) {
$this->clonesCount = 0;
$this->isCollected = true;
$h = headers_list();
$i = \count($h);
array_unshift($h, 'Content-Type: '.\ini_get('default_mimetype'));
while (0 !== stripos($h[$i], 'Content-Type:')) {
--$i;
}
if ($this->webMode) {
$dumper = new HtmlDumper('php://output', $this->charset);
$dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]);
} else {
$dumper = new CliDumper('php://output', $this->charset);
$dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]);
}
foreach ($this->data as $i => $dump) {
$this->data[$i] = null;
$this->doDump($dumper, $dump['data'], $dump['name'], $dump['file'], $dump['line'], $dump['label'] ?? '');
}
$this->data = [];
$this->dataCount = 0;
}
}
private function doDump(DataDumperInterface $dumper, Data $data, string $name, string $file, int $line, string $label): void
{
if ($dumper instanceof CliDumper) {
$contextDumper = function ($name, $file, $line, $fmt, $label) {
$this->line = '' !== $label ? $this->style('meta', $label).' in ' : '';
if ($this instanceof HtmlDumper) {
if ($file) {
$s = $this->style('meta', '%s');
$f = strip_tags($this->style('', $file));
$name = strip_tags($this->style('', $name));
if ($fmt && $link = \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line)) {
$name = sprintf('<a href="%s" title="%s">'.$s.'</a>', strip_tags($this->style('', $link)), $f, $name);
} else {
$name = sprintf('<abbr title="%s">'.$s.'</abbr>', $f, $name);
}
} else {
$name = $this->style('meta', $name);
}
$this->line .= $name.' on line '.$this->style('meta', $line).':';
} else {
$this->line .= $this->style('meta', $name).' on line '.$this->style('meta', $line).':';
}
$this->dumpLine(0);
};
$contextDumper = $contextDumper->bindTo($dumper, $dumper);
$contextDumper($name, $file, $line, $this->fileLinkFormat, $label);
} else {
$cloner = new VarCloner();
$dumper->dump($cloner->cloneVar(('' !== $label ? $label.' in ' : '').$name.' on line '.$line.':'));
}
$dumper->dump($data);
}
}
@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @see TraceableEventDispatcher
*
* @final
*/
class EventDataCollector extends DataCollector implements LateDataCollectorInterface
{
/** @var iterable<EventDispatcherInterface> */
private iterable $dispatchers;
private ?Request $currentRequest = null;
/**
* @param iterable<EventDispatcherInterface>|EventDispatcherInterface|null $dispatchers
*/
public function __construct(
iterable|EventDispatcherInterface|null $dispatchers = null,
private ?RequestStack $requestStack = null,
private string $defaultDispatcher = 'event_dispatcher',
) {
if ($dispatchers instanceof EventDispatcherInterface) {
$dispatchers = [$this->defaultDispatcher => $dispatchers];
}
$this->dispatchers = $dispatchers ?? [];
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->currentRequest = $this->requestStack && $this->requestStack->getMainRequest() !== $request ? $request : null;
$this->data = [];
}
public function reset(): void
{
parent::reset();
foreach ($this->dispatchers as $dispatcher) {
if ($dispatcher instanceof ResetInterface) {
$dispatcher->reset();
}
}
}
public function lateCollect(): void
{
foreach ($this->dispatchers as $name => $dispatcher) {
if (!$dispatcher instanceof TraceableEventDispatcher) {
continue;
}
$this->setCalledListeners($dispatcher->getCalledListeners($this->currentRequest), $name);
$this->setNotCalledListeners($dispatcher->getNotCalledListeners($this->currentRequest), $name);
$this->setOrphanedEvents($dispatcher->getOrphanedEvents($this->currentRequest), $name);
}
$this->data = $this->cloneVar($this->data);
}
public function getData(): array|Data
{
return $this->data;
}
/**
* @see TraceableEventDispatcher
*/
public function setCalledListeners(array $listeners, ?string $dispatcher = null): void
{
$this->data[$dispatcher ?? $this->defaultDispatcher]['called_listeners'] = $listeners;
}
/**
* @see TraceableEventDispatcher
*/
public function getCalledListeners(?string $dispatcher = null): array|Data
{
return $this->data[$dispatcher ?? $this->defaultDispatcher]['called_listeners'] ?? [];
}
/**
* @see TraceableEventDispatcher
*/
public function setNotCalledListeners(array $listeners, ?string $dispatcher = null): void
{
$this->data[$dispatcher ?? $this->defaultDispatcher]['not_called_listeners'] = $listeners;
}
/**
* @see TraceableEventDispatcher
*/
public function getNotCalledListeners(?string $dispatcher = null): array|Data
{
return $this->data[$dispatcher ?? $this->defaultDispatcher]['not_called_listeners'] ?? [];
}
/**
* @param array $events An array of orphaned events
*
* @see TraceableEventDispatcher
*/
public function setOrphanedEvents(array $events, ?string $dispatcher = null): void
{
$this->data[$dispatcher ?? $this->defaultDispatcher]['orphaned_events'] = $events;
}
/**
* @see TraceableEventDispatcher
*/
public function getOrphanedEvents(?string $dispatcher = null): array|Data
{
return $this->data[$dispatcher ?? $this->defaultDispatcher]['orphaned_events'] ?? [];
}
public function getName(): string
{
return 'events';
}
}
@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class ExceptionDataCollector extends DataCollector
{
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
if (null !== $exception) {
$this->data = [
'exception' => FlattenException::createWithDataRepresentation($exception),
];
}
}
public function hasException(): bool
{
return isset($this->data['exception']);
}
public function getException(): \Exception|FlattenException
{
return $this->data['exception'];
}
public function getMessage(): string
{
return $this->data['exception']->getMessage();
}
public function getCode(): int
{
return $this->data['exception']->getCode();
}
public function getStatusCode(): int
{
return $this->data['exception']->getStatusCode();
}
public function getTrace(): array
{
return $this->data['exception']->getTrace();
}
public function getName(): string
{
return 'exception';
}
}
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
/**
* LateDataCollectorInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface LateDataCollectorInterface
{
/**
* Collects data as late as possible.
*
* @return void
*/
public function lateCollect();
}
@@ -0,0 +1,335 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface
{
private ?DebugLoggerInterface $logger;
private ?string $containerPathPrefix;
private ?Request $currentRequest = null;
private ?RequestStack $requestStack;
private ?array $processedLogs = null;
public function __construct(?object $logger = null, ?string $containerPathPrefix = null, ?RequestStack $requestStack = null)
{
$this->logger = DebugLoggerConfigurator::getDebugLogger($logger);
$this->containerPathPrefix = $containerPathPrefix;
$this->requestStack = $requestStack;
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->currentRequest = $this->requestStack && $this->requestStack->getMainRequest() !== $request ? $request : null;
}
public function lateCollect(): void
{
if ($this->logger) {
$containerDeprecationLogs = $this->getContainerDeprecationLogs();
$this->data = $this->computeErrorsCount($containerDeprecationLogs);
// get compiler logs later (only when they are needed) to improve performance
$this->data['compiler_logs'] = [];
$this->data['compiler_logs_filepath'] = $this->containerPathPrefix.'Compiler.log';
$this->data['logs'] = $this->sanitizeLogs(array_merge($this->logger->getLogs($this->currentRequest), $containerDeprecationLogs));
$this->data = $this->cloneVar($this->data);
}
$this->currentRequest = null;
}
public function getLogs(): Data|array
{
return $this->data['logs'] ?? [];
}
public function getProcessedLogs(): array
{
if (null !== $this->processedLogs) {
return $this->processedLogs;
}
$rawLogs = $this->getLogs();
if ([] === $rawLogs) {
return $this->processedLogs = $rawLogs;
}
$logs = [];
foreach ($this->getLogs()->getValue() as $rawLog) {
$rawLogData = $rawLog->getValue();
if ($rawLogData['priority']->getValue() > 300) {
$logType = 'error';
} elseif (isset($rawLogData['scream']) && false === $rawLogData['scream']->getValue()) {
$logType = 'deprecation';
} elseif (isset($rawLogData['scream']) && true === $rawLogData['scream']->getValue()) {
$logType = 'silenced';
} else {
$logType = 'regular';
}
$logs[] = [
'type' => $logType,
'errorCount' => $rawLog['errorCount'] ?? 1,
'timestamp' => $rawLogData['timestamp_rfc3339']->getValue(),
'priority' => $rawLogData['priority']->getValue(),
'priorityName' => $rawLogData['priorityName']->getValue(),
'channel' => $rawLogData['channel']->getValue(),
'message' => $rawLogData['message'],
'context' => $rawLogData['context'],
];
}
// sort logs from oldest to newest
usort($logs, static fn ($logA, $logB) => $logA['timestamp'] <=> $logB['timestamp']);
return $this->processedLogs = $logs;
}
public function getFilters(): array
{
$filters = [
'channel' => [],
'priority' => [
'Debug' => 100,
'Info' => 200,
'Notice' => 250,
'Warning' => 300,
'Error' => 400,
'Critical' => 500,
'Alert' => 550,
'Emergency' => 600,
],
];
$allChannels = [];
foreach ($this->getProcessedLogs() as $log) {
if ('' === trim($log['channel'] ?? '')) {
continue;
}
$allChannels[] = $log['channel'];
}
$channels = array_unique($allChannels);
sort($channels);
$filters['channel'] = $channels;
return $filters;
}
public function getPriorities(): Data|array
{
return $this->data['priorities'] ?? [];
}
public function countErrors(): int
{
return $this->data['error_count'] ?? 0;
}
public function countDeprecations(): int
{
return $this->data['deprecation_count'] ?? 0;
}
public function countWarnings(): int
{
return $this->data['warning_count'] ?? 0;
}
public function countScreams(): int
{
return $this->data['scream_count'] ?? 0;
}
public function getCompilerLogs(): Data
{
return $this->cloneVar($this->getContainerCompilerLogs($this->data['compiler_logs_filepath'] ?? null));
}
public function getName(): string
{
return 'logger';
}
private function getContainerDeprecationLogs(): array
{
if (null === $this->containerPathPrefix || !is_file($file = $this->containerPathPrefix.'Deprecations.log')) {
return [];
}
if ('' === $logContent = trim(file_get_contents($file))) {
return [];
}
$bootTime = filemtime($file);
$logs = [];
foreach (unserialize($logContent) as $log) {
$log['context'] = ['exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'], $log['trace'], $log['count'])];
$log['timestamp'] = $bootTime;
$log['timestamp_rfc3339'] = (new \DateTimeImmutable())->setTimestamp($bootTime)->format(\DateTimeInterface::RFC3339_EXTENDED);
$log['priority'] = 100;
$log['priorityName'] = 'DEBUG';
$log['channel'] = null;
$log['scream'] = false;
unset($log['type'], $log['file'], $log['line'], $log['trace'], $log['trace'], $log['count']);
$logs[] = $log;
}
return $logs;
}
private function getContainerCompilerLogs(?string $compilerLogsFilepath = null): array
{
if (!$compilerLogsFilepath || !is_file($compilerLogsFilepath)) {
return [];
}
$logs = [];
foreach (file($compilerLogsFilepath, \FILE_IGNORE_NEW_LINES) as $log) {
$log = explode(': ', $log, 2);
if (!isset($log[1]) || !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)++$/', $log[0])) {
$log = ['Unknown Compiler Pass', implode(': ', $log)];
}
$logs[$log[0]][] = ['message' => $log[1]];
}
return $logs;
}
private function sanitizeLogs(array $logs): array
{
$sanitizedLogs = [];
$silencedLogs = [];
foreach ($logs as $log) {
if (!$this->isSilencedOrDeprecationErrorLog($log)) {
$sanitizedLogs[] = $log;
continue;
}
$message = '_'.$log['message'];
$exception = $log['context']['exception'];
if ($exception instanceof SilencedErrorContext) {
if (isset($silencedLogs[$h = spl_object_hash($exception)])) {
continue;
}
$silencedLogs[$h] = true;
if (!isset($sanitizedLogs[$message])) {
$sanitizedLogs[$message] = $log + [
'errorCount' => 0,
'scream' => true,
];
}
$sanitizedLogs[$message]['errorCount'] += $exception->count;
continue;
}
$errorId = hash('xxh128', "{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}\0{$message}", true);
if (isset($sanitizedLogs[$errorId])) {
++$sanitizedLogs[$errorId]['errorCount'];
} else {
$log += [
'errorCount' => 1,
'scream' => false,
];
$sanitizedLogs[$errorId] = $log;
}
}
return array_values($sanitizedLogs);
}
private function isSilencedOrDeprecationErrorLog(array $log): bool
{
if (!isset($log['context']['exception'])) {
return false;
}
$exception = $log['context']['exception'];
if ($exception instanceof SilencedErrorContext) {
return true;
}
if ($exception instanceof \ErrorException && \in_array($exception->getSeverity(), [\E_DEPRECATED, \E_USER_DEPRECATED], true)) {
return true;
}
return false;
}
private function computeErrorsCount(array $containerDeprecationLogs): array
{
$silencedLogs = [];
$count = [
'error_count' => $this->logger->countErrors($this->currentRequest),
'deprecation_count' => 0,
'warning_count' => 0,
'scream_count' => 0,
'priorities' => [],
];
foreach ($this->logger->getLogs($this->currentRequest) as $log) {
if (isset($count['priorities'][$log['priority']])) {
++$count['priorities'][$log['priority']]['count'];
} else {
$count['priorities'][$log['priority']] = [
'count' => 1,
'name' => $log['priorityName'],
];
}
if ('WARNING' === $log['priorityName']) {
++$count['warning_count'];
}
if ($this->isSilencedOrDeprecationErrorLog($log)) {
$exception = $log['context']['exception'];
if ($exception instanceof SilencedErrorContext) {
if (isset($silencedLogs[$h = spl_object_hash($exception)])) {
continue;
}
$silencedLogs[$h] = true;
$count['scream_count'] += $exception->count;
} else {
++$count['deprecation_count'];
}
}
}
foreach ($containerDeprecationLogs as $deprecationLog) {
$count['deprecation_count'] += $deprecationLog['context']['exception']->count;
}
ksort($count['priorities']);
return $count;
}
}
@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class MemoryDataCollector extends DataCollector implements LateDataCollectorInterface
{
public function __construct()
{
$this->reset();
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->updateMemoryUsage();
}
public function reset(): void
{
$this->data = [
'memory' => 0,
'memory_limit' => $this->convertToBytes(\ini_get('memory_limit')),
];
}
public function lateCollect(): void
{
$this->updateMemoryUsage();
}
public function getMemory(): int
{
return $this->data['memory'];
}
public function getMemoryLimit(): int|float
{
return $this->data['memory_limit'];
}
public function updateMemoryUsage(): void
{
$this->data['memory'] = memory_get_peak_usage(true);
}
public function getName(): string
{
return 'memory';
}
private function convertToBytes(string $memoryLimit): int|float
{
if ('-1' === $memoryLimit) {
return -1;
}
$memoryLimit = strtolower($memoryLimit);
$max = strtolower(ltrim($memoryLimit, '+'));
if (str_starts_with($max, '0x')) {
$max = \intval($max, 16);
} elseif (str_starts_with($max, '0')) {
$max = \intval($max, 8);
} else {
$max = (int) $max;
}
switch (substr($memoryLimit, -1)) {
case 't': $max *= 1024;
// no break
case 'g': $max *= 1024;
// no break
case 'm': $max *= 1024;
// no break
case 'k': $max *= 1024;
}
return $max;
}
}
@@ -0,0 +1,535 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class RequestDataCollector extends DataCollector implements EventSubscriberInterface, LateDataCollectorInterface
{
/**
* @var \SplObjectStorage<Request, callable>
*/
private \SplObjectStorage $controllers;
private array $sessionUsages = [];
private ?RequestStack $requestStack;
public function __construct(?RequestStack $requestStack = null)
{
$this->controllers = new \SplObjectStorage();
$this->requestStack = $requestStack;
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
// attributes are serialized and as they can be anything, they need to be converted to strings.
$attributes = [];
$route = '';
foreach ($request->attributes->all() as $key => $value) {
if ('_route' === $key) {
$route = \is_object($value) ? $value->getPath() : $value;
$attributes[$key] = $route;
} else {
$attributes[$key] = $value;
}
}
$content = $request->getContent();
$sessionMetadata = [];
$sessionAttributes = [];
$flashes = [];
if ($request->hasSession()) {
$session = $request->getSession();
if ($session->isStarted()) {
$sessionMetadata['Created'] = date(\DATE_RFC822, $session->getMetadataBag()->getCreated());
$sessionMetadata['Last used'] = date(\DATE_RFC822, $session->getMetadataBag()->getLastUsed());
$sessionMetadata['Lifetime'] = $session->getMetadataBag()->getLifetime();
$sessionAttributes = $session->all();
$flashes = $session->getFlashBag()->peekAll();
}
}
$statusCode = $response->getStatusCode();
$responseCookies = [];
foreach ($response->headers->getCookies() as $cookie) {
$responseCookies[$cookie->getName()] = $cookie;
}
$dotenvVars = [];
foreach (explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '') as $name) {
if ('' !== $name && isset($_ENV[$name])) {
$dotenvVars[$name] = $_ENV[$name];
}
}
$this->data = [
'method' => $request->getMethod(),
'format' => $request->getRequestFormat(),
'content_type' => $response->headers->get('Content-Type', 'text/html'),
'status_text' => Response::$statusTexts[$statusCode] ?? '',
'status_code' => $statusCode,
'request_query' => $request->query->all(),
'request_request' => $request->request->all(),
'request_files' => $request->files->all(),
'request_headers' => $request->headers->all(),
'request_server' => $request->server->all(),
'request_cookies' => $request->cookies->all(),
'request_attributes' => $attributes,
'route' => $route,
'response_headers' => $response->headers->all(),
'response_cookies' => $responseCookies,
'session_metadata' => $sessionMetadata,
'session_attributes' => $sessionAttributes,
'session_usages' => array_values($this->sessionUsages),
'stateless_check' => $this->requestStack?->getMainRequest()?->attributes->get('_stateless') ?? false,
'flashes' => $flashes,
'path_info' => $request->getPathInfo(),
'controller' => 'n/a',
'locale' => $request->getLocale(),
'dotenv_vars' => $dotenvVars,
];
if (isset($this->data['request_headers']['php-auth-pw'])) {
$this->data['request_headers']['php-auth-pw'] = '******';
}
if (isset($this->data['request_server']['PHP_AUTH_PW'])) {
$this->data['request_server']['PHP_AUTH_PW'] = '******';
}
if (isset($this->data['request_request']['_password'])) {
$encodedPassword = rawurlencode($this->data['request_request']['_password']);
$content = str_replace('_password='.$encodedPassword, '_password=******', $content);
$this->data['request_request']['_password'] = '******';
}
$this->data['content'] = $content;
foreach ($this->data as $key => $value) {
if (!\is_array($value)) {
continue;
}
if ('request_headers' === $key || 'response_headers' === $key) {
$this->data[$key] = array_map(fn ($v) => isset($v[0]) && !isset($v[1]) ? $v[0] : $v, $value);
}
}
if (isset($this->controllers[$request])) {
$this->data['controller'] = $this->parseController($this->controllers[$request]);
unset($this->controllers[$request]);
}
if ($request->attributes->has('_redirected') && $redirectCookie = $request->cookies->get('sf_redirect')) {
$this->data['redirect'] = json_decode($redirectCookie, true);
$response->headers->clearCookie('sf_redirect');
}
if ($response->isRedirect()) {
$response->headers->setCookie(new Cookie(
'sf_redirect',
json_encode([
'token' => $response->headers->get('x-debug-token'),
'route' => $request->attributes->get('_route', 'n/a'),
'method' => $request->getMethod(),
'controller' => $this->parseController($request->attributes->get('_controller')),
'status_code' => $statusCode,
'status_text' => Response::$statusTexts[$statusCode],
]),
0, '/', null, $request->isSecure(), true, false, 'lax'
));
}
$this->data['identifier'] = $this->data['route'] ?: (\is_array($this->data['controller']) ? $this->data['controller']['class'].'::'.$this->data['controller']['method'].'()' : $this->data['controller']);
if ($response->headers->has('x-previous-debug-token')) {
$this->data['forward_token'] = $response->headers->get('x-previous-debug-token');
}
}
public function lateCollect(): void
{
$this->data = $this->cloneVar($this->data);
}
public function reset(): void
{
parent::reset();
$this->controllers = new \SplObjectStorage();
$this->sessionUsages = [];
}
public function getMethod(): string
{
return $this->data['method'];
}
public function getPathInfo(): string
{
return $this->data['path_info'];
}
/**
* @return ParameterBag
*/
public function getRequestRequest()
{
return new ParameterBag($this->data['request_request']->getValue());
}
/**
* @return ParameterBag
*/
public function getRequestQuery()
{
return new ParameterBag($this->data['request_query']->getValue());
}
/**
* @return ParameterBag
*/
public function getRequestFiles()
{
return new ParameterBag($this->data['request_files']->getValue());
}
/**
* @return ParameterBag
*/
public function getRequestHeaders()
{
return new ParameterBag($this->data['request_headers']->getValue());
}
/**
* @return ParameterBag
*/
public function getRequestServer(bool $raw = false)
{
return new ParameterBag($this->data['request_server']->getValue($raw));
}
/**
* @return ParameterBag
*/
public function getRequestCookies(bool $raw = false)
{
return new ParameterBag($this->data['request_cookies']->getValue($raw));
}
/**
* @return ParameterBag
*/
public function getRequestAttributes()
{
return new ParameterBag($this->data['request_attributes']->getValue());
}
/**
* @return ParameterBag
*/
public function getResponseHeaders()
{
return new ParameterBag($this->data['response_headers']->getValue());
}
/**
* @return ParameterBag
*/
public function getResponseCookies()
{
return new ParameterBag($this->data['response_cookies']->getValue());
}
public function getSessionMetadata(): array
{
return $this->data['session_metadata']->getValue();
}
public function getSessionAttributes(): array
{
return $this->data['session_attributes']->getValue();
}
public function getStatelessCheck(): bool
{
return $this->data['stateless_check'];
}
public function getSessionUsages(): Data|array
{
return $this->data['session_usages'];
}
public function getFlashes(): array
{
return $this->data['flashes']->getValue();
}
/**
* @return string|resource
*/
public function getContent()
{
return $this->data['content'];
}
/**
* @return bool
*/
public function isJsonRequest()
{
return 1 === preg_match('{^application/(?:\w+\++)*json$}i', $this->data['request_headers']['content-type']);
}
/**
* @return string|null
*/
public function getPrettyJson()
{
$decoded = json_decode($this->getContent());
return \JSON_ERROR_NONE === json_last_error() ? json_encode($decoded, \JSON_PRETTY_PRINT) : null;
}
public function getContentType(): string
{
return $this->data['content_type'];
}
public function getStatusText(): string
{
return $this->data['status_text'];
}
public function getStatusCode(): int
{
return $this->data['status_code'];
}
public function getFormat(): string
{
return $this->data['format'];
}
public function getLocale(): string
{
return $this->data['locale'];
}
/**
* @return ParameterBag
*/
public function getDotenvVars()
{
return new ParameterBag($this->data['dotenv_vars']->getValue());
}
/**
* Gets the route name.
*
* The _route request attributes is automatically set by the Router Matcher.
*/
public function getRoute(): string
{
return $this->data['route'];
}
public function getIdentifier(): string
{
return $this->data['identifier'];
}
/**
* Gets the route parameters.
*
* The _route_params request attributes is automatically set by the RouterListener.
*/
public function getRouteParams(): array
{
return isset($this->data['request_attributes']['_route_params']) ? $this->data['request_attributes']['_route_params']->getValue() : [];
}
/**
* Gets the parsed controller.
*
* @return array|string|Data The controller as a string or array of data
* with keys 'class', 'method', 'file' and 'line'
*/
public function getController(): array|string|Data
{
return $this->data['controller'];
}
/**
* Gets the previous request attributes.
*
* @return array|Data|false A legacy array of data from the previous redirection response
* or false otherwise
*/
public function getRedirect(): array|Data|false
{
return $this->data['redirect'] ?? false;
}
public function getForwardToken(): ?string
{
return $this->data['forward_token'] ?? null;
}
public function onKernelController(ControllerEvent $event): void
{
$this->controllers[$event->getRequest()] = $event->getController();
}
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
if ($event->getRequest()->cookies->has('sf_redirect')) {
$event->getRequest()->attributes->set('_redirected', true);
}
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'onKernelController',
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
public function getName(): string
{
return 'request';
}
public function collectSessionUsage(): void
{
$trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
$traceEndIndex = \count($trace) - 1;
for ($i = $traceEndIndex; $i > 0; --$i) {
if (null !== ($class = $trace[$i]['class'] ?? null) && (is_subclass_of($class, SessionInterface::class) || is_subclass_of($class, SessionBagInterface::class))) {
$traceEndIndex = $i;
break;
}
}
if ((\count($trace) - 1) === $traceEndIndex) {
return;
}
// Remove part of the backtrace that belongs to session only
array_splice($trace, 0, $traceEndIndex);
// Merge identical backtraces generated by internal call reports
$name = sprintf('%s:%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[0]['line']);
if (!\array_key_exists($name, $this->sessionUsages)) {
$this->sessionUsages[$name] = [
'name' => $name,
'file' => $trace[0]['file'],
'line' => $trace[0]['line'],
'trace' => $trace,
];
}
}
/**
* @return array|string An array of controller data or a simple string
*/
private function parseController(array|object|string|null $controller): array|string
{
if (\is_string($controller) && str_contains($controller, '::')) {
$controller = explode('::', $controller);
}
if (\is_array($controller)) {
try {
$r = new \ReflectionMethod($controller[0], $controller[1]);
return [
'class' => \is_object($controller[0]) ? get_debug_type($controller[0]) : $controller[0],
'method' => $controller[1],
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
} catch (\ReflectionException) {
if (\is_callable($controller)) {
// using __call or __callStatic
return [
'class' => \is_object($controller[0]) ? get_debug_type($controller[0]) : $controller[0],
'method' => $controller[1],
'file' => 'n/a',
'line' => 'n/a',
];
}
}
}
if ($controller instanceof \Closure) {
$r = new \ReflectionFunction($controller);
$controller = [
'class' => $r->getName(),
'method' => null,
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
if (str_contains($r->name, '{closure}')) {
return $controller;
}
$controller['method'] = $r->name;
if ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) {
$controller['class'] = $class->name;
} else {
return $r->name;
}
return $controller;
}
if (\is_object($controller)) {
$r = new \ReflectionClass($controller);
return [
'class' => $r->getName(),
'method' => null,
'file' => $r->getFileName(),
'line' => $r->getStartLine(),
];
}
return \is_string($controller) ? $controller : 'n/a';
}
}
@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class RouterDataCollector extends DataCollector
{
/**
* @var \SplObjectStorage<Request, callable>
*/
protected $controllers;
public function __construct()
{
$this->reset();
}
/**
* @final
*/
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
if ($response instanceof RedirectResponse) {
$this->data['redirect'] = true;
$this->data['url'] = $response->getTargetUrl();
if ($this->controllers->contains($request)) {
$this->data['route'] = $this->guessRoute($request, $this->controllers[$request]);
}
}
unset($this->controllers[$request]);
}
/**
* @return void
*/
public function reset()
{
$this->controllers = new \SplObjectStorage();
$this->data = [
'redirect' => false,
'url' => null,
'route' => null,
];
}
/**
* @return string
*/
protected function guessRoute(Request $request, string|object|array $controller)
{
return 'n/a';
}
/**
* Remembers the controller associated to each request.
*
* @return void
*/
public function onKernelController(ControllerEvent $event)
{
$this->controllers[$event->getRequest()] = $event->getController();
}
/**
* @return bool Whether this request will result in a redirect
*/
public function getRedirect(): bool
{
return $this->data['redirect'];
}
public function getTargetUrl(): ?string
{
return $this->data['url'];
}
public function getTargetRoute(): ?string
{
return $this->data['route'];
}
public function getName(): string
{
return 'router';
}
}
@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Stopwatch\StopwatchEvent;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class TimeDataCollector extends DataCollector implements LateDataCollectorInterface
{
private ?KernelInterface $kernel;
private ?Stopwatch $stopwatch;
public function __construct(?KernelInterface $kernel = null, ?Stopwatch $stopwatch = null)
{
$this->kernel = $kernel;
$this->stopwatch = $stopwatch;
$this->data = ['events' => [], 'stopwatch_installed' => false, 'start_time' => 0];
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
if (null !== $this->kernel) {
$startTime = $this->kernel->getStartTime();
} else {
$startTime = $request->server->get('REQUEST_TIME_FLOAT');
}
$this->data = [
'token' => $request->attributes->get('_stopwatch_token'),
'start_time' => $startTime * 1000,
'events' => [],
'stopwatch_installed' => class_exists(Stopwatch::class, false),
];
}
public function reset(): void
{
$this->data = ['events' => [], 'stopwatch_installed' => false, 'start_time' => 0];
$this->stopwatch?->reset();
}
public function lateCollect(): void
{
if (null !== $this->stopwatch && isset($this->data['token'])) {
$this->setEvents($this->stopwatch->getSectionEvents($this->data['token']));
}
unset($this->data['token']);
}
/**
* @param StopwatchEvent[] $events The request events
*/
public function setEvents(array $events): void
{
foreach ($events as $event) {
$event->ensureStopped();
}
$this->data['events'] = $events;
}
/**
* @return StopwatchEvent[]
*/
public function getEvents(): array
{
return $this->data['events'];
}
/**
* Gets the request elapsed time.
*/
public function getDuration(): float
{
if (!isset($this->data['events']['__section__'])) {
return 0;
}
$lastEvent = $this->data['events']['__section__'];
return $lastEvent->getOrigin() + $lastEvent->getDuration() - $this->getStartTime();
}
/**
* Gets the initialization time.
*
* This is the time spent until the beginning of the request handling.
*/
public function getInitTime(): float
{
if (!isset($this->data['events']['__section__'])) {
return 0;
}
return $this->data['events']['__section__']->getOrigin() - $this->getStartTime();
}
public function getStartTime(): float
{
return $this->data['start_time'];
}
public function isStopwatchInstalled(): bool
{
return $this->data['stopwatch_installed'];
}
public function getName(): string
{
return 'time';
}
}