vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php line 188

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. /*
  3.  * This file is part of the Monolog package.
  4.  *
  5.  * (c) Jordi Boggiano <j.boggiano@seld.be>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Monolog\Handler;
  11. use Monolog\Logger;
  12. use Monolog\Utils;
  13. /**
  14.  * Stores to any stream resource
  15.  *
  16.  * Can be used to store into php://stderr, remote and local files, etc.
  17.  *
  18.  * @author Jordi Boggiano <j.boggiano@seld.be>
  19.  *
  20.  * @phpstan-import-type FormattedRecord from AbstractProcessingHandler
  21.  */
  22. class StreamHandler extends AbstractProcessingHandler
  23. {
  24.     /** @const int */
  25.     protected const MAX_CHUNK_SIZE 2147483647;
  26.     /** @const int 10MB */
  27.     protected const DEFAULT_CHUNK_SIZE 10 1024 1024;
  28.     /** @var int */
  29.     protected $streamChunkSize;
  30.     /** @var resource|null */
  31.     protected $stream;
  32.     /** @var ?string */
  33.     protected $url null;
  34.     /** @var ?string */
  35.     private $errorMessage null;
  36.     /** @var ?int */
  37.     protected $filePermission;
  38.     /** @var bool */
  39.     protected $useLocking;
  40.     /** @var string */
  41.     protected $fileOpenMode;
  42.     /** @var true|null */
  43.     private $dirCreated null;
  44.     /** @var bool */
  45.     private $retrying false;
  46.     /**
  47.      * @param resource|string $stream         If a missing path can't be created, an UnexpectedValueException will be thrown on first write
  48.      * @param int|null        $filePermission Optional file permissions (default (0644) are only for owner read/write)
  49.      * @param bool            $useLocking     Try to lock log file before doing any writes
  50.      * @param string          $fileOpenMode   The fopen() mode used when opening a file, if $stream is a file path
  51.      *
  52.      * @throws \InvalidArgumentException If stream is not a resource or string
  53.      */
  54.     public function __construct($stream$level Logger::DEBUGbool $bubble true, ?int $filePermission nullbool $useLocking false$fileOpenMode 'a')
  55.     {
  56.         parent::__construct($level$bubble);
  57.         if (($phpMemoryLimit Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
  58.             if ($phpMemoryLimit 0) {
  59.                 // use max 10% of allowed memory for the chunk size, and at least 100KB
  60.                 $this->streamChunkSize min(static::MAX_CHUNK_SIZEmax((int) ($phpMemoryLimit 10), 100 1024));
  61.             } else {
  62.                 // memory is unlimited, set to the default 10MB
  63.                 $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
  64.             }
  65.         } else {
  66.             // no memory limit information, set to the default 10MB
  67.             $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
  68.         }
  69.         if (is_resource($stream)) {
  70.             $this->stream $stream;
  71.             stream_set_chunk_size($this->stream$this->streamChunkSize);
  72.         } elseif (is_string($stream)) {
  73.             $this->url Utils::canonicalizePath($stream);
  74.         } else {
  75.             throw new \InvalidArgumentException('A stream must either be a resource or a string.');
  76.         }
  77.         $this->fileOpenMode $fileOpenMode;
  78.         $this->filePermission $filePermission;
  79.         $this->useLocking $useLocking;
  80.     }
  81.     /**
  82.      * {@inheritDoc}
  83.      */
  84.     public function close(): void
  85.     {
  86.         if ($this->url && is_resource($this->stream)) {
  87.             fclose($this->stream);
  88.         }
  89.         $this->stream null;
  90.         $this->dirCreated null;
  91.     }
  92.     /**
  93.      * Return the currently active stream if it is open
  94.      *
  95.      * @return resource|null
  96.      */
  97.     public function getStream()
  98.     {
  99.         return $this->stream;
  100.     }
  101.     /**
  102.      * Return the stream URL if it was configured with a URL and not an active resource
  103.      *
  104.      * @return string|null
  105.      */
  106.     public function getUrl(): ?string
  107.     {
  108.         return $this->url;
  109.     }
  110.     /**
  111.      * @return int
  112.      */
  113.     public function getStreamChunkSize(): int
  114.     {
  115.         return $this->streamChunkSize;
  116.     }
  117.     /**
  118.      * {@inheritDoc}
  119.      */
  120.     protected function write(array $record): void
  121.     {
  122.         if (!is_resource($this->stream)) {
  123.             $url $this->url;
  124.             if (null === $url || '' === $url) {
  125.                 throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().' Utils::getRecordMessageForException($record));
  126.             }
  127.             $this->createDir($url);
  128.             $this->errorMessage null;
  129.             set_error_handler(function (...$args) {
  130.                 return $this->customErrorHandler(...$args);
  131.             });
  132.             try {
  133.                 $stream fopen($url$this->fileOpenMode);
  134.                 if ($this->filePermission !== null) {
  135.                     @chmod($url$this->filePermission);
  136.                 }
  137.             } finally {
  138.                 restore_error_handler();
  139.             }
  140.             if (!is_resource($stream)) {
  141.                 $this->stream null;
  142.                 throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage$url) . Utils::getRecordMessageForException($record));
  143.             }
  144.             stream_set_chunk_size($stream$this->streamChunkSize);
  145.             $this->stream $stream;
  146.         }
  147.         $stream $this->stream;
  148.         if (!is_resource($stream)) {
  149.             throw new \LogicException('No stream was opened yet' Utils::getRecordMessageForException($record));
  150.         }
  151.         if ($this->useLocking) {
  152.             // ignoring errors here, there's not much we can do about them
  153.             flock($streamLOCK_EX);
  154.         }
  155.         $this->errorMessage null;
  156.         set_error_handler(function (...$args) {
  157.             return $this->customErrorHandler(...$args);
  158.         });
  159.         try {
  160.             $this->streamWrite($stream$record);
  161.         } finally {
  162.             restore_error_handler();
  163.         }
  164.         if ($this->errorMessage !== null) {
  165.             $error $this->errorMessage;
  166.             // close the resource if possible to reopen it, and retry the failed write
  167.             if (!$this->retrying && $this->url !== null && $this->url !== 'php://memory') {
  168.                 $this->retrying true;
  169.                 $this->close();
  170.                 $this->write($record);
  171.                 return;
  172.             }
  173.             throw new \UnexpectedValueException('Writing to the log file failed: '.$error Utils::getRecordMessageForException($record));
  174.         }
  175.         $this->retrying false;
  176.         if ($this->useLocking) {
  177.             flock($streamLOCK_UN);
  178.         }
  179.     }
  180.     /**
  181.      * Write to stream
  182.      * @param resource $stream
  183.      * @param array    $record
  184.      *
  185.      * @phpstan-param FormattedRecord $record
  186.      */
  187.     protected function streamWrite($stream, array $record): void
  188.     {
  189.         fwrite($stream, (string) $record['formatted']);
  190.     }
  191.     private function customErrorHandler(int $codestring $msg): bool
  192.     {
  193.         $this->errorMessage preg_replace('{^(fopen|mkdir|fwrite)\(.*?\): }'''$msg);
  194.         return true;
  195.     }
  196.     private function getDirFromStream(string $stream): ?string
  197.     {
  198.         $pos strpos($stream'://');
  199.         if ($pos === false) {
  200.             return dirname($stream);
  201.         }
  202.         if ('file://' === substr($stream07)) {
  203.             return dirname(substr($stream7));
  204.         }
  205.         return null;
  206.     }
  207.     private function createDir(string $url): void
  208.     {
  209.         // Do not try to create dir if it has already been tried.
  210.         if ($this->dirCreated) {
  211.             return;
  212.         }
  213.         $dir $this->getDirFromStream($url);
  214.         if (null !== $dir && !is_dir($dir)) {
  215.             $this->errorMessage null;
  216.             set_error_handler(function (...$args) {
  217.                 return $this->customErrorHandler(...$args);
  218.             });
  219.             $status mkdir($dir0777true);
  220.             restore_error_handler();
  221.             if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage'File exists') === false) {
  222.                 throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage$dir));
  223.             }
  224.         }
  225.         $this->dirCreated true;
  226.     }
  227. }