Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Handler\ChromePHPHandler as BaseChromePhpHandler;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
/**
* ChromePhpHandler.
*
* @author Christophe Coevoet <stof@notk.org>
*
* @final
*/
class ChromePhpHandler extends BaseChromePhpHandler
{
private array $headers = [];
private Response $response;
/**
* Adds the headers to the response once it's created.
*/
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
if (!preg_match(static::USER_AGENT_REGEX, $event->getRequest()->headers->get('User-Agent', ''))) {
self::$sendHeaders = false;
$this->headers = [];
return;
}
$this->response = $event->getResponse();
foreach ($this->headers as $header => $content) {
$this->response->headers->set($header, $content);
}
$this->headers = [];
}
protected function sendHeader($header, $content): void
{
if (!self::$sendHeaders) {
return;
}
if (isset($this->response)) {
$this->response->headers->set($header, $content);
} else {
$this->headers[$header] = $content;
}
}
/**
* Override default behavior since we check it in onKernelResponse.
*/
protected function headersAccepted(): bool
{
return true;
}
}

View File

@@ -0,0 +1,51 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Logger;
use Monolog\LogRecord;
if (Logger::API >= 3) {
/**
* The base class for compatibility between Monolog 3 LogRecord and Monolog 1/2 array records.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @internal
*/
trait CompatibilityHandler
{
abstract private function doHandle(array|LogRecord $record): bool;
public function handle(LogRecord $record): bool
{
return $this->doHandle($record);
}
}
} else {
/**
* The base class for compatibility between Monolog 3 LogRecord and Monolog 1/2 array records.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @internal
*/
trait CompatibilityHandler
{
abstract private function doHandle(array|LogRecord $record): bool;
public function handle(array $record): bool
{
return $this->doHandle($record);
}
}
}

View File

@@ -0,0 +1,51 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Logger;
use Monolog\LogRecord;
if (Logger::API >= 3) {
/**
* The base class for compatibility between Monolog 3 LogRecord and Monolog 1/2 array records.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @internal
*/
trait CompatibilityProcessingHandler
{
abstract private function doWrite(array|LogRecord $record): void;
protected function write(LogRecord $record): void
{
$this->doWrite($record);
}
}
} else {
/**
* The base class for compatibility between Monolog 3 LogRecord and Monolog 1/2 array records.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @internal
*/
trait CompatibilityProcessingHandler
{
abstract private function doWrite(array|LogRecord $record): void;
protected function write(array $record): void
{
$this->doWrite($record);
}
}
}

View File

@@ -0,0 +1,225 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use Monolog\LogRecord;
use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\VarDumper\Dumper\CliDumper;
if (Logger::API >= 3) {
/**
* The base class for compatibility between Monolog 3 LogRecord and Monolog 1/2 array records.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @internal
*/
trait CompatibilityIsHandlingHandler
{
abstract private function doIsHandling(array|LogRecord $record): bool;
public function isHandling(LogRecord $record): bool
{
return $this->doIsHandling($record);
}
}
} else {
/**
* The base class for compatibility between Monolog 3 LogRecord and Monolog 1/2 array records.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @internal
*/
trait CompatibilityIsHandlingHandler
{
abstract private function doIsHandling(array|LogRecord $record): bool;
public function isHandling(array $record): bool
{
return $this->doIsHandling($record);
}
}
}
/**
* Writes logs to the console output depending on its verbosity setting.
*
* It is disabled by default and gets activated as soon as a command is executed.
* Instead of listening to the console events, the output can also be set manually.
*
* The minimum logging level at which this handler will be triggered depends on the
* verbosity setting of the console output. The default mapping is:
* - OutputInterface::VERBOSITY_NORMAL will show all WARNING and higher logs
* - OutputInterface::VERBOSITY_VERBOSE (-v) will show all NOTICE and higher logs
* - OutputInterface::VERBOSITY_VERY_VERBOSE (-vv) will show all INFO and higher logs
* - OutputInterface::VERBOSITY_DEBUG (-vvv) will show all DEBUG and higher logs, i.e. all logs
*
* This mapping can be customized with the $verbosityLevelMap constructor parameter.
*
* @author Tobias Schultze <http://tobion.de>
*
* @final since Symfony 6.1
*/
class ConsoleHandler extends AbstractProcessingHandler implements EventSubscriberInterface
{
use CompatibilityHandler;
use CompatibilityIsHandlingHandler;
use CompatibilityProcessingHandler;
private ?OutputInterface $output;
private array $verbosityLevelMap = [
OutputInterface::VERBOSITY_QUIET => Logger::ERROR,
OutputInterface::VERBOSITY_NORMAL => Logger::WARNING,
OutputInterface::VERBOSITY_VERBOSE => Logger::NOTICE,
OutputInterface::VERBOSITY_VERY_VERBOSE => Logger::INFO,
OutputInterface::VERBOSITY_DEBUG => Logger::DEBUG,
];
private array $consoleFormatterOptions;
/**
* @param OutputInterface|null $output The console output to use (the handler remains disabled when passing null
* until the output is set, e.g. by using console events)
* @param bool $bubble Whether the messages that are handled can bubble up the stack
* @param array $verbosityLevelMap Array that maps the OutputInterface verbosity to a minimum logging
* level (leave empty to use the default mapping)
*/
public function __construct(?OutputInterface $output = null, bool $bubble = true, array $verbosityLevelMap = [], array $consoleFormatterOptions = [])
{
parent::__construct(Logger::DEBUG, $bubble);
$this->output = $output;
if ($verbosityLevelMap) {
$this->verbosityLevelMap = $verbosityLevelMap;
}
$this->consoleFormatterOptions = $consoleFormatterOptions;
}
private function doIsHandling(array|LogRecord $record): bool
{
return $this->updateLevel() && parent::isHandling($record);
}
private function doHandle(array|LogRecord $record): bool
{
// we have to update the logging level each time because the verbosity of the
// console output might have changed in the meantime (it is not immutable)
return $this->updateLevel() && parent::handle($record);
}
/**
* Sets the console output to use for printing logs.
*
* @return void
*/
public function setOutput(OutputInterface $output)
{
$this->output = $output;
}
/**
* Disables the output.
*/
public function close(): void
{
$this->output = null;
parent::close();
}
/**
* Before a command is executed, the handler gets activated and the console output
* is set in order to know where to write the logs.
*
* @return void
*/
public function onCommand(ConsoleCommandEvent $event)
{
$output = $event->getOutput();
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
$this->setOutput($output);
}
/**
* After a command has been executed, it disables the output.
*
* @return void
*/
public function onTerminate(ConsoleTerminateEvent $event)
{
$this->close();
}
public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::COMMAND => ['onCommand', 255],
ConsoleEvents::TERMINATE => ['onTerminate', -255],
];
}
private function doWrite(array|LogRecord $record): void
{
// at this point we've determined for sure that we want to output the record, so use the output's own verbosity
$this->output->write((string) $record['formatted'], false, $this->output->getVerbosity());
}
protected function getDefaultFormatter(): FormatterInterface
{
if (!class_exists(CliDumper::class)) {
return new LineFormatter();
}
if (!$this->output) {
return new ConsoleFormatter($this->consoleFormatterOptions);
}
return new ConsoleFormatter(array_replace([
'colors' => $this->output->isDecorated(),
'multiline' => OutputInterface::VERBOSITY_DEBUG <= $this->output->getVerbosity(),
], $this->consoleFormatterOptions));
}
/**
* Updates the logging level based on the verbosity setting of the console output.
*
* @return bool Whether the handler is enabled and verbosity is not set to quiet
*/
private function updateLevel(): bool
{
if (null === $this->output) {
return false;
}
$verbosity = $this->output->getVerbosity();
if (isset($this->verbosityLevelMap[$verbosity])) {
$this->setLevel($this->verbosityLevelMap[$verbosity]);
} else {
$this->setLevel(Logger::DEBUG);
}
return true;
}
}

View File

@@ -0,0 +1,189 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\LogstashFormatter;
use Monolog\Handler\AbstractHandler;
use Monolog\Handler\FormattableHandlerTrait;
use Monolog\Handler\ProcessableHandlerTrait;
use Monolog\Level;
use Monolog\Logger;
use Monolog\LogRecord;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Push logs directly to Elasticsearch and format them according to Logstash specification.
*
* This handler dials directly with the HTTP interface of Elasticsearch. This
* means it will slow down your application if Elasticsearch takes times to
* answer. Even if all HTTP calls are done asynchronously.
*
* In a development environment, it's fine to keep the default configuration:
* for each log, an HTTP request will be made to push the log to Elasticsearch.
*
* In a production environment, it's highly recommended to wrap this handler
* in a handler with buffering capabilities (like the FingersCrossedHandler, or
* BufferHandler) in order to call Elasticsearch only once with a bulk push. For
* even better performance and fault tolerance, a proper ELK (https://www.elastic.co/what-is/elk-stack)
* stack is recommended.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @final since Symfony 6.1
*/
class ElasticsearchLogstashHandler extends AbstractHandler
{
use CompatibilityHandler;
use FormattableHandlerTrait;
use ProcessableHandlerTrait;
private string $endpoint;
private string $index;
private HttpClientInterface $client;
private string $elasticsearchVersion;
/**
* @var \SplObjectStorage<ResponseInterface, null>
*/
private \SplObjectStorage $responses;
public function __construct(string $endpoint = 'http://127.0.0.1:9200', string $index = 'monolog', ?HttpClientInterface $client = null, string|int|Level $level = Logger::DEBUG, bool $bubble = true, string $elasticsearchVersion = '1.0.0')
{
if (!interface_exists(HttpClientInterface::class)) {
throw new \LogicException(\sprintf('The "%s" handler needs an HTTP client. Try running "composer require symfony/http-client".', __CLASS__));
}
parent::__construct($level, $bubble);
$this->endpoint = $endpoint;
$this->index = $index;
$this->client = $client ?: HttpClient::create(['timeout' => 1]);
$this->responses = new \SplObjectStorage();
$this->elasticsearchVersion = $elasticsearchVersion;
}
private function doHandle(array|LogRecord $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
$this->sendToElasticsearch([$record]);
return !$this->bubble;
}
public function handleBatch(array $records): void
{
$records = array_filter($records, $this->isHandling(...));
if ($records) {
$this->sendToElasticsearch($records);
}
}
protected function getDefaultFormatter(): FormatterInterface
{
// Monolog 1.X
if (\defined(LogstashFormatter::class.'::V1')) {
return new LogstashFormatter('application', null, null, 'ctxt_', LogstashFormatter::V1);
}
// Monolog 2.X
return new LogstashFormatter('application');
}
private function sendToElasticsearch(array $records): void
{
$formatter = $this->getFormatter();
if (version_compare($this->elasticsearchVersion, '7', '>=')) {
$headers = json_encode([
'index' => [
'_index' => $this->index,
],
]);
} else {
$headers = json_encode([
'index' => [
'_index' => $this->index,
'_type' => '_doc',
],
]);
}
$body = '';
foreach ($records as $record) {
foreach ($this->processors as $processor) {
$record = $processor($record);
}
$body .= $headers;
$body .= "\n";
$body .= $formatter->format($record);
$body .= "\n";
}
$response = $this->client->request('POST', $this->endpoint.'/_bulk', [
'body' => $body,
'headers' => [
'Content-Type' => 'application/json',
],
]);
$this->responses[$response] = null;
$this->wait(false);
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
/**
* @return void
*/
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->wait(true);
}
private function wait(bool $blocking): void
{
foreach ($this->client->stream($this->responses, $blocking ? null : 0.0) as $response => $chunk) {
try {
if ($chunk->isTimeout() && !$blocking) {
continue;
}
if (!$chunk->isFirst() && !$chunk->isLast()) {
continue;
}
if ($chunk->isLast()) {
unset($this->responses[$response]);
}
} catch (ExceptionInterface $e) {
unset($this->responses[$response]);
error_log(\sprintf("Could not push logs to Elasticsearch:\n%s", (string) $e));
}
}
}
}

View File

@@ -0,0 +1,70 @@
<?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\Bridge\Monolog\Handler\FingersCrossed;
use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
use Monolog\LogRecord;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Activation strategy that ignores certain HTTP codes.
*
* @author Shaun Simmons <shaun@envysphere.com>
* @author Pierrick Vignand <pierrick.vignand@gmail.com>
*/
final class HttpCodeActivationStrategy implements ActivationStrategyInterface
{
/**
* @param array $exclusions each exclusion must have a "code" and "urls" keys
*/
public function __construct(
private RequestStack $requestStack,
private array $exclusions,
private ActivationStrategyInterface $inner,
) {
foreach ($exclusions as $exclusion) {
if (!\array_key_exists('code', $exclusion)) {
throw new \LogicException('An exclusion must have a "code" key.');
}
if (!\array_key_exists('urls', $exclusion)) {
throw new \LogicException('An exclusion must have a "urls" key.');
}
}
}
public function isHandlerActivated(array|LogRecord $record): bool
{
$isActivated = $this->inner->isHandlerActivated($record);
if (
$isActivated
&& isset($record['context']['exception'])
&& $record['context']['exception'] instanceof HttpException
&& ($request = $this->requestStack->getMainRequest())
) {
foreach ($this->exclusions as $exclusion) {
if ($record['context']['exception']->getStatusCode() !== $exclusion['code']) {
continue;
}
if (\count($exclusion['urls'])) {
return !preg_match('{('.implode('|', $exclusion['urls']).')}i', $request->getPathInfo());
}
return false;
}
}
return $isActivated;
}
}

View File

@@ -0,0 +1,54 @@
<?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\Bridge\Monolog\Handler\FingersCrossed;
use Monolog\Handler\FingersCrossed\ActivationStrategyInterface;
use Monolog\LogRecord;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Activation strategy that ignores 404s for certain URLs.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
* @author Fabien Potencier <fabien@symfony.com>
* @author Pierrick Vignand <pierrick.vignand@gmail.com>
*/
final class NotFoundActivationStrategy implements ActivationStrategyInterface
{
private string $exclude;
public function __construct(
private RequestStack $requestStack,
array $excludedUrls,
private ActivationStrategyInterface $inner,
) {
$this->exclude = '{('.implode('|', $excludedUrls).')}i';
}
public function isHandlerActivated(array|LogRecord $record): bool
{
$isActivated = $this->inner->isHandlerActivated($record);
if (
$isActivated
&& isset($record['context']['exception'])
&& $record['context']['exception'] instanceof HttpException
&& 404 == $record['context']['exception']->getStatusCode()
&& ($request = $this->requestStack->getMainRequest())
) {
return !preg_match($this->exclude, $request->getPathInfo());
}
return $isActivated;
}
}

View File

@@ -0,0 +1,75 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Handler\FirePHPHandler as BaseFirePHPHandler;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
/**
* FirePHPHandler.
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*
* @final
*/
class FirePHPHandler extends BaseFirePHPHandler
{
private array $headers = [];
private ?Response $response = null;
/**
* Adds the headers to the response once it's created.
*/
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
if (!preg_match('{\bFirePHP/\d+\.\d+\b}', $request->headers->get('User-Agent', ''))
&& !$request->headers->has('X-FirePHP-Version')) {
self::$sendHeaders = false;
$this->headers = [];
return;
}
$this->response = $event->getResponse();
foreach ($this->headers as $header => $content) {
$this->response->headers->set($header, $content);
}
$this->headers = [];
}
protected function sendHeader($header, $content): void
{
if (!self::$sendHeaders) {
return;
}
if (null !== $this->response) {
$this->response->headers->set($header, $content);
} else {
$this->headers[$header] = $content;
}
}
/**
* Override default behavior since we check the user agent in onKernelResponse.
*/
protected function headersAccepted(): bool
{
return true;
}
}

View File

@@ -0,0 +1,150 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\HtmlFormatter;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\LogRecord;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
/**
* @author Alexander Borisov <boshurik@gmail.com>
*
* @final since Symfony 6.1
*/
class MailerHandler extends AbstractProcessingHandler
{
use CompatibilityProcessingHandler;
private MailerInterface $mailer;
private \Closure|Email $messageTemplate;
public function __construct(MailerInterface $mailer, callable|Email $messageTemplate, string|int|Level $level = Logger::DEBUG, bool $bubble = true)
{
parent::__construct($level, $bubble);
$this->mailer = $mailer;
$this->messageTemplate = $messageTemplate instanceof Email ? $messageTemplate : $messageTemplate(...);
}
public function handleBatch(array $records): void
{
$messages = [];
if (Logger::API >= 3) {
/** @var LogRecord $record */
foreach ($records as $record) {
if ($record->level->isLowerThan($this->level)) {
continue;
}
$messages[] = $this->processRecord($record);
}
} else {
foreach ($records as $record) {
if ($record['level'] < $this->level) {
continue;
}
$messages[] = $this->processRecord($record);
}
}
if ($messages) {
$this->send((string) $this->getFormatter()->formatBatch($messages), $messages);
}
}
private function doWrite(array|LogRecord $record): void
{
$this->send((string) $record['formatted'], [$record]);
}
/**
* Send a mail with the given content.
*
* @param string $content formatted email body to be sent
* @param array $records the array of log records that formed this content
*
* @return void
*/
protected function send(string $content, array $records)
{
$this->mailer->send($this->buildMessage($content, $records));
}
/**
* Gets the formatter for the Message subject.
*
* @param string $format The format of the subject
*/
protected function getSubjectFormatter(string $format): FormatterInterface
{
return new LineFormatter($format);
}
/**
* Creates instance of Message to be sent.
*
* @param string $content formatted email body to be sent
* @param array $records Log records that formed the content
*/
protected function buildMessage(string $content, array $records): Email
{
if ($this->messageTemplate instanceof Email) {
$message = clone $this->messageTemplate;
} elseif (\is_callable($this->messageTemplate)) {
$message = ($this->messageTemplate)($content, $records);
if (!$message instanceof Email) {
throw new \InvalidArgumentException(\sprintf('Could not resolve message from a callable. Instance of "%s" is expected.', Email::class));
}
} else {
throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it.');
}
if ($records) {
$subjectFormatter = $this->getSubjectFormatter($message->getSubject());
$message->subject($subjectFormatter->format($this->getHighestRecord($records)));
}
if ($this->getFormatter() instanceof HtmlFormatter) {
if ($message->getHtmlCharset()) {
$message->html($content, $message->getHtmlCharset());
} else {
$message->html($content);
}
} else {
if ($message->getTextCharset()) {
$message->text($content, $message->getTextCharset());
} else {
$message->text($content);
}
}
return $message;
}
protected function getHighestRecord(array $records): array|LogRecord
{
$highestRecord = null;
foreach ($records as $record) {
if (null === $highestRecord || $highestRecord['level'] < $record['level']) {
$highestRecord = $record;
}
}
return $highestRecord;
}
}

View File

@@ -0,0 +1,85 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Handler\AbstractHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\LogRecord;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\Notifier\NotifierInterface;
/**
* Uses Notifier as a log handler.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 6.1
*/
class NotifierHandler extends AbstractHandler
{
use CompatibilityHandler;
private NotifierInterface $notifier;
public function __construct(NotifierInterface $notifier, string|int|Level $level = Logger::ERROR, bool $bubble = true)
{
$this->notifier = $notifier;
parent::__construct(Logger::toMonologLevel($level) < Logger::ERROR ? Logger::ERROR : $level, $bubble);
}
private function doHandle(array|LogRecord $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
$this->notify([$record]);
return !$this->bubble;
}
public function handleBatch(array $records): void
{
if ($records = array_filter($records, $this->isHandling(...))) {
$this->notify($records);
}
}
private function notify(array $records): void
{
$record = $this->getHighestRecord($records);
if (($record['context']['exception'] ?? null) instanceof \Throwable) {
$notification = Notification::fromThrowable($record['context']['exception']);
} else {
$notification = new Notification($record['message']);
}
$notification->importanceFromLogLevelName(Logger::getLevelName($record['level']));
$this->notifier->send($notification, ...$this->notifier->getAdminRecipients());
}
private function getHighestRecord(array $records): array|LogRecord
{
$highestRecord = null;
foreach ($records as $record) {
if (null === $highestRecord || $highestRecord['level'] < $record['level']) {
$highestRecord = $record;
}
}
return $highestRecord;
}
}

View File

@@ -0,0 +1,156 @@
<?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\Bridge\Monolog\Handler;
use Monolog\Formatter\FormatterInterface;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Handler\FormattableHandlerTrait;
use Monolog\Level;
use Monolog\Logger;
use Monolog\LogRecord;
use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter;
if (trait_exists(FormattableHandlerTrait::class)) {
/**
* @final since Symfony 6.1
*/
class ServerLogHandler extends AbstractProcessingHandler
{
use CompatibilityHandler;
use CompatibilityProcessingHandler;
use ServerLogHandlerTrait;
protected function getDefaultFormatter(): FormatterInterface
{
return new VarDumperFormatter();
}
}
} else {
/**
* @final since Symfony 6.1
*/
class ServerLogHandler extends AbstractProcessingHandler
{
use CompatibilityHandler;
use CompatibilityProcessingHandler;
use ServerLogHandlerTrait;
protected function getDefaultFormatter()
{
return new VarDumperFormatter();
}
}
}
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @internal since Symfony 6.1
*/
trait ServerLogHandlerTrait
{
private string $host;
/**
* @var resource
*/
private $context;
/**
* @var resource|null
*/
private $socket;
public function __construct(string $host, string|int|Level $level = Logger::DEBUG, bool $bubble = true, array $context = [])
{
parent::__construct($level, $bubble);
if (!str_contains($host, '://')) {
$host = 'tcp://'.$host;
}
$this->host = $host;
$this->context = stream_context_create($context);
}
private function doHandle(array|LogRecord $record): bool
{
if (!$this->isHandling($record)) {
return false;
}
set_error_handler(static fn () => null);
try {
if (!$this->socket = $this->socket ?: $this->createSocket()) {
return false === $this->bubble;
}
} finally {
restore_error_handler();
}
return parent::handle($record);
}
private function doWrite(array|LogRecord $record): void
{
$recordFormatted = $this->formatRecord($record);
set_error_handler(static fn () => null);
try {
if (-1 === stream_socket_sendto($this->socket, $recordFormatted)) {
stream_socket_shutdown($this->socket, \STREAM_SHUT_RDWR);
// Let's retry: the persistent connection might just be stale
if ($this->socket = $this->createSocket()) {
stream_socket_sendto($this->socket, $recordFormatted);
}
}
} finally {
restore_error_handler();
}
}
protected function getDefaultFormatter(): FormatterInterface
{
return new VarDumperFormatter();
}
/**
* @return resource
*/
private function createSocket()
{
$socket = stream_socket_client($this->host, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT | \STREAM_CLIENT_PERSISTENT, $this->context);
if ($socket) {
stream_set_blocking($socket, false);
}
return $socket;
}
private function formatRecord(array|LogRecord $record): string
{
$recordFormatted = $record['formatted'];
foreach (['log_uuid', 'uuid', 'uid'] as $key) {
if (isset($record['extra'][$key])) {
$recordFormatted['log_id'] = $record['extra'][$key];
break;
}
}
return base64_encode(serialize($recordFormatted))."\n";
}
}