One Hat Cyber Team
Your IP:
216.73.216.171
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
proc
/
self
/
root
/
home
/
fluxyjvi
/
www
/
assets
/
images
/
Edit File:
spatie.tar
image-optimizer/src/Optimizer.php 0000644 00000002063 15105603711 0013130 0 ustar 00 <?php namespace Spatie\ImageOptimizer; interface Optimizer { /** * Returns the name of the binary to be executed. * * @return string */ public function binaryName(): string; /** * Determines if the given image can be handled by the optimizer. * * @param \Spatie\ImageOptimizer\Image $image * * @return bool */ public function canHandle(Image $image): bool; /** * Set the path to the image that should be optimized. * * @param string $imagePath * * @return $this */ public function setImagePath(string $imagePath); /** * Set the options the optimizer should use. * * @param array $options * * @return $this */ public function setOptions(array $options = []); /** * Get the command that should be executed. * * @return string */ public function getCommand(): string; /** * Get the temporary file's path. * * @return null|string */ public function getTmpPath(): ?string; } image-optimizer/src/OptimizerChain.php 0000644 00000005266 15105603711 0014103 0 ustar 00 <?php namespace Spatie\ImageOptimizer; use Psr\Log\LoggerInterface; use Symfony\Component\Process\Process; class OptimizerChain { /* @var \Spatie\ImageOptimizer\Optimizer[] */ protected $optimizers = []; /** @var \Psr\Log\LoggerInterface */ protected $logger; /** @var int */ protected $timeout = 60; public function __construct() { $this->useLogger(new DummyLogger()); } public function getOptimizers(): array { return $this->optimizers; } public function addOptimizer(Optimizer $optimizer) { $this->optimizers[] = $optimizer; return $this; } public function setOptimizers(array $optimizers) { $this->optimizers = []; foreach ($optimizers as $optimizer) { $this->addOptimizer($optimizer); } return $this; } /* * Set the amount of seconds each separate optimizer may use. */ public function setTimeout(int $timeoutInSeconds) { $this->timeout = $timeoutInSeconds; return $this; } public function useLogger(LoggerInterface $log) { $this->logger = $log; return $this; } public function optimize(string $pathToImage, string $pathToOutput = null) { if ($pathToOutput) { copy($pathToImage, $pathToOutput); $pathToImage = $pathToOutput; } $image = new Image($pathToImage); $this->logger->info("Start optimizing {$pathToImage}"); foreach ($this->optimizers as $optimizer) { $this->applyOptimizer($optimizer, $image); } } protected function applyOptimizer(Optimizer $optimizer, Image $image) { if (! $optimizer->canHandle($image)) { return; } $optimizerClass = get_class($optimizer); $this->logger->info("Using optimizer: `{$optimizerClass}`"); $optimizer->setImagePath($image->path()); $command = $optimizer->getCommand(); $this->logger->info("Executing `{$command}`"); $process = Process::fromShellCommandline($command); $process ->setTimeout($this->timeout) ->run(); if ( ($tmpPath = $optimizer->getTmpPath()) && file_exists($tmpPath) ) { unlink($tmpPath); } $this->logResult($process); } protected function logResult(Process $process) { if (! $process->isSuccessful()) { $this->logger->error("Process errored with `{$process->getErrorOutput()}`"); return; } $this->logger->info("Process successfully ended with output `{$process->getOutput()}`"); } } image-optimizer/src/Optimizers/Optipng.php 0000644 00000000420 15105603711 0014726 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; class Optipng extends BaseOptimizer { public $binaryName = 'optipng'; public function canHandle(Image $image): bool { return $image->mime() === 'image/png'; } } image-optimizer/src/Optimizers/BaseOptimizer.php 0000644 00000002412 15105603711 0016066 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Optimizer; abstract class BaseOptimizer implements Optimizer { public $options = []; public $imagePath = ''; public $binaryPath = ''; public $tmpPath = null; public function __construct($options = []) { $this->setOptions($options); } public function binaryName(): string { return $this->binaryName; } public function setBinaryPath(string $binaryPath) { if (strlen($binaryPath) > 0 && substr($binaryPath, -1) !== DIRECTORY_SEPARATOR) { $binaryPath = $binaryPath.DIRECTORY_SEPARATOR; } $this->binaryPath = $binaryPath; return $this; } public function setImagePath(string $imagePath) { $this->imagePath = $imagePath; return $this; } public function setOptions(array $options = []) { $this->options = $options; return $this; } public function getCommand(): string { $optionString = implode(' ', $this->options); return "\"{$this->binaryPath}{$this->binaryName}\" {$optionString} ".escapeshellarg($this->imagePath); } public function getTmpPath(): ?string { return $this->tmpPath; } } image-optimizer/src/Optimizers/Avifenc.php 0000644 00000002464 15105603711 0014673 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; class Avifenc extends BaseOptimizer { public $binaryName = 'avifenc'; public $decodeBinaryName = 'avifdec'; public function canHandle(Image $image): bool { if (version_compare(PHP_VERSION, '8.1.0', '<')) { return $image->extension() === 'avif'; } return $image->mime() === 'image/avif'; } public function getCommand(): string { return $this->getDecodeCommand().' && ' .$this->getEncodeCommand(); } protected function getDecodeCommand() { $this->tmpPath = tempnam(sys_get_temp_dir(), 'avifdec').'.png'; $optionString = implode(' ', [ '-j all', '--ignore-icc', '--no-strict', '--png-compress 0', ]); return "\"{$this->binaryPath}{$this->decodeBinaryName}\" {$optionString}" .' '.escapeshellarg($this->imagePath) .' '.escapeshellarg($this->tmpPath); } protected function getEncodeCommand() { $optionString = implode(' ', $this->options); return "\"{$this->binaryPath}{$this->binaryName}\" {$optionString}" .' '.escapeshellarg($this->tmpPath) .' '.escapeshellarg($this->imagePath); } } image-optimizer/src/Optimizers/Pngquant.php 0000644 00000001071 15105603711 0015106 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; class Pngquant extends BaseOptimizer { public $binaryName = 'pngquant'; public function canHandle(Image $image): bool { return $image->mime() === 'image/png'; } public function getCommand(): string { $optionString = implode(' ', $this->options); return "\"{$this->binaryPath}{$this->binaryName}\" {$optionString}" .' '.escapeshellarg($this->imagePath) .' --output='.escapeshellarg($this->imagePath); } } image-optimizer/src/Optimizers/Cwebp.php 0000644 00000001056 15105603711 0014354 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; class Cwebp extends BaseOptimizer { public $binaryName = 'cwebp'; public function canHandle(Image $image): bool { return $image->mime() === 'image/webp'; } public function getCommand(): string { $optionString = implode(' ', $this->options); return "\"{$this->binaryPath}{$this->binaryName}\" {$optionString}" .' '.escapeshellarg($this->imagePath) .' -o '.escapeshellarg($this->imagePath); } } image-optimizer/src/Optimizers/Svgo.php 0000644 00000001373 15105603711 0014234 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; class Svgo extends BaseOptimizer { public $binaryName = 'svgo'; public function canHandle(Image $image): bool { if ($image->extension() !== 'svg') { return false; } return in_array($image->mime(), [ 'text/html', 'image/svg', 'image/svg+xml', 'text/plain', ]); } public function getCommand(): string { $optionString = implode(' ', $this->options); return "\"{$this->binaryPath}{$this->binaryName}\" {$optionString}" .' --input='.escapeshellarg($this->imagePath) .' --output='.escapeshellarg($this->imagePath); } } image-optimizer/src/Optimizers/Jpegoptim.php 0000644 00000000425 15105603711 0015251 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; class Jpegoptim extends BaseOptimizer { public $binaryName = 'jpegoptim'; public function canHandle(Image $image): bool { return $image->mime() === 'image/jpeg'; } } image-optimizer/src/Optimizers/Gifsicle.php 0000644 00000001066 15105603711 0015042 0 ustar 00 <?php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; class Gifsicle extends BaseOptimizer { public $binaryName = 'gifsicle'; public function canHandle(Image $image): bool { return $image->mime() === 'image/gif'; } public function getCommand(): string { $optionString = implode(' ', $this->options); return "\"{$this->binaryPath}{$this->binaryName}\" {$optionString}" .' -i '.escapeshellarg($this->imagePath) .' -o '.escapeshellarg($this->imagePath); } } image-optimizer/src/DummyLogger.php 0000644 00000001462 15105603711 0013403 0 ustar 00 <?php namespace Spatie\ImageOptimizer; use Psr\Log\LoggerInterface; class DummyLogger implements LoggerInterface { public function emergency($message, array $context = []): void { } public function alert($message, array $context = []): void { } public function critical($message, array $context = []): void { } public function error($message, array $context = []): void { } public function warning($message, array $context = []): void { } public function notice($message, array $context = []): void { } public function info($message, array $context = []): void { } public function debug($message, array $context = []): void { } public function log($level, $message, array $context = []): void { } } image-optimizer/src/OptimizerChainFactory.php 0000644 00000004016 15105603711 0015423 0 ustar 00 <?php namespace Spatie\ImageOptimizer; use Spatie\ImageOptimizer\Optimizers\Avifenc; use Spatie\ImageOptimizer\Optimizers\Cwebp; use Spatie\ImageOptimizer\Optimizers\Gifsicle; use Spatie\ImageOptimizer\Optimizers\Jpegoptim; use Spatie\ImageOptimizer\Optimizers\Optipng; use Spatie\ImageOptimizer\Optimizers\Pngquant; use Spatie\ImageOptimizer\Optimizers\Svgo; class OptimizerChainFactory { public static function create(array $config = []): OptimizerChain { $jpegQuality = '--max=85'; $pngQuality = '--quality=85'; $webpQuality = '-q 80'; $avifQuality = '-a cq-level=23'; if (isset($config['quality'])) { $jpegQuality = '--max='.$config['quality']; $pngQuality = '--quality='.$config['quality']; $webpQuality = '-q '.$config['quality']; $avifQuality = '-a cq-level='.round(63 - $config['quality'] * 0.63); } return (new OptimizerChain()) ->addOptimizer(new Jpegoptim([ $jpegQuality, '--strip-all', '--all-progressive', ])) ->addOptimizer(new Pngquant([ $pngQuality, '--force', '--skip-if-larger', ])) ->addOptimizer(new Optipng([ '-i0', '-o2', '-quiet', ])) ->addOptimizer(new Svgo([ '--config=svgo.config.js', ])) ->addOptimizer(new Gifsicle([ '-b', '-O3', ])) ->addOptimizer(new Cwebp([ $webpQuality, '-m 6', '-pass 10', '-mt', ])) ->addOptimizer(new Avifenc([ $avifQuality, '-j all', '--min 0', '--max 63', '--minalpha 0', '--maxalpha 63', '-a end-usage=q', '-a tune=ssim', ])); } } image-optimizer/src/Image.php 0000644 00000001314 15105603711 0012166 0 ustar 00 <?php namespace Spatie\ImageOptimizer; use InvalidArgumentException; class Image { protected $pathToImage = ''; public function __construct(string $pathToImage) { if (! file_exists($pathToImage)) { throw new InvalidArgumentException("`{$pathToImage}` does not exist"); } $this->pathToImage = $pathToImage; } public function mime(): string { return mime_content_type($this->pathToImage); } public function path(): string { return $this->pathToImage; } public function extension(): string { $extension = pathinfo($this->pathToImage, PATHINFO_EXTENSION); return strtolower($extension); } } image-optimizer/README.md 0000644 00000033310 15105603711 0011124 0 ustar 00 # Easily optimize images using PHP [](https://packagist.org/packages/spatie/image-optimizer)  [](https://packagist.org/packages/spatie/image-optimizer) This package can optimize PNGs, JPGs, WEBPs, AVIFs, SVGs and GIFs by running them through a chain of various [image optimization tools](#optimization-tools). Here's how you can use it: ```php use Spatie\ImageOptimizer\OptimizerChainFactory; $optimizerChain = OptimizerChainFactory::create(); $optimizerChain->optimize($pathToImage); ``` The image at `$pathToImage` will be overwritten by an optimized version which should be smaller. The package will automatically detect which optimization binaries are installed on your system and use them. Here are some [example conversions](#example-conversions) that have been done by this package. Loving Laravel? Then head over to [the Laravel specific integration](https://github.com/spatie/laravel-image-optimizer). Using WordPress? Then try out [the WP CLI command](https://github.com/TypistTech/image-optimize-command). SilverStripe enthusiast? Don't waste time, go to [the SilverStripe module](https://github.com/axllent/silverstripe-image-optimiser). ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/image-optimizer.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/image-optimizer) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation You can install the package via composer: ```bash composer require spatie/image-optimizer ``` ### Optimization tools The package will use these optimizers if they are present on your system: - [JpegOptim](https://github.com/tjko/jpegoptim) - [Optipng](http://optipng.sourceforge.net/) - [Pngquant 2](https://pngquant.org/) - [SVGO 1](https://github.com/svg/svgo) - [Gifsicle](http://www.lcdf.org/gifsicle/) - [cwebp](https://developers.google.com/speed/webp/docs/precompiled) - [avifenc](https://github.com/AOMediaCodec/libavif/blob/main/doc/avifenc.1.md) Here's how to install all the optimizers on Ubuntu/Debian: ```bash sudo apt-get install jpegoptim sudo apt-get install optipng sudo apt-get install pngquant sudo npm install -g svgo sudo apt-get install gifsicle sudo apt-get install webp sudo apt-get install libavif-bin # minimum 0.9.3 ``` And here's how to install the binaries on MacOS (using [Homebrew](https://brew.sh/)): ```bash brew install jpegoptim brew install optipng brew install pngquant npm install -g svgo brew install gifsicle brew install webp brew install libavif ``` And here's how to install the binaries on Fedora/RHEL/CentOS: ```bash sudo dnf install epel-release sudo dnf install jpegoptim sudo dnf install optipng sudo dnf install pngquant sudo npm install -g svgo sudo dnf install gifsicle sudo dnf install libwebp-tools sudo dnf install libavif-tools ``` ## Which tools will do what? The package will automatically decide which tools to use on a particular image. ### JPGs JPGs will be made smaller by running them through [JpegOptim](http://freecode.com/projects/jpegoptim). These options are used: - `-m85`: this will store the image with 85% quality. This setting [seems to satisfy Google's Pagespeed compression rules](https://webmasters.stackexchange.com/questions/102094/google-pagespeed-how-to-satisfy-the-new-image-compression-rules) - `--strip-all`: this strips out all text information such as comments and EXIF data - `--all-progressive`: this will make sure the resulting image is a progressive one, meaning it can be downloaded using multiple passes of progressively higher details. ### PNGs PNGs will be made smaller by running them through two tools. The first one is [Pngquant 2](https://pngquant.org/), a lossy PNG compressor. We set no extra options, their defaults are used. After that we run the image through a second one: [Optipng](http://optipng.sourceforge.net/). These options are used: - `-i0`: this will result in a non-interlaced, progressive scanned image - `-o2`: this set the optimization level to two (multiple IDAT compression trials) ### SVGs SVGs will be minified by [SVGO](https://github.com/svg/svgo). SVGO's default configuration will be used, with the omission of the `cleanupIDs` and `removeViewBox` plugins because these are known to cause troubles when displaying multiple optimized SVGs on one page. Please be aware that SVGO can break your svg. You'll find more info on that in this [excellent blogpost](https://www.sarasoueidan.com/blog/svgo-tools/) by [Sara Soueidan](https://twitter.com/SaraSoueidan). ### GIFs GIFs will be optimized by [Gifsicle](http://www.lcdf.org/gifsicle/). These options will be used: - `-O3`: this sets the optimization level to Gifsicle's maximum, which produces the slowest but best results ### WEBPs WEBPs will be optimized by [Cwebp](https://developers.google.com/speed/webp/docs/cwebp). These options will be used: - `-m 6` for the slowest compression method in order to get the best compression. - `-pass 10` for maximizing the amount of analysis pass. - `-mt` multithreading for some speed improvements. - `-q 90` Quality factor that brings the least noticeable changes. (Settings are original taken from [here](https://medium.com/@vinhlh/how-i-apply-webp-for-optimizing-images-9b11068db349)) ### AVIFs AVIFs will be optimized by [avifenc](https://github.com/AOMediaCodec/libavif/blob/main/doc/avifenc.1.md). These options will be used: - `-a cq-level=23`: Constant Quality level. Lower values mean better quality and greater file size (0-63). - `-j all`: Number of jobs (worker threads, `all` uses all available cores). - `--min 0`: Min quantizer for color (0-63). - `--max 63`: Max quantizer for color (0-63). - `--minalpha 0`: Min quantizer for alpha (0-63). - `--maxalpha 63`: Max quantizer for alpha (0-63). - `-a end-usage=q` Rate control mode set to Constant Quality mode. - `-a tune=ssim`: SSIM as tune the encoder for distortion metric. (Settings are original taken from [here](https://web.dev/compress-images-avif/#create-an-avif-image-with-default-settings) and [here](https://github.com/feat-agency/avif)) ## Usage This is the default way to use the package: ``` php use Spatie\ImageOptimizer\OptimizerChainFactory; $optimizerChain = OptimizerChainFactory::create(); $optimizerChain->optimize($pathToImage); ``` The image at `$pathToImage` will be overwritten by an optimized version which should be smaller. The package will automatically detect which optimization binaries are installed on your system and use them. To keep the original image, you can pass through a second argument`optimize`: ```php use Spatie\ImageOptimizer\OptimizerChainFactory; $optimizerChain = OptimizerChainFactory::create(); $optimizerChain->optimize($pathToImage, $pathToOutput); ``` In that example the package won't touch `$pathToImage` and write an optimized version to `$pathToOutput`. ### Setting a timeout You can set the maximum of time in seconds that each individual optimizer in a chain can use by calling `setTimeout`: ```php $optimizerChain ->setTimeout(10) ->optimize($pathToImage); ``` In this example each optimizer in the chain will get a maximum 10 seconds to do it's job. ### Creating your own optimization chains If you want to customize the chain of optimizers you can do so by adding `Optimizer`s manually to an `OptimizerChain`. Here's an example where we only want `optipng` and `jpegoptim` to be used: ```php use Spatie\ImageOptimizer\OptimizerChain; use Spatie\ImageOptimizer\Optimizers\Jpegoptim; use Spatie\ImageOptimizer\Optimizers\Pngquant; $optimizerChain = (new OptimizerChain) ->addOptimizer(new Jpegoptim([ '--strip-all', '--all-progressive', ])) ->addOptimizer(new Pngquant([ '--force', ])) ``` Notice that you can pass the options an `Optimizer` should use to its constructor. ### Writing a custom optimizers Want to use another command line utility to optimize your images? No problem. Just write your own optimizer. An optimizer is any class that implements the `Spatie\ImageOptimizer\Optimizers\Optimizer` interface: ```php namespace Spatie\ImageOptimizer\Optimizers; use Spatie\ImageOptimizer\Image; interface Optimizer { /** * Returns the name of the binary to be executed. * * @return string */ public function binaryName(): string; /** * Determines if the given image can be handled by the optimizer. * * @param \Spatie\ImageOptimizer\Image $image * * @return bool */ public function canHandle(Image $image): bool; /** * Set the path to the image that should be optimized. * * @param string $imagePath * * @return $this */ public function setImagePath(string $imagePath); /** * Set the options the optimizer should use. * * @param array $options * * @return $this */ public function setOptions(array $options = []); /** * Get the command that should be executed. * * @return string */ public function getCommand(): string; } ``` If you want to view an example implementation take a look at [the existing optimizers](https://github.com/spatie/image-optimizer/tree/master/src/Optimizers) shipped with this package. You can easily add your optimizer by using the `addOptimizer` method on an `OptimizerChain`. ``` php use Spatie\ImageOptimizer\ImageOptimizerFactory; $optimizerChain = OptimizerChainFactory::create(); $optimizerChain ->addOptimizer(new YourCustomOptimizer()) ->optimize($pathToImage); ``` ## Logging the optimization process By default the package will not throw any errors and just operate silently. To verify what the package is doing you can set a logger: ```php use Spatie\ImageOptimizer\OptimizerChainFactory; $optimizerChain = OptimizerChainFactory::create(); $optimizerChain ->useLogger(new MyLogger()) ->optimize($pathToImage); ``` A logger is a class that implements `Psr\Log\LoggerInterface`. A good logging library that's fully compliant is [Monolog](https://github.com/Seldaek/monolog). The package will write to log which `Optimizers` are used, which commands are executed and their output. ## Example conversions Here are some real life example conversions done by this package. Methodology for JPG, WEBP, AVIF images: the [original image](https://unsplash.com/photos/jTeQavJjBDs) has been fed to [spatie/image](https://github.com/spatie/image) (using the default GD driver) and resized to 2048px width: ```php Spatie\Image\Image::load('original.jpg') ->width(2048) ->save('image.jpg'); // image.png, image.webp, image.avif ``` ### jpg  Original<br> 771 KB  Optimized<br> 511 KB (-33.7%, DSSIM: 0.00052061) credits: Jeff Sheldon, via [Unsplash](https://unsplash.com) ### webp  Original<br> 461 KB  Optimized<br> 184 KB (-60.0%, DSSIM: 0.00166036) credits: Jeff Sheldon, via [Unsplash](https://unsplash.com) ### avif  Original<br> 725 KB  Optimized<br> 194 KB (-73.2%, DSSIM: 0.00163751) credits: Jeff Sheldon, via [Unsplash](https://unsplash.com) ### png Original: Photoshop 'Save for web' | PNG-24 with transparency<br> 39 KB  Optimized<br> 16 KB (-59%, DSSIM: 0.00000251)  ### svg Original: Illustrator | Web optimized SVG export<br> 25 KB  Optimized<br> 20 KB (-21.5%)  ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. ## Testing ``` bash composer test ``` ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. ## Postcardware You're free to use this package (it's [MIT-licensed](.github/LICENSE.md)), but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). ## Credits - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) This package has been inspired by [psliwa/image-optimizer](https://github.com/psliwa/image-optimizer) Emotional support provided by [Joke Forment](https://twitter.com/pronneur) ## License The MIT License (MIT). Please see [License File](.github/LICENSE.md) for more information. image-optimizer/LICENSE 0000644 00000002042 15105603711 0010650 0 ustar 00 MIT License Copyright (c) Spatie 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. image-optimizer/composer.json 0000644 00000002340 15105603711 0012366 0 ustar 00 { "name": "spatie/image-optimizer", "description": "Easily optimize images using PHP", "keywords": [ "spatie", "image-optimizer" ], "homepage": "https://github.com/spatie/image-optimizer", "license": "MIT", "authors": [ { "name": "Freek Van der Herten", "email": "freek@spatie.be", "homepage": "https://spatie.be", "role": "Developer" } ], "require": { "php": "^7.3|^8.0", "ext-fileinfo": "*", "psr/log": "^1.0 | ^2.0 | ^3.0", "symfony/process": "^4.2|^5.0|^6.0|^7.0" }, "require-dev": { "pestphp/pest": "^1.21", "phpunit/phpunit": "^8.5.21|^9.4.4", "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0" }, "autoload": { "psr-4": { "Spatie\\ImageOptimizer\\": "src" } }, "autoload-dev": { "psr-4": { "Spatie\\ImageOptimizer\\Test\\": "tests" }, "files": [ "tests/helpers.php" ] }, "scripts": { "test": "vendor/bin/phpunit" }, "config": { "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true } } } image-optimizer/svgo.config.js 0000644 00000000310 15105603711 0012417 0 ustar 00 module.exports = { plugins: [ { name: 'preset-default', params: { overrides: { cleanupIDs: false, removeViewBox: false, }, }, }, ], }; image-optimizer/CHANGELOG.md 0000644 00000011266 15105603711 0011464 0 ustar 00 # Changelog All notable changes to `image-optimizer` will be documented in this file ## 1.7.1 - 2023-07-27 ### What's Changed - libavif version note by @0xb4lint in https://github.com/spatie/image-optimizer/pull/199 **Full Changelog**: https://github.com/spatie/image-optimizer/compare/1.7.0...1.7.1 ## 1.7.0 - 2023-07-22 ### What's Changed - README.md file size fixes, DSSIM score, optimized webp replaced by @0xb4lint in https://github.com/spatie/image-optimizer/pull/197 - added AVIF support by @0xb4lint in https://github.com/spatie/image-optimizer/pull/198 **Full Changelog**: https://github.com/spatie/image-optimizer/compare/1.6.4...1.7.0 ## 1.6.4 - 2023-03-10 ### What's Changed - SVGO 3 Support by @l-alexandrov in https://github.com/spatie/image-optimizer/pull/186 ### New Contributors - @l-alexandrov made their first contribution in https://github.com/spatie/image-optimizer/pull/186 **Full Changelog**: https://github.com/spatie/image-optimizer/compare/1.6.3...1.6.4 ## 1.6.3 - 2023-02-28 ### What's Changed - Update .gitattributes by @PaolaRuby in https://github.com/spatie/image-optimizer/pull/161 - Feature: Convert PHPUnit tests to Pest by @mansoorkhan96 in https://github.com/spatie/image-optimizer/pull/167 - Add dependabot automation by @patinthehat in https://github.com/spatie/image-optimizer/pull/173 - Allow Pest Composer Plugin (fix failing tests) by @patinthehat in https://github.com/spatie/image-optimizer/pull/176 - Update Dependabot Automation by @patinthehat in https://github.com/spatie/image-optimizer/pull/175 - DOC: adding SilverStripe link by @sunnysideup in https://github.com/spatie/image-optimizer/pull/177 - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/image-optimizer/pull/183 - WebP Quality Option by @jan-tricks in https://github.com/spatie/image-optimizer/pull/185 - Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/spatie/image-optimizer/pull/174 ### New Contributors - @PaolaRuby made their first contribution in https://github.com/spatie/image-optimizer/pull/161 - @mansoorkhan96 made their first contribution in https://github.com/spatie/image-optimizer/pull/167 - @patinthehat made their first contribution in https://github.com/spatie/image-optimizer/pull/173 - @sunnysideup made their first contribution in https://github.com/spatie/image-optimizer/pull/177 - @dependabot made their first contribution in https://github.com/spatie/image-optimizer/pull/183 - @jan-tricks made their first contribution in https://github.com/spatie/image-optimizer/pull/185 **Full Changelog**: https://github.com/spatie/image-optimizer/compare/1.6.2...1.6.3 ## 1.6.2 - 2021-12-21 ## What's Changed - add support for Symfony 6 by @Nielsvanpach in https://github.com/spatie/image-optimizer/pull/155 **Full Changelog**: https://github.com/spatie/image-optimizer/compare/1.6.1...1.6.2 ## 1.6.1 - 2021-11-17 ## What's Changed - Add PHP 8.1 support by @freekmurze in https://github.com/spatie/image-optimizer/pull/154 **Full Changelog**: https://github.com/spatie/image-optimizer/compare/1.5.0...1.6.1 ## 1.5.0 - 2021-10-18 - Support new releases of psr/log (#150) ## 1.4.0 - 2021-04-22 - use `--skip-if-larger` pngquant option by default (#140) ## 1.3.2 - 2020-11-28 - improve gifsicle (#131) ## 1.3.1 - 2020-10-20 - fix empty string setBinaryPath() (#129) ## 1.3.0 - 2020-10-10 - add support for php 8.0 ## 1.2.1 - 2019-11-23 - allow symfony 5 components ## 1.2.0 - 2019-08-28 - add support for webp ## 1.1.6 - 2019-08-26 - do not export docs directory ## 1.1.5 - 2019-01-15 - fix for svg's - make compatible with PHPUnit 8 ## 1.1.4 - 2019-01-14 - fix deprecation warning for passing strings to processes ## 1.1.3 - 2018-11-19 - require the fileinfo extension ## 1.1.2 - 2018-10-10 - make sure all optimizers use `binaryPath` ## 1.1.1 - 2018-09-10 - fix logger output ## 1.1.0 - 2018-06-05 - add `setBinaryPath` ## 1.0.14 - 2018-03-07 - support more symfony versions ## 1.0.13 - 2018-02-26 - added `text/plain` to the list of valid svg mime types ## 1.0.12. - 2018-02-21 - added `image/svg+xml` mime type ## 1.0.11 - 2018-02-08 - SVG mime type detection in PHP 7.2 ## 1.0.10 - 2018-02-08 - Support symfony ^4.0 - Support phpunit ^7.0 ## 1.0.9 - 2017-11-03 - fix shell command quotes ## 1.0.8 - 2017-09-14 - allow Symfony 2 components - make Google Pagespeed tests pass ## 1.0.7 - 2017-07-29 - lower requirements of dependencies ## 1.0.6 - 2017-07-10 - fix `jpegoptim` parameters ## 1.0.4 - 2017-07-07 - make `setTimeout` chainable ## 1.0.3 - 2017-07-06 - fix `composer.json` ## 1.0.2 - 2017-07-06 - fix for Laravel 5.5 users ## 1.0.1 - 2017-07-06 - improve security ## 1.0.0 - 2017-07-05 - initial release laravel-ignition/src/helpers.php 0000644 00000000773 15105603711 0012760 0 ustar 00 <?php use Spatie\LaravelIgnition\Renderers\ErrorPageRenderer; if (! function_exists('ddd')) { function ddd() { $args = func_get_args(); if (count($args) === 0) { throw new Exception('You should pass at least 1 argument to `ddd`'); } call_user_func_array('dump', $args); $renderer = app()->make(ErrorPageRenderer::class); $exception = new Exception('Dump, Die, Debug'); $renderer->render($exception); die(); } } laravel-ignition/src/Renderers/IgnitionExceptionRenderer.php 0000644 00000001005 15105603711 0020362 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Renderers; use Illuminate\Contracts\Foundation\ExceptionRenderer; class IgnitionExceptionRenderer implements ExceptionRenderer { protected ErrorPageRenderer $errorPageHandler; public function __construct(ErrorPageRenderer $errorPageHandler) { $this->errorPageHandler = $errorPageHandler; } public function render($throwable) { ob_start(); $this->errorPageHandler->render($throwable); return ob_get_clean(); } } laravel-ignition/src/Renderers/ErrorPageRenderer.php 0000644 00000002746 15105603711 0016626 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Renderers; use Spatie\FlareClient\Flare; use Spatie\Ignition\Config\IgnitionConfig; use Spatie\Ignition\Contracts\SolutionProviderRepository; use Spatie\Ignition\Ignition; use Spatie\LaravelIgnition\ContextProviders\LaravelContextProviderDetector; use Spatie\LaravelIgnition\Solutions\SolutionTransformers\LaravelSolutionTransformer; use Spatie\LaravelIgnition\Support\LaravelDocumentationLinkFinder; use Throwable; class ErrorPageRenderer { public function render(Throwable $throwable): void { $viteJsAutoRefresh = ''; if (class_exists('Illuminate\Foundation\Vite')) { $vite = app(\Illuminate\Foundation\Vite::class); if (is_file($vite->hotFile())) { $viteJsAutoRefresh = $vite->__invoke([]); } } app(Ignition::class) ->resolveDocumentationLink( fn (Throwable $throwable) => (new LaravelDocumentationLinkFinder())->findLinkForThrowable($throwable) ) ->setFlare(app(Flare::class)) ->setConfig(app(IgnitionConfig::class)) ->setSolutionProviderRepository(app(SolutionProviderRepository::class)) ->setContextProviderDetector(new LaravelContextProviderDetector()) ->setSolutionTransformerClass(LaravelSolutionTransformer::class) ->applicationPath(base_path()) ->addCustomHtmlToHead($viteJsAutoRefresh) ->renderException($throwable); } } laravel-ignition/src/Exceptions/ViewExceptionWithSolution.php 0000644 00000000713 15105603711 0020613 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use Spatie\Ignition\Contracts\ProvidesSolution; use Spatie\Ignition\Contracts\Solution; class ViewExceptionWithSolution extends ViewException implements ProvidesSolution { protected Solution $solution; public function setSolution(Solution $solution): void { $this->solution = $solution; } public function getSolution(): Solution { return $this->solution; } } laravel-ignition/src/Exceptions/ViewException.php 0000644 00000002307 15105603711 0016223 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use ErrorException; use Spatie\FlareClient\Contracts\ProvidesFlareContext; use Spatie\LaravelIgnition\Recorders\DumpRecorder\HtmlDumper; class ViewException extends ErrorException implements ProvidesFlareContext { /** @var array<string, mixed> */ protected array $viewData = []; protected string $view = ''; /** * @param array<string, mixed> $data * * @return void */ public function setViewData(array $data): void { $this->viewData = $data; } /** @return array<string, mixed> */ public function getViewData(): array { return $this->viewData; } public function setView(string $path): void { $this->view = $path; } protected function dumpViewData(mixed $variable): string { return (new HtmlDumper())->dumpVariable($variable); } /** @return array<string, mixed> */ public function context(): array { $context = [ 'view' => [ 'view' => $this->view, ], ]; $context['view']['data'] = array_map([$this, 'dumpViewData'], $this->viewData); return $context; } } laravel-ignition/src/Exceptions/CannotExecuteSolutionForNonLocalIp.php 0000644 00000001500 15105603711 0022314 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\ProvidesSolution; use Spatie\Ignition\Contracts\Solution; use Symfony\Component\HttpKernel\Exception\HttpException; class CannotExecuteSolutionForNonLocalIp extends HttpException implements ProvidesSolution { public static function make(): self { return new self(403, 'Solutions cannot be run from your current IP address.'); } public function getSolution(): Solution { return BaseSolution::create() ->setSolutionTitle('Checking your environment settings') ->setSolutionDescription("Solutions can only be executed by requests from a local IP address. Keep in mind that `APP_DEBUG` should set to false on any production environment."); } } laravel-ignition/src/Exceptions/InvalidConfig.php 0000644 00000001676 15105603711 0016156 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Exceptions; use Exception; use Monolog\Level; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\ProvidesSolution; use Spatie\Ignition\Contracts\Solution; class InvalidConfig extends Exception implements ProvidesSolution { public static function invalidLogLevel(string $logLevel): self { return new self("Invalid log level `{$logLevel}` specified."); } public function getSolution(): Solution { $validLogLevels = array_map( fn (string $level) => strtolower($level), array_keys(Level::VALUES) ); $validLogLevelsString = implode(',', $validLogLevels); return BaseSolution::create() ->setSolutionTitle('You provided an invalid log level') ->setSolutionDescription("Please change the log level in your `config/logging.php` file. Valid log levels are {$validLogLevelsString}."); } } laravel-ignition/src/Facades/Flare.php 0000644 00000001246 15105603711 0013671 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Facades; use Illuminate\Support\Facades\Facade; use Spatie\LaravelIgnition\Support\SentReports; /** * @method static void glow(string $name, string $messageLevel = \Spatie\FlareClient\Enums\MessageLevels::INFO, array $metaData = []) * @method static void context($key, $value) * @method static void group(string $groupName, array $properties) * * @see \Spatie\FlareClient\Flare */ class Flare extends Facade { protected static function getFacadeAccessor() { return \Spatie\FlareClient\Flare::class; } public static function sentReports(): SentReports { return app(SentReports::class); } } laravel-ignition/src/Commands/SolutionMakeCommand.php 0000644 00000001554 15105603711 0016766 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Commands; use Illuminate\Console\GeneratorCommand; use Symfony\Component\Console\Input\InputOption; class SolutionMakeCommand extends GeneratorCommand { protected $name = 'ignition:make-solution'; protected $description = 'Create a new custom Ignition solution class'; protected $type = 'Solution'; protected function getStub(): string { return $this->option('runnable') ? __DIR__.'/stubs/runnable-solution.stub' : __DIR__.'/stubs/solution.stub'; } protected function getDefaultNamespace($rootNamespace) { return "{$rootNamespace}\\Solutions"; } /** @return array<int, mixed> */ protected function getOptions(): array { return [ ['runnable', null, InputOption::VALUE_NONE, 'Create runnable solution'], ]; } } laravel-ignition/src/Commands/TestCommand.php 0000644 00000011250 15105603711 0015265 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Commands; use Composer\InstalledVersions; use Exception; use Illuminate\Config\Repository; use Illuminate\Console\Command; use Illuminate\Log\LogManager; use Spatie\FlareClient\Flare; use Spatie\FlareClient\Http\Exceptions\BadResponseCode; class TestCommand extends Command { protected $signature = 'flare:test'; protected $description = 'Send a test notification to Flare'; protected Repository $config; public function handle(Repository $config): void { $this->config = $config; $this->checkFlareKey(); if (app()->make('log') instanceof LogManager) { $this->checkFlareLogger(); } $this->sendTestException(); } protected function checkFlareKey(): self { $message = empty($this->config->get('flare.key')) ? '❌ Flare key not specified. Make sure you specify a value in the `key` key of the `flare` config file.' : '✅ Flare key specified'; $this->info($message); return $this; } public function checkFlareLogger(): self { $defaultLogChannel = $this->config->get('logging.default'); $activeStack = $this->config->get("logging.channels.{$defaultLogChannel}"); if (is_null($activeStack)) { $this->info("❌ The default logging channel `{$defaultLogChannel}` is not configured in the `logging` config file"); } if (! isset($activeStack['channels']) || ! in_array('flare', $activeStack['channels'])) { $this->info("❌ The logging channel `{$defaultLogChannel}` does not contain the 'flare' channel"); } if (is_null($this->config->get('logging.channels.flare'))) { $this->info('❌ There is no logging channel named `flare` in the `logging` config file'); } if ($this->config->get('logging.channels.flare.driver') !== 'flare') { $this->info('❌ The `flare` logging channel defined in the `logging` config file is not set to `flare`.'); } if ($this->config->get('ignition.with_stack_frame_arguments') && ini_get('zend.exception_ignore_args')) { $this->info('⚠️ The `zend.exception_ignore_args` php ini setting is enabled. This will prevent Flare from showing stack trace arguments.'); } $this->info('✅ The Flare logging driver was configured correctly.'); return $this; } protected function sendTestException(): void { $testException = new Exception('This is an exception to test if the integration with Flare works.'); try { app(Flare::class)->sendTestReport($testException); $this->info(''); } catch (Exception $exception) { $this->warn('❌ We were unable to send an exception to Flare. '); if ($exception instanceof BadResponseCode) { $this->info(''); $message = 'Unknown error'; $body = $exception->response->getBody(); if (is_array($body) && isset($body['message'])) { $message = $body['message']; } $this->warn("{$exception->response->getHttpResponseCode()} - {$message}"); } else { $this->warn($exception->getMessage()); } $this->warn('Make sure that your key is correct and that you have a valid subscription.'); $this->info(''); $this->info('For more info visit the docs on https://flareapp.io/docs/ignition-for-laravel/introduction'); $this->info('You can see the status page of Flare at https://status.flareapp.io'); $this->info('Flare support can be reached at support@flareapp.io'); $this->line(''); $this->line('Extra info'); $this->table([], [ ['Platform', PHP_OS], ['PHP', phpversion()], ['Laravel', app()->version()], ['spatie/ignition', InstalledVersions::getVersion('spatie/ignition')], ['spatie/laravel-ignition', InstalledVersions::getVersion('spatie/laravel-ignition')], ['spatie/flare-client-php', InstalledVersions::getVersion('spatie/flare-client-php')], /** @phpstan-ignore-next-line */ ['Curl', curl_version()['version'] ?? 'Unknown'], /** @phpstan-ignore-next-line */ ['SSL', curl_version()['ssl_version'] ?? 'Unknown'], ]); if ($this->output->isVerbose()) { throw $exception; } return; } $this->info('We tried to send an exception to Flare. Please check if it arrived!'); } } laravel-ignition/src/Commands/SolutionProviderMakeCommand.php 0000644 00000001103 15105603711 0020467 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Commands; use Illuminate\Console\GeneratorCommand; class SolutionProviderMakeCommand extends GeneratorCommand { protected $name = 'ignition:make-solution-provider'; protected $description = 'Create a new custom Ignition solution provider class'; protected $type = 'Solution Provider'; protected function getStub(): string { return __DIR__.'/stubs/solution-provider.stub'; } protected function getDefaultNamespace($rootNamespace) { return "{$rootNamespace}\\SolutionProviders"; } } laravel-ignition/src/Commands/stubs/runnable-solution.stub 0000644 00000001272 15105603711 0020060 0 ustar 00 <?php namespace DummyNamespace; use Spatie\Ignition\Contracts\RunnableSolution; class DummyClass implements RunnableSolution { public function getSolutionTitle(): string { return ''; } public function getDocumentationLinks(): array { return []; } public function getSolutionActionDescription(): string { return ''; } public function getRunButtonText(): string { return ''; } public function getSolutionDescription(): string { return ''; } public function getRunParameters(): array { return []; } public function run(array $parameters = []) { // } } laravel-ignition/src/Commands/stubs/solution.stub 0000644 00000000552 15105603711 0016254 0 ustar 00 <?php namespace DummyNamespace; use Spatie\Ignition\Contracts\Solution; class DummyClass implements Solution { public function getSolutionTitle(): string { return ''; } public function getSolutionDescription(): string { return ''; } public function getDocumentationLinks(): array { return []; } } laravel-ignition/src/Commands/stubs/solution-provider.stub 0000644 00000000534 15105603711 0020104 0 ustar 00 <?php namespace DummyNamespace; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Throwable; class DummyClass implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return false; } public function getSolutions(Throwable $throwable): array { return []; } } laravel-ignition/src/Recorders/QueryRecorder/Query.php 0000644 00000003157 15105603711 0017145 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\QueryRecorder; use Illuminate\Database\Events\QueryExecuted; class Query { protected string $sql; protected float $time; protected string $connectionName; /** @var array<string, string>|null */ protected ?array $bindings; protected float $microtime; public static function fromQueryExecutedEvent(QueryExecuted $queryExecuted, bool $reportBindings = false): self { return new self( $queryExecuted->sql, $queryExecuted->time, /** @phpstan-ignore-next-line */ $queryExecuted->connectionName ?? '', $reportBindings ? $queryExecuted->bindings : null ); } /** * @param string $sql * @param float $time * @param string $connectionName * @param array<string, string>|null $bindings * @param float|null $microtime */ protected function __construct( string $sql, float $time, string $connectionName, ?array $bindings = null, ?float $microtime = null ) { $this->sql = $sql; $this->time = $time; $this->connectionName = $connectionName; $this->bindings = $bindings; $this->microtime = $microtime ?? microtime(true); } /** * @return array<string, mixed> */ public function toArray(): array { return [ 'sql' => $this->sql, 'time' => $this->time, 'connection_name' => $this->connectionName, 'bindings' => $this->bindings, 'microtime' => $this->microtime, ]; } } laravel-ignition/src/Recorders/QueryRecorder/QueryRecorder.php 0000644 00000003662 15105603711 0020634 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\QueryRecorder; use Illuminate\Contracts\Foundation\Application; use Illuminate\Database\Events\QueryExecuted; class QueryRecorder { /** @var \Spatie\LaravelIgnition\Recorders\QueryRecorder\Query[] */ protected array $queries = []; protected Application $app; protected bool $reportBindings = true; protected ?int $maxQueries; public function __construct( Application $app, bool $reportBindings = true, ?int $maxQueries = 200 ) { $this->app = $app; $this->reportBindings = $reportBindings; $this->maxQueries = $maxQueries; } public function start(): self { /** @phpstan-ignore-next-line */ $this->app['events']->listen(QueryExecuted::class, [$this, 'record']); return $this; } public function record(QueryExecuted $queryExecuted): void { $this->queries[] = Query::fromQueryExecutedEvent($queryExecuted, $this->reportBindings); if (is_int($this->maxQueries)) { $this->queries = array_slice($this->queries, -$this->maxQueries); } } /** * @return array<int, array<string, mixed>> */ public function getQueries(): array { $queries = []; foreach ($this->queries as $query) { $queries[] = $query->toArray(); } return $queries; } public function reset(): void { $this->queries = []; } public function getReportBindings(): bool { return $this->reportBindings; } public function setReportBindings(bool $reportBindings): self { $this->reportBindings = $reportBindings; return $this; } public function getMaxQueries(): ?int { return $this->maxQueries; } public function setMaxQueries(?int $maxQueries): self { $this->maxQueries = $maxQueries; return $this; } } laravel-ignition/src/Recorders/DumpRecorder/HtmlDumper.php 0000644 00000001363 15105603711 0017716 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper as BaseHtmlDumper; class HtmlDumper extends BaseHtmlDumper { protected $dumpHeader = ''; public function dumpVariable($variable): string { $cloner = new VarCloner(); $clonedData = $cloner->cloneVar($variable)->withMaxDepth(3); return $this->dump($clonedData); } public function dump(Data $data, $output = null, array $extraDisplayOptions = []): string { return (string)parent::dump($data, true, [ 'maxDepth' => 3, 'maxStringLength' => 160, ]); } } laravel-ignition/src/Recorders/DumpRecorder/DumpHandler.php 0000644 00000000704 15105603711 0020036 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; use Symfony\Component\VarDumper\Cloner\VarCloner; class DumpHandler { protected DumpRecorder $dumpRecorder; public function __construct(DumpRecorder $dumpRecorder) { $this->dumpRecorder = $dumpRecorder; } public function dump(mixed $value): void { $data = (new VarCloner)->cloneVar($value); $this->dumpRecorder->record($data); } } laravel-ignition/src/Recorders/DumpRecorder/MultiDumpHandler.php 0000644 00000000765 15105603711 0021060 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; class MultiDumpHandler { /** @var array<int, callable|null> */ protected array $handlers = []; public function dump(mixed $value): void { foreach ($this->handlers as $handler) { if ($handler) { $handler($value); } } } public function addHandler(callable $callable = null): self { $this->handlers[] = $callable; return $this; } } laravel-ignition/src/Recorders/DumpRecorder/Dump.php 0000644 00000001421 15105603711 0016535 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; class Dump { protected string $htmlDump; protected ?string $file; protected ?int $lineNumber; protected float $microtime; public function __construct(string $htmlDump, ?string $file, ?int $lineNumber, ?float $microtime = null) { $this->htmlDump = $htmlDump; $this->file = $file; $this->lineNumber = $lineNumber; $this->microtime = $microtime ?? microtime(true); } /** @return array<string, mixed> */ public function toArray(): array { return [ 'html_dump' => $this->htmlDump, 'file' => $this->file, 'line_number' => $this->lineNumber, 'microtime' => $this->microtime, ]; } } laravel-ignition/src/Recorders/DumpRecorder/DumpRecorder.php 0000644 00000007424 15105603711 0020234 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\DumpRecorder; use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\Arr; use ReflectionMethod; use ReflectionProperty; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\VarDumper; class DumpRecorder { /** @var array<array<int,mixed>> */ protected array $dumps = []; protected Application $app; protected static bool $registeredHandler = false; public function __construct(Application $app) { $this->app = $app; } public function start(): self { $multiDumpHandler = new MultiDumpHandler(); $this->app->singleton(MultiDumpHandler::class, fn () => $multiDumpHandler); if (! self::$registeredHandler) { static::$registeredHandler = true; $this->ensureOriginalHandlerExists(); $originalHandler = VarDumper::setHandler(fn ($dumpedVariable) => $multiDumpHandler->dump($dumpedVariable)); $multiDumpHandler?->addHandler($originalHandler); $multiDumpHandler->addHandler(fn ($var) => (new DumpHandler($this))->dump($var)); } return $this; } public function record(Data $data): void { $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 11); $sourceFrame = $this->findSourceFrame($backtrace); $file = (string) Arr::get($sourceFrame, 'file'); $lineNumber = (int) Arr::get($sourceFrame, 'line'); $htmlDump = (new HtmlDumper())->dump($data); $this->dumps[] = new Dump($htmlDump, $file, $lineNumber); } public function getDumps(): array { return $this->toArray(); } public function reset() { $this->dumps = []; } public function toArray(): array { $dumps = []; foreach ($this->dumps as $dump) { $dumps[] = $dump->toArray(); } return $dumps; } /* * Only the `VarDumper` knows how to create the orignal HTML or CLI VarDumper. * Using reflection and the private VarDumper::register() method we can force it * to create and register a new VarDumper::$handler before we'll overwrite it. * Of course, we only need to do this if there isn't a registered VarDumper::$handler. * * @throws \ReflectionException */ protected function ensureOriginalHandlerExists(): void { $reflectionProperty = new ReflectionProperty(VarDumper::class, 'handler'); $reflectionProperty->setAccessible(true); $handler = $reflectionProperty->getValue(); if (! $handler) { // No handler registered yet, so we'll force VarDumper to create one. $reflectionMethod = new ReflectionMethod(VarDumper::class, 'register'); $reflectionMethod->setAccessible(true); $reflectionMethod->invoke(null); } } /** * Find the first meaningful stack frame that is not the `DumpRecorder` itself. * * @template T of array{class?: class-string, function?: string, line?: int, file?: string} * * @param array<T> $stacktrace * * @return null|T */ protected function findSourceFrame(array $stacktrace): ?array { $seenVarDumper = false; foreach ($stacktrace as $frame) { // Keep looping until we're past the VarDumper::dump() call in Symfony's helper functions file. if (Arr::get($frame, 'class') === VarDumper::class && Arr::get($frame, 'function') === 'dump') { $seenVarDumper = true; continue; } if (! $seenVarDumper) { continue; } // Return the next frame in the stack after the VarDumper::dump() call: return $frame; } return null; } } laravel-ignition/src/Recorders/JobRecorder/JobRecorder.php 0000644 00000011600 15105603711 0017635 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\JobRecorder; use DateTime; use Error; use Exception; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Queue\Job; use Illuminate\Queue\CallQueuedClosure; use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Jobs\RedisJob; use Illuminate\Support\Str; use ReflectionClass; use ReflectionProperty; use RuntimeException; class JobRecorder { protected ?Job $job = null; public function __construct( protected Application $app, protected int $maxChainedJobReportingDepth = 5, ) { } public function start(): self { /** @phpstan-ignore-next-line */ $this->app['events']->listen(JobExceptionOccurred::class, [$this, 'record']); return $this; } public function record(JobExceptionOccurred $event): void { $this->job = $event->job; } /** * @return array<string, mixed>|null */ public function getJob(): ?array { if ($this->job === null) { return null; } return array_merge( $this->getJobProperties(), [ 'name' => $this->job->resolveName(), 'connection' => $this->job->getConnectionName(), 'queue' => $this->job->getQueue(), ] ); } public function reset(): void { $this->job = null; } protected function getJobProperties(): array { $payload = collect($this->resolveJobPayload()); $properties = []; foreach ($payload as $key => $value) { if (! in_array($key, ['job', 'data', 'displayName'])) { $properties[$key] = $value; } } if ($pushedAt = DateTime::createFromFormat('U.u', $payload->get('pushedAt', ''))) { $properties['pushedAt'] = $pushedAt->format(DATE_ATOM); } try { $properties['data'] = $this->resolveCommandProperties( $this->resolveObjectFromCommand($payload['data']['command']), $this->maxChainedJobReportingDepth ); } catch (Exception $exception) { } return $properties; } protected function resolveJobPayload(): array { if (! $this->job instanceof RedisJob) { return $this->job->payload(); } try { return json_decode($this->job->getReservedJob(), true, 512, JSON_THROW_ON_ERROR); } catch (Exception $e) { return $this->job->payload(); } } protected function resolveCommandProperties(object $command, int $maxChainDepth): array { $propertiesToIgnore = ['job', 'closure']; $properties = collect((new ReflectionClass($command))->getProperties()) ->reject(function (ReflectionProperty $property) use ($propertiesToIgnore) { return in_array($property->name, $propertiesToIgnore); }) ->mapWithKeys(function (ReflectionProperty $property) use ($command) { try { $property->setAccessible(true); return [$property->name => $property->getValue($command)]; } catch (Error $error) { return [$property->name => 'uninitialized']; } }); if ($properties->has('chained')) { $properties['chained'] = $this->resolveJobChain($properties->get('chained'), $maxChainDepth); } return $properties->all(); } /** * @param array<string, mixed> $chainedCommands * @param int $maxDepth * * @return array */ protected function resolveJobChain(array $chainedCommands, int $maxDepth): array { if ($maxDepth === 0) { return ['Ignition stopped recording jobs after this point since the max chain depth was reached']; } return array_map( function (string $command) use ($maxDepth) { $commandObject = $this->resolveObjectFromCommand($command); return [ 'name' => $commandObject instanceof CallQueuedClosure ? $commandObject->displayName() : get_class($commandObject), 'data' => $this->resolveCommandProperties($commandObject, $maxDepth - 1), ]; }, $chainedCommands ); } // Taken from Illuminate\Queue\CallQueuedHandler protected function resolveObjectFromCommand(string $command): object { if (Str::startsWith($command, 'O:')) { return unserialize($command); } if ($this->app->bound(Encrypter::class)) { /** @phpstan-ignore-next-line */ return unserialize($this->app[Encrypter::class]->decrypt($command)); } throw new RuntimeException('Unable to extract job payload.'); } } laravel-ignition/src/Recorders/LogRecorder/LogMessage.php 0000644 00000002364 15105603711 0017501 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\LogRecorder; use Illuminate\Log\Events\MessageLogged; class LogMessage { protected ?string $message; protected string $level; /** @var array<string, string> */ protected array $context = []; protected ?float $microtime; /** * @param string|null $message * @param string $level * @param array<string, string> $context * @param float|null $microtime */ public function __construct( ?string $message, string $level, array $context = [], ?float $microtime = null ) { $this->message = $message; $this->level = $level; $this->context = $context; $this->microtime = $microtime ?? microtime(true); } public static function fromMessageLoggedEvent(MessageLogged $event): self { return new self( $event->message, $event->level, $event->context ); } /** @return array<string, mixed> */ public function toArray(): array { return [ 'message' => $this->message, 'level' => $this->level, 'context' => $this->context, 'microtime' => $this->microtime, ]; } } laravel-ignition/src/Recorders/LogRecorder/LogRecorder.php 0000644 00000003730 15105603711 0017660 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Recorders\LogRecorder; use Illuminate\Contracts\Foundation\Application; use Illuminate\Log\Events\MessageLogged; use Throwable; class LogRecorder { /** @var \Spatie\LaravelIgnition\Recorders\LogRecorder\LogMessage[] */ protected array $logMessages = []; protected Application $app; protected ?int $maxLogs; public function __construct(Application $app, ?int $maxLogs = null) { $this->app = $app; $this->maxLogs = $maxLogs; } public function start(): self { /** @phpstan-ignore-next-line */ $this->app['events']->listen(MessageLogged::class, [$this, 'record']); return $this; } public function record(MessageLogged $event): void { if ($this->shouldIgnore($event)) { return; } $this->logMessages[] = LogMessage::fromMessageLoggedEvent($event); if (is_int($this->maxLogs)) { $this->logMessages = array_slice($this->logMessages, -$this->maxLogs); } } /** @return array<array<int,string>> */ public function getLogMessages(): array { return $this->toArray(); } /** @return array<int, mixed> */ public function toArray(): array { $logMessages = []; foreach ($this->logMessages as $log) { $logMessages[] = $log->toArray(); } return $logMessages; } protected function shouldIgnore(mixed $event): bool { if (! isset($event->context['exception'])) { return false; } if (! $event->context['exception'] instanceof Throwable) { return false; } return true; } public function reset(): void { $this->logMessages = []; } public function getMaxLogs(): ?int { return $this->maxLogs; } public function setMaxLogs(?int $maxLogs): self { $this->maxLogs = $maxLogs; return $this; } } laravel-ignition/src/ignition-routes.php 0000644 00000001413 15105603711 0014445 0 ustar 00 <?php use Illuminate\Support\Facades\Route; use Spatie\LaravelIgnition\Http\Controllers\ExecuteSolutionController; use Spatie\LaravelIgnition\Http\Controllers\HealthCheckController; use Spatie\LaravelIgnition\Http\Controllers\UpdateConfigController; use Spatie\LaravelIgnition\Http\Middleware\RunnableSolutionsEnabled; Route::group([ 'as' => 'ignition.', 'prefix' => config('ignition.housekeeping_endpoint_prefix'), 'middleware' => [RunnableSolutionsEnabled::class], ], function () { Route::get('health-check', HealthCheckController::class)->name('healthCheck'); Route::post('execute-solution', ExecuteSolutionController::class) ->name('executeSolution'); Route::post('update-config', UpdateConfigController::class)->name('updateConfig'); }); laravel-ignition/src/Support/SentReports.php 0000644 00000002172 15105603711 0015255 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use Illuminate\Support\Arr; use Spatie\FlareClient\Report; class SentReports { /** @var array<int, Report> */ protected array $reports = []; public function add(Report $report): self { $this->reports[] = $report; return $this; } /** @return array<int, Report> */ public function all(): array { return $this->reports; } /** @return array<int, string> */ public function uuids(): array { return array_map(fn (Report $report) => $report->trackingUuid(), $this->reports); } /** @return array<int, string> */ public function urls(): array { return array_map(function (string $trackingUuid) { return "https://flareapp.io/tracked-occurrence/{$trackingUuid}"; }, $this->uuids()); } public function latestUuid(): ?string { return Arr::last($this->reports)?->trackingUuid(); } public function latestUrl(): ?string { return Arr::last($this->urls()); } public function clear(): void { $this->reports = []; } } laravel-ignition/src/Support/LaravelDocumentationLinkFinder.php 0000644 00000004114 15105603711 0021051 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Spatie\LaravelIgnition\Exceptions\ViewException; use Throwable; class LaravelDocumentationLinkFinder { public function findLinkForThrowable(Throwable $throwable): ?string { if ($throwable instanceof ViewException) { $throwable = $throwable->getPrevious(); } $majorVersion = LaravelVersion::major(); if (str_contains($throwable->getMessage(), Collection::class)) { return "https://laravel.com/docs/{$majorVersion}.x/collections#available-methods"; } $type = $this->getType($throwable); if (! $type) { return null; } return match ($type) { 'Auth' => "https://laravel.com/docs/{$majorVersion}.x/authentication", 'Broadcasting' => "https://laravel.com/docs/{$majorVersion}.x/broadcasting", 'Container' => "https://laravel.com/docs/{$majorVersion}.x/container", 'Database' => "https://laravel.com/docs/{$majorVersion}.x/eloquent", 'Pagination' => "https://laravel.com/docs/{$majorVersion}.x/pagination", 'Queue' => "https://laravel.com/docs/{$majorVersion}.x/queues", 'Routing' => "https://laravel.com/docs/{$majorVersion}.x/routing", 'Session' => "https://laravel.com/docs/{$majorVersion}.x/session", 'Validation' => "https://laravel.com/docs/{$majorVersion}.x/validation", 'View' => "https://laravel.com/docs/{$majorVersion}.x/views", default => null, }; } protected function getType(?Throwable $throwable): ?string { if (! $throwable) { return null; } if (str_contains($throwable::class, 'Illuminate')) { return Str::between($throwable::class, 'Illuminate\\', '\\'); } if (str_contains($throwable->getMessage(), 'Illuminate')) { return explode('\\', Str::between($throwable->getMessage(), 'Illuminate\\', '\\'))[0]; } return null; } } laravel-ignition/src/Support/LaravelVersion.php 0000644 00000000264 15105603711 0015721 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; class LaravelVersion { public static function major(): string { return explode('.', app()->version())[0]; } } laravel-ignition/src/Support/StringComparator.php 0000644 00000003041 15105603711 0016257 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use Illuminate\Support\Collection; class StringComparator { /** * @param array<int|string, string> $strings * @param string $input * @param int $sensitivity * * @return string|null */ public static function findClosestMatch(array $strings, string $input, int $sensitivity = 4): ?string { $closestDistance = -1; $closestMatch = null; foreach ($strings as $string) { $levenshteinDistance = levenshtein($input, $string); if ($levenshteinDistance === 0) { $closestMatch = $string; $closestDistance = 0; break; } if ($levenshteinDistance <= $closestDistance || $closestDistance < 0) { $closestMatch = $string; $closestDistance = $levenshteinDistance; } } if ($closestDistance <= $sensitivity) { return $closestMatch; } return null; } /** * @param array<int, string> $strings * @param string $input * * @return string|null */ public static function findSimilarText(array $strings, string $input): ?string { if (empty($strings)) { return null; } return Collection::make($strings) ->sortByDesc(function (string $string) use ($input) { similar_text($input, $string, $percentage); return $percentage; }) ->first(); } } laravel-ignition/src/Support/Composer/FakeComposer.php 0000644 00000000674 15105603711 0017137 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support\Composer; class FakeComposer implements Composer { /** @return array<string, mixed> */ public function getClassMap(): array { return []; } /** @return array<string, mixed> */ public function getPrefixes(): array { return []; } /** @return array<string, mixed> */ public function getPrefixesPsr4(): array { return []; } } laravel-ignition/src/Support/Composer/Composer.php 0000644 00000000516 15105603711 0016343 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support\Composer; interface Composer { /** @return array<string, mixed> */ public function getClassMap(): array; /** @return array<string, mixed> */ public function getPrefixes(): array; /** @return array<string, mixed> */ public function getPrefixesPsr4(): array; } laravel-ignition/src/Support/Composer/ComposerClassMap.php 0000644 00000007355 15105603711 0017777 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support\Composer; use function app_path; use function base_path; use Illuminate\Support\Str; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; class ComposerClassMap { /** @var \Spatie\LaravelIgnition\Support\Composer\Composer */ protected object $composer; protected string $basePath; public function __construct(?string $autoloaderPath = null) { $autoloaderPath = $autoloaderPath ?? base_path('/vendor/autoload.php'); $this->composer = file_exists($autoloaderPath) ? require $autoloaderPath : new FakeComposer(); $this->basePath = app_path(); } /** @return array<string, string> */ public function listClasses(): array { $classes = $this->composer->getClassMap(); return array_merge($classes, $this->listClassesInPsrMaps()); } public function searchClassMap(string $missingClass): ?string { foreach ($this->composer->getClassMap() as $fqcn => $file) { $basename = basename($file, '.php'); if ($basename === $missingClass) { return $fqcn; } } return null; } /** @return array<string, mixed> */ public function listClassesInPsrMaps(): array { // TODO: This is incorrect. Doesnt list all fqcns. Need to parse namespace? e.g. App\LoginController is wrong $prefixes = array_merge( $this->composer->getPrefixes(), $this->composer->getPrefixesPsr4() ); $classes = []; foreach ($prefixes as $namespace => $directories) { foreach ($directories as $directory) { if (file_exists($directory)) { $files = (new Finder) ->in($directory) ->files() ->name('*.php'); foreach ($files as $file) { if ($file instanceof SplFileInfo) { $fqcn = $this->getFullyQualifiedClassNameFromFile($namespace, $file); $classes[$fqcn] = $file->getRelativePathname(); } } } } } return $classes; } public function searchPsrMaps(string $missingClass): ?string { $prefixes = array_merge( $this->composer->getPrefixes(), $this->composer->getPrefixesPsr4() ); foreach ($prefixes as $namespace => $directories) { foreach ($directories as $directory) { if (file_exists($directory)) { $files = (new Finder) ->in($directory) ->files() ->name('*.php'); foreach ($files as $file) { if ($file instanceof SplFileInfo) { $basename = basename($file->getRelativePathname(), '.php'); if ($basename === $missingClass) { return $namespace . basename($file->getRelativePathname(), '.php'); } } } } } } return null; } protected function getFullyQualifiedClassNameFromFile(string $rootNamespace, SplFileInfo $file): string { $class = trim(str_replace($this->basePath, '', (string)$file->getRealPath()), DIRECTORY_SEPARATOR); $class = str_replace( [DIRECTORY_SEPARATOR, 'App\\'], ['\\', app()->getNamespace()], ucfirst(Str::replaceLast('.php', '', $class)) ); return $rootNamespace . $class; } } laravel-ignition/src/Support/RunnableSolutionsGuard.php 0000644 00000002053 15105603711 0017434 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; class RunnableSolutionsGuard { /** * Check if runnable solutions are allowed based on the current * environment and config. * * @return bool */ public static function check(): bool { if (! config('app.debug')) { // Never run solutions in when debug mode is not enabled. return false; } if (config('ignition.enable_runnable_solutions') !== null) { // Allow enabling or disabling runnable solutions regardless of environment // if the IGNITION_ENABLE_RUNNABLE_SOLUTIONS env var is explicitly set. return config('ignition.enable_runnable_solutions'); } if (! app()->environment('local') && ! app()->environment('development')) { // Never run solutions on non-local environments. This avoids exposing // applications that are somehow APP_ENV=production with APP_DEBUG=true. return false; } return config('app.debug'); } } laravel-ignition/src/Support/LivewireComponentParser.php 0000644 00000005735 15105603711 0017623 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Livewire\LivewireManager; use ReflectionClass; use ReflectionMethod; use ReflectionProperty; class LivewireComponentParser { protected string $componentClass; protected ReflectionClass $reflectionClass; public static function create(string $componentAlias): self { return new self($componentAlias); } public function __construct(protected string $componentAlias) { $this->componentClass = app(LivewireManager::class)->getClass($this->componentAlias); $this->reflectionClass = new ReflectionClass($this->componentClass); } public function getComponentClass(): string { return $this->componentClass; } public function getPropertyNamesLike(string $similar): Collection { $properties = collect($this->reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC)) // @phpstan-ignore-next-line ->reject(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->class !== $this->reflectionClass->name) ->map(fn (ReflectionProperty $reflectionProperty) => $reflectionProperty->name); $computedProperties = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC)) // @phpstan-ignore-next-line ->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name) ->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'get') && str_ends_with($reflectionMethod->name, 'Property')) ->map(fn (ReflectionMethod $reflectionMethod) => lcfirst(Str::of($reflectionMethod->name)->after('get')->before('Property'))); return $this->filterItemsBySimilarity( $properties->merge($computedProperties), $similar ); } public function getMethodNamesLike(string $similar): Collection { $methods = collect($this->reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC)) // @phpstan-ignore-next-line ->reject(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->class !== $this->reflectionClass->name) ->map(fn (ReflectionMethod $reflectionMethod) => $reflectionMethod->name); return $this->filterItemsBySimilarity($methods, $similar); } protected function filterItemsBySimilarity(Collection $items, string $similar): Collection { return $items ->map(function (string $name) use ($similar) { similar_text($similar, $name, $percentage); return ['match' => $percentage, 'value' => $name]; }) ->sortByDesc('match') ->filter(function (array $item) { return $item['match'] > 40; }) ->map(function (array $item) { return $item['value']; }) ->values(); } } laravel-ignition/src/Support/FlareLogHandler.php 0000644 00000005423 15105603711 0015760 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Support; use InvalidArgumentException; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Level; use Monolog\Logger; use Monolog\LogRecord; use Spatie\FlareClient\Flare; use Spatie\FlareClient\Report; use Throwable; class FlareLogHandler extends AbstractProcessingHandler { protected Flare $flare; protected SentReports $sentReports; protected int $minimumReportLogLevel; public function __construct(Flare $flare, SentReports $sentReports, $level = Level::Debug, $bubble = true) { $this->flare = $flare; $this->minimumReportLogLevel = Level::Error->value; $this->sentReports = $sentReports; parent::__construct($level, $bubble); } public function setMinimumReportLogLevel(int $level): void { if (! in_array($level, Level::VALUES)) { throw new InvalidArgumentException('The given minimum log level is not supported.'); } $this->minimumReportLogLevel = $level; } protected function write(LogRecord $record): void { if (! $this->shouldReport($record->toArray())) { return; } if ($this->hasException($record->toArray())) { $report = $this->flare->report($record['context']['exception']); if ($report) { $this->sentReports->add($report); } return; } if (config('flare.send_logs_as_events')) { if ($this->hasValidLogLevel($record->toArray())) { $this->flare->reportMessage( $record['message'], 'Log ' . Logger::toMonologLevel($record['level'])->getName(), function (Report $flareReport) use ($record) { foreach ($record['context'] as $key => $value) { $flareReport->context($key, $value); } } ); } } } /** * @param array<string, mixed> $report * * @return bool */ protected function shouldReport(array $report): bool { if (! config('flare.key')) { return false; } return $this->hasException($report) || $this->hasValidLogLevel($report); } /** * @param array<string, mixed> $report * * @return bool */ protected function hasException(array $report): bool { $context = $report['context']; return isset($context['exception']) && $context['exception'] instanceof Throwable; } /** * @param array<string, mixed> $report * * @return bool */ protected function hasValidLogLevel(array $report): bool { return $report['level'] >= $this->minimumReportLogLevel; } } laravel-ignition/src/Solutions/LivewireDiscoverSolution.php 0000644 00000002353 15105603711 0020333 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Livewire\LivewireComponentsFinder; use Spatie\Ignition\Contracts\RunnableSolution; class LivewireDiscoverSolution implements RunnableSolution { protected string $customTitle; public function __construct(string $customTitle = '') { $this->customTitle = $customTitle; } public function getSolutionTitle(): string { return $this->customTitle; } public function getSolutionDescription(): string { return 'You might have forgotten to discover your Livewire components.'; } public function getDocumentationLinks(): array { return [ 'Livewire: Artisan Commands' => 'https://laravel-livewire.com/docs/2.x/artisan-commands', ]; } public function getRunParameters(): array { return []; } public function getSolutionActionDescription(): string { return 'You can discover your Livewire components using `php artisan livewire:discover`.'; } public function getRunButtonText(): string { return 'Run livewire:discover'; } public function run(array $parameters = []): void { app(LivewireComponentsFinder::class)->build(); } } laravel-ignition/src/Solutions/SuggestUsingCorrectDbNameSolution.php 0000644 00000001460 15105603711 0022064 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Spatie\Ignition\Contracts\Solution; class SuggestUsingCorrectDbNameSolution implements Solution { public function getSolutionTitle(): string { return 'Database name seems incorrect'; } public function getSolutionDescription(): string { $defaultDatabaseName = env('DB_DATABASE'); return "You're using the default database name `$defaultDatabaseName`. This database does not exist.\n\nEdit the `.env` file and use the correct database name in the `DB_DATABASE` key."; } /** @return array<string, string> */ public function getDocumentationLinks(): array { return [ 'Database: Getting Started docs' => 'https://laravel.com/docs/master/database#configuration', ]; } } laravel-ignition/src/Solutions/MakeViewVariableOptionalSolution.php 0000644 00000007362 15105603711 0021737 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Str; use Spatie\Ignition\Contracts\RunnableSolution; class MakeViewVariableOptionalSolution implements RunnableSolution { protected ?string $variableName; protected ?string $viewFile; public function __construct(string $variableName = null, string $viewFile = null) { $this->variableName = $variableName; $this->viewFile = $viewFile; } public function getSolutionTitle(): string { return "$$this->variableName is undefined"; } public function getDocumentationLinks(): array { return []; } public function getSolutionActionDescription(): string { $output = [ 'Make the variable optional in the blade template.', "Replace `{{ $$this->variableName }}` with `{{ $$this->variableName ?? '' }}`", ]; return implode(PHP_EOL, $output); } public function getRunButtonText(): string { return 'Make variable optional'; } public function getSolutionDescription(): string { return ''; } public function getRunParameters(): array { return [ 'variableName' => $this->variableName, 'viewFile' => $this->viewFile, ]; } /** * @param array<string, mixed> $parameters * * @return bool */ public function isRunnable(array $parameters = []): bool { return $this->makeOptional($this->getRunParameters()) !== false; } /** * @param array<string, string> $parameters * * @return void */ public function run(array $parameters = []): void { $output = $this->makeOptional($parameters); if ($output !== false) { file_put_contents($parameters['viewFile'], $output); } } protected function isSafePath(string $path): bool { if (! Str::startsWith($path, ['/', './'])) { return false; } if (! Str::endsWith($path, '.blade.php')) { return false; } return true; } /** * @param array<string, string> $parameters * * @return bool|string */ public function makeOptional(array $parameters = []): bool|string { if (! $this->isSafePath($parameters['viewFile'])) { return false; } $originalContents = (string)file_get_contents($parameters['viewFile']); $newContents = str_replace('$'.$parameters['variableName'], '$'.$parameters['variableName']." ?? ''", $originalContents); $originalTokens = token_get_all(Blade::compileString($originalContents)); $newTokens = token_get_all(Blade::compileString($newContents)); $expectedTokens = $this->generateExpectedTokens($originalTokens, $parameters['variableName']); if ($expectedTokens !== $newTokens) { return false; } return $newContents; } /** * @param array<int, mixed> $originalTokens * @param string $variableName * * @return array<int, mixed> */ protected function generateExpectedTokens(array $originalTokens, string $variableName): array { $expectedTokens = []; foreach ($originalTokens as $token) { $expectedTokens[] = $token; if ($token[0] === T_VARIABLE && $token[1] === '$'.$variableName) { $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]]; $expectedTokens[] = [T_COALESCE, '??', $token[2]]; $expectedTokens[] = [T_WHITESPACE, ' ', $token[2]]; $expectedTokens[] = [T_CONSTANT_ENCAPSED_STRING, "''", $token[2]]; } } return $expectedTokens; } } laravel-ignition/src/Solutions/SolutionProviders/UnknownValidationSolutionProvider.php 0000644 00000004734 15105603711 0025752 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use BadMethodCallException; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Validation\Validator; use ReflectionClass; use ReflectionMethod; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Support\StringComparator; use Throwable; class UnknownValidationSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/Illuminate\\\\Validation\\\\Validator::(?P<method>validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof BadMethodCallException) { return false; } return ! is_null($this->getMethodFromExceptionMessage($throwable->getMessage())); } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create() ->setSolutionTitle('Unknown Validation Rule') ->setSolutionDescription($this->getSolutionDescription($throwable)), ]; } protected function getSolutionDescription(Throwable $throwable): string { $method = (string)$this->getMethodFromExceptionMessage($throwable->getMessage()); $possibleMethod = StringComparator::findSimilarText( $this->getAvailableMethods()->toArray(), $method ); if (empty($possibleMethod)) { return ''; } $rule = Str::snake(str_replace('validate', '', $possibleMethod)); return "Did you mean `{$rule}` ?"; } protected function getMethodFromExceptionMessage(string $message): ?string { if (! preg_match(self::REGEX, $message, $matches)) { return null; } return $matches['method']; } protected function getAvailableMethods(): Collection { $class = new ReflectionClass(Validator::class); $extensions = Collection::make((app('validator')->make([], []))->extensions) ->keys() ->map(fn (string $extension) => 'validate'.Str::studly($extension)); return Collection::make($class->getMethods()) ->filter(fn (ReflectionMethod $method) => preg_match('/(validate(?!(Attribute|UsingCustomRule))[A-Z][a-zA-Z]+)/', $method->name)) ->map(fn (ReflectionMethod $method) => $method->name) ->merge($extensions); } } laravel-ignition/src/Solutions/SolutionProviders/MissingViteManifestSolutionProvider.php 0000644 00000003451 15105603711 0026223 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Support\Str; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\Ignition\Contracts\Solution; use Throwable; class MissingViteManifestSolutionProvider implements HasSolutionsForThrowable { /** @var array<string, string> */ protected array $links = [ 'Asset bundling with Vite' => 'https://laravel.com/docs/9.x/vite#running-vite', ]; public function canSolve(Throwable $throwable): bool { return Str::startsWith($throwable->getMessage(), 'Vite manifest not found'); } public function getSolutions(Throwable $throwable): array { return [ $this->getSolution(), ]; } public function getSolution(): Solution { /** @var string */ $baseCommand = collect([ 'pnpm-lock.yaml' => 'pnpm', 'yarn.lock' => 'yarn', ])->first(fn ($_, $lockfile) => file_exists(base_path($lockfile)), 'npm run'); return app()->environment('local') ? $this->getLocalSolution($baseCommand) : $this->getProductionSolution($baseCommand); } protected function getLocalSolution(string $baseCommand): Solution { return BaseSolution::create('Start the development server') ->setSolutionDescription("Run `{$baseCommand} dev` in your terminal and refresh the page.") ->setDocumentationLinks($this->links); } protected function getProductionSolution(string $baseCommand): Solution { return BaseSolution::create('Build the production assets') ->setSolutionDescription("Run `{$baseCommand} build` in your deployment script.") ->setDocumentationLinks($this->links); } } laravel-ignition/src/Solutions/SolutionProviders/LazyLoadingViolationSolutionProvider.php 0000644 00000002573 15105603711 0026401 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Database\LazyLoadingViolationException; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Support\LaravelVersion; use Throwable; class LazyLoadingViolationSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if ($throwable instanceof LazyLoadingViolationException) { return true; } if (! $previous = $throwable->getPrevious()) { return false; } return $previous instanceof LazyLoadingViolationException; } public function getSolutions(Throwable $throwable): array { $majorVersion = LaravelVersion::major(); return [BaseSolution::create( 'Lazy loading was disabled to detect N+1 problems' ) ->setSolutionDescription( 'Either avoid lazy loading the relation or allow lazy loading.' ) ->setDocumentationLinks([ 'Read the docs on preventing lazy loading' => "https://laravel.com/docs/{$majorVersion}.x/eloquent-relationships#preventing-lazy-loading", 'Watch a video on how to deal with the N+1 problem' => 'https://www.youtube.com/watch?v=ZE7KBeraVpc', ]),]; } } laravel-ignition/src/Solutions/SolutionProviders/RouteNotDefinedSolutionProvider.php 0000644 00000003166 15105603711 0025334 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Support\Facades\Route; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Support\StringComparator; use Symfony\Component\Routing\Exception\RouteNotFoundException; use Throwable; class RouteNotDefinedSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/Route \[(.*)\] not defined/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof RouteNotFoundException) { return false; } return (bool)preg_match(self::REGEX, $throwable->getMessage(), $matches); } public function getSolutions(Throwable $throwable): array { preg_match(self::REGEX, $throwable->getMessage(), $matches); $missingRoute = $matches[1] ?? ''; $suggestedRoute = $this->findRelatedRoute($missingRoute); if ($suggestedRoute) { return [ BaseSolution::create("{$missingRoute} was not defined.") ->setSolutionDescription("Did you mean `{$suggestedRoute}`?"), ]; } return [ BaseSolution::create("{$missingRoute} was not defined.") ->setSolutionDescription('Are you sure that the route is defined'), ]; } protected function findRelatedRoute(string $missingRoute): ?string { Route::getRoutes()->refreshNameLookups(); return StringComparator::findClosestMatch(array_keys(Route::getRoutes()->getRoutesByName()), $missingRoute); } } laravel-ignition/src/Solutions/SolutionProviders/MissingAppKeySolutionProvider.php 0000644 00000001252 15105603711 0025013 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use RuntimeException; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\GenerateAppKeySolution; use Throwable; class MissingAppKeySolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof RuntimeException) { return false; } return $throwable->getMessage() === 'No application encryption key has been specified.'; } public function getSolutions(Throwable $throwable): array { return [new GenerateAppKeySolution()]; } } laravel-ignition/src/Solutions/SolutionProviders/MissingImportSolutionProvider.php 0000644 00000002646 15105603711 0025104 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\SuggestImportSolution; use Spatie\LaravelIgnition\Support\Composer\ComposerClassMap; use Throwable; class MissingImportSolutionProvider implements HasSolutionsForThrowable { protected ?string $foundClass; protected ComposerClassMap $composerClassMap; public function canSolve(Throwable $throwable): bool { $pattern = '/Class \"([^\s]+)\" not found/m'; if (! preg_match($pattern, $throwable->getMessage(), $matches)) { return false; } $class = $matches[1]; $this->composerClassMap = new ComposerClassMap(); $this->search($class); return ! is_null($this->foundClass); } /** * @param \Throwable $throwable * * @return array<int, SuggestImportSolution> */ public function getSolutions(Throwable $throwable): array { if (is_null($this->foundClass)) { return []; } return [new SuggestImportSolution($this->foundClass)]; } protected function search(string $missingClass): void { $this->foundClass = $this->composerClassMap->searchClassMap($missingClass); if (is_null($this->foundClass)) { $this->foundClass = $this->composerClassMap->searchPsrMaps($missingClass); } } } laravel-ignition/src/Solutions/SolutionProviders/RunningLaravelDuskInProductionProvider.php 0000644 00000002122 15105603711 0026644 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Exception; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Throwable; class RunningLaravelDuskInProductionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof Exception) { return false; } return $throwable->getMessage() === 'It is unsafe to run Dusk in production.'; } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create() ->setSolutionTitle('Laravel Dusk should not be run in production.') ->setSolutionDescription('Install the dependencies with the `--no-dev` flag.'), BaseSolution::create() ->setSolutionTitle('Laravel Dusk can be run in other environments.') ->setSolutionDescription('Consider setting the `APP_ENV` to something other than `production` like `local` for example.'), ]; } } laravel-ignition/src/Solutions/SolutionProviders/UndefinedViewVariableSolutionProvider.php 0000644 00000006760 15105603711 0026503 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\Ignition\Contracts\Solution; use Spatie\LaravelIgnition\Exceptions\ViewException; use Spatie\LaravelIgnition\Solutions\MakeViewVariableOptionalSolution; use Spatie\LaravelIgnition\Solutions\SuggestCorrectVariableNameSolution; use Throwable; class UndefinedViewVariableSolutionProvider implements HasSolutionsForThrowable { protected string $variableName; protected string $viewFile; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof ViewException) { return false; } return $this->getNameAndView($throwable) !== null; } public function getSolutions(Throwable $throwable): array { $solutions = []; /** @phpstan-ignore-next-line */ extract($this->getNameAndView($throwable)); if (! isset($variableName)) { return []; } if (isset($viewFile)) { /** @phpstan-ignore-next-line */ $solutions = $this->findCorrectVariableSolutions($throwable, $variableName, $viewFile); $solutions[] = $this->findOptionalVariableSolution($variableName, $viewFile); } return $solutions; } /** * @param \Spatie\LaravelIgnition\Exceptions\ViewException $throwable * @param string $variableName * @param string $viewFile * * @return array<int, \Spatie\Ignition\Contracts\Solution> */ protected function findCorrectVariableSolutions( ViewException $throwable, string $variableName, string $viewFile ): array { return collect($throwable->getViewData()) ->map(function ($value, $key) use ($variableName) { similar_text($variableName, $key, $percentage); return ['match' => $percentage, 'value' => $value]; }) ->sortByDesc('match') ->filter(fn ($var) => $var['match'] > 40) ->keys() ->map(fn ($suggestion) => new SuggestCorrectVariableNameSolution($variableName, $viewFile, $suggestion)) ->map(function ($solution) { return $solution->isRunnable() ? $solution : BaseSolution::create($solution->getSolutionTitle()) ->setSolutionDescription($solution->getSolutionDescription()); }) ->toArray(); } protected function findOptionalVariableSolution(string $variableName, string $viewFile): Solution { $optionalSolution = new MakeViewVariableOptionalSolution($variableName, $viewFile); return $optionalSolution->isRunnable() ? $optionalSolution : BaseSolution::create($optionalSolution->getSolutionTitle()) ->setSolutionDescription($optionalSolution->getSolutionDescription()); } /** * @param \Throwable $throwable * * @return array<string, string>|null */ protected function getNameAndView(Throwable $throwable): ?array { $pattern = '/Undefined variable:? (.*?) \(View: (.*?)\)/'; preg_match($pattern, $throwable->getMessage(), $matches); if (count($matches) === 3) { [, $variableName, $viewFile] = $matches; $variableName = ltrim($variableName, '$'); return compact('variableName', 'viewFile'); } return null; } } laravel-ignition/src/Solutions/SolutionProviders/SolutionProviderRepository.php 0000644 00000006143 15105603711 0024453 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Support\Collection; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\Ignition\Contracts\ProvidesSolution; use Spatie\Ignition\Contracts\Solution; use Spatie\Ignition\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract; use Throwable; class SolutionProviderRepository implements SolutionProviderRepositoryContract { /** * @param array<int, ProvidesSolution> $solutionProviders */ protected Collection $solutionProviders; /** * @param array<int, ProvidesSolution> $solutionProviders */ public function __construct(array $solutionProviders = []) { $this->solutionProviders = Collection::make($solutionProviders); } public function registerSolutionProvider(string $solutionProviderClass): SolutionProviderRepositoryContract { $this->solutionProviders->push($solutionProviderClass); return $this; } public function registerSolutionProviders(array $solutionProviderClasses): SolutionProviderRepositoryContract { $this->solutionProviders = $this->solutionProviders->merge($solutionProviderClasses); return $this; } public function getSolutionsForThrowable(Throwable $throwable): array { $solutions = []; if ($throwable instanceof Solution) { $solutions[] = $throwable; } if ($throwable instanceof ProvidesSolution) { $solutions[] = $throwable->getSolution(); } /** @phpstan-ignore-next-line */ $providedSolutions = $this->solutionProviders ->filter(function (string $solutionClass) { if (! in_array(HasSolutionsForThrowable::class, class_implements($solutionClass) ?: [])) { return false; } if (in_array($solutionClass, config('ignition.ignored_solution_providers', []))) { return false; } return true; }) ->map(fn (string $solutionClass) => app($solutionClass)) ->filter(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) { try { return $solutionProvider->canSolve($throwable); } catch (Throwable $e) { return false; } }) ->map(function (HasSolutionsForThrowable $solutionProvider) use ($throwable) { try { return $solutionProvider->getSolutions($throwable); } catch (Throwable $e) { return []; } }) ->flatten() ->toArray(); return array_merge($solutions, $providedSolutions); } public function getSolutionForClass(string $solutionClass): ?Solution { if (! class_exists($solutionClass)) { return null; } if (! in_array(Solution::class, class_implements($solutionClass) ?: [])) { return null; } return app($solutionClass); } } laravel-ignition/src/Solutions/SolutionProviders/ViewNotFoundSolutionProvider.php 0000644 00000007224 15105603711 0024664 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Support\Arr; use Illuminate\Support\Facades\View; use InvalidArgumentException; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Exceptions\ViewException; use Spatie\LaravelIgnition\Support\StringComparator; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Throwable; class ViewNotFoundSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/View \[(.*)\] not found/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof InvalidArgumentException && ! $throwable instanceof ViewException) { return false; } return (bool)preg_match(self::REGEX, $throwable->getMessage(), $matches); } public function getSolutions(Throwable $throwable): array { preg_match(self::REGEX, $throwable->getMessage(), $matches); $missingView = $matches[1] ?? null; $suggestedView = $this->findRelatedView($missingView); if ($suggestedView) { return [ BaseSolution::create() ->setSolutionTitle("{$missingView} was not found.") ->setSolutionDescription("Did you mean `{$suggestedView}`?"), ]; } return [ BaseSolution::create() ->setSolutionTitle("{$missingView} was not found.") ->setSolutionDescription('Are you sure the view exists and is a `.blade.php` file?'), ]; } protected function findRelatedView(string $missingView): ?string { $views = $this->getAllViews(); return StringComparator::findClosestMatch($views, $missingView); } /** @return array<int, string> */ protected function getAllViews(): array { /** @var \Illuminate\View\FileViewFinder $fileViewFinder */ $fileViewFinder = View::getFinder(); $extensions = $fileViewFinder->getExtensions(); $viewsForHints = collect($fileViewFinder->getHints()) ->flatMap(function ($paths, string $namespace) use ($extensions) { $paths = Arr::wrap($paths); return collect($paths) ->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions)) ->map(fn (string $view) => "{$namespace}::{$view}") ->toArray(); }); $viewsForViewPaths = collect($fileViewFinder->getPaths()) ->flatMap(fn (string $path) => $this->getViewsInPath($path, $extensions)); return $viewsForHints->merge($viewsForViewPaths)->toArray(); } /** * @param string $path * @param array<int, string> $extensions * * @return array<int, string> */ protected function getViewsInPath(string $path, array $extensions): array { $filePatterns = array_map(fn (string $extension) => "*.{$extension}", $extensions); $extensionsWithDots = array_map(fn (string $extension) => ".{$extension}", $extensions); $files = (new Finder()) ->in($path) ->files(); foreach ($filePatterns as $filePattern) { $files->name($filePattern); } $views = []; foreach ($files as $file) { if ($file instanceof SplFileInfo) { $view = $file->getRelativePathname(); $view = str_replace($extensionsWithDots, '', $view); $view = str_replace('/', '.', $view); $views[] = $view; } } return $views; } } laravel-ignition/src/Solutions/SolutionProviders/SailNetworkSolutionProvider.php 0000644 00000002017 15105603711 0024532 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Throwable; class SailNetworkSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return app()->runningInConsole() && str_contains($throwable->getMessage(), 'php_network_getaddresses') && file_exists(base_path('vendor/bin/sail')) && file_exists(base_path('docker-compose.yml')) && env('LARAVEL_SAIL') === null; } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create('Network address not found') ->setSolutionDescription('Did you mean to use `sail artisan`?') ->setDocumentationLinks([ 'Sail: Executing Artisan Commands' => 'https://laravel.com/docs/sail#executing-artisan-commands', ]), ]; } } laravel-ignition/src/Solutions/SolutionProviders/InvalidRouteActionSolutionProvider.php 0000644 00000005623 15105603711 0026041 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Support\Str; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Support\Composer\ComposerClassMap; use Spatie\LaravelIgnition\Support\StringComparator; use Throwable; use UnexpectedValueException; class InvalidRouteActionSolutionProvider implements HasSolutionsForThrowable { protected const REGEX = '/\[([a-zA-Z\\\\]+)\]/m'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof UnexpectedValueException) { return false; } if (! preg_match(self::REGEX, $throwable->getMessage(), $matches)) { return false; } return Str::startsWith($throwable->getMessage(), 'Invalid route action: '); } public function getSolutions(Throwable $throwable): array { preg_match(self::REGEX, $throwable->getMessage(), $matches); $invalidController = $matches[1] ?? null; $suggestedController = $this->findRelatedController($invalidController); if ($suggestedController === $invalidController) { return [ BaseSolution::create("`{$invalidController}` is not invokable.") ->setSolutionDescription("The controller class `{$invalidController}` is not invokable. Did you forget to add the `__invoke` method or is the controller's method missing in your routes file?"), ]; } if ($suggestedController) { return [ BaseSolution::create("`{$invalidController}` was not found.") ->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Did you mean `{$suggestedController}`?"), ]; } return [ BaseSolution::create("`{$invalidController}` was not found.") ->setSolutionDescription("Controller class `{$invalidController}` for one of your routes was not found. Are you sure this controller exists and is imported correctly?"), ]; } protected function findRelatedController(string $invalidController): ?string { $composerClassMap = app(ComposerClassMap::class); $controllers = collect($composerClassMap->listClasses()) ->filter(function (string $file, string $fqcn) { return Str::endsWith($fqcn, 'Controller'); }) ->mapWithKeys(function (string $file, string $fqcn) { return [$fqcn => class_basename($fqcn)]; }) ->toArray(); $basenameMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4); $controllers = array_flip($controllers); $fqcnMatch = StringComparator::findClosestMatch($controllers, $invalidController, 4); return $fqcnMatch ?? $basenameMatch; } } laravel-ignition/src/Solutions/SolutionProviders/GenericLaravelExceptionSolutionProvider.php 0000644 00000003427 15105603711 0027040 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Broadcasting\BroadcastException; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Support\LaravelVersion; use Throwable; class GenericLaravelExceptionSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return ! is_null($this->getSolutionTexts($throwable)); } public function getSolutions(Throwable $throwable): array { if (! $texts = $this->getSolutionTexts($throwable)) { return []; } $solution = BaseSolution::create($texts['title']) ->setSolutionDescription($texts['description']) ->setDocumentationLinks($texts['links']); return ([$solution]); } /** * @param \Throwable $throwable * * @return array<string, mixed>|null */ protected function getSolutionTexts(Throwable $throwable) : ?array { foreach ($this->getSupportedExceptions() as $supportedClass => $texts) { if ($throwable instanceof $supportedClass) { return $texts; } } return null; } /** @return array<string, mixed> */ protected function getSupportedExceptions(): array { $majorVersion = LaravelVersion::major(); return [ BroadcastException::class => [ 'title' => 'Here are some links that might help solve this problem', 'description' => '', 'links' => [ 'Laravel docs on authentication' => "https://laravel.com/docs/{$majorVersion}.x/authentication", ], ], ]; } } laravel-ignition/src/Solutions/SolutionProviders/UndefinedLivewirePropertySolutionProvider.php 0000644 00000003213 15105603711 0027444 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Livewire\Exceptions\PropertyNotFoundException; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\SuggestLivewirePropertyNameSolution; use Spatie\LaravelIgnition\Support\LivewireComponentParser; use Throwable; class UndefinedLivewirePropertySolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return $throwable instanceof PropertyNotFoundException; } public function getSolutions(Throwable $throwable): array { ['variable' => $variable, 'component' => $component] = $this->getMethodAndComponent($throwable); if ($variable === null || $component === null) { return []; } $parsed = LivewireComponentParser::create($component); return $parsed->getPropertyNamesLike($variable) ->map(function (string $suggested) use ($parsed, $variable) { return new SuggestLivewirePropertyNameSolution( $variable, $parsed->getComponentClass(), '$'.$suggested ); }) ->toArray(); } /** * @param \Throwable $throwable * * @return array<string, string|null> */ protected function getMethodAndComponent(Throwable $throwable): array { preg_match_all('/\[([\d\w\-_\$]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER, 0); return [ 'variable' => $matches[0][1] ?? null, 'component' => $matches[1][1] ?? null, ]; } } laravel-ignition/src/Solutions/SolutionProviders/DefaultDbNameSolutionProvider.php 0000644 00000001620 15105603711 0024722 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Database\QueryException; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\SuggestUsingCorrectDbNameSolution; use Throwable; class DefaultDbNameSolutionProvider implements HasSolutionsForThrowable { const MYSQL_UNKNOWN_DATABASE_CODE = 1049; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } if ($throwable->getCode() !== self::MYSQL_UNKNOWN_DATABASE_CODE) { return false; } if (! in_array(env('DB_DATABASE'), ['homestead', 'laravel'])) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [new SuggestUsingCorrectDbNameSolution()]; } } laravel-ignition/src/Solutions/SolutionProviders/MissingMixManifestSolutionProvider.php 0000644 00000001307 15105603711 0026047 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Support\Str; use Spatie\Ignition\Contracts\BaseSolution; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Throwable; class MissingMixManifestSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return Str::startsWith($throwable->getMessage(), 'Mix manifest not found'); } public function getSolutions(Throwable $throwable): array { return [ BaseSolution::create('Missing Mix Manifest File') ->setSolutionDescription('Did you forget to run `npm install && npm run dev`?'), ]; } } laravel-ignition/src/Solutions/SolutionProviders/TableNotFoundSolutionProvider.php 0000644 00000001730 15105603711 0024775 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Database\QueryException; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\RunMigrationsSolution; use Throwable; class TableNotFoundSolutionProvider implements HasSolutionsForThrowable { /** * See https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_bad_table_error. */ const MYSQL_BAD_TABLE_CODE = '42S02'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } return $this->isBadTableErrorCode($throwable->getCode()); } protected function isBadTableErrorCode(string $code): bool { return $code === static::MYSQL_BAD_TABLE_CODE; } public function getSolutions(Throwable $throwable): array { return [new RunMigrationsSolution('A table was not found')]; } } laravel-ignition/src/Solutions/SolutionProviders/MissingColumnSolutionProvider.php 0000644 00000001731 15105603711 0025061 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Database\QueryException; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\RunMigrationsSolution; use Throwable; class MissingColumnSolutionProvider implements HasSolutionsForThrowable { /** * See https://dev.mysql.com/doc/refman/8.0/en/server-error-reference.html#error_er_bad_field_error. */ const MYSQL_BAD_FIELD_CODE = '42S22'; public function canSolve(Throwable $throwable): bool { if (! $throwable instanceof QueryException) { return false; } return $this->isBadTableErrorCode($throwable->getCode()); } protected function isBadTableErrorCode(string $code): bool { return $code === static::MYSQL_BAD_FIELD_CODE; } public function getSolutions(Throwable $throwable): array { return [new RunMigrationsSolution('A column was not found')]; } } laravel-ignition/src/Solutions/SolutionProviders/OpenAiSolutionProvider.php 0000644 00000002102 15105603711 0023436 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Support\Str; use OpenAI\Client; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\Ignition\Solutions\OpenAi\OpenAiSolutionProvider as BaseOpenAiSolutionProvider; use Throwable; class OpenAiSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! class_exists(Client::class)) { return false; } if (config('ignition.open_ai_key') === null) { return false; } return true; } public function getSolutions(Throwable $throwable): array { $solutionProvider = new BaseOpenAiSolutionProvider( openAiKey: config('ignition.open_ai_key'), cache: cache()->store(config('cache.default')), cacheTtlInSeconds: 60, applicationType: 'Laravel ' . Str::before(app()->version(), '.'), applicationPath: base_path(), ); return $solutionProvider->getSolutions($throwable); } } laravel-ignition/src/Solutions/SolutionProviders/MissingLivewireComponentSolutionProvider.php 0000644 00000002116 15105603711 0027273 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Livewire\Exceptions\ComponentNotFoundException; use Livewire\LivewireComponentsFinder; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\LivewireDiscoverSolution; use Throwable; class MissingLivewireComponentSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { if (! $this->livewireIsInstalled()) { return false; } if (! $throwable instanceof ComponentNotFoundException) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [new LivewireDiscoverSolution('A Livewire component was not found')]; } public function livewireIsInstalled(): bool { if (! class_exists(ComponentNotFoundException::class)) { return false; } if (! class_exists(LivewireComponentsFinder::class)) { return false; } return true; } } laravel-ignition/src/Solutions/SolutionProviders/IncorrectValetDbCredentialsSolutionProvider.php 0000644 00000003142 15105603711 0027640 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Illuminate\Database\QueryException; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\UseDefaultValetDbCredentialsSolution; use Throwable; class IncorrectValetDbCredentialsSolutionProvider implements HasSolutionsForThrowable { const MYSQL_ACCESS_DENIED_CODE = 1045; public function canSolve(Throwable $throwable): bool { if (PHP_OS !== 'Darwin') { return false; } if (! $throwable instanceof QueryException) { return false; } if (! $this->isAccessDeniedCode($throwable->getCode())) { return false; } if (! $this->envFileExists()) { return false; } if (! $this->isValetInstalled()) { return false; } if ($this->usingCorrectDefaultCredentials()) { return false; } return true; } public function getSolutions(Throwable $throwable): array { return [new UseDefaultValetDbCredentialsSolution()]; } protected function envFileExists(): bool { return file_exists(base_path('.env')); } protected function isAccessDeniedCode(string $code): bool { return $code === static::MYSQL_ACCESS_DENIED_CODE; } protected function isValetInstalled(): bool { return file_exists('/usr/local/bin/valet'); } protected function usingCorrectDefaultCredentials(): bool { return env('DB_USERNAME') === 'root' && env('DB_PASSWORD') === ''; } } laravel-ignition/src/Solutions/SolutionProviders/UndefinedLivewireMethodSolutionProvider.php 0000644 00000003115 15105603711 0027041 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionProviders; use Livewire\Exceptions\MethodNotFoundException; use Spatie\Ignition\Contracts\HasSolutionsForThrowable; use Spatie\LaravelIgnition\Solutions\SuggestLivewireMethodNameSolution; use Spatie\LaravelIgnition\Support\LivewireComponentParser; use Throwable; class UndefinedLivewireMethodSolutionProvider implements HasSolutionsForThrowable { public function canSolve(Throwable $throwable): bool { return $throwable instanceof MethodNotFoundException; } public function getSolutions(Throwable $throwable): array { ['methodName' => $methodName, 'component' => $component] = $this->getMethodAndComponent($throwable); if ($methodName === null || $component === null) { return []; } $parsed = LivewireComponentParser::create($component); return $parsed->getMethodNamesLike($methodName) ->map(function (string $suggested) use ($parsed, $methodName) { return new SuggestLivewireMethodNameSolution( $methodName, $parsed->getComponentClass(), $suggested ); }) ->toArray(); } /** @return array<string, string|null> */ protected function getMethodAndComponent(Throwable $throwable): array { preg_match_all('/\[([\d\w\-_]*)\]/m', $throwable->getMessage(), $matches, PREG_SET_ORDER); return [ 'methodName' => $matches[0][1] ?? null, 'component' => $matches[1][1] ?? null, ]; } } laravel-ignition/src/Solutions/SuggestImportSolution.php 0000644 00000001154 15105603711 0017660 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Spatie\Ignition\Contracts\Solution; class SuggestImportSolution implements Solution { protected string $class; public function __construct(string $class = '') { $this->class = $class; } public function getSolutionTitle(): string { return 'A class import is missing'; } public function getSolutionDescription(): string { return 'You have a missing class import. Try importing this class: `'.$this->class.'`.'; } public function getDocumentationLinks(): array { return []; } } laravel-ignition/src/Solutions/GenerateAppKeySolution.php 0000644 00000002000 15105603711 0017677 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Illuminate\Support\Facades\Artisan; use Spatie\Ignition\Contracts\RunnableSolution; class GenerateAppKeySolution implements RunnableSolution { public function getSolutionTitle(): string { return 'Your app key is missing'; } public function getDocumentationLinks(): array { return [ 'Laravel installation' => 'https://laravel.com/docs/master/installation#configuration', ]; } public function getSolutionActionDescription(): string { return 'Generate your application encryption key using `php artisan key:generate`.'; } public function getRunButtonText(): string { return 'Generate app key'; } public function getSolutionDescription(): string { return ''; } public function getRunParameters(): array { return []; } public function run(array $parameters = []): void { Artisan::call('key:generate'); } } laravel-ignition/src/Solutions/SuggestLivewireMethodNameSolution.php 0000644 00000001370 15105603711 0022136 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Spatie\Ignition\Contracts\Solution; class SuggestLivewireMethodNameSolution implements Solution { public function __construct( protected string $methodName, protected string $componentClass, protected string $suggested ) { } public function getSolutionTitle(): string { return "Possible typo `{$this->componentClass}::{$this->methodName}`"; } public function getDocumentationLinks(): array { return []; } public function getSolutionDescription(): string { return "Did you mean `{$this->componentClass}::{$this->suggested}`?"; } public function isRunnable(): bool { return false; } } laravel-ignition/src/Solutions/RunMigrationsSolution.php 0000644 00000002305 15105603711 0017644 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Illuminate\Support\Facades\Artisan; use Spatie\Ignition\Contracts\RunnableSolution; class RunMigrationsSolution implements RunnableSolution { protected string $customTitle; public function __construct(string $customTitle = '') { $this->customTitle = $customTitle; } public function getSolutionTitle(): string { return $this->customTitle; } public function getSolutionDescription(): string { return 'You might have forgotten to run your database migrations.'; } public function getDocumentationLinks(): array { return [ 'Database: Running Migrations docs' => 'https://laravel.com/docs/master/migrations#running-migrations', ]; } public function getRunParameters(): array { return []; } public function getSolutionActionDescription(): string { return 'You can try to run your migrations using `php artisan migrate`.'; } public function getRunButtonText(): string { return 'Run migrations'; } public function run(array $parameters = []): void { Artisan::call('migrate'); } } laravel-ignition/src/Solutions/SolutionTransformers/LaravelSolutionTransformer.php 0000644 00000003306 15105603711 0025100 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions\SolutionTransformers; use Spatie\Ignition\Contracts\RunnableSolution; use Spatie\Ignition\Solutions\SolutionTransformer; use Spatie\LaravelIgnition\Http\Controllers\ExecuteSolutionController; use Throwable; class LaravelSolutionTransformer extends SolutionTransformer { /** @return array<string|mixed> */ public function toArray(): array { $baseProperties = parent::toArray(); if (! $this->isRunnable()) { return $baseProperties; } /** @var RunnableSolution $solution Type shenanigans */ $solution = $this->solution; $runnableProperties = [ 'is_runnable' => true, 'action_description' => $solution->getSolutionActionDescription(), 'run_button_text' => $solution->getRunButtonText(), 'execute_endpoint' => $this->executeEndpoint(), 'run_parameters' => $solution->getRunParameters(), ]; return array_merge($baseProperties, $runnableProperties); } protected function isRunnable(): bool { if (! $this->solution instanceof RunnableSolution) { return false; } if (! $this->executeEndpoint()) { return false; } return true; } protected function executeEndpoint(): ?string { try { // The action class needs to be prefixed with a `\` to Laravel from trying // to add its own global namespace from RouteServiceProvider::$namespace. return action('\\'.ExecuteSolutionController::class); } catch (Throwable $exception) { report($exception); return null; } } } laravel-ignition/src/Solutions/UseDefaultValetDbCredentialsSolution.php 0000644 00000003150 15105603711 0022523 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Illuminate\Support\Str; use Spatie\Ignition\Contracts\RunnableSolution; class UseDefaultValetDbCredentialsSolution implements RunnableSolution { public function getSolutionActionDescription(): string { return 'Pressing the button will change `DB_USER` and `DB_PASSWORD` in your `.env` file.'; } public function getRunButtonText(): string { return 'Use default Valet credentials'; } public function getSolutionTitle(): string { return 'Could not connect to database'; } public function run(array $parameters = []): void { if (! file_exists(base_path('.env'))) { return; } $this->ensureLineExists('DB_USERNAME', 'root'); $this->ensureLineExists('DB_PASSWORD', ''); } protected function ensureLineExists(string $key, string $value): void { $envPath = base_path('.env'); $envLines = array_map(fn (string $envLine) => Str::startsWith($envLine, $key) ? "{$key}={$value}".PHP_EOL : $envLine, file($envPath) ?: []); file_put_contents($envPath, implode('', $envLines)); } public function getRunParameters(): array { return []; } public function getDocumentationLinks(): array { return [ 'Valet documentation' => 'https://laravel.com/docs/master/valet', ]; } public function getSolutionDescription(): string { return 'You seem to be using Valet, but the .env file does not contain the right default database credentials.'; } } laravel-ignition/src/Solutions/SuggestCorrectVariableNameSolution.php 0000644 00000001605 15105603711 0022257 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Spatie\Ignition\Contracts\Solution; class SuggestCorrectVariableNameSolution implements Solution { protected ?string $variableName; protected ?string $viewFile; protected ?string $suggested; public function __construct(string $variableName = null, string $viewFile = null, string $suggested = null) { $this->variableName = $variableName; $this->viewFile = $viewFile; $this->suggested = $suggested; } public function getSolutionTitle(): string { return 'Possible typo $'.$this->variableName; } public function getDocumentationLinks(): array { return []; } public function getSolutionDescription(): string { return "Did you mean `$$this->suggested`?"; } public function isRunnable(): bool { return false; } } laravel-ignition/src/Solutions/SuggestLivewirePropertyNameSolution.php 0000644 00000001311 15105603711 0022535 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Solutions; use Spatie\Ignition\Contracts\Solution; class SuggestLivewirePropertyNameSolution implements Solution { public function __construct( protected string $variableName, protected string $componentClass, protected string $suggested, ) { } public function getSolutionTitle(): string { return "Possible typo {$this->variableName}"; } public function getDocumentationLinks(): array { return []; } public function getSolutionDescription(): string { return "Did you mean `$this->suggested`?"; } public function isRunnable(): bool { return false; } } laravel-ignition/src/ContextProviders/LaravelLivewireRequestContextProvider.php 0000644 00000005370 15105603711 0024364 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Livewire\LivewireManager; class LaravelLivewireRequestContextProvider extends LaravelRequestContextProvider { public function __construct( Request $request, protected LivewireManager $livewireManager ) { parent::__construct($request); } /** @return array<string, string> */ public function getRequest(): array { $properties = parent::getRequest(); $properties['method'] = $this->livewireManager->originalMethod(); $properties['url'] = $this->livewireManager->originalUrl(); return $properties; } /** @return array<int|string, mixed> */ public function toArray(): array { $properties = parent::toArray(); $properties['livewire'] = $this->getLivewireInformation(); return $properties; } /** @return array<string, mixed> */ protected function getLivewireInformation(): array { /** @phpstan-ignore-next-line */ $componentId = $this->request->input('fingerprint.id'); /** @phpstan-ignore-next-line */ $componentAlias = $this->request->input('fingerprint.name'); if ($componentAlias === null) { return []; } try { $componentClass = $this->livewireManager->getClass($componentAlias); } catch (Exception $e) { $componentClass = null; } return [ 'component_class' => $componentClass, 'component_alias' => $componentAlias, 'component_id' => $componentId, 'data' => $this->resolveData(), 'updates' => $this->resolveUpdates(), ]; } /** @return array<string, mixed> */ protected function resolveData(): array { /** @phpstan-ignore-next-line */ $data = $this->request->input('serverMemo.data') ?? []; /** @phpstan-ignore-next-line */ $dataMeta = $this->request->input('serverMemo.dataMeta') ?? []; foreach ($dataMeta['modelCollections'] ?? [] as $key => $value) { $data[$key] = array_merge($data[$key] ?? [], $value); } foreach ($dataMeta['models'] ?? [] as $key => $value) { $data[$key] = array_merge($data[$key] ?? [], $value); } return $data; } /** @return array<string, mixed> */ protected function resolveUpdates(): array { /** @phpstan-ignore-next-line */ $updates = $this->request->input('updates') ?? []; return array_map(function (array $update) { $update['payload'] = Arr::except($update['payload'] ?? [], ['id']); return $update; }, $updates); } } laravel-ignition/src/ContextProviders/LaravelRequestContextProvider.php 0000644 00000005314 15105603711 0022653 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request as LaravelRequest; use Spatie\FlareClient\Context\RequestContextProvider; use Symfony\Component\HttpFoundation\Request as SymphonyRequest; use Throwable; class LaravelRequestContextProvider extends RequestContextProvider { protected LaravelRequest|SymphonyRequest|null $request; public function __construct(LaravelRequest $request) { $this->request = $request; } /** @return null|array<string, mixed> */ public function getUser(): array|null { try { /** @var object|null $user */ /** @phpstan-ignore-next-line */ $user = $this->request?->user(); if (! $user) { return null; } } catch (Throwable) { return null; } try { if (method_exists($user, 'toFlare')) { return $user->toFlare(); } if (method_exists($user, 'toArray')) { return $user->toArray(); } } catch (Throwable $e) { return null; } return null; } /** @return null|array<string, mixed> */ public function getRoute(): array|null { /** * @phpstan-ignore-next-line * @var \Illuminate\Routing\Route|null $route */ $route = $this->request->route(); if (! $route) { return null; } return [ 'route' => $route->getName(), 'routeParameters' => $this->getRouteParameters(), 'controllerAction' => $route->getActionName(), 'middleware' => array_values($route->gatherMiddleware() ?? []), ]; } /** @return array<int, mixed> */ protected function getRouteParameters(): array { try { /** @phpstan-ignore-next-line */ return collect(optional($this->request->route())->parameters ?? []) ->map(fn ($parameter) => $parameter instanceof Model ? $parameter->withoutRelations() : $parameter) ->map(function ($parameter) { return method_exists($parameter, 'toFlare') ? $parameter->toFlare() : $parameter; }) ->toArray(); } catch (Throwable) { return []; } } /** @return array<int, mixed> */ public function toArray(): array { $properties = parent::toArray(); if ($route = $this->getRoute()) { $properties['route'] = $route; } if ($user = $this->getUser()) { $properties['user'] = $user; } return $properties; } } laravel-ignition/src/ContextProviders/LaravelConsoleContextProvider.php 0000644 00000000272 15105603711 0022623 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Spatie\FlareClient\Context\ConsoleContextProvider; class LaravelConsoleContextProvider extends ConsoleContextProvider { } laravel-ignition/src/ContextProviders/LaravelContextProviderDetector.php 0000644 00000001636 15105603711 0022777 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ContextProviders; use Illuminate\Http\Request; use Livewire\LivewireManager; use Spatie\FlareClient\Context\ContextProvider; use Spatie\FlareClient\Context\ContextProviderDetector; class LaravelContextProviderDetector implements ContextProviderDetector { public function detectCurrentContext(): ContextProvider { if (app()->runningInConsole()) { return new LaravelConsoleContextProvider($_SERVER['argv'] ?? []); } $request = app(Request::class); if ($this->isRunningLiveWire($request)) { return new LaravelLivewireRequestContextProvider($request, app(LivewireManager::class)); } return new LaravelRequestContextProvider($request); } protected function isRunningLiveWire(Request $request): bool { return $request->hasHeader('x-livewire') && $request->hasHeader('referer'); } } laravel-ignition/src/Views/ViewExceptionMapper.php 0000644 00000013732 15105603711 0016350 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Views; use Exception; use Illuminate\Contracts\View\Engine; use Illuminate\Foundation\Application; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\View\Engines\PhpEngine; use Illuminate\View\ViewException; use ReflectionClass; use ReflectionProperty; use Spatie\Ignition\Contracts\ProvidesSolution; use Spatie\LaravelIgnition\Exceptions\ViewException as IgnitionViewException; use Spatie\LaravelIgnition\Exceptions\ViewExceptionWithSolution; use Throwable; class ViewExceptionMapper { protected Engine $compilerEngine; protected BladeSourceMapCompiler $bladeSourceMapCompiler; protected array $knownPaths; public function __construct(BladeSourceMapCompiler $bladeSourceMapCompiler) { $resolver = app('view.engine.resolver'); $this->compilerEngine = $resolver->resolve('blade'); $this->bladeSourceMapCompiler = $bladeSourceMapCompiler; } public function map(ViewException $viewException): IgnitionViewException { $baseException = $this->getRealException($viewException); if ($baseException instanceof IgnitionViewException) { return $baseException; } preg_match('/\(View: (?P<path>.*?)\)/', $viewException->getMessage(), $matches); $compiledViewPath = $matches['path']; $exception = $this->createException($baseException); if ($baseException instanceof ProvidesSolution) { /** @var ViewExceptionWithSolution $exception */ $exception->setSolution($baseException->getSolution()); } $this->modifyViewsInTrace($exception); $exception->setView($compiledViewPath); $exception->setViewData($this->getViewData($exception)); return $exception; } protected function createException(Throwable $baseException): IgnitionViewException { $viewExceptionClass = $baseException instanceof ProvidesSolution ? ViewExceptionWithSolution::class : IgnitionViewException::class; $viewFile = $this->findCompiledView($baseException->getFile()); $file = $viewFile ?? $baseException->getFile(); $line = $viewFile ? $this->getBladeLineNumber($file, $baseException->getLine()) : $baseException->getLine(); return new $viewExceptionClass( $baseException->getMessage(), 0, 1, $file, $line, $baseException ); } protected function modifyViewsInTrace(IgnitionViewException $exception): void { $trace = Collection::make($exception->getPrevious()->getTrace()) ->map(function ($trace) { if ($originalPath = $this->findCompiledView(Arr::get($trace, 'file', ''))) { $trace['file'] = $originalPath; $trace['line'] = $this->getBladeLineNumber($trace['file'], $trace['line']); } return $trace; })->toArray(); $traceProperty = new ReflectionProperty('Exception', 'trace'); $traceProperty->setAccessible(true); $traceProperty->setValue($exception, $trace); } /** * Look at the previous exceptions to find the original exception. * This is usually the first Exception that is not a ViewException. */ protected function getRealException(Throwable $exception): Throwable { $rootException = $exception->getPrevious() ?? $exception; while ($rootException instanceof ViewException && $rootException->getPrevious()) { $rootException = $rootException->getPrevious(); } return $rootException; } protected function findCompiledView(string $compiledPath): ?string { $this->knownPaths ??= $this->getKnownPaths(); return $this->knownPaths[$compiledPath] ?? null; } protected function getKnownPaths(): array { $compilerEngineReflection = new ReflectionClass($this->compilerEngine); if (! $compilerEngineReflection->hasProperty('lastCompiled') && $compilerEngineReflection->hasProperty('engine')) { $compilerEngine = $compilerEngineReflection->getProperty('engine'); $compilerEngine->setAccessible(true); $compilerEngine = $compilerEngine->getValue($this->compilerEngine); $lastCompiled = new ReflectionProperty($compilerEngine, 'lastCompiled'); $lastCompiled->setAccessible(true); $lastCompiled = $lastCompiled->getValue($compilerEngine); } else { $lastCompiled = $compilerEngineReflection->getProperty('lastCompiled'); $lastCompiled->setAccessible(true); $lastCompiled = $lastCompiled->getValue($this->compilerEngine); } $knownPaths = []; foreach ($lastCompiled as $lastCompiledPath) { $compiledPath = $this->compilerEngine->getCompiler()->getCompiledPath($lastCompiledPath); $knownPaths[realpath($compiledPath ?? $lastCompiledPath)] = realpath($lastCompiledPath); } return $knownPaths; } protected function getBladeLineNumber(string $view, int $compiledLineNumber): int { return $this->bladeSourceMapCompiler->detectLineNumber($view, $compiledLineNumber); } protected function getViewData(Throwable $exception): array { foreach ($exception->getTrace() as $frame) { if (Arr::get($frame, 'class') === PhpEngine::class) { $data = Arr::get($frame, 'args.1', []); return $this->filterViewData($data); } } return []; } protected function filterViewData(array $data): array { // By default, Laravel views get two data keys: // __env and app. We try to filter them out. return array_filter($data, function ($value, $key) { if ($key === 'app') { return ! $value instanceof Application; } return $key !== '__env'; }, ARRAY_FILTER_USE_BOTH); } } laravel-ignition/src/Views/BladeSourceMapCompiler.php 0000644 00000010660 15105603711 0016730 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Views; use Illuminate\View\Compilers\BladeCompiler; use Throwable; class BladeSourceMapCompiler { protected BladeCompiler $bladeCompiler; public function __construct() { $this->bladeCompiler = app('blade.compiler'); } public function detectLineNumber(string $filename, int $compiledLineNumber): int { $map = $this->compileSourcemap((string)file_get_contents($filename)); return $this->findClosestLineNumberMapping($map, $compiledLineNumber); } protected function compileSourcemap(string $value): string { 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; } } protected function addEchoLineNumbers(string $value): string { $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; } protected function addStatementLineNumbers(string $value): string { // Matches @bladeStatements() like @if, @component(...), @etc; $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; } protected function addBladeComponentLineNumbers(string $value): string { // Matches the start of `<x-blade-component` $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; } protected function insertLineNumberAtPosition(int $position, string $value): string { $before = mb_substr($value, 0, $position); $lineNumber = count(explode("\n", $before)); return mb_substr($value, 0, $position)."|---LINE:{$lineNumber}---|".mb_substr($value, $position); } protected function trimEmptyLines(string $value): string { $value = preg_replace('/^\|---LINE:([0-9]+)---\|$/m', '', $value); return ltrim((string)$value, PHP_EOL); } protected function findClosestLineNumberMapping(string $map, int $compiledLineNumber): int { $map = explode("\n", $map); // Max 20 lines between compiled and source line number. // Blade components can span multiple lines and the compiled line number is often // a couple lines below the source-mapped `<x-component>` code. $maxDistance = 20; $pattern = '/\|---LINE:(?P<line>[0-9]+)---\|/m'; $lineNumberToCheck = $compiledLineNumber - 1; while (true) { if ($lineNumberToCheck < $compiledLineNumber - $maxDistance) { // Something wrong. Return the $compiledLineNumber (unless it's out of range) return min($compiledLineNumber, count($map)); } if (preg_match($pattern, $map[$lineNumberToCheck] ?? '', $matches)) { return (int)$matches['line']; } $lineNumberToCheck--; } } } laravel-ignition/src/IgnitionServiceProvider.php 0000644 00000026762 15105603711 0016140 0 ustar 00 <?php namespace Spatie\LaravelIgnition; use Exception; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Foundation\Application; use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; use Illuminate\View\ViewException; use Laravel\Octane\Events\RequestReceived; use Laravel\Octane\Events\RequestTerminated; use Laravel\Octane\Events\TaskReceived; use Laravel\Octane\Events\TickReceived; use Monolog\Level; use Monolog\Logger; use Spatie\FlareClient\Flare; use Spatie\FlareClient\FlareMiddleware\AddSolutions; use Spatie\Ignition\Config\FileConfigManager; use Spatie\Ignition\Config\IgnitionConfig; use Spatie\Ignition\Contracts\ConfigManager; use Spatie\Ignition\Contracts\SolutionProviderRepository as SolutionProviderRepositoryContract; use Spatie\Ignition\Ignition; use Spatie\LaravelIgnition\Commands\SolutionMakeCommand; use Spatie\LaravelIgnition\Commands\SolutionProviderMakeCommand; use Spatie\LaravelIgnition\Commands\TestCommand; use Spatie\LaravelIgnition\ContextProviders\LaravelContextProviderDetector; use Spatie\LaravelIgnition\Exceptions\InvalidConfig; use Spatie\LaravelIgnition\FlareMiddleware\AddJobs; use Spatie\LaravelIgnition\FlareMiddleware\AddLogs; use Spatie\LaravelIgnition\FlareMiddleware\AddQueries; use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder; use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder; use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder; use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder; use Spatie\LaravelIgnition\Renderers\IgnitionExceptionRenderer; use Spatie\LaravelIgnition\Solutions\SolutionProviders\SolutionProviderRepository; use Spatie\LaravelIgnition\Support\FlareLogHandler; use Spatie\LaravelIgnition\Support\SentReports; use Spatie\LaravelIgnition\Views\ViewExceptionMapper; class IgnitionServiceProvider extends ServiceProvider { public function register(): void { $this->registerConfig(); $this->registerFlare(); $this->registerIgnition(); $this->registerRenderer(); $this->registerRecorders(); $this->registerLogHandler(); } public function boot() { if ($this->app->runningInConsole()) { $this->registerCommands(); $this->publishConfigs(); } $this->registerRoutes(); $this->configureTinker(); $this->configureOctane(); $this->registerViewExceptionMapper(); $this->startRecorders(); $this->configureQueue(); } protected function registerConfig(): void { $this->mergeConfigFrom(__DIR__ . '/../config/flare.php', 'flare'); $this->mergeConfigFrom(__DIR__ . '/../config/ignition.php', 'ignition'); } protected function registerCommands(): void { if ($this->app['config']->get('flare.key')) { $this->commands([ TestCommand::class, ]); } if ($this->app['config']->get('ignition.register_commands')) { $this->commands([ SolutionMakeCommand::class, SolutionProviderMakeCommand::class, ]); } } protected function publishConfigs(): void { $this->publishes([ __DIR__ . '/../config/ignition.php' => config_path('ignition.php'), ], 'ignition-config'); $this->publishes([ __DIR__ . '/../config/flare.php' => config_path('flare.php'), ], 'flare-config'); } protected function registerRenderer(): void { $this->app->bind( 'Illuminate\Contracts\Foundation\ExceptionRenderer', fn (Application $app) => $app->make(IgnitionExceptionRenderer::class) ); } protected function registerFlare(): void { $this->app->singleton(Flare::class, function () { return Flare::make() ->setApiToken(config('flare.key') ?? '') ->setBaseUrl(config('flare.base_url', 'https://flareapp.io/api')) ->applicationPath(base_path()) ->setStage(app()->environment()) ->setContextProviderDetector(new LaravelContextProviderDetector()) ->registerMiddleware($this->getFlareMiddleware()) ->registerMiddleware(new AddSolutions(new SolutionProviderRepository($this->getSolutionProviders()))) ->argumentReducers(config('ignition.argument_reducers', [])) ->withStackFrameArguments(config('ignition.with_stack_frame_arguments', true)); }); $this->app->singleton(SentReports::class); } protected function registerIgnition(): void { $this->app->singleton( ConfigManager::class, fn () => new FileConfigManager(config('ignition.settings_file_path', '')) ); $ignitionConfig = (new IgnitionConfig()) ->merge(config('ignition', [])) ->loadConfigFile(); $solutionProviders = $this->getSolutionProviders(); $solutionProviderRepository = new SolutionProviderRepository($solutionProviders); $this->app->singleton(IgnitionConfig::class, fn () => $ignitionConfig); $this->app->singleton(SolutionProviderRepositoryContract::class, fn () => $solutionProviderRepository); $this->app->singleton( Ignition::class, fn () => (new Ignition()) ->applicationPath(base_path()) ); } protected function registerRecorders(): void { $this->app->singleton(DumpRecorder::class); $this->app->singleton(LogRecorder::class, function (Application $app): LogRecorder { return new LogRecorder( $app, config()->get('flare.flare_middleware.' . AddLogs::class . '.maximum_number_of_collected_logs') ); }); $this->app->singleton( QueryRecorder::class, function (Application $app): QueryRecorder { return new QueryRecorder( $app, config('flare.flare_middleware.' . AddQueries::class . '.report_query_bindings', true), config('flare.flare_middleware.' . AddQueries::class . '.maximum_number_of_collected_queries', 200) ); } ); $this->app->singleton(JobRecorder::class, function (Application $app): JobRecorder { return new JobRecorder( $app, config('flare.flare_middleware.' . AddJobs::class . '.max_chained_job_reporting_depth', 5) ); }); } public function configureTinker(): void { if (! $this->app->runningInConsole()) { if (isset($_SERVER['argv']) && ['artisan', 'tinker'] === $_SERVER['argv']) { app(Flare::class)->sendReportsImmediately(); } } } protected function configureOctane(): void { if (isset($_SERVER['LARAVEL_OCTANE'])) { $this->setupOctane(); } } protected function registerViewExceptionMapper(): void { $handler = $this->app->make(ExceptionHandler::class); if (! method_exists($handler, 'map')) { return; } $handler->map(function (ViewException $viewException) { return $this->app->make(ViewExceptionMapper::class)->map($viewException); }); } protected function registerRoutes(): void { $this->loadRoutesFrom(realpath(__DIR__ . '/ignition-routes.php')); } protected function registerLogHandler(): void { $this->app->singleton('flare.logger', function ($app) { $handler = new FlareLogHandler( $app->make(Flare::class), $app->make(SentReports::class), ); $logLevelString = config('logging.channels.flare.level', 'error'); $logLevel = $this->getLogLevel($logLevelString); $handler->setMinimumReportLogLevel($logLevel); return tap( new Logger('Flare'), fn (Logger $logger) => $logger->pushHandler($handler) ); }); Log::extend('flare', fn ($app) => $app['flare.logger']); } protected function startRecorders(): void { foreach ($this->app->config['ignition.recorders'] ?? [] as $recorder) { $this->app->make($recorder)->start(); } } protected function configureQueue(): void { if (! $this->app->bound('queue')) { return; } $queue = $this->app->get('queue'); // Reset before executing a queue job to make sure the job's log/query/dump recorders are empty. // When using a sync queue this also reports the queued reports from previous exceptions. $queue->before(function () { $this->resetFlareAndLaravelIgnition(); app(Flare::class)->sendReportsImmediately(); }); // Send queued reports (and reset) after executing a queue job. $queue->after(function () { $this->resetFlareAndLaravelIgnition(); }); // Note: the $queue->looping() event can't be used because it's not triggered on Vapor } protected function getLogLevel(string $logLevelString): int { try { $logLevel = Level::fromName($logLevelString); } catch (Exception $exception) { $logLevel = null; } if (! $logLevel) { throw InvalidConfig::invalidLogLevel($logLevelString); } return $logLevel->value; } protected function getFlareMiddleware(): array { return collect(config('flare.flare_middleware')) ->map(function ($value, $key) { if (is_string($key)) { $middlewareClass = $key; $parameters = $value ?? []; } else { $middlewareClass = $value; $parameters = []; } return new $middlewareClass(...array_values($parameters)); }) ->values() ->toArray(); } protected function getSolutionProviders(): array { return collect(config('ignition.solution_providers')) ->reject( fn (string $class) => in_array($class, config('ignition.ignored_solution_providers')) ) ->toArray(); } protected function setupOctane(): void { $this->app['events']->listen(RequestReceived::class, function () { $this->resetFlareAndLaravelIgnition(); }); $this->app['events']->listen(TaskReceived::class, function () { $this->resetFlareAndLaravelIgnition(); }); $this->app['events']->listen(TickReceived::class, function () { $this->resetFlareAndLaravelIgnition(); }); $this->app['events']->listen(RequestTerminated::class, function () { $this->resetFlareAndLaravelIgnition(); }); } protected function resetFlareAndLaravelIgnition(): void { $this->app->get(SentReports::class)->clear(); $this->app->get(Ignition::class)->reset(); if (config('flare.flare_middleware.' . AddLogs::class)) { $this->app->make(LogRecorder::class)->reset(); } if (config('flare.flare_middleware.' . AddQueries::class)) { $this->app->make(QueryRecorder::class)->reset(); } if (config('flare.flare_middleware.' . AddJobs::class)) { $this->app->make(JobRecorder::class)->reset(); } $this->app->make(DumpRecorder::class)->reset(); } } laravel-ignition/src/ArgumentReducers/ModelArgumentReducer.php 0000644 00000001351 15105603711 0020643 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ArgumentReducers; use Illuminate\Database\Eloquent\Model; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; class ModelArgumentReducer implements ArgumentReducer { public function execute(mixed $argument): ReducedArgumentContract { if (! $argument instanceof Model) { return UnReducedArgument::create(); } return new ReducedArgument( "{$argument->getKeyName()}:{$argument->getKey()}", get_class($argument) ); } } laravel-ignition/src/ArgumentReducers/CollectionArgumentReducer.php 0000644 00000001170 15105603711 0021675 0 ustar 00 <?php namespace Spatie\LaravelIgnition\ArgumentReducers; use Illuminate\Support\Collection; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer; class CollectionArgumentReducer extends ArrayArgumentReducer { public function execute(mixed $argument): ReducedArgumentContract { if (! $argument instanceof Collection) { return UnReducedArgument::create(); } return $this->reduceArgument($argument->toArray(), get_class($argument)); } } laravel-ignition/src/FlareMiddleware/AddQueries.php 0000644 00000000754 15105603711 0016372 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder; class AddQueries { protected QueryRecorder $queryRecorder; public function __construct() { $this->queryRecorder = app(QueryRecorder::class); } public function handle(Report $report, $next) { $report->group('queries', $this->queryRecorder->getQueries()); return $next($report); } } laravel-ignition/src/FlareMiddleware/AddNotifierName.php 0000644 00000000617 15105603711 0017333 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; class AddNotifierName implements FlareMiddleware { public const NOTIFIER_NAME = 'Laravel Client'; public function handle(Report $report, $next) { $report->notifierName(static::NOTIFIER_NAME); return $next($report); } } laravel-ignition/src/FlareMiddleware/AddExceptionInformation.php 0000644 00000002717 15105603711 0021122 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Illuminate\Database\QueryException; use Spatie\FlareClient\Contracts\ProvidesFlareContext; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; class AddExceptionInformation implements FlareMiddleware { public function handle(Report $report, $next) { $throwable = $report->getThrowable(); $this->addUserDefinedContext($report); if (! $throwable instanceof QueryException) { return $next($report); } $report->group('exception', [ 'raw_sql' => $throwable->getSql(), ]); return $next($report); } private function addUserDefinedContext(Report $report): void { $throwable = $report->getThrowable(); if ($throwable === null) { return; } if ($throwable instanceof ProvidesFlareContext) { // ProvidesFlareContext writes directly to context groups and is handled in the flare-client-php package. return; } if (! method_exists($throwable, 'context')) { return; } $context = $throwable->context(); if (! is_array($context)) { return; } $exceptionContextGroup = []; foreach ($context as $key => $value) { $exceptionContextGroup[$key] = $value; } $report->group('exception', $exceptionContextGroup); } } laravel-ignition/src/FlareMiddleware/AddDumps.php 0000644 00000001107 15105603711 0016036 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Closure; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder; class AddDumps implements FlareMiddleware { protected DumpRecorder $dumpRecorder; public function __construct() { $this->dumpRecorder = app(DumpRecorder::class); } public function handle(Report $report, Closure $next) { $report->group('dumps', $this->dumpRecorder->getDumps()); return $next($report); } } laravel-ignition/src/FlareMiddleware/AddLogs.php 0000644 00000001057 15105603711 0015656 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder; class AddLogs implements FlareMiddleware { protected LogRecorder $logRecorder; public function __construct() { $this->logRecorder = app(LogRecorder::class); } public function handle(Report $report, $next) { $report->group('logs', $this->logRecorder->getLogMessages()); return $next($report); } } laravel-ignition/src/FlareMiddleware/AddEnvironmentInformation.php 0000644 00000001333 15105603711 0021461 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Closure; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; class AddEnvironmentInformation implements FlareMiddleware { public function handle(Report $report, Closure $next) { $report->frameworkVersion(app()->version()); $report->group('env', [ 'laravel_version' => app()->version(), 'laravel_locale' => app()->getLocale(), 'laravel_config_cached' => app()->configurationIsCached(), 'app_debug' => config('app.debug'), 'app_env' => config('app.env'), 'php_version' => phpversion(), ]); return $next($report); } } laravel-ignition/src/FlareMiddleware/AddJobs.php 0000644 00000001117 15105603711 0015644 0 ustar 00 <?php namespace Spatie\LaravelIgnition\FlareMiddleware; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\Report; use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder; class AddJobs implements FlareMiddleware { protected JobRecorder $jobRecorder; public function __construct() { $this->jobRecorder = app(JobRecorder::class); } public function handle(Report $report, $next) { if ($job = $this->jobRecorder->getJob()) { $report->group('job', $job); } return $next($report); } } laravel-ignition/src/Http/Controllers/HealthCheckController.php 0000644 00000000767 15105603711 0020755 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Controllers; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Str; class HealthCheckController { public function __invoke() { return [ 'can_execute_commands' => $this->canExecuteCommands(), ]; } protected function canExecuteCommands(): bool { Artisan::call('help', ['--version']); $output = Artisan::output(); return Str::contains($output, app()->version()); } } laravel-ignition/src/Http/Controllers/ExecuteSolutionController.php 0000644 00000002645 15105603711 0021746 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Controllers; use Illuminate\Foundation\Validation\ValidatesRequests; use Spatie\Ignition\Contracts\SolutionProviderRepository; use Spatie\LaravelIgnition\Exceptions\CannotExecuteSolutionForNonLocalIp; use Spatie\LaravelIgnition\Http\Requests\ExecuteSolutionRequest; use Spatie\LaravelIgnition\Support\RunnableSolutionsGuard; class ExecuteSolutionController { use ValidatesRequests; public function __invoke( ExecuteSolutionRequest $request, SolutionProviderRepository $solutionProviderRepository ) { $this ->ensureRunnableSolutionsEnabled() ->ensureLocalRequest(); $solution = $request->getRunnableSolution(); $solution->run($request->get('parameters', [])); return response()->noContent(); } public function ensureRunnableSolutionsEnabled(): self { // Should already be checked in middleware but we want to be 100% certain. abort_unless(RunnableSolutionsGuard::check(), 400); return $this; } public function ensureLocalRequest(): self { $ipIsPublic = filter_var( request()->ip(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); if ($ipIsPublic) { throw CannotExecuteSolutionForNonLocalIp::make(); } return $this; } } laravel-ignition/src/Http/Controllers/UpdateConfigController.php 0000644 00000000605 15105603711 0021151 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Controllers; use Spatie\Ignition\Config\IgnitionConfig; use Spatie\LaravelIgnition\Http\Requests\UpdateConfigRequest; class UpdateConfigController { public function __invoke(UpdateConfigRequest $request) { $result = (new IgnitionConfig())->saveValues($request->validated()); return response()->json($result); } } laravel-ignition/src/Http/Requests/ExecuteSolutionRequest.php 0000644 00000001747 15105603711 0020562 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Spatie\Ignition\Contracts\RunnableSolution; use Spatie\Ignition\Contracts\Solution; use Spatie\Ignition\Contracts\SolutionProviderRepository; class ExecuteSolutionRequest extends FormRequest { public function rules(): array { return [ 'solution' => 'required', 'parameters' => 'array', ]; } public function getSolution(): Solution { $solution = app(SolutionProviderRepository::class) ->getSolutionForClass($this->get('solution')); abort_if(is_null($solution), 404, 'Solution could not be found'); return $solution; } public function getRunnableSolution(): RunnableSolution { $solution = $this->getSolution(); if (! $solution instanceof RunnableSolution) { abort(404, 'Runnable solution could not be found'); } return $solution; } } laravel-ignition/src/Http/Requests/UpdateConfigRequest.php 0000644 00000000654 15105603711 0017767 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; class UpdateConfigRequest extends FormRequest { public function rules(): array { return [ 'theme' => ['required', Rule::in(['light', 'dark', 'auto'])], 'editor' => ['required'], 'hide_solutions' => ['required', 'boolean'], ]; } } laravel-ignition/src/Http/Middleware/RunnableSolutionsEnabled.php 0000644 00000000532 15105603711 0021244 0 ustar 00 <?php namespace Spatie\LaravelIgnition\Http\Middleware; use Closure; use Spatie\LaravelIgnition\Support\RunnableSolutionsGuard; class RunnableSolutionsEnabled { public function handle($request, Closure $next) { if (! RunnableSolutionsGuard::check()) { abort(404); } return $next($request); } } laravel-ignition/README.md 0000644 00000005503 15105603711 0011271 0 ustar 00 # Ignition: a beautiful error page for Laravel apps [](https://packagist.org/packages/spatie/laravel-ignition)  [](https://packagist.org/packages/spatie/laravel-ignition) [Ignition](https://flareapp.io/docs/ignition-for-laravel/introduction) is a beautiful and customizable error page for Laravel applications. It is the default error page for new Laravel applications. It also allows to publicly share your errors on [Flare](https://flareapp.io). If configured with a valid Flare API key, your errors in production applications will be tracked, and you'll get notified when they happen. `spatie/laravel-ignition` works for Laravel 8 and 9 applications running on PHP 8.0 and above. Looking for Ignition for Laravel 5.x, 6.x or 7.x or old PHP versions? `facade/ignition` is still compatible.  ## Are you a visual learner? In [this video on YouTube](https://youtu.be/LEY0N0Bteew?t=739), you'll see a demo of all of the features. Do know more about the design decisions we made, read [this blog post](https://freek.dev/2168-ignition-the-most-beautiful-error-page-for-laravel-and-php-got-a-major-redesign). ## Official Documentation The official documentation for Ignition can be found on the [Flare website](https://flareapp.io/docs/ignition/introducing-ignition/overview). ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-ignition.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-ignition) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ### Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security Vulnerabilities Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits - [Spatie](https://spatie.be) - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. laravel-ignition/composer.json 0000644 00000005203 15105603711 0012531 0 ustar 00 { "name": "spatie/laravel-ignition", "description": "A beautiful error page for Laravel applications.", "keywords": [ "error", "page", "laravel", "flare" ], "authors": [ { "name": "Spatie", "email": "info@spatie.be", "role": "Developer" } ], "homepage": "https://flareapp.io/ignition", "license": "MIT", "require": { "php": "^8.1", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", "illuminate/support": "^10.0", "spatie/flare-client-php": "^1.3.5", "spatie/ignition": "^1.9", "symfony/console": "^6.2.3", "symfony/var-dumper": "^6.2.3" }, "require-dev": { "livewire/livewire": "^2.11", "mockery/mockery": "^1.5.1", "openai-php/client": "^0.3.4", "orchestra/testbench": "^8.0", "pestphp/pest": "^1.22.3", "phpstan/extension-installer": "^1.2", "phpstan/phpstan-deprecation-rules": "^1.1.1", "phpstan/phpstan-phpunit": "^1.3.3", "vlucas/phpdotenv": "^5.5" }, "suggest": { "openai-php/client": "Require get solutions from OpenAI", "psr/simple-cache-implementation": "Needed to cache solutions from OpenAI" }, "config": { "sort-packages": true, "allow-plugins": { "phpstan/extension-installer": true, "pestphp/pest-plugin": true } }, "extra": { "laravel": { "providers": [ "Spatie\\LaravelIgnition\\IgnitionServiceProvider" ], "aliases": { "Flare": "Spatie\\LaravelIgnition\\Facades\\Flare" } } }, "autoload": { "psr-4": { "Spatie\\LaravelIgnition\\": "src" }, "files": [ "src/helpers.php" ] }, "autoload-dev": { "psr-4": { "Spatie\\LaravelIgnition\\Tests\\": "tests" } }, "minimum-stability": "dev", "prefer-stable": true, "scripts": { "analyse": "vendor/bin/phpstan analyse", "baseline": "vendor/bin/phpstan --generate-baseline", "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes", "test": "vendor/bin/pest", "test-coverage": "vendor/bin/phpunit --coverage-html coverage" }, "support": { "issues": "https://github.com/spatie/laravel-ignition/issues", "forum": "https://twitter.com/flareappio", "source": "https://github.com/spatie/laravel-ignition", "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction" } } laravel-ignition/LICENSE.md 0000644 00000002075 15105603711 0011417 0 ustar 00 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. laravel-ignition/config/flare.php 0000644 00000005061 15105603711 0013060 0 ustar 00 <?php use Spatie\FlareClient\FlareMiddleware\AddGitInformation; use Spatie\FlareClient\FlareMiddleware\RemoveRequestIp; use Spatie\FlareClient\FlareMiddleware\CensorRequestBodyFields; use Spatie\FlareClient\FlareMiddleware\CensorRequestHeaders; use Spatie\LaravelIgnition\FlareMiddleware\AddDumps; use Spatie\LaravelIgnition\FlareMiddleware\AddEnvironmentInformation; use Spatie\LaravelIgnition\FlareMiddleware\AddExceptionInformation; use Spatie\LaravelIgnition\FlareMiddleware\AddJobs; use Spatie\LaravelIgnition\FlareMiddleware\AddLogs; use Spatie\LaravelIgnition\FlareMiddleware\AddQueries; use Spatie\LaravelIgnition\FlareMiddleware\AddNotifierName; return [ /* | |-------------------------------------------------------------------------- | Flare API key |-------------------------------------------------------------------------- | | Specify Flare's API key below to enable error reporting to the service. | | More info: https://flareapp.io/docs/general/projects | */ 'key' => env('FLARE_KEY'), /* |-------------------------------------------------------------------------- | Middleware |-------------------------------------------------------------------------- | | These middleware will modify the contents of the report sent to Flare. | */ 'flare_middleware' => [ RemoveRequestIp::class, AddGitInformation::class, AddNotifierName::class, AddEnvironmentInformation::class, AddExceptionInformation::class, AddDumps::class, AddLogs::class => [ 'maximum_number_of_collected_logs' => 200, ], AddQueries::class => [ 'maximum_number_of_collected_queries' => 200, 'report_query_bindings' => true, ], AddJobs::class => [ 'max_chained_job_reporting_depth' => 5, ], CensorRequestBodyFields::class => [ 'censor_fields' => [ 'password', 'password_confirmation', ], ], CensorRequestHeaders::class => [ 'headers' => [ 'API-KEY', ] ] ], /* |-------------------------------------------------------------------------- | Reporting log statements |-------------------------------------------------------------------------- | | If this setting is `false` log statements won't be sent as events to Flare, | no matter which error level you specified in the Flare log channel. | */ 'send_logs_as_events' => true, ]; laravel-ignition/config/ignition.php 0000644 00000027054 15105603711 0013615 0 ustar 00 <?php use Spatie\Ignition\Solutions\SolutionProviders\BadMethodCallSolutionProvider; use Spatie\Ignition\Solutions\SolutionProviders\MergeConflictSolutionProvider; use Spatie\Ignition\Solutions\SolutionProviders\UndefinedPropertySolutionProvider; use Spatie\LaravelIgnition\Recorders\DumpRecorder\DumpRecorder; use Spatie\LaravelIgnition\Recorders\JobRecorder\JobRecorder; use Spatie\LaravelIgnition\Recorders\LogRecorder\LogRecorder; use Spatie\LaravelIgnition\Recorders\QueryRecorder\QueryRecorder; use Spatie\LaravelIgnition\Solutions\SolutionProviders\DefaultDbNameSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\GenericLaravelExceptionSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\IncorrectValetDbCredentialsSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\InvalidRouteActionSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingAppKeySolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingColumnSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingImportSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingLivewireComponentSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingMixManifestSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\MissingViteManifestSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\RunningLaravelDuskInProductionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\TableNotFoundSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\UndefinedViewVariableSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\UnknownValidationSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\ViewNotFoundSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\OpenAiSolutionProvider; use Spatie\LaravelIgnition\Solutions\SolutionProviders\SailNetworkSolutionProvider; return [ /* |-------------------------------------------------------------------------- | Editor |-------------------------------------------------------------------------- | | Choose your preferred editor to use when clicking any edit button. | | Supported: "phpstorm", "vscode", "vscode-insiders", "textmate", "emacs", | "sublime", "atom", "nova", "macvim", "idea", "netbeans", | "xdebug", "phpstorm-remote" | */ 'editor' => env('IGNITION_EDITOR', 'phpstorm'), /* |-------------------------------------------------------------------------- | Theme |-------------------------------------------------------------------------- | | Here you may specify which theme Ignition should use. | | Supported: "light", "dark", "auto" | */ 'theme' => env('IGNITION_THEME', 'auto'), /* |-------------------------------------------------------------------------- | Sharing |-------------------------------------------------------------------------- | | You can share local errors with colleagues or others around the world. | Sharing is completely free and doesn't require an account on Flare. | | If necessary, you can completely disable sharing below. | */ 'enable_share_button' => env('IGNITION_SHARING_ENABLED', true), /* |-------------------------------------------------------------------------- | Register Ignition commands |-------------------------------------------------------------------------- | | Ignition comes with an additional make command that lets you create | new solution classes more easily. To keep your default Laravel | installation clean, this command is not registered by default. | | You can enable the command registration below. | */ 'register_commands' => env('REGISTER_IGNITION_COMMANDS', false), /* |-------------------------------------------------------------------------- | Solution Providers |-------------------------------------------------------------------------- | | You may specify a list of solution providers (as fully qualified class | names) that should be loaded. Ignition will ignore these classes | and possible solutions provided by them will never be displayed. | */ 'solution_providers' => [ // from spatie/ignition BadMethodCallSolutionProvider::class, MergeConflictSolutionProvider::class, UndefinedPropertySolutionProvider::class, // from spatie/laravel-ignition IncorrectValetDbCredentialsSolutionProvider::class, MissingAppKeySolutionProvider::class, DefaultDbNameSolutionProvider::class, TableNotFoundSolutionProvider::class, MissingImportSolutionProvider::class, InvalidRouteActionSolutionProvider::class, ViewNotFoundSolutionProvider::class, RunningLaravelDuskInProductionProvider::class, MissingColumnSolutionProvider::class, UnknownValidationSolutionProvider::class, MissingMixManifestSolutionProvider::class, MissingViteManifestSolutionProvider::class, MissingLivewireComponentSolutionProvider::class, UndefinedViewVariableSolutionProvider::class, GenericLaravelExceptionSolutionProvider::class, OpenAiSolutionProvider::class, SailNetworkSolutionProvider::class, ], /* |-------------------------------------------------------------------------- | Ignored Solution Providers |-------------------------------------------------------------------------- | | You may specify a list of solution providers (as fully qualified class | names) that shouldn't be loaded. Ignition will ignore these classes | and possible solutions provided by them will never be displayed. | */ 'ignored_solution_providers' => [ ], /* |-------------------------------------------------------------------------- | Runnable Solutions |-------------------------------------------------------------------------- | | Some solutions that Ignition displays are runnable and can perform | various tasks. By default, runnable solutions are only enabled when your | app has debug mode enabled and the environment is `local` or | `development`. | | Using the `IGNITION_ENABLE_RUNNABLE_SOLUTIONS` environment variable, you | can override this behaviour and enable or disable runnable solutions | regardless of the application's environment. | | Default: env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS') | */ 'enable_runnable_solutions' => env('IGNITION_ENABLE_RUNNABLE_SOLUTIONS'), /* |-------------------------------------------------------------------------- | Remote Path Mapping |-------------------------------------------------------------------------- | | If you are using a remote dev server, like Laravel Homestead, Docker, or | even a remote VPS, it will be necessary to specify your path mapping. | | Leaving one, or both of these, empty or null will not trigger the remote | URL changes and Ignition will treat your editor links as local files. | | "remote_sites_path" is an absolute base path for your sites or projects | in Homestead, Vagrant, Docker, or another remote development server. | | Example value: "/home/vagrant/Code" | | "local_sites_path" is an absolute base path for your sites or projects | on your local computer where your IDE or code editor is running on. | | Example values: "/Users/<name>/Code", "C:\Users\<name>\Documents\Code" | */ 'remote_sites_path' => env('IGNITION_REMOTE_SITES_PATH', base_path()), 'local_sites_path' => env('IGNITION_LOCAL_SITES_PATH', ''), /* |-------------------------------------------------------------------------- | Housekeeping Endpoint Prefix |-------------------------------------------------------------------------- | | Ignition registers a couple of routes when it is enabled. Below you may | specify a route prefix that will be used to host all internal links. | */ 'housekeeping_endpoint_prefix' => '_ignition', /* |-------------------------------------------------------------------------- | Settings File |-------------------------------------------------------------------------- | | Ignition allows you to save your settings to a specific global file. | | If no path is specified, a file with settings will be saved to the user's | home directory. The directory depends on the OS and its settings but it's | typically `~/.ignition.json`. In this case, the settings will be applied | to all of your projects where Ignition is used and the path is not | specified. | | However, if you want to store your settings on a project basis, or you | want to keep them in another directory, you can specify a path where | the settings file will be saved. The path should be an existing directory | with correct write access. | For example, create a new `ignition` folder in the storage directory and | use `storage_path('ignition')` as the `settings_file_path`. | | Default value: '' (empty string) */ 'settings_file_path' => '', /* |-------------------------------------------------------------------------- | Recorders |-------------------------------------------------------------------------- | | Ignition registers a couple of recorders when it is enabled. Below you may | specify a recorders will be used to record specific events. | */ 'recorders' => [ DumpRecorder::class, JobRecorder::class, LogRecorder::class, QueryRecorder::class, ], /* * When a key is set, we'll send your exceptions to Open AI to generate a solution */ 'open_ai_key' => env('IGNITION_OPEN_AI_KEY'), /* |-------------------------------------------------------------------------- | Include arguments |-------------------------------------------------------------------------- | | Ignition show you stack traces of exceptions with the arguments that were | passed to each method. This feature can be disabled here. | */ 'with_stack_frame_arguments' => true, /* |-------------------------------------------------------------------------- | Argument reducers |-------------------------------------------------------------------------- | | Ignition show you stack traces of exceptions with the arguments that were | passed to each method. To make these variables more readable, you can | specify a list of classes here which summarize the variables. | */ 'argument_reducers' => [ \Spatie\Backtrace\Arguments\Reducers\BaseTypeArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\StdClassArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\EnumArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\ClosureArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\DateTimeArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\DateTimeZoneArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\SymphonyRequestArgumentReducer::class, \Spatie\LaravelIgnition\ArgumentReducers\ModelArgumentReducer::class, \Spatie\LaravelIgnition\ArgumentReducers\CollectionArgumentReducer::class, \Spatie\Backtrace\Arguments\Reducers\StringableArgumentReducer::class, ], ]; image/src/Exceptions/CouldNotConvert.php 0000644 00000000434 15105603711 0014337 0 ustar 00 <?php namespace Spatie\Image\Exceptions; use Exception; class CouldNotConvert extends Exception { public static function unknownManipulation(string $operationName): self { return new self("Can not convert image. Unknown operation `{$operationName}` used"); } } image/src/Exceptions/InvalidImageDriver.php 0000644 00000000402 15105603711 0014747 0 ustar 00 <?php namespace Spatie\Image\Exceptions; use Exception; class InvalidImageDriver extends Exception { public static function driver(string $driver): self { return new self("Driver must be `gd` or `imagick`. `{$driver}` provided."); } } image/src/Exceptions/InvalidTemporaryDirectory.php 0000644 00000000772 15105603711 0016432 0 ustar 00 <?php namespace Spatie\Image\Exceptions; use Exception; class InvalidTemporaryDirectory extends Exception { public static function temporaryDirectoryNotCreatable(string $directory): self { return new self("the temporary directory `{$directory}` does not exist and can not be created"); } public static function temporaryDirectoryNotWritable(string $directory): self { return new self("the temporary directory `{$directory}` does exist but is not writable"); } } image/src/Exceptions/InvalidManipulation.php 0000644 00000002305 15105603711 0015215 0 ustar 00 <?php namespace Spatie\Image\Exceptions; use Exception; class InvalidManipulation extends Exception { public static function invalidWidth(int $width): self { return new self("Width should be a positive number. `{$width}` given."); } public static function invalidHeight(int $height): self { return new self("Height should be a positive number. `{$height}` given."); } public static function invalidParameter(string $name, $invalidValue, array $validValues): self { $validValues = self::formatValues($validValues); $name = ucfirst($name); return new self("{$name} should be one of {$validValues}. `{$invalidValue}` given."); } public static function valueNotInRange(string $name, $invalidValue, $minValue, $maxValue): self { $name = ucfirst($name); return new self("{$name} should be a number in the range {$minValue} until {$maxValue}. `{$invalidValue}` given."); } protected static function formatValues(array $values): string { $quotedValues = array_map(function (string $value) { return "`{$value}`"; }, $values); return implode(', ', $quotedValues); } } image/src/Manipulations.php 0000644 00000043135 15105603711 0011756 0 ustar 00 <?php namespace Spatie\Image; use League\Glide\Filesystem\FileNotFoundException; use ReflectionClass; use Spatie\Image\Exceptions\InvalidManipulation; class Manipulations { public const CROP_TOP_LEFT = 'crop-top-left'; public const CROP_TOP = 'crop-top'; public const CROP_TOP_RIGHT = 'crop-top-right'; public const CROP_LEFT = 'crop-left'; public const CROP_CENTER = 'crop-center'; public const CROP_RIGHT = 'crop-right'; public const CROP_BOTTOM_LEFT = 'crop-bottom-left'; public const CROP_BOTTOM = 'crop-bottom'; public const CROP_BOTTOM_RIGHT = 'crop-bottom-right'; public const ORIENTATION_AUTO = 'auto'; public const ORIENTATION_0 = 0; public const ORIENTATION_90 = 90; public const ORIENTATION_180 = 180; public const ORIENTATION_270 = 270; public const FLIP_HORIZONTALLY = 'h'; public const FLIP_VERTICALLY = 'v'; public const FLIP_BOTH = 'both'; public const FIT_CONTAIN = 'contain'; public const FIT_MAX = 'max'; public const FIT_FILL = 'fill'; public const FIT_FILL_MAX = 'fill-max'; public const FIT_STRETCH = 'stretch'; public const FIT_CROP = 'crop'; public const BORDER_OVERLAY = 'overlay'; public const BORDER_SHRINK = 'shrink'; public const BORDER_EXPAND = 'expand'; public const FORMAT_JPG = 'jpg'; public const FORMAT_PJPG = 'pjpg'; public const FORMAT_PNG = 'png'; public const FORMAT_GIF = 'gif'; public const FORMAT_WEBP = 'webp'; public const FORMAT_AVIF = 'avif'; public const FORMAT_TIFF = 'tiff'; public const FILTER_GREYSCALE = 'greyscale'; public const FILTER_SEPIA = 'sepia'; public const UNIT_PIXELS = 'px'; public const UNIT_PERCENT = '%'; public const POSITION_TOP_LEFT = 'top-left'; public const POSITION_TOP = 'top'; public const POSITION_TOP_RIGHT = 'top-right'; public const POSITION_LEFT = 'left'; public const POSITION_CENTER = 'center'; public const POSITION_RIGHT = 'right'; public const POSITION_BOTTOM_LEFT = 'bottom-left'; public const POSITION_BOTTOM = 'bottom'; public const POSITION_BOTTOM_RIGHT = 'bottom-right'; protected ManipulationSequence $manipulationSequence; public function __construct(array $manipulations = []) { if (! $this->hasMultipleConversions($manipulations)) { $manipulations = [$manipulations]; } foreach ($manipulations as $manipulation) { $this->manipulationSequence = new ManipulationSequence($manipulation); } } public static function create(array $manipulations = []): Manipulations { return new self($manipulations); } /** * @throws InvalidManipulation */ public function orientation(string $orientation): static { if (! $this->validateManipulation($orientation, 'orientation')) { throw InvalidManipulation::invalidParameter( 'orientation', $orientation, $this->getValidManipulationOptions('orientation') ); } return $this->addManipulation('orientation', $orientation); } /** * @throws InvalidManipulation */ public function flip(string $orientation): static { if (! $this->validateManipulation($orientation, 'flip')) { throw InvalidManipulation::invalidParameter( 'flip', $orientation, $this->getValidManipulationOptions('flip') ); } return $this->addManipulation('flip', $orientation); } /** * @throws InvalidManipulation */ public function crop(string $cropMethod, int $width, int $height): static { if (! $this->validateManipulation($cropMethod, 'crop')) { throw InvalidManipulation::invalidParameter( 'cropmethod', $cropMethod, $this->getValidManipulationOptions('crop') ); } $this->width($width); $this->height($height); return $this->addManipulation('crop', $cropMethod); } /** * @param int $focalX Crop center X in percent * @param int $focalY Crop center Y in percent * * @throws InvalidManipulation */ public function focalCrop(int $width, int $height, int $focalX, int $focalY, float $zoom = 1): static { if ($zoom < 1 || $zoom > 100) { throw InvalidManipulation::valueNotInRange('zoom', $zoom, 1, 100); } $this->width($width); $this->height($height); return $this->addManipulation('crop', "crop-{$focalX}-{$focalY}-{$zoom}"); } /** * @throws InvalidManipulation */ public function manualCrop(int $width, int $height, int $x, int $y): static { if ($width < 0) { throw InvalidManipulation::invalidWidth($width); } if ($height < 0) { throw InvalidManipulation::invalidWidth($height); } return $this->addManipulation('manualCrop', "{$width},{$height},{$x},{$y}"); } /** * @throws InvalidManipulation */ public function width(int $width): static { if ($width < 0) { throw InvalidManipulation::invalidWidth($width); } return $this->addManipulation('width', (string)$width); } /** * @throws InvalidManipulation */ public function height(int $height): static { if ($height < 0) { throw InvalidManipulation::invalidHeight($height); } return $this->addManipulation('height', (string)$height); } /** * @throws InvalidManipulation */ public function fit(string $fitMethod, ?int $width = null, ?int $height = null): static { if (! $this->validateManipulation($fitMethod, 'fit')) { throw InvalidManipulation::invalidParameter( 'fit', $fitMethod, $this->getValidManipulationOptions('fit') ); } if ($width === null && $height === null) { throw new InvalidManipulation('Width or height or both must be provided'); } if ($width !== null) { $this->width($width); } if ($height !== null) { $this->height($height); } return $this->addManipulation('fit', $fitMethod); } /** * @param int $ratio A value between 1 and 8 * * @throws InvalidManipulation */ public function devicePixelRatio(int $ratio): static { if ($ratio < 1 || $ratio > 8) { throw InvalidManipulation::valueNotInRange('ratio', $ratio, 1, 8); } return $this->addManipulation('devicePixelRatio', (string)$ratio); } /** * @param int $brightness A value between -100 and 100 * * @throws InvalidManipulation */ public function brightness(int $brightness): static { if ($brightness < -100 || $brightness > 100) { throw InvalidManipulation::valueNotInRange('brightness', $brightness, -100, 100); } return $this->addManipulation('brightness', (string)$brightness); } /** * @param float $gamma A value between 0.01 and 9.99 * * @throws InvalidManipulation */ public function gamma(float $gamma): static { if ($gamma < 0.01 || $gamma > 9.99) { throw InvalidManipulation::valueNotInRange('gamma', $gamma, 0.01, 9.00); } return $this->addManipulation('gamma', (string)$gamma); } /** * @param int $contrast A value between -100 and 100 * * @throws InvalidManipulation */ public function contrast(int $contrast): static { if ($contrast < -100 || $contrast > 100) { throw InvalidManipulation::valueNotInRange('contrast', $contrast, -100, 100); } return $this->addManipulation('contrast', (string)$contrast); } /** * @param int $sharpen A value between 0 and 100 * * @throws InvalidManipulation */ public function sharpen(int $sharpen): static { if ($sharpen < 0 || $sharpen > 100) { throw InvalidManipulation::valueNotInRange('sharpen', $sharpen, 0, 100); } return $this->addManipulation('sharpen', (string)$sharpen); } /** * @param int $blur A value between 0 and 100 * * @throws InvalidManipulation */ public function blur(int $blur): static { if ($blur < 0 || $blur > 100) { throw InvalidManipulation::valueNotInRange('blur', $blur, 0, 100); } return $this->addManipulation('blur', (string)$blur); } /** * @param int $pixelate A value between 0 and 1000 * * @throws InvalidManipulation */ public function pixelate(int $pixelate): static { if ($pixelate < 0 || $pixelate > 1000) { throw InvalidManipulation::valueNotInRange('pixelate', $pixelate, 0, 1000); } return $this->addManipulation('pixelate', (string)$pixelate); } /** * @throws InvalidManipulation */ public function greyscale(): static { return $this->filter('greyscale'); } /** * @throws InvalidManipulation */ public function sepia(): static { return $this->filter('sepia'); } public function background(string $colorName): static { return $this->addManipulation('background', $colorName); } /** * @throws InvalidManipulation */ public function border(int $width, string $color, string $borderType = 'overlay'): static { if ($width < 0) { throw InvalidManipulation::invalidWidth($width); } if (! $this->validateManipulation($borderType, 'border')) { throw InvalidManipulation::invalidParameter( 'border', $borderType, $this->getValidManipulationOptions('border') ); } return $this->addManipulation('border', "{$width},{$color},{$borderType}"); } /** * @throws InvalidManipulation */ public function quality(int $quality): static { if ($quality < 0 || $quality > 100) { throw InvalidManipulation::valueNotInRange('quality', $quality, 0, 100); } return $this->addManipulation('quality', (string)$quality); } /** * @throws InvalidManipulation */ public function format(string $format): static { if (! $this->validateManipulation($format, 'format')) { throw InvalidManipulation::invalidParameter( 'format', $format, $this->getValidManipulationOptions('format') ); } return $this->addManipulation('format', $format); } /** * @throws InvalidManipulation */ protected function filter(string $filterName): static { if (! $this->validateManipulation($filterName, 'filter')) { throw InvalidManipulation::invalidParameter( 'filter', $filterName, $this->getValidManipulationOptions('filter') ); } return $this->addManipulation('filter', $filterName); } /** * @throws FileNotFoundException */ public function watermark(string $filePath): static { if (! file_exists($filePath)) { throw new FileNotFoundException($filePath); } $this->addManipulation('watermark', $filePath); return $this; } /** * @param int $width The width of the watermark in pixels (default) or percent. * @param string $unit The unit of the `$width` parameter. Use `Manipulations::UNIT_PERCENT` or `Manipulations::UNIT_PIXELS`. */ public function watermarkWidth(int $width, string $unit = 'px'): static { $width = ($unit === static::UNIT_PERCENT ? $width.'w' : $width); return $this->addManipulation('watermarkWidth', (string)$width); } /** * @param int $height The height of the watermark in pixels (default) or percent. * @param string $unit The unit of the `$height` parameter. Use `Manipulations::UNIT_PERCENT` or `Manipulations::UNIT_PIXELS`. */ public function watermarkHeight(int $height, string $unit = 'px'): static { $height = ($unit === static::UNIT_PERCENT ? $height.'h' : $height); return $this->addManipulation('watermarkHeight', (string)$height); } /** * @param string $fitMethod How is the watermark fitted into the watermarkWidth and watermarkHeight properties. * * @throws InvalidManipulation */ public function watermarkFit(string $fitMethod): static { if (! $this->validateManipulation($fitMethod, 'fit')) { throw InvalidManipulation::invalidParameter( 'watermarkFit', $fitMethod, $this->getValidManipulationOptions('fit') ); } return $this->addManipulation('watermarkFit', $fitMethod); } /** * @param int $xPadding How far is the watermark placed from the left and right edges of the image. * @param int|null $yPadding How far is the watermark placed from the top and bottom edges of the image. * @param string $unit Unit of the padding values. Use `Manipulations::UNIT_PERCENT` or `Manipulations::UNIT_PIXELS`. */ public function watermarkPadding(int $xPadding, int $yPadding = null, string $unit = 'px'): static { $yPadding = $yPadding ?? $xPadding; $xPadding = ($unit === static::UNIT_PERCENT ? $xPadding.'w' : $xPadding); $yPadding = ($unit === static::UNIT_PERCENT ? $yPadding.'h' : $yPadding); $this->addManipulation('watermarkPaddingX', (string)$xPadding); $this->addManipulation('watermarkPaddingY', (string)$yPadding); return $this; } /** * @throws InvalidManipulation */ public function watermarkPosition(string $position): static { if (! $this->validateManipulation($position, 'position')) { throw InvalidManipulation::invalidParameter( 'watermarkPosition', $position, $this->getValidManipulationOptions('position') ); } return $this->addManipulation('watermarkPosition', $position); } /** * Sets the opacity of the watermark. Only works with the `imagick` driver. * * @param int $opacity A value between 0 and 100. * * @throws InvalidManipulation */ public function watermarkOpacity(int $opacity): static { if ($opacity < 0 || $opacity > 100) { throw InvalidManipulation::valueNotInRange('opacity', $opacity, 0, 100); } return $this->addManipulation('watermarkOpacity', (string)$opacity); } /** * Shave off some kilobytes by optimizing the image. */ public function optimize(array $optimizationOptions = []): static { return $this->addManipulation('optimize', json_encode($optimizationOptions)); } public function apply(): static { $this->manipulationSequence->startNewGroup(); return $this; } public function toArray(): array { return $this->manipulationSequence->toArray(); } /** * Checks if the given manipulations has arrays inside or not. */ private function hasMultipleConversions(array $manipulations): bool { foreach ($manipulations as $manipulation) { if (isset($manipulation[0]) && is_array($manipulation[0])) { return true; } } return false; } public function removeManipulation(string $name): void { $this->manipulationSequence->removeManipulation($name); } public function hasManipulation(string $manipulationName): bool { return ! is_null($this->getManipulationArgument($manipulationName)); } public function getManipulationArgument(string $manipulationName) { foreach ($this->manipulationSequence->getGroups() as $manipulationSet) { if (array_key_exists($manipulationName, $manipulationSet)) { return $manipulationSet[$manipulationName]; } } } protected function addManipulation(string $manipulationName, string $manipulationArgument): static { $this->manipulationSequence->addManipulation($manipulationName, $manipulationArgument); return $this; } public function mergeManipulations(self $manipulations): static { $this->manipulationSequence->merge($manipulations->manipulationSequence); return $this; } public function getManipulationSequence(): ManipulationSequence { return $this->manipulationSequence; } protected function validateManipulation(string $value, string $constantNamePrefix): bool { return in_array($value, $this->getValidManipulationOptions($constantNamePrefix)); } protected function getValidManipulationOptions(string $manipulation): array { $options = (new ReflectionClass(static::class))->getConstants(); return array_filter($options, function ($value, $name) use ($manipulation) { return str_starts_with($name, mb_strtoupper($manipulation)); }, ARRAY_FILTER_USE_BOTH); } public function isEmpty(): bool { return $this->manipulationSequence->isEmpty(); } /* * Get the first manipulation with the given name. * * @return mixed */ public function getFirstManipulationArgument(string $manipulationName) { return $this->manipulationSequence->getFirstManipulationArgument($manipulationName); } } image/src/ManipulationSequence.php 0000644 00000006017 15105603711 0013262 0 ustar 00 <?php namespace Spatie\Image; use ArrayIterator; use IteratorAggregate; class ManipulationSequence implements IteratorAggregate { protected array $groups = []; public function __construct(array $sequenceArray = []) { $this->startNewGroup(); $this->mergeArray($sequenceArray); } public function addManipulation(string $operation, string $argument): static { $lastIndex = count($this->groups) - 1; $this->groups[$lastIndex][$operation] = $argument; return $this; } public function merge(self $sequence): static { $sequenceArray = $sequence->toArray(); $this->mergeArray($sequenceArray); return $this; } public function mergeArray(array $sequenceArray): void { foreach ($sequenceArray as $group) { foreach ($group as $name => $argument) { $this->addManipulation($name, $argument); } if (next($sequenceArray)) { $this->startNewGroup(); } } } public function startNewGroup(): static { $this->groups[] = []; return $this; } public function toArray(): array { return $this->getGroups(); } public function getGroups(): array { return $this->sanitizeManipulationSets($this->groups); } public function getIterator(): ArrayIterator { return new ArrayIterator($this->toArray()); } public function removeManipulation(string $manipulationName): static { foreach ($this->groups as &$group) { if (array_key_exists($manipulationName, $group)) { unset($group[$manipulationName]); } } return $this; } public function isEmpty(): bool { if (count($this->groups) > 1) { return false; } if (count($this->groups[0]) > 0) { return false; } return true; } protected function sanitizeManipulationSets(array $groups): array { return array_values(array_filter($groups, function (array $manipulationSet) { return count($manipulationSet); })); } /* * Determine if the sequences contain a manipulation with the given name. */ public function getFirstManipulationArgument($searchManipulationName) { foreach ($this->groups as $group) { foreach ($group as $name => $argument) { if ($name === $searchManipulationName) { return $argument; } } } } /* * Determine if the sequences contain a manipulation with the given name. */ public function contains($searchManipulationName): bool { foreach ($this->groups as $group) { foreach ($group as $name => $argument) { if ($name === $searchManipulationName) { return true; } } return false; } return false; } } image/src/Image.php 0000644 00000013226 15105603711 0010153 0 ustar 00 <?php namespace Spatie\Image; use BadMethodCallException; use Intervention\Image\ImageManagerStatic as InterventionImage; use Spatie\Image\Exceptions\InvalidImageDriver; use Spatie\ImageOptimizer\OptimizerChain; use Spatie\ImageOptimizer\OptimizerChainFactory; use Spatie\ImageOptimizer\Optimizers\BaseOptimizer; /** @mixin \Spatie\Image\Manipulations */ class Image { protected Manipulations $manipulations; protected string $imageDriver = 'gd'; protected ?string $temporaryDirectory = null; protected ?OptimizerChain $optimizerChain = null; public function __construct(protected string $pathToImage) { $this->manipulations = new Manipulations(); } public static function load(string $pathToImage): static { return new static($pathToImage); } public function setTemporaryDirectory($tempDir): static { $this->temporaryDirectory = $tempDir; return $this; } public function setOptimizeChain(OptimizerChain $optimizerChain): static { $this->optimizerChain = $optimizerChain; return $this; } /** * @param string $imageDriver * @return $this * @throws InvalidImageDriver */ public function useImageDriver(string $imageDriver): static { if (! in_array($imageDriver, ['gd', 'imagick'])) { throw InvalidImageDriver::driver($imageDriver); } $this->imageDriver = $imageDriver; InterventionImage::configure([ 'driver' => $this->imageDriver, ]); return $this; } public function manipulate(callable | Manipulations $manipulations): static { if (is_callable($manipulations)) { $manipulations($this->manipulations); } if ($manipulations instanceof Manipulations) { $this->manipulations->mergeManipulations($manipulations); } return $this; } public function __call($name, $arguments): static { if (! method_exists($this->manipulations, $name)) { throw new BadMethodCallException("Manipulation `{$name}` does not exist"); } $this->manipulations->$name(...$arguments); return $this; } public function getWidth(): int { return InterventionImage::make($this->pathToImage)->width(); } public function getHeight(): int { return InterventionImage::make($this->pathToImage)->height(); } public function getManipulationSequence(): ManipulationSequence { return $this->manipulations->getManipulationSequence(); } public function save(string $outputPath = ''): void { if ($outputPath === '') { $outputPath = $this->pathToImage; } $this->addFormatManipulation($outputPath); $glideConversion = GlideConversion::create($this->pathToImage)->useImageDriver($this->imageDriver); if (! is_null($this->temporaryDirectory)) { $glideConversion->setTemporaryDirectory($this->temporaryDirectory); } $glideConversion->performManipulations($this->manipulations); $glideConversion->save($outputPath); if ($this->shouldOptimize()) { $optimizerChainConfiguration = $this->manipulations->getFirstManipulationArgument('optimize'); $optimizerChainConfiguration = json_decode($optimizerChainConfiguration, true); $this->performOptimization($outputPath, $optimizerChainConfiguration); } } protected function shouldOptimize(): bool { return ! is_null($this->manipulations->getFirstManipulationArgument('optimize')); } protected function performOptimization($path, array $optimizerChainConfiguration): void { $optimizerChain = $this->optimizerChain ?? OptimizerChainFactory::create(); if (count($optimizerChainConfiguration)) { $existingOptimizers = $optimizerChain->getOptimizers(); $optimizers = array_map(function (array $optimizerOptions, string $optimizerClassName) use ($existingOptimizers) { $optimizer = array_values(array_filter($existingOptimizers, function ($optimizer) use ($optimizerClassName) { return $optimizer::class === $optimizerClassName; })); $optimizer = isset($optimizer[0]) && $optimizer[0] instanceof BaseOptimizer ? $optimizer[0] : new $optimizerClassName(); return $optimizer->setOptions($optimizerOptions)->setBinaryPath($optimizer->binaryPath); }, $optimizerChainConfiguration, array_keys($optimizerChainConfiguration)); $optimizerChain->setOptimizers($optimizers); } $optimizerChain->optimize($path); } protected function addFormatManipulation($outputPath): void { if ($this->manipulations->hasManipulation('format')) { return; } $inputExtension = strtolower(pathinfo($this->pathToImage, PATHINFO_EXTENSION)); $outputExtension = strtolower(pathinfo($outputPath, PATHINFO_EXTENSION)); if ($inputExtension === $outputExtension) { return; } $supportedFormats = [ Manipulations::FORMAT_JPG, Manipulations::FORMAT_PJPG, Manipulations::FORMAT_PNG, Manipulations::FORMAT_GIF, Manipulations::FORMAT_WEBP, Manipulations::FORMAT_AVIF, ]; //gd driver doesn't support TIFF if ($this->imageDriver === 'imagick') { $supportedFormats[] = Manipulations::FORMAT_TIFF; } if (in_array($outputExtension, $supportedFormats)) { $this->manipulations->format($outputExtension); } } } image/src/GlideConversion.php 0000644 00000013010 15105603711 0012212 0 ustar 00 <?php namespace Spatie\Image; use Exception; use FilesystemIterator; use League\Glide\Server; use League\Glide\ServerFactory; use Spatie\Image\Exceptions\CouldNotConvert; use Spatie\Image\Exceptions\InvalidTemporaryDirectory; final class GlideConversion { private string $imageDriver = 'gd'; private ?string $conversionResult = null; private string $temporaryDirectory; public function __construct(private string $inputImage) { $this->temporaryDirectory = sys_get_temp_dir(); } public static function create(string $inputImage): self { return new self($inputImage); } public function setTemporaryDirectory(string $temporaryDirectory): self { if (! is_dir($temporaryDirectory)) { try { mkdir($temporaryDirectory); } catch (Exception) { throw InvalidTemporaryDirectory::temporaryDirectoryNotCreatable($temporaryDirectory); } } if (! is_writable($temporaryDirectory)) { throw InvalidTemporaryDirectory::temporaryDirectoryNotWritable($temporaryDirectory); } $this->temporaryDirectory = $temporaryDirectory; return $this; } public function getTemporaryDirectory(): string { return $this->temporaryDirectory; } public function useImageDriver(string $imageDriver): self { $this->imageDriver = $imageDriver; return $this; } public function performManipulations(Manipulations $manipulations): GlideConversion { foreach ($manipulations->getManipulationSequence() as $manipulationGroup) { $inputFile = $this->conversionResult ?? $this->inputImage; $watermarkPath = $this->extractWatermarkPath($manipulationGroup); $glideServer = $this->createGlideServer($inputFile, $watermarkPath); $glideServer->setGroupCacheInFolders(false); $manipulatedImage = $this->temporaryDirectory.DIRECTORY_SEPARATOR.$glideServer->makeImage( pathinfo($inputFile, PATHINFO_BASENAME), $this->prepareManipulations($manipulationGroup) ); if ($this->conversionResult) { unlink($this->conversionResult); } $this->conversionResult = $manipulatedImage; } return $this; } /** * Removes the watermark path from the manipulationGroup and returns it. * This way it can be injected into the Glide server as the `watermarks` path. */ private function extractWatermarkPath(&$manipulationGroup) { if (array_key_exists('watermark', $manipulationGroup)) { $watermarkPath = dirname($manipulationGroup['watermark']); $manipulationGroup['watermark'] = basename($manipulationGroup['watermark']); return $watermarkPath; } } private function createGlideServer($inputFile, string $watermarkPath = null): Server { $config = [ 'source' => dirname($inputFile), 'cache' => $this->temporaryDirectory, 'driver' => $this->imageDriver, ]; if ($watermarkPath) { $config['watermarks'] = $watermarkPath; } return ServerFactory::create($config); } public function save(string $outputFile): void { if ($this->conversionResult === '' || $this->conversionResult === null) { copy($this->inputImage, $outputFile); return; } $conversionResultDirectory = pathinfo($this->conversionResult, PATHINFO_DIRNAME); copy($this->conversionResult, $outputFile); unlink($this->conversionResult); if ($conversionResultDirectory !== sys_get_temp_dir() && $this->directoryIsEmpty($conversionResultDirectory)) { rmdir($conversionResultDirectory); } } private function prepareManipulations(array $manipulationGroup): array { $glideManipulations = []; foreach ($manipulationGroup as $name => $argument) { if ($name !== 'optimize') { $glideManipulations[$this->convertToGlideParameter($name)] = $argument; } } return $glideManipulations; } private function convertToGlideParameter(string $manipulationName): string { return match ($manipulationName) { 'width' => 'w', 'height' => 'h', 'blur' => 'blur', 'pixelate' => 'pixel', 'crop' => 'fit', 'manualCrop' => 'crop', 'orientation' => 'or', 'flip' => 'flip', 'fit' => 'fit', 'devicePixelRatio' => 'dpr', 'brightness' => 'bri', 'contrast' => 'con', 'gamma' => 'gam', 'sharpen' => 'sharp', 'filter' => 'filt', 'background' => 'bg', 'border' => 'border', 'quality' => 'q', 'format' => 'fm', 'watermark' => 'mark', 'watermarkWidth' => 'markw', 'watermarkHeight' => 'markh', 'watermarkFit' => 'markfit', 'watermarkPaddingX' => 'markx', 'watermarkPaddingY' => 'marky', 'watermarkPosition' => 'markpos', 'watermarkOpacity' => 'markalpha', default => throw CouldNotConvert::unknownManipulation($manipulationName) }; } private function directoryIsEmpty(string $directory): bool { $iterator = new FilesystemIterator($directory); return ! $iterator->valid(); } } image/README.md 0000644 00000007254 15105603711 0007114 0 ustar 00 # Manipulate images with an expressive API [](https://packagist.org/packages/spatie/image) [](LICENSE.md) [](https://github.com/spatie/image/actions/workflows/run-tests.yml) [](https://packagist.org/packages/spatie/image) Image manipulation doesn't have to be hard. Here are a few examples on how this package makes it very easy to manipulate images. ```php use Spatie\Image\Image; // modifying the image so it fits in a 100x100 rectangle without altering aspect ratio Image::load($pathToImage) ->width(100) ->height(100) ->save($pathToNewImage); // overwriting the original image with a greyscale version Image::load($pathToImage) ->greyscale() ->save(); // make image darker and save it in low quality Image::load($pathToImage) ->brightness(-30) ->quality(25) ->save(); // rotate the image and sharpen it Image::load($pathToImage) ->orientation(90) ->sharpen(15) ->save(); ``` You'll find more examples in [the full documentation](https://docs.spatie.be/image). Under the hood [Glide](http://glide.thephpleague.com/) by [Jonathan Reinink](https://twitter.com/reinink) is used. ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/image.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/image) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation You can install the package via composer: ``` bash composer require spatie/image ``` Please note that since version 1.5.3 this package requires exif extension to be enabled: http://php.net/manual/en/exif.installation.php ## Usage Head over to [the full documentation](https://spatie.be/docs/image). ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Testing ``` bash composer test ``` ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. ## Postcardware You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). ## Credits - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) Under the hood [Glide](http://glide.thephpleague.com/) by [Jonathan Reinink](https://twitter.com/reinink) is used. We've based our documentation and docblocks on text found in [the Glide documentation](http://glide.thephpleague.com/) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. image/composer.json 0000644 00000002457 15105603711 0010357 0 ustar 00 { "name": "spatie/image", "description": "Manipulate images with an expressive API", "keywords": [ "spatie", "image" ], "homepage": "https://github.com/spatie/image", "license": "MIT", "authors": [ { "name": "Freek Van der Herten", "email": "freek@spatie.be", "homepage": "https://spatie.be", "role": "Developer" } ], "require": { "php": "^8.0", "ext-exif": "*", "ext-mbstring": "*", "ext-json": "*", "league/glide": "^2.2.2", "spatie/image-optimizer": "^1.7", "spatie/temporary-directory": "^1.0|^2.0", "symfony/process": "^3.0|^4.0|^5.0|^6.0" }, "require-dev": { "pestphp/pest": "^1.22", "phpunit/phpunit": "^9.5", "symfony/var-dumper": "^4.0|^5.0|^6.0", "vimeo/psalm": "^4.6" }, "autoload": { "psr-4": { "Spatie\\Image\\": "src" } }, "autoload-dev": { "psr-4": { "Spatie\\Image\\Test\\": "tests" } }, "scripts": { "psalm": "vendor/bin/psalm", "test": "vendor/bin/pest" }, "config": { "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true } } } image/LICENSE.md 0000644 00000002102 15105603711 0007224 0 ustar 00 The MIT License (MIT) Copyright (c) Spatie bvba <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. image/CHANGELOG.md 0000644 00000013650 15105603711 0007443 0 ustar 00 # Changelog All notable changes to `image` will be documented in this file ## 2.2.6 - 2023-05-06 ### What's Changed - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/image/pull/185 - Bump dependabot/fetch-metadata from 1.3.6 to 1.4.0 by @dependabot in https://github.com/spatie/image/pull/188 - Fit with only width or height by @gdebrauwer in https://github.com/spatie/image/pull/190 ### New Contributors - @dependabot made their first contribution in https://github.com/spatie/image/pull/185 - @gdebrauwer made their first contribution in https://github.com/spatie/image/pull/190 **Full Changelog**: https://github.com/spatie/image/compare/2.2.5...2.2.6 ## 2.2.5 - 2023-01-19 ### What's Changed - Refactor tests to pest by @AyoobMH in https://github.com/spatie/image/pull/176 - Add Dependabot Automation by @patinthehat in https://github.com/spatie/image/pull/177 - Add PHP 8.2 Support by @patinthehat in https://github.com/spatie/image/pull/180 - Update Dependabot Automation by @patinthehat in https://github.com/spatie/image/pull/181 - Add fill-max fit mode by @Tofandel in https://github.com/spatie/image/pull/183 ### New Contributors - @AyoobMH made their first contribution in https://github.com/spatie/image/pull/176 - @patinthehat made their first contribution in https://github.com/spatie/image/pull/177 - @Tofandel made their first contribution in https://github.com/spatie/image/pull/183 **Full Changelog**: https://github.com/spatie/image/compare/2.2.4...2.2.5 ## 2.2.4 - 2022-08-09 ### What's Changed - Add zero orientation support ignoring EXIF by @danielcastrobalbi in https://github.com/spatie/image/pull/171 ### New Contributors - @danielcastrobalbi made their first contribution in https://github.com/spatie/image/pull/171 **Full Changelog**: https://github.com/spatie/image/compare/2.2.3...2.2.4 ## 2.2.3 - 2022-05-21 ## What's Changed - Fix permission issue with temporary directory by @sebastianpopp in https://github.com/spatie/image/pull/163 ## New Contributors - @sebastianpopp made their first contribution in https://github.com/spatie/image/pull/163 **Full Changelog**: https://github.com/spatie/image/compare/2.2.2...2.2.3 ## 2.2.2 - 2022-02-22 - add TIFF support ## 1.11.0 - 2022-02-21 ## What's Changed - Fix docs link by @pascalbaljet in https://github.com/spatie/image/pull/154 - Update .gitattributes by @PaolaRuby in https://github.com/spatie/image/pull/158 - Add TIFF support by @Synchro in https://github.com/spatie/image/pull/159 ## New Contributors - @PaolaRuby made their first contribution in https://github.com/spatie/image/pull/158 **Full Changelog**: https://github.com/spatie/image/compare/2.2.1...1.11.0 ## 2.2.1 - 2021-12-17 ## What's Changed - Use match expression in convertToGlideParameter method by @mohprilaksono in https://github.com/spatie/image/pull/149 - [REF] updated fit docs description by @JeremyRed in https://github.com/spatie/image/pull/150 - Adding compatibility to Symfony 6 by @spackmat in https://github.com/spatie/image/pull/152 ## New Contributors - @mohprilaksono made their first contribution in https://github.com/spatie/image/pull/149 - @JeremyRed made their first contribution in https://github.com/spatie/image/pull/150 - @spackmat made their first contribution in https://github.com/spatie/image/pull/152 **Full Changelog**: https://github.com/spatie/image/compare/2.2.0...2.2.1 ## 2.2.0 - 2021-10-31 - add avif support (#148) ## 2.1.0 - 2021-07-15 - Drop support for PHP 7 - Make codebase more strict with type hinting ## 2.0.0 - 2021-07-15 - Bump league/glide to v2 [#134](https://github.com/spatie/image/pull/134) ## 1.10.4 - 2021-04-07 - Allow spatie/temporary-directory v2 ## 1.10.3 - 2021-03-10 - Bump league/glide to 2.0 [#123](https://github.com/spatie/image/pull/123) ## 1.10.2 - 2020-01-26 - change condition to delete $conversionResultDirectory (#118) ## 1.10.1 - 2020-12-27 - adds zoom option to focalCrop (#112) ## 1.9.0 - 2020-11-13 - allow usage of a custom `OptimizerChain` #110 ## 1.8.1 - 2020-11-12 - revert changes from 1.8.0 ## 1.8.0 - 2020-11-12 - allow usage of a custom `OptimizerChain` (#108) ## 1.7.7 - 2020-11-12 - add support for PHP 8 ## 1.7.6 - 2020-01-26 - change uppercase function to mb_strtoupper instead of strtoupper (#99) ## 1.7.5 - 2019-11-23 - allow symfony 5 components ## 1.7.4 - 2019-08-28 - do not export docs ## 1.7.3 - 2019-08-03 - fix duplicated files (fixes #84) ## 1.7.2 - 2019-05-13 - fixes `optimize()` when used with `apply()` (#78) ## 1.7.1 - 2019-04-17 - change GlideConversion sequence (#76) ## 1.7.0 - 2019-02-22 - add support for `webp` ## 1.6.0 - 2019-01-27 - add `setTemporaryDirectory` ## 1.5.3 - 2019-01-10 - update lower deps ## 1.5.2 - 2018-05-05 - fix exception message ## 1.5.1 - 2018-04-18 - Prevent error when trying to remove `/tmp` ## 1.5.0 - 2018-04-13 - add `flip` ## 1.4.2 - 2018-04-11 - Use the correct driver for getting widths and height of images. ## 1.4.1 - 2018-02-08 - Support symfony ^4.0 - Support phpunit ^7.0 ## 1.4.0 - 2017-12-05 - add `getWidth` and `getHeight` ## 1.3.5 - 2017-12-04 - fix for problems when creating directories in the temporary directory ## 1.3.4 - 2017-07-25 - fix `optimize` docblock ## 1.3.3 - 2017-07-11 - make `optimize` method fluent ## 1.3.2 - 2017-07-05 - swap out underlying optimization package ## 1.3.1 - 2017-07-02 - internally treat `optimize` as a manipulation ## 1.3.0 - 2017-07-02 - add `optimize` method ## 1.2.1 - 2017-06-29 - add methods to determine emptyness to `Manipulations` and `ManipulationSequence` ## 1.2.0 - 2017-04-17 - allow `Manipulations` to be constructed with an array of arrays ## 1.1.3 - 2017-04-07 - improve support for multi-volume systems ## 1.1.2 - 2017-04-04 - remove conversion directory after converting image ## 1.1.1 - 2017-03-17 - avoid processing empty manipulations groups ## 1.1.0 - 2017-02-06 - added support for watermarks ## 1.0.0 - 2017-02-06 - initial release browsershot/src/Exceptions/FileUrlNotAllowed.php 0000644 00000000345 15105603711 0016102 0 ustar 00 <?php namespace Spatie\Browsershot\Exceptions; use Exception; class FileUrlNotAllowed extends Exception { public static function make() { return new static("An URL is not allow to start with file://"); } } browsershot/src/Exceptions/CouldNotTakeBrowsershot.php 0000644 00000001401 15105603711 0017337 0 ustar 00 <?php namespace Spatie\Browsershot\Exceptions; use Exception; class CouldNotTakeBrowsershot extends Exception { public static function chromeOutputEmpty(string $screenShotPath, string $output, array $command = []) { $command = json_encode($command); $message = <<<CONSOLE For some reason Chrome did not write a file at `{$screenShotPath}`. Command ======= {$command} Output ====== {$output} CONSOLE; return new static($message); } public static function outputFileDidNotHaveAnExtension(string $path) { return new static("The given path `{$path}` did not contain an extension. Please append an extension."); } } browsershot/src/Exceptions/HtmlIsNotAllowedToContainFile.php 0000644 00000000403 15105603711 0020352 0 ustar 00 <?php namespace Spatie\Browsershot\Exceptions; use Exception; class HtmlIsNotAllowedToContainFile extends Exception { public static function make() { return new static("The specified HTML contains `file://`. This is not allowed."); } } browsershot/src/Exceptions/FileDoesNotExistException.php 0000644 00000000354 15105603711 0017616 0 ustar 00 <?php namespace Spatie\Browsershot\Exceptions; use Exception; class FileDoesNotExistException extends Exception { public function __construct($file) { parent::__construct("The file `{$file}` does not exist"); } } browsershot/src/Exceptions/UnsuccessfulResponse.php 0000644 00000000375 15105603711 0016753 0 ustar 00 <?php namespace Spatie\Browsershot\Exceptions; use Exception; class UnsuccessfulResponse extends Exception { public function __construct($url, $code) { parent::__construct("The given url `{$url}` responds with code {$code}"); } } browsershot/src/Exceptions/ElementNotFound.php 0000644 00000000377 15105603711 0015622 0 ustar 00 <?php namespace Spatie\Browsershot\Exceptions; use Exception; class ElementNotFound extends Exception { public function __construct($selector) { parent::__construct("The given selector `{$selector} did not match any elements"); } } browsershot/src/Helpers.php 0000644 00000000556 15105603711 0012034 0 ustar 00 <?php namespace Spatie\Browsershot; class Helpers { public static function stringStartsWith($haystack, $needle): bool { $length = strlen($needle); return substr($haystack, 0, $length) === $needle; } public static function stringContains($haystack, $needle): bool { return strpos($haystack, $needle) !== false; } } browsershot/src/Browsershot.php 0000644 00000065104 15105603711 0012753 0 ustar 00 <?php namespace Spatie\Browsershot; use Spatie\Browsershot\Exceptions\CouldNotTakeBrowsershot; use Spatie\Browsershot\Exceptions\ElementNotFound; use Spatie\Browsershot\Exceptions\FileDoesNotExistException; use Spatie\Browsershot\Exceptions\FileUrlNotAllowed; use Spatie\Browsershot\Exceptions\HtmlIsNotAllowedToContainFile; use Spatie\Browsershot\Exceptions\UnsuccessfulResponse; use Spatie\Image\Image; use Spatie\Image\Manipulations; use Spatie\TemporaryDirectory\TemporaryDirectory; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; /** @mixin \Spatie\Image\Manipulations */ class Browsershot { protected $nodeBinary = null; protected $npmBinary = null; protected $nodeModulePath = null; protected $includePath = '$PATH:/usr/local/bin:/opt/homebrew/bin'; protected $binPath = null; protected $html = ''; protected $noSandbox = false; protected $proxyServer = ''; protected $showBackground = false; protected $showScreenshotBackground = true; protected $scale = null; protected $screenshotType = 'png'; protected $screenshotQuality = null; protected $temporaryHtmlDirectory; protected $timeout = 60; protected $transparentBackground = false; protected $url = ''; protected $postParams = []; protected $additionalOptions = []; protected $temporaryOptionsDirectory; protected $tempPath = ''; protected $writeOptionsToFile = false; protected $chromiumArguments = []; /** @var \Spatie\Image\Manipulations */ protected $imageManipulations; public const POLLING_REQUEST_ANIMATION_FRAME = 'raf'; public const POLLING_MUTATION = 'mutation'; public static function url(string $url): static { return (new static())->setUrl($url); } public static function html(string $html): static { return (new static())->setHtml($html); } public static function htmlFromFilePath(string $filePath): static { return (new static())->setHtmlFromFilePath($filePath); } public function __construct(string $url = '', bool $deviceEmulate = false) { $this->url = $url; $this->imageManipulations = new Manipulations(); if (! $deviceEmulate) { $this->windowSize(800, 600); } } public function setNodeBinary(string $nodeBinary) { $this->nodeBinary = $nodeBinary; return $this; } public function setNpmBinary(string $npmBinary) { $this->npmBinary = $npmBinary; return $this; } public function setIncludePath(string $includePath) { $this->includePath = $includePath; return $this; } public function setBinPath(string $binPath) { $this->binPath = $binPath; return $this; } public function setNodeModulePath(string $nodeModulePath) { $this->nodeModulePath = $nodeModulePath; return $this; } public function setChromePath(string $executablePath) { $this->setOption('executablePath', $executablePath); return $this; } public function setCustomTempPath(string $tempPath) { $this->tempPath = $tempPath; return $this; } public function post(array $postParams = []) { $this->postParams = $postParams; return $this; } public function useCookies(array $cookies, string $domain = null) { if (! count($cookies)) { return $this; } if (is_null($domain)) { $domain = parse_url($this->url)['host']; } $cookies = array_map(function ($value, $name) use ($domain) { return compact('name', 'value', 'domain'); }, $cookies, array_keys($cookies)); if (isset($this->additionalOptions['cookies'])) { $cookies = array_merge($this->additionalOptions['cookies'], $cookies); } $this->setOption('cookies', $cookies); return $this; } public function setExtraHttpHeaders(array $extraHTTPHeaders) { $this->setOption('extraHTTPHeaders', $extraHTTPHeaders); return $this; } public function setExtraNavigationHttpHeaders(array $extraNavigationHTTPHeaders) { $this->setOption('extraNavigationHTTPHeaders', $extraNavigationHTTPHeaders); return $this; } public function authenticate(string $username, string $password) { $this->setOption('authentication', compact('username', 'password')); return $this; } public function click(string $selector, string $button = 'left', int $clickCount = 1, int $delay = 0) { $clicks = $this->additionalOptions['clicks'] ?? []; $clicks[] = compact('selector', 'button', 'clickCount', 'delay'); $this->setOption('clicks', $clicks); return $this; } public function selectOption(string $selector, string $value = '') { $dropdownSelects = $this->additionalOptions['selects'] ?? []; $dropdownSelects[] = compact('selector', 'value'); $this->setOption('selects', $dropdownSelects); return $this; } public function type(string $selector, string $text = '', int $delay = 0) { $types = $this->additionalOptions['types'] ?? []; $types[] = compact('selector', 'text', 'delay'); $this->setOption('types', $types); return $this; } /** * @deprecated This option is no longer supported by modern versions of Puppeteer. */ public function setNetworkIdleTimeout(int $networkIdleTimeout) { $this->setOption('networkIdleTimeout'); return $this; } public function waitUntilNetworkIdle(bool $strict = true) { $this->setOption('waitUntil', $strict ? 'networkidle0' : 'networkidle2'); return $this; } public function waitForFunction(string $function, $polling = self::POLLING_REQUEST_ANIMATION_FRAME, int $timeout = 0) { $this->setOption('functionPolling', $polling); $this->setOption('functionTimeout', $timeout); return $this->setOption('function', $function); } public function waitForSelector(string $selector, array $options = []) { $this->setOption('waitForSelector', $selector); if (! empty($options)) { $this->setOption('waitForSelectorOptions', $options); } return $this; } public function setUrl(string $url) { if (Helpers::stringStartsWith(strtolower($url), 'file://')) { throw FileUrlNotAllowed::make(); } $this->url = $url; $this->html = ''; return $this; } public function setHtmlFromFilePath(string $filePath): self { if (false === file_exists($filePath)) { throw new FileDoesNotExistException($filePath); } $this->url = 'file://'.$filePath; $this->html = ''; return $this; } public function setProxyServer(string $proxyServer) { $this->proxyServer = $proxyServer; return $this; } public function setHtml(string $html) { if (Helpers::stringContains(strtolower($html), 'file://')) { throw HtmlIsNotAllowedToContainFile::make(); } $this->html = $html; $this->url = ''; $this->hideBrowserHeaderAndFooter(); return $this; } public function clip(int $x, int $y, int $width, int $height) { return $this->setOption('clip', compact('x', 'y', 'width', 'height')); } public function preventUnsuccessfulResponse(bool $preventUnsuccessfulResponse = true) { return $this->setOption('preventUnsuccessfulResponse', $preventUnsuccessfulResponse); } public function select($selector, $index = 0) { $this->selectorIndex($index); return $this->setOption('selector', $selector); } public function selectorIndex(int $index) { return $this->setOption('selectorIndex', $index); } public function showBrowserHeaderAndFooter() { return $this->setOption('displayHeaderFooter', true); } public function hideBrowserHeaderAndFooter() { return $this->setOption('displayHeaderFooter', false); } public function hideHeader() { return $this->headerHtml('<p></p>'); } public function hideFooter() { return $this->footerHtml('<p></p>'); } public function headerHtml(string $html) { return $this->setOption('headerTemplate', $html); } public function footerHtml(string $html) { return $this->setOption('footerTemplate', $html); } public function deviceScaleFactor(int $deviceScaleFactor) { // Google Chrome currently supports values of 1, 2, and 3. return $this->setOption('viewport.deviceScaleFactor', max(1, min(3, $deviceScaleFactor))); } public function fullPage() { return $this->setOption('fullPage', true); } public function showBackground() { $this->showBackground = true; $this->showScreenshotBackground = true; return $this; } public function hideBackground() { $this->showBackground = false; $this->showScreenshotBackground = false; return $this; } public function transparentBackground() { $this->transparentBackground = true; return $this; } public function setScreenshotType(string $type, int $quality = null) { $this->screenshotType = $type; if (! is_null($quality)) { $this->screenshotQuality = $quality; } return $this; } public function ignoreHttpsErrors() { return $this->setOption('ignoreHttpsErrors', true); } public function mobile(bool $mobile = true) { return $this->setOption('viewport.isMobile', $mobile); } public function touch(bool $touch = true) { return $this->setOption('viewport.hasTouch', $touch); } public function landscape(bool $landscape = true) { return $this->setOption('landscape', $landscape); } public function margins(float $top, float $right, float $bottom, float $left, string $unit = 'mm') { return $this->setOption('margin', [ 'top' => $top.$unit, 'right' => $right.$unit, 'bottom' => $bottom.$unit, 'left' => $left.$unit, ]); } public function noSandbox() { $this->noSandbox = true; return $this; } public function dismissDialogs() { return $this->setOption('dismissDialogs', true); } public function disableJavascript() { return $this->setOption('disableJavascript', true); } public function disableImages() { return $this->setOption('disableImages', true); } public function blockUrls($array) { return $this->setOption('blockUrls', $array); } public function blockDomains($array) { return $this->setOption('blockDomains', $array); } public function pages(string $pages) { return $this->setOption('pageRanges', $pages); } public function paperSize(float $width, float $height, string $unit = 'mm') { return $this ->setOption('width', $width.$unit) ->setOption('height', $height.$unit); } // paper format public function format(string $format) { return $this->setOption('format', $format); } public function scale(float $scale) { $this->scale = $scale; return $this; } public function timeout(int $timeout) { $this->timeout = $timeout; $this->setOption('timeout', $timeout * 1000); return $this; } public function userAgent(string $userAgent) { $this->setOption('userAgent', $userAgent); return $this; } public function device(string $device) { $this->setOption('device', $device); return $this; } public function emulateMedia(?string $media) { $this->setOption('emulateMedia', $media); return $this; } public function windowSize(int $width, int $height) { return $this ->setOption('viewport.width', $width) ->setOption('viewport.height', $height); } public function setDelay(int $delayInMilliseconds) { return $this->setOption('delay', $delayInMilliseconds); } public function delay(int $delayInMilliseconds) { return $this->setDelay($delayInMilliseconds); } public function setUserDataDir(string $absolutePath) { return $this->addChromiumArguments(['user-data-dir' => $absolutePath]); } public function userDataDir(string $absolutePath) { return $this->setUserDataDir($absolutePath); } public function writeOptionsToFile() { $this->writeOptionsToFile = true; return $this; } public function setOption($key, $value) { $this->arraySet($this->additionalOptions, $key, $value); return $this; } public function addChromiumArguments(array $arguments) { foreach ($arguments as $argument => $value) { if (is_numeric($argument)) { $this->chromiumArguments[] = "--$value"; } else { $this->chromiumArguments[] = "--$argument=$value"; } } return $this; } public function __call($name, $arguments) { $this->imageManipulations->$name(...$arguments); return $this; } public function save(string $targetPath) { $extension = strtolower(pathinfo($targetPath, PATHINFO_EXTENSION)); if ($extension === '') { throw CouldNotTakeBrowsershot::outputFileDidNotHaveAnExtension($targetPath); } if ($extension === 'pdf') { return $this->savePdf($targetPath); } $command = $this->createScreenshotCommand($targetPath); $output = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); if (! file_exists($targetPath)) { throw CouldNotTakeBrowsershot::chromeOutputEmpty($targetPath, $output, $command); } if (! $this->imageManipulations->isEmpty()) { $this->applyManipulations($targetPath); } } public function bodyHtml(): string { $command = $this->createBodyHtmlCommand(); $html = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return $html; } public function base64Screenshot(): string { $command = $this->createScreenshotCommand(); $encodedImage = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return $encodedImage; } public function screenshot(): string { if ($this->imageManipulations->isEmpty()) { $command = $this->createScreenshotCommand(); $encodedImage = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return base64_decode($encodedImage); } $temporaryDirectory = (new TemporaryDirectory($this->tempPath))->create(); $this->save($temporaryDirectory->path('screenshot.png')); $screenshot = file_get_contents($temporaryDirectory->path('screenshot.png')); $temporaryDirectory->delete(); return $screenshot; } public function pdf(): string { $command = $this->createPdfCommand(); $encodedPdf = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return base64_decode($encodedPdf); } public function savePdf(string $targetPath) { $command = $this->createPdfCommand($targetPath); $output = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); if (! file_exists($targetPath)) { throw CouldNotTakeBrowsershot::chromeOutputEmpty($targetPath, $output); } } public function base64pdf(): string { $command = $this->createPdfCommand(); $encodedPdf = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return $encodedPdf; } public function evaluate(string $pageFunction): string { $command = $this->createEvaluateCommand($pageFunction); $evaluation = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return $evaluation; } public function triggeredRequests(): array { $command = $this->createTriggeredRequestsListCommand(); $requests = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return json_decode($requests, true); } public function redirectHistory(): array { $command = $this->createRedirectHistoryCommand(); return json_decode($this->callBrowser($command), true); } /** * @return array{type: string, message: string, location:array} */ public function consoleMessages(): array { $command = $this->createConsoleMessagesCommand(); $messages = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return json_decode($messages, true); } public function failedRequests(): array { $command = $this->createFailedRequestsCommand(); $requests = $this->callBrowser($command); $this->cleanupTemporaryHtmlFile(); return json_decode($requests, true); } public function applyManipulations(string $imagePath) { Image::load($imagePath) ->manipulate($this->imageManipulations) ->save(); } public function createBodyHtmlCommand(): array { $url = $this->getFinalContentsUrl(); return $this->createCommand($url, 'content'); } public function createScreenshotCommand($targetPath = null): array { $url = $this->getFinalContentsUrl(); $options = [ 'type' => $this->screenshotType, ]; if ($targetPath) { $options['path'] = $targetPath; } if ($this->screenshotQuality) { $options['quality'] = $this->screenshotQuality; } $command = $this->createCommand($url, 'screenshot', $options); if (! $this->showScreenshotBackground) { $command['options']['omitBackground'] = true; } return $command; } public function createPdfCommand($targetPath = null): array { $url = $this->getFinalContentsUrl(); $options = []; if ($targetPath) { $options['path'] = $targetPath; } $command = $this->createCommand($url, 'pdf', $options); if ($this->showBackground) { $command['options']['printBackground'] = true; } if ($this->transparentBackground) { $command['options']['omitBackground'] = true; } if ($this->scale) { $command['options']['scale'] = $this->scale; } return $command; } public function createEvaluateCommand(string $pageFunction): array { $url = $this->getFinalContentsUrl(); $options = [ 'pageFunction' => $pageFunction, ]; return $this->createCommand($url, 'evaluate', $options); } public function createTriggeredRequestsListCommand(): array { $url = $this->html ? $this->createTemporaryHtmlFile() : $this->url; return $this->createCommand($url, 'requestsList'); } public function createRedirectHistoryCommand(): array { $url = $this->html ? $this->createTemporaryHtmlFile() : $this->url; return $this->createCommand($url, 'redirectHistory'); } public function createConsoleMessagesCommand(): array { $url = $this->html ? $this->createTemporaryHtmlFile() : $this->url; return $this->createCommand($url, 'consoleMessages'); } public function createFailedRequestsCommand(): array { $url = $this->html ? $this->createTemporaryHtmlFile() : $this->url; return $this->createCommand($url, 'failedRequests'); } public function setRemoteInstance(string $ip = '127.0.0.1', int $port = 9222): self { // assuring that ip and port does actually contains a value if ($ip && $port) { $this->setOption('remoteInstanceUrl', 'http://'.$ip.':'.$port); } return $this; } public function setWSEndpoint(string $endpoint): self { if (! is_null($endpoint)) { $this->setOption('browserWSEndpoint', $endpoint); } return $this; } public function usePipe(): self { $this->setOption('pipe', true); return $this; } public function setEnvironmentOptions(array $options = []): self { return $this->setOption('env', $options); } public function setContentUrl(string $contentUrl): self { return $this->html ? $this->setOption('contentUrl', $contentUrl) : $this; } protected function getOptionArgs(): array { $args = $this->chromiumArguments; if ($this->noSandbox) { $args[] = '--no-sandbox'; } if ($this->proxyServer) { $args[] = '--proxy-server='.$this->proxyServer; } return $args; } protected function createCommand(string $url, string $action, array $options = []): array { $command = compact('url', 'action', 'options'); $command['options']['args'] = $this->getOptionArgs(); if (! empty($this->postParams)) { $command['postParams'] = $this->postParams; } if (! empty($this->additionalOptions)) { $command['options'] = array_merge_recursive($command['options'], $this->additionalOptions); } return $command; } protected function createTemporaryHtmlFile(): string { $this->temporaryHtmlDirectory = (new TemporaryDirectory($this->tempPath))->create(); file_put_contents($temporaryHtmlFile = $this->temporaryHtmlDirectory->path('index.html'), $this->html); return "file://{$temporaryHtmlFile}"; } protected function cleanupTemporaryHtmlFile() { if ($this->temporaryHtmlDirectory) { $this->temporaryHtmlDirectory->delete(); } } protected function createTemporaryOptionsFile(string $command): string { $this->temporaryOptionsDirectory = (new TemporaryDirectory($this->tempPath))->create(); file_put_contents($temporaryOptionsFile = $this->temporaryOptionsDirectory->path('command.js'), $command); return "file://{$temporaryOptionsFile}"; } protected function cleanupTemporaryOptionsFile() { if ($this->temporaryOptionsDirectory) { $this->temporaryOptionsDirectory->delete(); } } protected function callBrowser(array $command): string { $fullCommand = $this->getFullCommand($command); $process = $this->isWindows() ? new Process($fullCommand) : Process::fromShellCommandline($fullCommand); $process->setTimeout($this->timeout); $process->run(); if ($process->isSuccessful()) { return rtrim($process->getOutput()); } $this->cleanupTemporaryOptionsFile(); $process->clearOutput(); $exitCode = $process->getExitCode(); if ($exitCode === 3) { throw new UnsuccessfulResponse($this->url, $process->getErrorOutput()); } if ($exitCode === 2) { throw new ElementNotFound($this->additionalOptions['selector']); } throw new ProcessFailedException($process); } protected function getFullCommand(array $command) { $nodeBinary = $this->nodeBinary ?: 'node'; $binPath = $this->binPath ?: __DIR__.'/../bin/browser.cjs'; $optionsCommand = $this->getOptionsCommand(json_encode($command)); if ($this->isWindows()) { // on Windows we will let Symfony/process handle the command escaping // by passing an array to the process instance return [ $nodeBinary, $binPath, $optionsCommand, ]; } $setIncludePathCommand = "PATH={$this->includePath}"; $setNodePathCommand = $this->getNodePathCommand($nodeBinary); return $setIncludePathCommand.' ' .$setNodePathCommand.' ' .$nodeBinary.' ' .escapeshellarg($binPath).' ' .$optionsCommand; } protected function getNodePathCommand(string $nodeBinary): string { if ($this->nodeModulePath) { return "NODE_PATH='{$this->nodeModulePath}'"; } if ($this->npmBinary) { return "NODE_PATH=`{$nodeBinary} {$this->npmBinary} root -g`"; } return 'NODE_PATH=`npm root -g`'; } protected function getOptionsCommand(string $command): string { if ($this->writeOptionsToFile) { $temporaryOptionsFile = $this->createTemporaryOptionsFile($command); $command = "-f {$temporaryOptionsFile}"; } if ($this->isWindows()) { return $command; } return escapeshellarg($command); } protected function arraySet(array &$array, string $key, $value): array { if (is_null($key)) { return $array = $value; } $keys = explode('.', $key); while (count($keys) > 1) { $key = array_shift($keys); // If the key doesn't exist at this depth, we will just create an empty array // to hold the next value, allowing us to create the arrays to hold final // values at the correct depth. Then we'll keep digging into the array. if (! isset($array[$key]) || ! is_array($array[$key])) { $array[$key] = []; } $array = &$array[$key]; } $array[array_shift($keys)] = $value; return $array; } public function initialPageNumber(int $initialPage = 1) { return $this ->setOption('initialPageNumber', ($initialPage - 1)) ->pages($initialPage.'-'); } private function isWindows() { return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; } private function getFinalContentsUrl(): string { $url = $this->html ? $this->createTemporaryHtmlFile() : $this->url; return $url; } public function newHeadless(): self { return $this->setOption('newHeadless', true); } } browsershot/README.md 0000644 00000007767 15105603711 0010424 0 ustar 00 <p align="center"><img src="/art/socialcard.png" alt="Social Card of Spatie's Browsershot"></p> # Convert a webpage to an image or pdf using headless Chrome [](https://github.com/spatie/browsershot/releases) [](LICENSE.md) [](https://github.com/spatie/browsershot/actions) [](https://packagist.org/packages/spatie/browsershot) The package can convert a webpage to an image or pdf. The conversion is done behind the scenes by [Puppeteer](https://github.com/GoogleChrome/puppeteer) which controls a headless version of Google Chrome. Here's a quick example: ```php use Spatie\Browsershot\Browsershot; // an image will be saved Browsershot::url('https://example.com')->save($pathToImage); ``` It will save a pdf if the path passed to the `save` method has a `pdf` extension. ```php // a pdf will be saved Browsershot::url('https://example.com')->save('example.pdf'); ``` You can also use an arbitrary html input, simply replace the `url` method with `html`: ```php Browsershot::html('<h1>Hello world!!</h1>')->save('example.pdf'); ``` If your HTML input is already in a file locally use the : ```php Browsershot::htmlFromFilePath('/local/path/to/file.html')->save('example.pdf'); ``` Browsershot also can get the body of an html page after JavaScript has been executed: ```php Browsershot::url('https://example.com')->bodyHtml(); // returns the html of the body ``` If you wish to retrieve an array list with all of the requests that the page triggered you can do so: ```php $requests = Browsershot::url('https://example.com') ->triggeredRequests(); foreach ($requests as $request) { $url = $request['url']; //https://example.com/ } ``` To use Chrome's new [headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome) pass the `newHeadless` method: ```php Browsershot::url('https://example.com')->newHeadless()->save($pathToImage); ``` ## Support us Learn how to create a package like this one, by watching our premium video course: [](https://laravelpackage.training) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Documentation All documentation is available [on our documentation site](https://spatie.be/docs/browsershot). ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. ## Alternatives If you're not able to install Node and Puppeteer, take a look at [v2 of browsershot](https://github.com/spatie/browsershot/tree/2.4.1), which uses Chrome headless CLI to take a screenshot. `v2` is not maintained anymore, but should work pretty well. If using headless Chrome does not work for you take a look at at `v1` of this package which uses the abandoned `PhantomJS` binary. ## Credits - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) And a special thanks to [Caneco](https://twitter.com/caneco) for the logo ✨ ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. browsershot/bin/browser.cjs 0000644 00000033261 15105603711 0012065 0 ustar 00 const fs = require('fs'); const URL = require('url').URL; const URLParse = require('url').parse; const [, , ...args] = process.argv; /** * There are two ways for Browsershot to communicate with puppeteer: * - By giving a options JSON dump as an argument * - Or by providing a temporary file with the options JSON dump, * the path to this file is then given as an argument with the flag -f */ const request = args[0].startsWith('-f ') ? JSON.parse(fs.readFileSync(new URL(args[0].substring(3)))) : JSON.parse(args[0]); const requestsList = []; const redirectHistory = []; const consoleMessages = []; const failedRequests = []; const getOutput = async (page, request) => { let output; if (request.action == 'requestsList') { output = JSON.stringify(requestsList); return output; } if (request.action == 'redirectHistory') { output = JSON.stringify(redirectHistory); return output; } if (request.action == 'consoleMessages') { output = JSON.stringify(consoleMessages); return output; } if (request.action == 'failedRequests') { output = JSON.stringify(failedRequests); return output; } if (request.action == 'evaluate') { output = await page.evaluate(request.options.pageFunction); return output; } output = await page[request.action](request.options); return output.toString('base64'); }; const callChrome = async pup => { let browser; let page; let output; let remoteInstance; const puppet = (pup || require('puppeteer')); try { if (request.options.remoteInstanceUrl || request.options.browserWSEndpoint ) { // default options let options = { ignoreHTTPSErrors: request.options.ignoreHttpsErrors }; // choose only one method to connect to the browser instance if ( request.options.remoteInstanceUrl ) { options.browserURL = request.options.remoteInstanceUrl; } else if ( request.options.browserWSEndpoint ) { options.browserWSEndpoint = request.options.browserWSEndpoint; } try { browser = await puppet.connect( options ); remoteInstance = true; } catch (exception) { /** does nothing. fallbacks to launching a chromium instance */} } if (!browser) { browser = await puppet.launch({ headless: request.options.newHeadless ? 'new' : true, ignoreHTTPSErrors: request.options.ignoreHttpsErrors, executablePath: request.options.executablePath, args: request.options.args || [], pipe: request.options.pipe || false, env: { ...(request.options.env || {}), ...process.env }, }); } page = await browser.newPage(); if (request.options && request.options.disableJavascript) { await page.setJavaScriptEnabled(false); } await page.setRequestInterception(true); const contentUrl = request.options.contentUrl; const parsedContentUrl = contentUrl ? contentUrl.replace(/\/$/, "") : undefined; let pageContent; if (contentUrl) { pageContent = fs.readFileSync(request.url.replace('file://', '')); request.url = contentUrl; } page.on('console', message => consoleMessages.push({ type: message.type(), message: message.text(), location: message.location() })); page.on('response', function (response) { if (response.request().isNavigationRequest() && response.request().frame().parentFrame() === null) { redirectHistory.push({ url: response.request().url(), status: response.status(), reason: response.statusText(), headers: response.headers() }) } if (response.status() >= 200 && response.status() <= 399) { return; } failedRequests.push({ status: response.status(), url: response.url(), }); }) page.on('request', interceptedRequest => { var headers = interceptedRequest.headers(); requestsList.push({ url: interceptedRequest.url(), }); if (request.options && request.options.disableImages) { if (interceptedRequest.resourceType() === 'image') { interceptedRequest.abort(); return; } } if (request.options && request.options.blockDomains) { const hostname = URLParse(interceptedRequest.url()).hostname; if (request.options.blockDomains.includes(hostname)) { interceptedRequest.abort(); return; } } if (request.options && request.options.blockUrls) { for (const element of request.options.blockUrls) { if (interceptedRequest.url().indexOf(element) >= 0) { interceptedRequest.abort(); return; } } } if (request.options && request.options.extraNavigationHTTPHeaders) { // Do nothing in case of non-navigation requests. if (interceptedRequest.isNavigationRequest()) { headers = Object.assign({}, headers, request.options.extraNavigationHTTPHeaders); } } if (pageContent) { const interceptedUrl = interceptedRequest.url().replace(/\/$/, ""); // if content url matches the intercepted request url, will return the content fetched from the local file system if (interceptedUrl === parsedContentUrl) { interceptedRequest.respond({ headers, body: pageContent, }); return; } } if (request.postParams) { const postParamsArray = request.postParams; const queryString = Object.keys(postParamsArray) .map(key => `${key}=${postParamsArray[key]}`) .join('&'); interceptedRequest.continue({ method: "POST", postData: queryString, headers: { ...interceptedRequest.headers(), "Content-Type": "application/x-www-form-urlencoded" } }); return; } interceptedRequest.continue({ headers }); }); if (request.options && request.options.dismissDialogs) { page.on('dialog', async dialog => { await dialog.dismiss(); }); } if (request.options && request.options.userAgent) { await page.setUserAgent(request.options.userAgent); } if (request.options && request.options.device) { const devices = puppet.devices; const device = devices[request.options.device]; await page.emulate(device); } if (request.options && request.options.emulateMedia) { await page.emulateMediaType(request.options.emulateMedia); } if (request.options && request.options.viewport) { await page.setViewport(request.options.viewport); } if (request.options && request.options.extraHTTPHeaders) { await page.setExtraHTTPHeaders(request.options.extraHTTPHeaders); } if (request.options && request.options.authentication) { await page.authenticate(request.options.authentication); } if (request.options && request.options.cookies) { await page.setCookie(...request.options.cookies); } if (request.options && request.options.timeout) { await page.setDefaultNavigationTimeout(request.options.timeout); } const requestOptions = {}; if (request.options && request.options.networkIdleTimeout) { requestOptions.waitUntil = 'networkidle'; requestOptions.networkIdleTimeout = request.options.networkIdleTimeout; } else if (request.options && request.options.waitUntil) { requestOptions.waitUntil = request.options.waitUntil; } const response = await page.goto(request.url, requestOptions); if (request.options.preventUnsuccessfulResponse) { const status = response.status() if (status >= 400 && status < 600) { throw {type: "UnsuccessfulResponse", status}; } } if (request.options && request.options.disableImages) { await page.evaluate(() => { let images = document.getElementsByTagName('img'); while (images.length > 0) { images[0].parentNode.removeChild(images[0]); } }); } if (request.options && request.options.types) { for (let i = 0, len = request.options.types.length; i < len; i++) { let typeOptions = request.options.types[i]; await page.type(typeOptions.selector, typeOptions.text, { 'delay': typeOptions.delay, }); } } if (request.options && request.options.selects) { for (let i = 0, len = request.options.selects.length; i < len; i++) { let selectOptions = request.options.selects[i]; await page.select(selectOptions.selector, selectOptions.value); } } if (request.options && request.options.clicks) { for (let i = 0, len = request.options.clicks.length; i < len; i++) { let clickOptions = request.options.clicks[i]; await page.click(clickOptions.selector, { 'button': clickOptions.button, 'clickCount': clickOptions.clickCount, 'delay': clickOptions.delay, }); } } if (request.options && request.options.addStyleTag) { await page.addStyleTag(JSON.parse(request.options.addStyleTag)); } if (request.options && request.options.addScriptTag) { await page.addScriptTag(JSON.parse(request.options.addScriptTag)); } if (request.options.delay) { await page.waitForTimeout(request.options.delay); } if (request.options.initialPageNumber) { await page.evaluate((initialPageNumber) => { window.pageStart = initialPageNumber; const style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = '.empty-page { page-break-after: always; visibility: hidden; }'; document.getElementsByTagName('head')[0].appendChild(style); const emptyPages = Array.from({length: window.pageStart}).map(() => { const emptyPage = document.createElement('div'); emptyPage.className = "empty-page"; emptyPage.textContent = "empty"; return emptyPage; }); document.body.prepend(...emptyPages); }, request.options.initialPageNumber); } if (request.options.selector) { var element; const index = request.options.selectorIndex || 0; if(index){ element = await page.$$(request.options.selector); if(!element.length || typeof element[index] === 'undefined'){ element = null; }else{ element = element[index]; } }else{ element = await page.$(request.options.selector); } if (element === null) { throw {type: 'ElementNotFound'}; } request.options.clip = await element.boundingBox(); } if (request.options.function) { let functionOptions = { polling: request.options.functionPolling, timeout: request.options.functionTimeout || request.options.timeout }; await page.waitForFunction(request.options.function, functionOptions); } if (request.options.waitForSelector) { await page.waitForSelector(request.options.waitForSelector, request.options.waitForSelectorOptions ?? undefined); } output = await getOutput(page, request); if (!request.options.path) { console.log(output); } if (remoteInstance && page) { await page.close(); } await remoteInstance ? browser.disconnect() : browser.close(); } catch (exception) { if (browser) { if (remoteInstance && page) { await page.close(); } await remoteInstance ? browser.disconnect() : browser.close(); } if (exception.type === 'UnsuccessfulResponse') { console.error(exception.status) process.exit(3); } console.error(exception); if (exception.type === 'ElementNotFound') { process.exit(2); } process.exit(1); } }; if (require.main === module) { callChrome(); } exports.callChrome = callChrome; browsershot/composer.json 0000644 00000002432 15105603711 0011647 0 ustar 00 { "name": "spatie/browsershot", "description": "Convert a webpage to an image or pdf using headless Chrome", "homepage": "https://github.com/spatie/browsershot", "keywords": [ "convert", "webpage", "image", "pdf", "screenshot", "chrome", "headless", "puppeteer" ], "license": "MIT", "authors": [ { "name": "Freek Van der Herten", "email": "freek@spatie.be", "homepage": "https://github.com/freekmurze", "role": "Developer" } ], "require": { "php": "^8.0", "spatie/image": "^1.5.3|^2.0", "spatie/temporary-directory": "^1.1|^2.0", "symfony/process": "^4.2|^5.0|^6.0", "ext-json": "*" }, "require-dev": { "spatie/phpunit-snapshot-assertions": "^4.2.3", "pestphp/pest": "^1.20" }, "autoload": { "psr-4": { "Spatie\\Browsershot\\": "src" } }, "autoload-dev": { "psr-4": { "Spatie\\Browsershot\\Test\\": "tests" } }, "scripts": { "test": "vendor/bin/pest" }, "config": { "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true } } } browsershot/LICENSE.md 0000644 00000002102 15105603711 0010523 0 ustar 00 The MIT License (MIT) Copyright (c) Spatie bvba <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. laravel-package-tools/src/Exceptions/InvalidPackage.php 0000644 00000000445 15105603711 0017206 0 ustar 00 <?php namespace Spatie\LaravelPackageTools\Exceptions; use Exception; class InvalidPackage extends Exception { public static function nameIsRequired(): self { return new static('This package does not have a name. You can set one with `$package->name("yourName")`'); } } laravel-package-tools/src/PackageServiceProvider.php 0000644 00000016317 15105603711 0016617 0 ustar 00 <?php namespace Spatie\LaravelPackageTools; use Carbon\Carbon; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; use ReflectionClass; use Spatie\LaravelPackageTools\Exceptions\InvalidPackage; abstract class PackageServiceProvider extends ServiceProvider { protected Package $package; abstract public function configurePackage(Package $package): void; public function register() { $this->registeringPackage(); $this->package = $this->newPackage(); $this->package->setBasePath($this->getPackageBaseDir()); $this->configurePackage($this->package); if (empty($this->package->name)) { throw InvalidPackage::nameIsRequired(); } foreach ($this->package->configFileNames as $configFileName) { $this->mergeConfigFrom($this->package->basePath("/../config/{$configFileName}.php"), $configFileName); } $this->packageRegistered(); return $this; } public function newPackage(): Package { return new Package(); } public function boot() { $this->bootingPackage(); if ($this->package->hasTranslations) { $langPath = 'vendor/' . $this->package->shortName(); $langPath = (function_exists('lang_path')) ? lang_path($langPath) : resource_path('lang/' . $langPath); } if ($this->app->runningInConsole()) { foreach ($this->package->configFileNames as $configFileName) { $this->publishes([ $this->package->basePath("/../config/{$configFileName}.php") => config_path("{$configFileName}.php"), ], "{$this->package->shortName()}-config"); } if ($this->package->hasViews) { $this->publishes([ $this->package->basePath('/../resources/views') => base_path("resources/views/vendor/{$this->packageView($this->package->viewNamespace)}"), ], "{$this->packageView($this->package->viewNamespace)}-views"); } if ($this->package->hasInertiaComponents) { $packageDirectoryName = Str::of($this->packageView($this->package->viewNamespace))->studly()->remove('-')->value(); $this->publishes([ $this->package->basePath('/../resources/js/Pages') => base_path("resources/js/Pages/{$packageDirectoryName}"), ], "{$this->packageView($this->package->viewNamespace)}-inertia-components"); } $now = Carbon::now(); foreach ($this->package->migrationFileNames as $migrationFileName) { $filePath = $this->package->basePath("/../database/migrations/{$migrationFileName}.php"); if (! file_exists($filePath)) { // Support for the .stub file extension $filePath .= '.stub'; } $this->publishes([ $filePath => $this->generateMigrationName( $migrationFileName, $now->addSecond() ), ], "{$this->package->shortName()}-migrations"); if ($this->package->runsMigrations) { $this->loadMigrationsFrom($filePath); } } if ($this->package->hasTranslations) { $this->publishes([ $this->package->basePath('/../resources/lang') => $langPath, ], "{$this->package->shortName()}-translations"); } if ($this->package->hasAssets) { $this->publishes([ $this->package->basePath('/../resources/dist') => public_path("vendor/{$this->package->shortName()}"), ], "{$this->package->shortName()}-assets"); } } if (! empty($this->package->commands)) { $this->commands($this->package->commands); } if (! empty($this->package->consoleCommands) && $this->app->runningInConsole()) { $this->commands($this->package->consoleCommands); } if ($this->package->hasTranslations) { $this->loadTranslationsFrom( $this->package->basePath('/../resources/lang/'), $this->package->shortName() ); $this->loadJsonTranslationsFrom($this->package->basePath('/../resources/lang/')); $this->loadJsonTranslationsFrom($langPath); } if ($this->package->hasViews) { $this->loadViewsFrom($this->package->basePath('/../resources/views'), $this->package->viewNamespace()); } foreach ($this->package->viewComponents as $componentClass => $prefix) { $this->loadViewComponentsAs($prefix, [$componentClass]); } if (count($this->package->viewComponents)) { $this->publishes([ $this->package->basePath('/Components') => base_path("app/View/Components/vendor/{$this->package->shortName()}"), ], "{$this->package->name}-components"); } if ($this->package->publishableProviderName) { $this->publishes([ $this->package->basePath("/../resources/stubs/{$this->package->publishableProviderName}.php.stub") => base_path("app/Providers/{$this->package->publishableProviderName}.php"), ], "{$this->package->shortName()}-provider"); } foreach ($this->package->routeFileNames as $routeFileName) { $this->loadRoutesFrom("{$this->package->basePath('/../routes/')}{$routeFileName}.php"); } foreach ($this->package->sharedViewData as $name => $value) { View::share($name, $value); } foreach ($this->package->viewComposers as $viewName => $viewComposer) { View::composer($viewName, $viewComposer); } $this->packageBooted(); return $this; } public static function generateMigrationName(string $migrationFileName, Carbon $now): string { $migrationsPath = 'migrations/'; $len = strlen($migrationFileName) + 4; if (Str::contains($migrationFileName, '/')) { $migrationsPath .= Str::of($migrationFileName)->beforeLast('/')->finish('/'); $migrationFileName = Str::of($migrationFileName)->afterLast('/'); } foreach (glob(database_path("{$migrationsPath}*.php")) as $filename) { if ((substr($filename, -$len) === $migrationFileName . '.php')) { return $filename; } } return database_path($migrationsPath . $now->format('Y_m_d_His') . '_' . Str::of($migrationFileName)->snake()->finish('.php')); } public function registeringPackage() { } public function packageRegistered() { } public function bootingPackage() { } public function packageBooted() { } protected function getPackageBaseDir(): string { $reflector = new ReflectionClass(get_class($this)); return dirname($reflector->getFileName()); } public function packageView(?string $namespace) { return is_null($namespace) ? $this->package->shortName() : $this->package->viewNamespace; } } laravel-package-tools/src/Package.php 0000644 00000012456 15105603711 0013563 0 ustar 00 <?php namespace Spatie\LaravelPackageTools; use Illuminate\Support\Str; use Spatie\LaravelPackageTools\Commands\InstallCommand; class Package { public string $name; public array $configFileNames = []; public bool $hasViews = false; public bool $hasInertiaComponents = false; public ?string $viewNamespace = null; public bool $hasTranslations = false; public bool $hasAssets = false; public bool $runsMigrations = false; public array $migrationFileNames = []; public array $routeFileNames = []; public array $commands = []; public array $consoleCommands = []; public array $viewComponents = []; public array $sharedViewData = []; public array $viewComposers = []; public string $basePath; public ?string $publishableProviderName = null; public function name(string $name): static { $this->name = $name; return $this; } public function hasConfigFile($configFileName = null): static { $configFileName = $configFileName ?? $this->shortName(); if (! is_array($configFileName)) { $configFileName = [$configFileName]; } $this->configFileNames = $configFileName; return $this; } public function publishesServiceProvider(string $providerName): static { $this->publishableProviderName = $providerName; return $this; } public function hasInstallCommand($callable): static { $installCommand = new InstallCommand($this); $callable($installCommand); $this->consoleCommands[] = $installCommand; return $this; } public function shortName(): string { return Str::after($this->name, 'laravel-'); } public function hasViews(string $namespace = null): static { $this->hasViews = true; $this->viewNamespace = $namespace; return $this; } public function hasInertiaComponents(string $namespace = null): static { $this->hasInertiaComponents = true; $this->viewNamespace = $namespace; return $this; } public function hasViewComponent(string $prefix, string $viewComponentName): static { $this->viewComponents[$viewComponentName] = $prefix; return $this; } public function hasViewComponents(string $prefix, ...$viewComponentNames): static { foreach ($viewComponentNames as $componentName) { $this->viewComponents[$componentName] = $prefix; } return $this; } public function sharesDataWithAllViews(string $name, $value): static { $this->sharedViewData[$name] = $value; return $this; } public function hasViewComposer($view, $viewComposer): static { if (! is_array($view)) { $view = [$view]; } foreach ($view as $viewName) { $this->viewComposers[$viewName] = $viewComposer; } return $this; } public function hasTranslations(): static { $this->hasTranslations = true; return $this; } public function hasAssets(): static { $this->hasAssets = true; return $this; } public function runsMigrations(bool $runsMigrations = true): static { $this->runsMigrations = $runsMigrations; return $this; } public function hasMigration(string $migrationFileName): static { $this->migrationFileNames[] = $migrationFileName; return $this; } public function hasMigrations(...$migrationFileNames): static { $this->migrationFileNames = array_merge( $this->migrationFileNames, collect($migrationFileNames)->flatten()->toArray() ); return $this; } public function hasCommand(string $commandClassName): static { $this->commands[] = $commandClassName; return $this; } public function hasCommands(...$commandClassNames): static { $this->commands = array_merge($this->commands, collect($commandClassNames)->flatten()->toArray()); return $this; } public function hasConsoleCommand(string $commandClassName): static { $this->consoleCommands[] = $commandClassName; return $this; } public function hasConsoleCommands(...$commandClassNames): static { $this->consoleCommands = array_merge($this->consoleCommands, collect($commandClassNames)->flatten()->toArray()); return $this; } public function hasRoute(string $routeFileName): static { $this->routeFileNames[] = $routeFileName; return $this; } public function hasRoutes(...$routeFileNames): static { $this->routeFileNames = array_merge($this->routeFileNames, collect($routeFileNames)->flatten()->toArray()); return $this; } public function basePath(string $directory = null): string { if ($directory === null) { return $this->basePath; } return $this->basePath . DIRECTORY_SEPARATOR . ltrim($directory, DIRECTORY_SEPARATOR); } public function viewNamespace(): string { return $this->viewNamespace ?? $this->shortName(); } public function setBasePath(string $path): static { $this->basePath = $path; return $this; } } laravel-package-tools/src/Commands/InstallCommand.php 0000644 00000011202 15105603711 0016662 0 ustar 00 <?php namespace Spatie\LaravelPackageTools\Commands; use Closure; use Illuminate\Console\Command; use Illuminate\Support\Str; use Spatie\LaravelPackageTools\Package; class InstallCommand extends Command { protected Package $package; public ?Closure $startWith = null; protected array $publishes = []; protected bool $askToRunMigrations = false; protected bool $copyServiceProviderInApp = false; protected ?string $starRepo = null; public ?Closure $endWith = null; public $hidden = true; public function __construct(Package $package) { $this->signature = $package->shortName() . ':install'; $this->description = 'Install ' . $package->name; $this->package = $package; parent::__construct(); } public function handle() { if ($this->startWith) { ($this->startWith)($this); } foreach ($this->publishes as $tag) { $name = str_replace('-', ' ', $tag); $this->comment("Publishing {$name}..."); $this->callSilently("vendor:publish", [ '--tag' => "{$this->package->shortName()}-{$tag}", ]); } if ($this->askToRunMigrations) { if ($this->confirm('Would you like to run the migrations now?')) { $this->comment('Running migrations...'); $this->call('migrate'); } } if ($this->copyServiceProviderInApp) { $this->comment('Publishing service provider...'); $this->copyServiceProviderInApp(); } if ($this->starRepo) { if ($this->confirm('Would you like to star our repo on GitHub?')) { $repoUrl = "https://github.com/{$this->starRepo}"; if (PHP_OS_FAMILY == 'Darwin') { exec("open {$repoUrl}"); } if (PHP_OS_FAMILY == 'Windows') { exec("start {$repoUrl}"); } if (PHP_OS_FAMILY == 'Linux') { exec("xdg-open {$repoUrl}"); } } } $this->info("{$this->package->shortName()} has been installed!"); if ($this->endWith) { ($this->endWith)($this); } } public function publish(string ...$tag): self { $this->publishes = array_merge($this->publishes, $tag); return $this; } public function publishConfigFile(): self { return $this->publish('config'); } public function publishAssets(): self { return $this->publish('assets'); } public function publishInertiaComponents(): self { return $this->publish('inertia-components'); } public function publishMigrations(): self { return $this->publish('migrations'); } public function askToRunMigrations(): self { $this->askToRunMigrations = true; return $this; } public function copyAndRegisterServiceProviderInApp(): self { $this->copyServiceProviderInApp = true; return $this; } public function askToStarRepoOnGitHub($vendorSlashRepoName): self { $this->starRepo = $vendorSlashRepoName; return $this; } public function startWith($callable): self { $this->startWith = $callable; return $this; } public function endWith($callable): self { $this->endWith = $callable; return $this; } protected function copyServiceProviderInApp(): self { $providerName = $this->package->publishableProviderName; if (! $providerName) { return $this; } $this->callSilent('vendor:publish', ['--tag' => $this->package->shortName() . '-provider']); $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); $appConfig = file_get_contents(config_path('app.php')); $class = '\\Providers\\' . $providerName . '::class'; if (Str::contains($appConfig, $namespace . $class)) { return $this; } file_put_contents(config_path('app.php'), str_replace( "{$namespace}\\Providers\\BroadcastServiceProvider::class,", "{$namespace}\\Providers\\BroadcastServiceProvider::class," . PHP_EOL . " {$namespace}{$class},", $appConfig )); file_put_contents(app_path('Providers/' . $providerName . '.php'), str_replace( "namespace App\Providers;", "namespace {$namespace}\Providers;", file_get_contents(app_path('Providers/' . $providerName . '.php')) )); return $this; } } laravel-package-tools/README.md 0000644 00000043257 15105603711 0012212 0 ustar 00 # Tools for creating Laravel packages [](https://packagist.org/packages/spatie/laravel-package-tools)  [](https://packagist.org/packages/spatie/laravel-package-tools) This package contains a `PackageServiceProvider` that you can use in your packages to easily register config files, migrations, and more. Here's an example of how it can be used. ```php use Spatie\LaravelPackageTools\PackageServiceProvider; use Spatie\LaravelPackageTools\Package; use MyPackage\ViewComponents\Alert; use Spatie\LaravelPackageTools\Commands\InstallCommand; class YourPackageServiceProvider extends PackageServiceProvider { public function configurePackage(Package $package): void { $package ->name('your-package-name') ->hasConfigFile() ->hasViews() ->hasViewComponent('spatie', Alert::class) ->hasViewComposer('*', MyViewComposer::class) ->sharesDataWithAllViews('downloads', 3) ->hasTranslations() ->hasAssets() ->publishesServiceProvider('MyProviderName') ->hasRoute('web') ->hasMigration('create_package_tables') ->hasCommand(YourCoolPackageCommand::class) ->hasInstallCommand(function(InstallCommand $command) { $command ->publishConfigFile() ->publishAssets() ->publishMigrations() ->copyAndRegisterServiceProviderInApp() ->askToStarRepoOnGitHub(); }); } } ``` Under the hood it will do the necessary work to register the necessary things and make all sorts of files publishable. ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-package-tools.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-package-tools) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Getting started This package is opinionated on how you should structure your package. To get started easily, consider using [our package-skeleton repo](https://github.com/spatie/package-skeleton-laravel) to start your package. The skeleton is structured perfectly to work perfectly with the `PackageServiceProvider` in this package. ## Usage In your package you should let your service provider extend `Spatie\LaravelPackageTools\PackageServiceProvider`. ```php use Spatie\LaravelPackageTools\PackageServiceProvider; use Spatie\LaravelPackageTools\Package; class YourPackageServiceProvider extends PackageServiceProvider { public function configurePackage(Package $package) : void { $package->name('your-package-name'); } } ``` Passing the package name to `name` is mandatory. ### Working with a config file To register a config file, you should create a php file with your package name in the `config` directory of your package. In this example it should be at `<package root>/config/your-package-name.php`. If your package name starts with `laravel-`, we expect that your config file does not contain that prefix. So if your package name is `laravel-cool-package`, the config file should be named `cool-package.php`. To register that config file, call `hasConfigFile()` on `$package` in the `configurePackage` method. ```php $package ->name('your-package-name') ->hasConfigFile(); ``` The `hasConfigFile` method will also make the config file publishable. Users of your package will be able to publish the config file with this command. ```bash php artisan vendor:publish --tag=your-package-name-config ``` Should your package have multiple config files, you can pass their names as an array to `hasConfigFile` ```php $package ->name('your-package-name') ->hasConfigFile(['my-config-file', 'another-config-file']); ``` ### Working with views Any views your package provides, should be placed in the `<package root>/resources/views` directory. You can register these views with the `hasViews` command. ```php $package ->name('your-package-name') ->hasViews(); ``` This will register your views with Laravel. If you have a view `<package root>/resources/views/myView.blade.php`, you can use it like this: `view('your-package-name::myView')`. Of course, you can also use subdirectories to organise your views. A view located at `<package root>/resources/views/subdirectory/myOtherView.blade.php` can be used with `view('your-package-name::subdirectory.myOtherView')`. #### Using a custom view namespace You can pass a custom view namespace to the `hasViews` method. ```php $package ->name('your-package-name') ->hasViews('custom-view-namespace'); ``` You can now use the views of the package like this: ```php view('custom-view-namespace::myView'); ``` #### Publishing the views Calling `hasViews` will also make views publishable. Users of your package will be able to publish the views with this command: ```bash php artisan vendor:publish --tag=your-package-name-views ``` > **Note:** > > If you use custom view namespace then you should change your publish command like this: ```bash php artisan vendor:publish --tag=custom-view-namespace-views ``` ### Sharing global data with views You can share data with all views using the `sharesDataWithAllViews` method. This will make the shared variable available to all views. ```php $package ->name('your-package-name') ->sharesDataWithAllViews('companyName', 'Spatie'); ``` ### Working with Blade view components Any Blade view components that your package provides should be placed in the `<package root>/src/Components` directory. You can register these views with the `hasViewComponents` command. ```php $package ->name('your-package-name') ->hasViewComponents('spatie', Alert::class); ``` This will register your view components with Laravel. In the case of `Alert::class`, it can be referenced in views as `<x-spatie-alert />`, where `spatie` is the prefix you provided during registration. Calling `hasViewComponents` will also make view components publishable, and will be published to `app/Views/Components/vendor/<package name>`. Users of your package will be able to publish the view components with this command: ```bash php artisan vendor:publish --tag=your-package-name-components ``` ### Working with view composers You can register any view composers that your project uses with the `hasViewComposers` method. You may also register a callback that receives a `$view` argument instead of a classname. To register a view composer with all views, use an asterisk as the view name `'*'`. ```php $package ->name('your-package-name') ->hasViewComposer('viewName', MyViewComposer::class) ->hasViewComposer('*', function($view) { $view->with('sharedVariable', 123); }); ``` ### Working with inertia components Any `.vue` or `.jsx` files your package provides, should be placed in the `<package root>/resources/js/Pages` directory. You can register these components with the `hasInertiaComponents` command. ```php $package ->name('your-package-name') ->hasInertiaComponents(); ``` This will register your components with Laravel. The user should publish the inertia components manually or using the [installer-command](#adding-an-installer-command) in order to use them. If you have an inertia component `<package root>/resources/js/Pages/myComponent.vue`, you can use it like this: `Inertia::render('YourPackageName/myComponent')`. Of course, you can also use subdirectories to organise your components. #### Publishing inertia components Calling `hasInertiaComponents` will also make inertia components publishable. Users of your package will be able to publish the views with this command: ```bash php artisan vendor:publish --tag=your-package-name-inertia-components ``` Also, the inertia components are available in a convenient way with your package [installer-command](#adding-an-installer-command) ### Working with translations Any translations your package provides, should be placed in the `<package root>/resources/lang/<language-code>` directory. You can register these translations with the `hasTranslations` command. ```php $package ->name('your-package-name') ->hasTranslations(); ``` This will register the translations with Laravel. Assuming you save this translation file at `<package root>/resources/lang/en/translations.php`... ```php return [ 'translatable' => 'translation', ]; ``` ... your package and users will be able to retrieve the translation with: ```php trans('your-package-name::translations.translatable'); // returns 'translation' ``` If your package name starts with `laravel-` then you should leave that off in the example above. Coding with translation strings as keys, you should create JSON files in `<package root>/resources/lang/<language-code>.json`. For example, creating `<package root>/resources/lang/it.json` file like so: ```json { "Hello!": "Ciao!" } ``` ...the output of... ```php trans('Hello!'); ``` ...will be `Ciao!` if the application uses the Italian language. Calling `hasTranslations` will also make translations publishable. Users of your package will be able to publish the translations with this command: ```bash php artisan vendor:publish --tag=your-package-name-translations ``` ### Working with assets Any assets your package provides, should be placed in the `<package root>/resources/dist/` directory. You can make these assets publishable the `hasAssets` method. ```php $package ->name('your-package-name') ->hasAssets(); ``` Users of your package will be able to publish the assets with this command: ```bash php artisan vendor:publish --tag=your-package-name-assets ``` This will copy over the assets to the `public/vendor/<your-package-name>` directory in the app where your package is installed in. ### Working with migrations The `PackageServiceProvider` assumes that any migrations are placed in this directory: `<package root>/database/migrations`. Inside that directory you can put any migrations. To register your migration, you should pass its name without the extension to the `hasMigration` table. If your migration file is called `create_my_package_tables.php.stub` you can register them like this: ```php $package ->name('your-package-name') ->hasMigration('create_my_package_tables'); ``` Should your package contain multiple migration files, you can just call `hasMigration` multiple times or use `hasMigrations`. ```php $package ->name('your-package-name') ->hasMigrations(['my_package_tables', 'some_other_migration']); ``` Calling `hasMigration` will also make migrations publishable. Users of your package will be able to publish the migrations with this command: ```bash php artisan vendor:publish --tag=your-package-name-migrations ``` Like you might expect, published migration files will be prefixed with the current datetime. You can also enable the migrations to be registered without needing the users of your package to publish them: ```php $package ->name('your-package-name') ->hasMigrations(['my_package_tables', 'some_other_migration']) ->runsMigrations(); ``` ### Working with a publishable service provider Some packages need an example service provider to be copied into the `app\Providers` directory of the Laravel app. Think of for instance, the `laravel/horizon` package that copies an `HorizonServiceProvider` into your app with some sensible defaults. ```php $package ->name('your-package-name') ->publishesServiceProvider($nameOfYourServiceProvider); ``` The file that will be copied to the app should be stored in your package in `/resources/stubs/{$nameOfYourServiceProvider}.php.stub`. When your package is installed into an app, running this command... ```bash php artisan vendor:publish --tag=your-package-name-provider ``` ... will copy `/resources/stubs/{$nameOfYourServiceProvider}.php.stub` in your package to `app/Providers/{$nameOfYourServiceProvider}.php` in the app of the user. ### Registering commands You can register any command you package provides with the `hasCommand` function. ```php $package ->name('your-package-name') ->hasCommand(YourCoolPackageCommand::class); ```` If your package provides multiple commands, you can either use `hasCommand` multiple times, or pass an array to `hasCommands` ```php $package ->name('your-package-name') ->hasCommands([ YourCoolPackageCommand::class, YourOtherCoolPackageCommand::class, ]); ``` ### Adding an installer command Instead of letting your users manually publishing config files, migrations, and other files manually, you could opt to add an install command that does all this work in one go. Packages like Laravel Horizon and Livewire provide such commands. When using Laravel Package Tools, you don't have to write an `InstallCommand` yourself. Instead, you can simply call, `hasInstallCommand` and configure it using a closure. Here's an example. ```php use Spatie\LaravelPackageTools\PackageServiceProvider; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\Commands\InstallCommand; class YourPackageServiceProvider extends PackageServiceProvider { public function configurePackage(Package $package): void { $package ->name('your-package-name') ->hasConfigFile() ->hasMigration('create_package_tables') ->publishesServiceProvider('MyServiceProviderName') ->hasInstallCommand(function(InstallCommand $command) { $command ->publishConfigFile() ->publishAssets() ->publishMigrations() ->askToRunMigrations() ->copyAndRegisterServiceProviderInApp() ->askToStarRepoOnGitHub('your-vendor/your-repo-name') }); } } ``` With this in place, the package user can call this command: ```bash php artisan your-package-name:install ``` Using the code above, that command will: - publish the config file - publish the assets - publish the migrations - copy the `/resources/stubs/MyProviderName.php.stub` from your package to `app/Providers/MyServiceProviderName.php`, and also register that provider in `config/app.php` - ask if migrations should be run now - prompt the user to open up `https://github.com/'your-vendor/your-repo-name'` in the browser in order to star it You can also call `startWith` and `endWith` on the `InstallCommand`. They will respectively be executed at the start and end when running `php artisan your-package-name:install`. You can use this to perform extra work or display extra output. ```php use use Spatie\LaravelPackageTools\Commands\InstallCommand; public function configurePackage(Package $package): void { $package // ... configure package ->hasInstallCommand(function(InstallCommand $command) { $command ->startWith(function(InstallCommand $command) { $command->info('Hello, and welcome to my great new package!'); }) ->publishConfigFile() ->publishAssets() ->publishMigrations() ->askToRunMigrations() ->copyAndRegisterServiceProviderInApp() ->askToStarRepoOnGitHub('your-vendor/your-repo-name') ->endWith(function(InstallCommand $command) { $command->info('Have a great day!'); }) }); } ``` ### Working with routes The `PackageServiceProvider` assumes that any route files are placed in this directory: `<package root>/routes`. Inside that directory you can put any route files. To register your route, you should pass its name without the extension to the `hasRoute` method. If your route file is called `web.php` you can register them like this: ```php $package ->name('your-package-name') ->hasRoute('web'); ``` Should your package contain multiple route files, you can just call `hasRoute` multiple times or use `hasRoutes`. ```php $package ->name('your-package-name') ->hasRoutes(['web', 'admin']); ``` ### Using lifecycle hooks You can put any custom logic your package needs while starting up in one of these methods: - `registeringPackage`: will be called at the start of the `register` method of `PackageServiceProvider` - `packageRegistered`: will be called at the end of the `register` method of `PackageServiceProvider` - `bootingPackage`: will be called at the start of the `boot` method of `PackageServiceProvider` - `packageBooted`: will be called at the end of the `boot` method of `PackageServiceProvider` ## Testing ```bash composer test ``` ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security Vulnerabilities Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. laravel-package-tools/composer.json 0000644 00000002371 15105603711 0013445 0 ustar 00 { "name": "spatie/laravel-package-tools", "description": "Tools for creating Laravel packages", "keywords": [ "spatie", "laravel-package-tools" ], "homepage": "https://github.com/spatie/laravel-package-tools", "license": "MIT", "authors": [ { "name": "Freek Van der Herten", "email": "freek@spatie.be", "role": "Developer" } ], "require": { "php": "^8.0", "illuminate/contracts": "^9.28|^10.0" }, "require-dev": { "mockery/mockery": "^1.5", "orchestra/testbench": "^7.7|^8.0", "pestphp/pest": "^1.22", "phpunit/phpunit": "^9.5.24", "spatie/pest-plugin-test-time": "^1.1" }, "autoload": { "psr-4": { "Spatie\\LaravelPackageTools\\": "src" } }, "autoload-dev": { "psr-4": { "Spatie\\LaravelPackageTools\\Tests\\": "tests" } }, "scripts": { "test": "vendor/bin/pest", "test-coverage": "vendor/bin/pest --coverage" }, "config": { "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true } }, "minimum-stability": "dev", "prefer-stable": true } laravel-package-tools/LICENSE.md 0000644 00000002076 15105603711 0012331 0 ustar 00 The MIT License (MIT) Copyright (c) spatie <freek@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. robots-txt/src/RobotsTxt.php 0000644 00000013104 15105603711 0012137 0 ustar 00 <?php namespace Spatie\Robots; class RobotsTxt { protected static array $robotsCache = []; protected array $disallowsPerUserAgent = []; public static function readFrom(string $source): self { $content = @file_get_contents($source); return new self($content !== false ? $content : ''); } public function __construct(string $content) { $this->disallowsPerUserAgent = $this->getDisallowsPerUserAgent($content); } public static function create(string $source): self { if ( strpos($source, 'http') !== false && strpos($source, 'robots.txt') !== false ) { return self::readFrom($source); } return new self($source); } public function allows(string $url, string | null $userAgent = '*'): bool { $requestUri = ''; $parts = parse_url($url); if ($parts !== false) { if (isset($parts['path'])) { $requestUri .= $parts['path']; } if (isset($parts['query'])) { $requestUri .= '?'.$parts['query']; } elseif ($this->hasEmptyQueryString($url)) { $requestUri .= '?'; } } $disallows = $this->disallowsPerUserAgent[strtolower(trim($userAgent))] ?? $this->disallowsPerUserAgent['*'] ?? []; return ! $this->pathIsDenied($requestUri, $disallows); } protected function pathIsDenied(string $requestUri, array $disallows): bool { foreach ($disallows as $disallow) { if ($disallow === '') { continue; } $stopAtEndOfString = false; if ($disallow[-1] === '$') { // if the pattern ends with a dollar sign, the string must end there $disallow = substr($disallow, 0, -1); $stopAtEndOfString = true; } // convert to regexp $disallowRegexp = preg_quote($disallow, '/'); // the pattern must start at the beginning of the string... $disallowRegexp = '^'.$disallowRegexp; // ...and optionally stop at the end of the string if ($stopAtEndOfString) { $disallowRegexp .= '$'; } // replace (preg_quote'd) stars with an eager match $disallowRegexp = str_replace('\\*', '.*', $disallowRegexp); // enclose in delimiters $disallowRegexp = '/'.$disallowRegexp.'/'; if (preg_match($disallowRegexp, $requestUri) === 1) { return true; } } return false; } /** * Checks for an empty query string. * * This works around the fact that parse_url() will not set the 'query' key when the query string is empty. * See: https://bugs.php.net/bug.php?id=78385 */ protected function hasEmptyQueryString(string $url): bool { if ($url === '') { return false; } if ($url[-1] === '?') { // ends with ? return true; } if (strpos($url, '?#') !== false) { // empty query string, followed by a fragment return true; } return false; } protected function getDisallowsPerUserAgent(string $content): array { $lines = explode(PHP_EOL, $content); $lines = array_filter($lines); $disallowsPerUserAgent = []; $currentUserAgents = []; $treatAllowDisallowLine = false; foreach ($lines as $line) { if ($this->isComment($line)) { continue; } if ($this->isEmptyLine($line)) { continue; } if ($this->isUserAgentLine($line)) { if ($treatAllowDisallowLine) { $treatAllowDisallowLine = false; $currentUserAgents = []; } $disallowsPerUserAgent[$this->parseUserAgent($line)] = []; $currentUserAgents[] = &$disallowsPerUserAgent[$this->parseUserAgent($line)]; continue; } if ($this->isDisallowLine($line)) { $treatAllowDisallowLine = true; } if ($this->isAllowLine($line)) { $treatAllowDisallowLine = true; continue; } $disallowUrl = $this->parseDisallow($line); foreach ($currentUserAgents as &$currentUserAgent) { $currentUserAgent[$disallowUrl] = $disallowUrl; } } return $disallowsPerUserAgent; } protected function isComment(string $line): bool { return strpos(trim($line), '#') === 0; } protected function isEmptyLine(string $line): bool { return trim($line) === ''; } protected function isUserAgentLine(string $line): bool { return strpos(trim(strtolower($line)), 'user-agent') === 0; } protected function parseUserAgent(string $line): string { return trim(str_replace('user-agent', '', strtolower(trim($line))), ': '); } protected function parseDisallow(string $line): string { return trim(substr_replace(strtolower(trim($line)), '', 0, 8), ': '); } protected function isDisallowLine(string $line): string { return trim(substr(str_replace(' ', '', strtolower(trim($line))), 0, 8), ': ') === 'disallow'; } protected function isAllowLine(string $line): string { return trim(substr(str_replace(' ', '', strtolower(trim($line))), 0, 6), ': ') === 'allow'; } } robots-txt/src/Robots.php 0000644 00000002765 15105603711 0011452 0 ustar 00 <?php namespace Spatie\Robots; class Robots { protected RobotsTxt | null $robotsTxt; public function __construct( protected string | null $userAgent = null, string | null $source = null, ) { $this->robotsTxt = $source ? RobotsTxt::readFrom($source) : null; } public function withTxt(string $source): self { $this->robotsTxt = RobotsTxt::readFrom($source); return $this; } public static function create(string $userAgent = null, string $source = null): self { return new self($userAgent, $source); } public function mayIndex(string $url, string $userAgent = null): bool { $userAgent = $userAgent ?? $this->userAgent; $robotsTxt = $this->robotsTxt ?? RobotsTxt::create($this->createRobotsUrl($url)); return $robotsTxt->allows($url, $userAgent) && RobotsMeta::readFrom($url)->mayIndex() && RobotsHeaders::readFrom($url)->mayIndex(); } public function mayFollowOn(string $url): bool { return RobotsMeta::readFrom($url)->mayFollow() && RobotsHeaders::readFrom($url)->mayFollow(); } protected function createRobotsUrl(string $url): string { $robotsUrl = parse_url($url, PHP_URL_SCHEME).'://'.parse_url($url, PHP_URL_HOST); if ($port = parse_url($url, PHP_URL_PORT)) { $robotsUrl .= ":{$port}"; } return "{$robotsUrl}/robots.txt"; } } robots-txt/src/RobotsMeta.php 0000644 00000003477 15105603711 0012262 0 ustar 00 <?php namespace Spatie\Robots; use InvalidArgumentException; use JetBrains\PhpStorm\ArrayShape; class RobotsMeta { protected array $robotsMetaTagProperties = []; public static function readFrom(string $source): self { $content = @file_get_contents($source); if ($content === false) { throw new InvalidArgumentException("Could not read from source `{$source}`"); } return new self($content); } public static function create(string $source): self { return new self($source); } public function __construct(string $html) { $this->robotsMetaTagProperties = $this->findRobotsMetaTagProperties($html); } public function mayIndex(): bool { return ! $this->noindex(); } public function mayFollow(): bool { return ! $this->nofollow(); } public function noindex(): bool { return $this->robotsMetaTagProperties['noindex'] ?? false; } public function nofollow(): bool { return $this->robotsMetaTagProperties['nofollow'] ?? false; } #[ArrayShape(['noindex' => "bool", 'nofollow' => "bool"])] protected function findRobotsMetaTagProperties(string $html): array { $metaTagLine = $this->findRobotsMetaTagLine($html); return [ 'noindex' => $metaTagLine ? strpos(strtolower($metaTagLine), 'noindex') !== false : false, 'nofollow' => $metaTagLine ? strpos(strtolower($metaTagLine), 'nofollow') !== false : false, ]; } protected function findRobotsMetaTagLine(string $html): ?string { if (preg_match('/\<meta name=("|\')robots("|\').*?\>/mis', $html, $matches)) { return $matches[0]; } return null; } } robots-txt/src/RobotsHeaders.php 0000644 00000005754 15105603711 0012747 0 ustar 00 <?php namespace Spatie\Robots; use InvalidArgumentException; class RobotsHeaders { protected array $robotHeadersProperties = []; public static function readFrom(string $source): self { $content = @file_get_contents($source); if ($content === false) { throw new InvalidArgumentException("Could not read from source `{$source}`"); } return new self($http_response_header ?? []); } public static function create(array $headers): self { return new self($headers); } public function __construct(array $headers) { $this->robotHeadersProperties = $this->parseHeaders($headers); } public function mayIndex(string $userAgent = '*'): bool { return $this->none($userAgent) ? false : ! $this->noindex($userAgent); } public function mayFollow(string $userAgent = '*'): bool { return $this->none($userAgent) ? false : ! $this->nofollow($userAgent); } public function noindex(string $userAgent = '*'): bool { return $this->robotHeadersProperties[$userAgent]['noindex'] ?? $this->robotHeadersProperties['*']['noindex'] ?? false; } public function nofollow(string $userAgent = '*'): bool { return $this->robotHeadersProperties[$userAgent]['nofollow'] ?? $this->robotHeadersProperties['*']['nofollow'] ?? false; } public function none(string $userAgent = '*'): bool { return $this->robotHeadersProperties[$userAgent]['none'] ?? $this->robotHeadersProperties['*']['none'] ?? false; } protected function parseHeaders(array $headers): array { $robotHeaders = $this->filterRobotHeaders($headers); return array_reduce($robotHeaders, function (array $parsedHeaders, $header) { $header = $this->normalizeHeaders($header); $headerParts = explode(':', $header); $userAgent = count($headerParts) === 3 ? trim($headerParts[1]) : '*'; $options = end($headerParts); $parsedHeaders[$userAgent] = [ 'noindex' => strpos(strtolower($options), 'noindex') !== false, 'nofollow' => strpos(strtolower($options), 'nofollow') !== false, 'none' => strpos(strtolower($options), 'none') !== false, ]; return $parsedHeaders; }, []); } protected function filterRobotHeaders(array $headers): array { return array_filter($headers, function ($header) use ($headers) { $headerContent = $this->normalizeHeaders($headers[$header] ?? []); return strpos(strtolower($header), 'x-robots-tag') === 0 || strpos(strtolower($headerContent), 'x-robots-tag') === 0; }, ARRAY_FILTER_USE_KEY); } protected function normalizeHeaders($headers): string { return implode(',', (array) $headers); } } robots-txt/README.md 0000644 00000006173 15105603711 0010156 0 ustar 00 [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/support-ukraine.svg?t=1" />](https://supportukrainenow.org) # Parse `robots.txt`, `robots` meta and headers [](https://packagist.org/packages/spatie/robots-txt)  [](https://scrutinizer-ci.com/g/spatie/robots-txt) [](https://packagist.org/packages/spatie/robots-txt) Determine if a page may be crawled from robots.txt, robots meta tags and robot headers. ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/robots-txt.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/robots-txt) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation You can install the package via composer: ```bash composer require spatie/robots-txt ``` ## Usage ``` php $robots = Spatie\Robots\Robots::create(); $robots->mayIndex('https://www.spatie.be/nl/admin'); $robots->mayFollowOn('https://www.spatie.be/nl/admin'); ``` You can also specify a user agent: ``` php $robots = Spatie\Robots\Robots::create('UserAgent007'); ``` By default, `Robots` will look for a `robots.txt` file on `https://host.com/robots.txt`. Another location can be specified like so: ``` php $robots = Spatie\Robots\Robots::create() ->withTxt('https://www.spatie.be/robots-custom.txt'); $robots = Spatie\Robots\Robots::create() ->withTxt(__DIR__ . '/public/robots.txt'); ``` ### Testing ``` bash composer test ``` ### Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security Vulnerabilities Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Postcardware You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). ## Credits - [Brent Roose](https://github.com/brendt) - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. robots-txt/composer.json 0000644 00000001733 15105603711 0011416 0 ustar 00 { "name": "spatie/robots-txt", "description": "Determine if a page may be crawled from robots.txt and robots meta tags", "keywords": [ "spatie", "robots-txt" ], "homepage": "https://github.com/spatie/robots-txt", "license": "MIT", "authors": [ { "name": "Brent Roose", "email": "brent@spatie.be", "homepage": "https://spatie.be", "role": "Developer" } ], "require": { "php": "^8.0" }, "require-dev": { "larapack/dd": "^1.0", "phpunit/phpunit": "^8.0 || ^9.0" }, "autoload": { "psr-4": { "Spatie\\Robots\\": "src" } }, "autoload-dev": { "psr-4": { "Spatie\\Robots\\Tests\\": "tests" } }, "scripts": { "test": "vendor/bin/phpunit", "test-coverage": "phpunit --coverage-html coverage" }, "config": { "sort-packages": true } } robots-txt/LICENSE.md 0000644 00000002102 15105603711 0010267 0 ustar 00 The MIT License (MIT) Copyright (c) Spatie bvba <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. robots-txt/CHANGELOG.md 0000644 00000002064 15105603711 0010503 0 ustar 00 # Changelog All notable changes to `robots-txt` will be documented in this file ## 2.0.1 - 2021-05-06 - added x-robots-tag: none (#32) ## 2.0.0 - 2021-03-28 - require PHP 8+ - drop support for PHP 7.x - convert syntax to PHP 8 - remove deprecated methods - use php-cs-fixer & github workflow ## 1.0.10 - 2020-12-08 - handle multiple user-agent (#29) ## 1.0.9 - 2020-11-27 - add support for PHP 8.0 + move to GitHub actions (#27) ## 1.0.8 - 2020-09-12 - make user agent checks case-insensitive ## 1.0.7 - 2020-04-29 - fix find robots meta tag line if minified code (#23) ## 1.0.6 - 2020-04-07 - fix headers checking (nofollow, noindex) for custom userAgent (#21) ## 1.0.5 - 2019-08-08 - improvements around handling of wildcards, end-of-string, query string ## 1.0.4 - 2019-08-07 - improve readability ## 1.0.3 - 2019-03-11 - fix parsing robotstxt urls with keywords (#14) ## 1.0.2 - 2019-01-11 - make robots.txt check case insensitive ## 1.0.1 - 2018-05-07 - prevent exception if the domain has no robots.txt ## 1.0.0 - 2018-05-07 - initial release robots-txt/.php_cs.cache 0000644 00000003273 15105603711 0011214 0 ustar 00 {"php":"8.0.3","version":"2.18.5","indent":" ","lineEnding":"\n","rules":{"blank_line_after_namespace":true,"braces":true,"class_definition":true,"constant_case":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline","keep_multiple_spaces_after_comma":true},"no_break_comment":true,"no_closing_tag":true,"no_spaces_after_function_name":true,"no_spaces_inside_parenthesis":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"visibility_required":true,"encoding":true,"full_opening_tag":true,"array_syntax":{"syntax":"short"},"ordered_imports":{"sortAlgorithm":"alpha"},"no_unused_imports":true,"not_operator_with_successor_space":true,"trailing_comma_in_multiline_array":true,"phpdoc_scalar":true,"unary_operator_spaces":true,"binary_operator_spaces":true,"blank_line_before_statement":{"statements":["break","continue","declare","return","throw","try"]},"phpdoc_single_line_var_spacing":true,"phpdoc_var_without_name":true,"class_attributes_separation":{"elements":["method"]},"single_trait_insert_per_statement":true},"hashes":{"src\/RobotsTxt.php":2756145462,"src\/RobotsMeta.php":3792602502,"src\/RobotsHeaders.php":3731502293,"src\/Robots.php":3883080972,"tests\/RobotsHeadersTest.php":105355989,"tests\/RobotsTxtTest.php":4268517613,"tests\/TestCase.php":855669975,"tests\/RobotsTest.php":651922754,"tests\/RobotsMetaTest.php":335948072}} robots-txt/.php_cs.dist 0000644 00000002377 15105603711 0011120 0 ustar 00 <?php $finder = Symfony\Component\Finder\Finder::create() ->in([ __DIR__ . '/src', __DIR__ . '/tests', ]) ->name('*.php') ->notName('*.blade.php') ->ignoreDotFiles(true) ->ignoreVCS(true); return PhpCsFixer\Config::create() ->setRules([ '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], 'ordered_imports' => ['sortAlgorithm' => 'alpha'], 'no_unused_imports' => true, 'not_operator_with_successor_space' => true, 'trailing_comma_in_multiline_array' => true, 'phpdoc_scalar' => true, 'unary_operator_spaces' => true, 'binary_operator_spaces' => true, 'blank_line_before_statement' => [ 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], ], 'phpdoc_single_line_var_spacing' => true, 'phpdoc_var_without_name' => true, 'class_attributes_separation' => [ 'elements' => [ 'method', ], ], 'method_argument_space' => [ 'on_multiline' => 'ensure_fully_multiline', 'keep_multiple_spaces_after_comma' => true, ], 'single_trait_insert_per_statement' => true, ]) ->setFinder($finder); robots-txt/.github/FUNDING.yml 0000644 00000000100 15105603711 0012034 0 ustar 00 github: spatie custom: https://spatie.be/open-source/support-us robots-txt/.github/workflows/run-tests.yml 0000644 00000002400 15105603711 0014750 0 ustar 00 name: Tests on: [push, pull_request] jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest] php: [8.1, 8.0] dependency-version: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v2 - name: Install and start test server run: | cd tests/server npm install (node server.js &) || /bin/true - name: Wait for server bootup run: sleep 5 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick coverage: none - name: Install dependencies run: composer update --no-interaction --prefer-source --no-suggest - name: Execute tests run: vendor/bin/phpunit robots-txt/.github/workflows/update-changelog.yml 0000644 00000001205 15105603711 0016215 0 ustar 00 name: "Update Changelog" on: release: types: [released] jobs: update: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 with: ref: main - name: Update Changelog uses: stefanzweifel/changelog-updater-action@v1 with: latest-version: ${{ github.event.release.name }} release-notes: ${{ github.event.release.body }} - name: Commit updated CHANGELOG uses: stefanzweifel/git-auto-commit-action@v4 with: branch: main commit_message: Update CHANGELOG file_pattern: CHANGELOG.md robots-txt/.github/workflows/php-cs-fixer.yml 0000644 00000000766 15105603711 0015326 0 ustar 00 name: Check & fix styling on: [push] jobs: php-cs-fixer: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} - name: Run PHP CS Fixer uses: docker://oskarstark/php-cs-fixer-ga with: args: --config=.php_cs.dist --allow-risky=yes - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: Fix styling crawler/src/CrawlObservers/CrawlObserver.php 0000644 00000002534 15105603711 0015231 0 ustar 00 <?php namespace Spatie\Crawler\CrawlObservers; use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; abstract class CrawlObserver { /** * Called when the crawler will crawl the url. * * @param \Psr\Http\Message\UriInterface $url */ public function willCrawl(UriInterface $url): void { } /** * Called when the crawler has crawled the given url successfully. * * @param \Psr\Http\Message\UriInterface $url * @param \Psr\Http\Message\ResponseInterface $response * @param \Psr\Http\Message\UriInterface|null $foundOnUrl */ abstract public function crawled( UriInterface $url, ResponseInterface $response, ?UriInterface $foundOnUrl = null ): void; /** * Called when the crawler had a problem crawling the given url. * * @param \Psr\Http\Message\UriInterface $url * @param \GuzzleHttp\Exception\RequestException $requestException * @param \Psr\Http\Message\UriInterface|null $foundOnUrl */ abstract public function crawlFailed( UriInterface $url, RequestException $requestException, ?UriInterface $foundOnUrl = null ): void; /** * Called when the crawl has ended. */ public function finishedCrawling(): void { } } crawler/src/CrawlObservers/CrawlObserverCollection.php 0000644 00000004132 15105603711 0017241 0 ustar 00 <?php namespace Spatie\Crawler\CrawlObservers; use ArrayAccess; use GuzzleHttp\Exception\RequestException; use Iterator; use Psr\Http\Message\ResponseInterface; use Spatie\Crawler\CrawlUrl; class CrawlObserverCollection implements ArrayAccess, Iterator { protected int $position; public function __construct(protected array $observers = []) { $this->position = 0; } public function addObserver(CrawlObserver $observer): void { $this->observers[] = $observer; } public function crawled(CrawlUrl $crawlUrl, ResponseInterface $response): void { foreach ($this->observers as $crawlObserver) { $crawlObserver->crawled( $crawlUrl->url, $response, $crawlUrl->foundOnUrl ); } } public function crawlFailed(CrawlUrl $crawlUrl, RequestException $exception): void { foreach ($this->observers as $crawlObserver) { $crawlObserver->crawlFailed( $crawlUrl->url, $exception, $crawlUrl->foundOnUrl ); } } public function current(): mixed { return $this->observers[$this->position]; } public function offsetGet(mixed $offset): mixed { return $this->observers[$offset] ?? null; } public function offsetSet(mixed $offset, mixed $value): void { if (is_null($offset)) { $this->observers[] = $value; } else { $this->observers[$offset] = $value; } } public function offsetExists(mixed $offset): bool { return isset($this->observers[$offset]); } public function offsetUnset(mixed $offset): void { unset($this->observers[$offset]); } public function next(): void { $this->position++; } public function key(): mixed { return $this->position; } public function valid(): bool { return isset($this->observers[$this->position]); } public function rewind(): void { $this->position = 0; } } crawler/src/Exceptions/InvalidCrawlRequestHandler.php 0000644 00000000562 15105603711 0017054 0 ustar 00 <?php namespace Spatie\Crawler\Exceptions; use RuntimeException; class InvalidCrawlRequestHandler extends RuntimeException { public static function doesNotExtendBaseClass(string $handlerClass, string $baseClass): static { return new static("`{$handlerClass} is not a valid handler class. A valid handler class should extend `{$baseClass}`."); } } crawler/src/Exceptions/InvalidUrl.php 0000644 00000001056 15105603711 0013676 0 ustar 00 <?php namespace Spatie\Crawler\Exceptions; use Exception; use Psr\Http\Message\UriInterface; use Spatie\Crawler\CrawlUrl; class InvalidUrl extends Exception { public static function unexpectedType(mixed $url): static { $crawlUrlClass = CrawlUrl::class; $uriInterfaceClass = UriInterface::class; $givenUrlClass = is_object($url) ? get_class($url) : gettype($url); return new static("You passed an invalid url of type `{$givenUrlClass}`. This should be either a {$crawlUrlClass} or `{$uriInterfaceClass}`"); } } crawler/src/Exceptions/UrlNotFoundByIndex.php 0000644 00000000172 15105603711 0015325 0 ustar 00 <?php namespace Spatie\Crawler\Exceptions; use RuntimeException; class UrlNotFoundByIndex extends RuntimeException { } crawler/src/ResponseWithCachedBody.php 0000644 00000001336 15105603711 0014045 0 ustar 00 <?php namespace Spatie\Crawler; use GuzzleHttp\Psr7\Response; use Psr\Http\Message\ResponseInterface; class ResponseWithCachedBody extends Response { protected ?string $cachedBody = null; public static function fromGuzzlePsr7Response(ResponseInterface $response): static { return new static( $response->getStatusCode(), $response->getHeaders(), $response->getBody(), $response->getProtocolVersion(), $response->getReasonPhrase() ); } public function setCachedBody(?string $body = null): void { $this->cachedBody = $body; } public function getCachedBody(): ?string { return $this->cachedBody; } } crawler/src/CrawlProfiles/CrawlProfile.php 0000644 00000000267 15105603711 0014654 0 ustar 00 <?php namespace Spatie\Crawler\CrawlProfiles; use Psr\Http\Message\UriInterface; abstract class CrawlProfile { abstract public function shouldCrawl(UriInterface $url): bool; } crawler/src/CrawlProfiles/CrawlAllUrls.php 0000644 00000000332 15105603711 0014623 0 ustar 00 <?php namespace Spatie\Crawler\CrawlProfiles; use Psr\Http\Message\UriInterface; class CrawlAllUrls extends CrawlProfile { public function shouldCrawl(UriInterface $url): bool { return true; } } crawler/src/CrawlProfiles/CrawlInternalUrls.php 0000644 00000001001 15105603711 0015661 0 ustar 00 <?php namespace Spatie\Crawler\CrawlProfiles; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\UriInterface; class CrawlInternalUrls extends CrawlProfile { protected mixed $baseUrl; public function __construct($baseUrl) { if (! $baseUrl instanceof UriInterface) { $baseUrl = new Uri($baseUrl); } $this->baseUrl = $baseUrl; } public function shouldCrawl(UriInterface $url): bool { return $this->baseUrl->getHost() === $url->getHost(); } } crawler/src/CrawlProfiles/CrawlSubdomains.php 0000644 00000001206 15105603711 0015352 0 ustar 00 <?php namespace Spatie\Crawler\CrawlProfiles; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\UriInterface; class CrawlSubdomains extends CrawlProfile { protected mixed $baseUrl; public function __construct($baseUrl) { if (! $baseUrl instanceof UriInterface) { $baseUrl = new Uri($baseUrl); } $this->baseUrl = $baseUrl; } public function shouldCrawl(UriInterface $url): bool { return $this->isSubdomainOfHost($url); } public function isSubdomainOfHost(UriInterface $url): bool { return str_ends_with($url->getHost(), $this->baseUrl->getHost()); } } crawler/src/LinkAdder.php 0000644 00000006351 15105603711 0011344 0 ustar 00 <?php namespace Spatie\Crawler; use GuzzleHttp\Psr7\Uri; use Illuminate\Support\Collection; use InvalidArgumentException; use Psr\Http\Message\UriInterface; use Symfony\Component\DomCrawler\Crawler as DomCrawler; use Symfony\Component\DomCrawler\Link; use Tree\Node\Node; class LinkAdder { protected Crawler $crawler; public function __construct(Crawler $crawler) { $this->crawler = $crawler; } public function addFromHtml(string $html, UriInterface $foundOnUrl): void { $allLinks = $this->extractLinksFromHtml($html, $foundOnUrl); collect($allLinks) ->filter(fn (UriInterface $url) => $this->hasCrawlableScheme($url)) ->map(fn (UriInterface $url) => $this->normalizeUrl($url)) ->filter(function (UriInterface $url) use ($foundOnUrl) { if (! $node = $this->crawler->addToDepthTree($url, $foundOnUrl)) { return false; } return $this->shouldCrawl($node); }) ->filter(fn (UriInterface $url) => ! str_contains($url->getPath(), '/tel:')) ->each(function (UriInterface $url) use ($foundOnUrl) { $crawlUrl = CrawlUrl::create($url, $foundOnUrl); $this->crawler->addToCrawlQueue($crawlUrl); }); } protected function extractLinksFromHtml(string $html, UriInterface $foundOnUrl): ?Collection { $domCrawler = new DomCrawler($html, $foundOnUrl); return collect($domCrawler->filterXpath('//a | //link[@rel="next" or @rel="prev"]')->links()) ->reject(function (Link $link) { if ($this->isInvalidHrefNode($link)) { return true; } if ($this->crawler->mustRejectNofollowLinks() && $link->getNode()->getAttribute('rel') === 'nofollow') { return true; } return false; }) ->map(function (Link $link) { try { return new Uri($link->getUri()); } catch (InvalidArgumentException $exception) { return; } }) ->filter(); } protected function hasCrawlableScheme(UriInterface $uri): bool { return in_array($uri->getScheme(), ['http', 'https']); } protected function normalizeUrl(UriInterface $url): UriInterface { return $url->withFragment(''); } protected function shouldCrawl(Node $node): bool { if ($this->crawler->mustRespectRobots() && ! $this->crawler->getRobotsTxt()->allows($node->getValue(), $this->crawler->getUserAgent())) { return false; } $maximumDepth = $this->crawler->getMaximumDepth(); if (is_null($maximumDepth)) { return true; } return $node->getDepth() <= $maximumDepth; } protected function isInvalidHrefNode(Link $link): bool { if ($link->getNode()->nodeName !== 'a') { return false; } if ($link->getNode()->nextSibling !== null) { return false; } if ($link->getNode()->childNodes->length !== 0) { return false; } return true; } } crawler/src/Handlers/CrawlRequestFulfilled.php 0000644 00000010242 15105603711 0015511 0 ustar 00 <?php namespace Spatie\Crawler\Handlers; use Exception; use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Utils; use GuzzleHttp\RedirectMiddleware; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; use Spatie\Crawler\Crawler; use Spatie\Crawler\CrawlerRobots; use Spatie\Crawler\CrawlProfiles\CrawlSubdomains; use Spatie\Crawler\CrawlUrl; use Spatie\Crawler\LinkAdder; use Spatie\Crawler\ResponseWithCachedBody; class CrawlRequestFulfilled { protected LinkAdder $linkAdder; public function __construct(protected Crawler $crawler) { $this->linkAdder = new LinkAdder($this->crawler); } public function __invoke(ResponseInterface $response, $index) { $body = $this->getBody($response); $robots = new CrawlerRobots( $response->getHeaders(), $body, $this->crawler->mustRespectRobots() ); $crawlUrl = $this->crawler->getCrawlQueue()->getUrlById($index); if ($this->crawler->mayExecuteJavaScript()) { $body = $this->getBodyAfterExecutingJavaScript($crawlUrl->url); $response = $response->withBody(Utils::streamFor($body)); } $responseWithCachedBody = ResponseWithCachedBody::fromGuzzlePsr7Response($response); $responseWithCachedBody->setCachedBody($body); if ($robots->mayIndex()) { $this->handleCrawled($responseWithCachedBody, $crawlUrl); } if (! $this->crawler->getCrawlProfile() instanceof CrawlSubdomains) { if ($crawlUrl->url->getHost() !== $this->crawler->getBaseUrl()->getHost()) { return; } } if (! $robots->mayFollow()) { return; } $baseUrl = $this->getBaseUrl($response, $crawlUrl); $this->linkAdder->addFromHtml($body, $baseUrl); usleep($this->crawler->getDelayBetweenRequests()); } protected function getBaseUrl(ResponseInterface $response, CrawlUrl $crawlUrl): Uri { $redirectHistory = $response->getHeader(RedirectMiddleware::HISTORY_HEADER); if (empty($redirectHistory)) { return $crawlUrl->url; } return new Uri(end($redirectHistory)); } protected function handleCrawled(ResponseInterface $response, CrawlUrl $crawlUrl): void { $this->crawler->getCrawlObservers()->crawled($crawlUrl, $response); } protected function getBody(ResponseInterface $response): string { $contentType = $response->getHeaderLine('Content-Type'); if (! $this->isMimetypeAllowedToParse($contentType)) { return ''; } return $this->convertBodyToString($response->getBody(), $this->crawler->getMaximumResponseSize()); } protected function convertBodyToString(StreamInterface $bodyStream, $readMaximumBytes = 1024 * 1024 * 2): string { if ($bodyStream->isSeekable()) { $bodyStream->rewind(); } $body = ''; $chunksToRead = $readMaximumBytes < 512 ? $readMaximumBytes : 512; for ($bytesRead = 0; $bytesRead < $readMaximumBytes; $bytesRead += $chunksToRead) { try { $newDataRead = $bodyStream->read($chunksToRead); } catch (Exception $exception) { $newDataRead = null; } if (! $newDataRead) { break; } $body .= $newDataRead; } return $body; } protected function getBodyAfterExecutingJavaScript(UriInterface $url): string { $browsershot = $this->crawler->getBrowsershot(); $html = $browsershot->setUrl((string) $url)->bodyHtml(); return html_entity_decode($html); } protected function isMimetypeAllowedToParse($contentType): bool { if (empty($contentType)) { return true; } if (! count($this->crawler->getParseableMimeTypes())) { return true; } foreach ($this->crawler->getParseableMimeTypes() as $allowedType) { if (stristr($contentType, $allowedType)) { return true; } } return false; } } crawler/src/Handlers/CrawlRequestFailed.php 0000644 00000001450 15105603711 0014770 0 ustar 00 <?php namespace Spatie\Crawler\Handlers; use Exception; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use Spatie\Crawler\Crawler; class CrawlRequestFailed { public function __construct(protected Crawler $crawler) { // } public function __invoke(Exception $exception, $index) { if ($exception instanceof ConnectException) { $exception = new RequestException($exception->getMessage(), $exception->getRequest()); } if ($exception instanceof RequestException) { $crawlUrl = $this->crawler->getCrawlQueue()->getUrlById($index); $this->crawler->getCrawlObservers()->crawlFailed($crawlUrl, $exception); } usleep($this->crawler->getDelayBetweenRequests()); } } crawler/src/CrawlUrl.php 0000644 00000001372 15105603711 0011240 0 ustar 00 <?php namespace Spatie\Crawler; use Psr\Http\Message\UriInterface; class CrawlUrl { public UriInterface $url; public ?UriInterface $foundOnUrl = null; protected mixed $id; public static function create(UriInterface $url, ?UriInterface $foundOnUrl = null, $id = null): static { $static = new static($url, $foundOnUrl); if ($id !== null) { $static->setId($id); } return $static; } protected function __construct(UriInterface $url, $foundOnUrl = null) { $this->url = $url; $this->foundOnUrl = $foundOnUrl; } public function getId(): mixed { return $this->id; } public function setId($id): void { $this->id = $id; } } crawler/src/CrawlQueues/ArrayCrawlQueue.php 0000644 00000004512 15105603711 0015020 0 ustar 00 <?php namespace Spatie\Crawler\CrawlQueues; use Psr\Http\Message\UriInterface; use Spatie\Crawler\CrawlUrl; use Spatie\Crawler\Exceptions\InvalidUrl; use Spatie\Crawler\Exceptions\UrlNotFoundByIndex; class ArrayCrawlQueue implements CrawlQueue { /** * All known URLs, indexed by URL string. * * @var CrawlUrl[] */ protected array $urls = []; /** * Pending URLs, indexed by URL string. * * @var CrawlUrl[] */ protected array $pendingUrls = []; public function add(CrawlUrl $crawlUrl): CrawlQueue { $urlString = (string) $crawlUrl->url; if (! isset($this->urls[$urlString])) { $crawlUrl->setId($urlString); $this->urls[$urlString] = $crawlUrl; $this->pendingUrls[$urlString] = $crawlUrl; } return $this; } public function hasPendingUrls(): bool { return (bool) $this->pendingUrls; } public function getUrlById($id): CrawlUrl { if (! isset($this->urls[$id])) { throw new UrlNotFoundByIndex("Crawl url {$id} not found in collection."); } return $this->urls[$id]; } public function hasAlreadyBeenProcessed(CrawlUrl $crawlUrl): bool { $urlString = (string) $crawlUrl->url; if (isset($this->pendingUrls[$urlString])) { return false; } if (isset($this->urls[$urlString])) { return true; } return false; } public function markAsProcessed(CrawlUrl $crawlUrl): void { $urlString = (string) $crawlUrl->url; unset($this->pendingUrls[$urlString]); } public function getProcessedUrlCount(): int { return count($this->urls) - count($this->pendingUrls); } public function has(CrawlUrl | UriInterface $crawlUrl): bool { if ($crawlUrl instanceof CrawlUrl) { $urlString = (string) $crawlUrl->url; } elseif ($crawlUrl instanceof UriInterface) { $urlString = (string) $crawlUrl; } else { throw InvalidUrl::unexpectedType($crawlUrl); } return isset($this->urls[$urlString]); } public function getPendingUrl(): ?CrawlUrl { foreach ($this->pendingUrls as $pendingUrl) { return $pendingUrl; } return null; } } crawler/src/CrawlQueues/CrawlQueue.php 0000644 00000001074 15105603711 0014021 0 ustar 00 <?php namespace Spatie\Crawler\CrawlQueues; use Psr\Http\Message\UriInterface; use Spatie\Crawler\CrawlUrl; interface CrawlQueue { public function add(CrawlUrl $url): self; public function has(CrawlUrl | UriInterface $crawlUrl): bool; public function hasPendingUrls(): bool; public function getUrlById($id): CrawlUrl; public function getPendingUrl(): ?CrawlUrl; public function hasAlreadyBeenProcessed(CrawlUrl $url): bool; public function markAsProcessed(CrawlUrl $crawlUrl): void; public function getProcessedUrlCount(): int; } crawler/src/Crawler.php 0000644 00000032344 15105603711 0011107 0 ustar 00 <?php namespace Spatie\Crawler; use Generator; use GuzzleHttp\Client; use GuzzleHttp\Pool; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Uri; use GuzzleHttp\RequestOptions; use Psr\Http\Message\UriInterface; use Spatie\Browsershot\Browsershot; use Spatie\Crawler\CrawlObservers\CrawlObserver; use Spatie\Crawler\CrawlObservers\CrawlObserverCollection; use Spatie\Crawler\CrawlProfiles\CrawlAllUrls; use Spatie\Crawler\CrawlProfiles\CrawlProfile; use Spatie\Crawler\CrawlQueues\ArrayCrawlQueue; use Spatie\Crawler\CrawlQueues\CrawlQueue; use Spatie\Crawler\Exceptions\InvalidCrawlRequestHandler; use Spatie\Crawler\Handlers\CrawlRequestFailed; use Spatie\Crawler\Handlers\CrawlRequestFulfilled; use Spatie\Robots\RobotsTxt; use Tree\Node\Node; class Crawler { public const DEFAULT_USER_AGENT = '*'; protected UriInterface $baseUrl; protected CrawlObserverCollection $crawlObservers; protected CrawlProfile $crawlProfile; protected CrawlQueue $crawlQueue; protected int $totalUrlCount = 0; protected int $currentUrlCount = 0; protected ?int $totalCrawlLimit = null; protected ?int $currentCrawlLimit = null; protected int $maximumResponseSize = 1024 * 1024 * 2; protected ?int $maximumDepth = null; protected bool $respectRobots = true; protected bool $rejectNofollowLinks = true; protected Node $depthTree; protected bool $executeJavaScript = false; protected ?Browsershot $browsershot = null; protected ?RobotsTxt $robotsTxt = null; protected string $crawlRequestFulfilledClass; protected string $crawlRequestFailedClass; protected int $delayBetweenRequests = 0; protected array $allowedMimeTypes = []; protected string $defaultScheme = 'http'; protected static array $defaultClientOptions = [ RequestOptions::COOKIES => true, RequestOptions::CONNECT_TIMEOUT => 10, RequestOptions::TIMEOUT => 10, RequestOptions::ALLOW_REDIRECTS => false, RequestOptions::HEADERS => [ 'User-Agent' => self::DEFAULT_USER_AGENT, ], ]; public static function create(array $clientOptions = []): static { $clientOptions = (count($clientOptions)) ? $clientOptions : static::$defaultClientOptions; $client = new Client($clientOptions); return new static($client); } public function __construct( protected Client $client, protected int $concurrency = 10, ) { $this->crawlProfile = new CrawlAllUrls(); $this->crawlQueue = new ArrayCrawlQueue(); $this->crawlObservers = new CrawlObserverCollection(); $this->crawlRequestFulfilledClass = CrawlRequestFulfilled::class; $this->crawlRequestFailedClass = CrawlRequestFailed::class; } public function getDefaultScheme(): string { return $this->defaultScheme; } public function setDefaultScheme(string $defaultScheme): self { $this->defaultScheme = $defaultScheme; return $this; } public function setConcurrency(int $concurrency): self { $this->concurrency = $concurrency; return $this; } public function setMaximumResponseSize(int $maximumResponseSizeInBytes): self { $this->maximumResponseSize = $maximumResponseSizeInBytes; return $this; } public function getMaximumResponseSize(): ?int { return $this->maximumResponseSize; } public function setTotalCrawlLimit(int $totalCrawlLimit): self { $this->totalCrawlLimit = $totalCrawlLimit; return $this; } public function getTotalCrawlLimit(): ?int { return $this->totalCrawlLimit; } public function getTotalCrawlCount(): int { return $this->totalUrlCount; } public function setCurrentCrawlLimit(int $currentCrawlLimit): self { $this->currentCrawlLimit = $currentCrawlLimit; return $this; } public function getCurrentCrawlLimit(): ?int { return $this->currentCrawlLimit; } public function getCurrentCrawlCount(): int { return $this->currentUrlCount; } public function setMaximumDepth(int $maximumDepth): self { $this->maximumDepth = $maximumDepth; return $this; } public function getMaximumDepth(): ?int { return $this->maximumDepth; } public function setDelayBetweenRequests(int $delayInMilliseconds): self { $this->delayBetweenRequests = ($delayInMilliseconds * 1000); return $this; } public function getDelayBetweenRequests(): int { return $this->delayBetweenRequests; } public function setParseableMimeTypes(array $types): self { $this->allowedMimeTypes = $types; return $this; } public function getParseableMimeTypes(): array { return $this->allowedMimeTypes; } public function ignoreRobots(): self { $this->respectRobots = false; return $this; } public function respectRobots(): self { $this->respectRobots = true; return $this; } public function mustRespectRobots(): bool { return $this->respectRobots; } public function acceptNofollowLinks(): self { $this->rejectNofollowLinks = false; return $this; } public function rejectNofollowLinks(): self { $this->rejectNofollowLinks = true; return $this; } public function mustRejectNofollowLinks(): bool { return $this->rejectNofollowLinks; } public function getRobotsTxt(): RobotsTxt { return $this->robotsTxt; } public function setCrawlQueue(CrawlQueue $crawlQueue): self { $this->crawlQueue = $crawlQueue; return $this; } public function getCrawlQueue(): CrawlQueue { return $this->crawlQueue; } public function executeJavaScript(): self { $this->executeJavaScript = true; return $this; } public function doNotExecuteJavaScript(): self { $this->executeJavaScript = false; return $this; } public function mayExecuteJavascript(): bool { return $this->executeJavaScript; } public function setCrawlObserver(CrawlObserver | array $crawlObservers): self { if (! is_array($crawlObservers)) { $crawlObservers = [$crawlObservers]; } return $this->setCrawlObservers($crawlObservers); } public function setCrawlObservers(array $crawlObservers): self { $this->crawlObservers = new CrawlObserverCollection($crawlObservers); return $this; } public function addCrawlObserver(CrawlObserver $crawlObserver): self { $this->crawlObservers->addObserver($crawlObserver); return $this; } public function getCrawlObservers(): CrawlObserverCollection { return $this->crawlObservers; } public function setCrawlProfile(CrawlProfile $crawlProfile): self { $this->crawlProfile = $crawlProfile; return $this; } public function getCrawlProfile(): CrawlProfile { return $this->crawlProfile; } public function setCrawlFulfilledHandlerClass(string $crawlRequestFulfilledClass): self { $baseClass = CrawlRequestFulfilled::class; if (! is_subclass_of($crawlRequestFulfilledClass, $baseClass)) { throw InvalidCrawlRequestHandler::doesNotExtendBaseClass($crawlRequestFulfilledClass, $baseClass); } $this->crawlRequestFulfilledClass = $crawlRequestFulfilledClass; return $this; } public function setCrawlFailedHandlerClass(string $crawlRequestFailedClass): self { $baseClass = CrawlRequestFailed::class; if (! is_subclass_of($crawlRequestFailedClass, $baseClass)) { throw InvalidCrawlRequestHandler::doesNotExtendBaseClass($crawlRequestFailedClass, $baseClass); } $this->crawlRequestFailedClass = $crawlRequestFailedClass; return $this; } public function setBrowsershot(Browsershot $browsershot) { $this->browsershot = $browsershot; return $this; } public function setUserAgent(string $userAgent): self { $clientOptions = $this->client->getConfig(); $headers = array_change_key_case($clientOptions['headers']); $headers['user-agent'] = $userAgent; $clientOptions['headers'] = $headers; $this->client = new Client($clientOptions); return $this; } public function getUserAgent(): string { $headers = $this->client->getConfig('headers'); foreach (array_keys($headers) as $name) { if (strtolower($name) === 'user-agent') { return (string) $headers[$name]; } } return static::DEFAULT_USER_AGENT; } public function getBrowsershot(): Browsershot { if (! $this->browsershot) { $this->browsershot = new Browsershot(); } return $this->browsershot; } public function getBaseUrl(): UriInterface { return $this->baseUrl; } public function startCrawling(UriInterface | string $baseUrl) { if (! $baseUrl instanceof UriInterface) { $baseUrl = new Uri($baseUrl); } if ($baseUrl->getScheme() === '') { $baseUrl = $baseUrl->withScheme($this->defaultScheme); } if ($baseUrl->getPath() === '') { $baseUrl = $baseUrl->withPath('/'); } $this->totalUrlCount = $this->crawlQueue->getProcessedUrlCount(); $this->baseUrl = $baseUrl; $crawlUrl = CrawlUrl::create($this->baseUrl); $this->robotsTxt = $this->createRobotsTxt($crawlUrl->url); if ($this->robotsTxt->allows((string) $crawlUrl->url, $this->getUserAgent()) || ! $this->respectRobots ) { $this->addToCrawlQueue($crawlUrl); } $this->depthTree = new Node((string) $this->baseUrl); $this->startCrawlingQueue(); foreach ($this->crawlObservers as $crawlObserver) { $crawlObserver->finishedCrawling(); } } public function addToDepthTree(UriInterface $url, UriInterface $parentUrl, Node $node = null): ?Node { if (is_null($this->maximumDepth)) { return new Node((string) $url); } $node = $node ?? $this->depthTree; $returnNode = null; if ($node->getValue() === (string) $parentUrl) { $newNode = new Node((string) $url); $node->addChild($newNode); return $newNode; } foreach ($node->getChildren() as $currentNode) { $returnNode = $this->addToDepthTree($url, $parentUrl, $currentNode); if (! is_null($returnNode)) { break; } } return $returnNode; } protected function startCrawlingQueue(): void { while ( $this->reachedCrawlLimits() === false && $this->crawlQueue->hasPendingUrls() ) { $pool = new Pool($this->client, $this->getCrawlRequests(), [ 'concurrency' => $this->concurrency, 'options' => $this->client->getConfig(), 'fulfilled' => new $this->crawlRequestFulfilledClass($this), 'rejected' => new $this->crawlRequestFailedClass($this), ]); $promise = $pool->promise(); $promise->wait(); } } protected function createRobotsTxt(UriInterface $uri): RobotsTxt { return RobotsTxt::create($uri->withPath('/robots.txt')); } protected function getCrawlRequests(): Generator { while ( $this->reachedCrawlLimits() === false && $crawlUrl = $this->crawlQueue->getPendingUrl() ) { if ( $this->crawlProfile->shouldCrawl($crawlUrl->url) === false || $this->crawlQueue->hasAlreadyBeenProcessed($crawlUrl) ) { $this->crawlQueue->markAsProcessed($crawlUrl); continue; } foreach ($this->crawlObservers as $crawlObserver) { $crawlObserver->willCrawl($crawlUrl->url); } $this->totalUrlCount++; $this->currentUrlCount++; $this->crawlQueue->markAsProcessed($crawlUrl); yield $crawlUrl->getId() => new Request('GET', $crawlUrl->url); } } public function addToCrawlQueue(CrawlUrl $crawlUrl): self { if (! $this->getCrawlProfile()->shouldCrawl($crawlUrl->url)) { return $this; } if ($this->getCrawlQueue()->has($crawlUrl->url)) { return $this; } $this->crawlQueue->add($crawlUrl); return $this; } public function reachedCrawlLimits(): bool { $totalCrawlLimit = $this->getTotalCrawlLimit(); if (! is_null($totalCrawlLimit) && $this->getTotalCrawlCount() >= $totalCrawlLimit) { return true; } $currentCrawlLimit = $this->getCurrentCrawlLimit(); if (! is_null($currentCrawlLimit) && $this->getCurrentCrawlCount() >= $currentCrawlLimit) { return true; } return false; } } crawler/src/CrawlerRobots.php 0000644 00000002214 15105603711 0012271 0 ustar 00 <?php namespace Spatie\Crawler; use Spatie\Robots\RobotsHeaders; use Spatie\Robots\RobotsMeta; class CrawlerRobots { protected RobotsHeaders $robotsHeaders; protected RobotsMeta $robotsMeta; protected bool $mustRespectRobots; public function __construct(array $headers, string $body, bool $mustRespectRobots) { $this->robotsHeaders = RobotsHeaders::create($headers); $this->robotsMeta = RobotsMeta::create($body); $this->mustRespectRobots = $mustRespectRobots; } public function mayIndex(): bool { if (! $this->mustRespectRobots) { return true; } if (! $this->robotsHeaders->mayIndex()) { return false; } if (! $this->robotsMeta->mayIndex()) { return false; } return true; } public function mayFollow(): bool { if (! $this->mustRespectRobots) { return true; } if (! $this->robotsHeaders->mayFollow()) { return false; } if (! $this->robotsMeta->mayFollow()) { return false; } return true; } } crawler/README.md 0000644 00000037500 15105603711 0007466 0 ustar 00 # 🕸 Crawl the web using PHP 🕷 [](https://packagist.org/packages/spatie/crawler) [](LICENSE.md)   [](https://packagist.org/packages/spatie/crawler) This package provides a class to crawl links on a website. Under the hood Guzzle promises are used to [crawl multiple urls concurrently](http://docs.guzzlephp.org/en/latest/quickstart.html?highlight=pool#concurrent-requests). Because the crawler can execute JavaScript, it can crawl JavaScript rendered sites. Under the hood [Chrome and Puppeteer](https://github.com/spatie/browsershot) are used to power this feature. ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/crawler.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/crawler) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation This package can be installed via Composer: ``` bash composer require spatie/crawler ``` ## Usage The crawler can be instantiated like this ```php use Spatie\Crawler\Crawler; Crawler::create() ->setCrawlObserver(<class that extends \Spatie\Crawler\CrawlObservers\CrawlObserver>) ->startCrawling($url); ``` The argument passed to `setCrawlObserver` must be an object that extends the `\Spatie\Crawler\CrawlObservers\CrawlObserver` abstract class: ```php namespace Spatie\Crawler\CrawlObservers; use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; abstract class CrawlObserver { /** * Called when the crawler will crawl the url. * * @param \Psr\Http\Message\UriInterface $url */ public function willCrawl(UriInterface $url): void { } /** * Called when the crawler has crawled the given url successfully. * * @param \Psr\Http\Message\UriInterface $url * @param \Psr\Http\Message\ResponseInterface $response * @param \Psr\Http\Message\UriInterface|null $foundOnUrl */ abstract public function crawled( UriInterface $url, ResponseInterface $response, ?UriInterface $foundOnUrl = null ): void; /** * Called when the crawler had a problem crawling the given url. * * @param \Psr\Http\Message\UriInterface $url * @param \GuzzleHttp\Exception\RequestException $requestException * @param \Psr\Http\Message\UriInterface|null $foundOnUrl */ abstract public function crawlFailed( UriInterface $url, RequestException $requestException, ?UriInterface $foundOnUrl = null ): void; /** * Called when the crawl has ended. */ public function finishedCrawling(): void { } } ``` ### Using multiple observers You can set multiple observers with `setCrawlObservers`: ```php Crawler::create() ->setCrawlObservers([ <class that extends \Spatie\Crawler\CrawlObservers\CrawlObserver>, <class that extends \Spatie\Crawler\CrawlObservers\CrawlObserver>, ... ]) ->startCrawling($url); ``` Alternatively you can set multiple observers one by one with `addCrawlObserver`: ```php Crawler::create() ->addCrawlObserver(<class that extends \Spatie\Crawler\CrawlObservers\CrawlObserver>) ->addCrawlObserver(<class that extends \Spatie\Crawler\CrawlObservers\CrawlObserver>) ->addCrawlObserver(<class that extends \Spatie\Crawler\CrawlObservers\CrawlObserver>) ->startCrawling($url); ``` ### Executing JavaScript By default, the crawler will not execute JavaScript. This is how you can enable the execution of JavaScript: ```php Crawler::create() ->executeJavaScript() ... ``` In order to make it possible to get the body html after the javascript has been executed, this package depends on our [Browsershot](https://github.com/spatie/browsershot) package. This package uses [Puppeteer](https://github.com/puppeteer/puppeteer) under the hood. Here are some pointers on [how to install it on your system](https://spatie.be/docs/browsershot/v2/requirements). Browsershot will make an educated guess as to where its dependencies are installed on your system. By default, the Crawler will instantiate a new Browsershot instance. You may find the need to set a custom created instance using the `setBrowsershot(Browsershot $browsershot)` method. ```php Crawler::create() ->setBrowsershot($browsershot) ->executeJavaScript() ... ``` Note that the crawler will still work even if you don't have the system dependencies required by Browsershot. These system dependencies are only required if you're calling `executeJavaScript()`. ### Filtering certain urls You can tell the crawler not to visit certain urls by using the `setCrawlProfile`-function. That function expects an object that extends `Spatie\Crawler\CrawlProfiles\CrawlProfile`: ```php /* * Determine if the given url should be crawled. */ public function shouldCrawl(UriInterface $url): bool; ``` This package comes with three `CrawlProfiles` out of the box: - `CrawlAllUrls`: this profile will crawl all urls on all pages including urls to an external site. - `CrawlInternalUrls`: this profile will only crawl the internal urls on the pages of a host. - `CrawlSubdomains`: this profile will only crawl the internal urls and its subdomains on the pages of a host. ### Ignoring robots.txt and robots meta By default, the crawler will respect robots data. It is possible to disable these checks like so: ```php Crawler::create() ->ignoreRobots() ... ``` Robots data can come from either a `robots.txt` file, meta tags or response headers. More information on the spec can be found here: [http://www.robotstxt.org/](http://www.robotstxt.org/). Parsing robots data is done by our package [spatie/robots-txt](https://github.com/spatie/robots-txt). ### Accept links with rel="nofollow" attribute By default, the crawler will reject all links containing attribute rel="nofollow". It is possible to disable these checks like so: ```php Crawler::create() ->acceptNofollowLinks() ... ``` ### Using a custom User Agent ### In order to respect robots.txt rules for a custom User Agent you can specify your own custom User Agent. ```php Crawler::create() ->setUserAgent('my-agent') ``` You can add your specific crawl rule group for 'my-agent' in robots.txt. This example disallows crawling the entire site for crawlers identified by 'my-agent'. ```txt // Disallow crawling for my-agent User-agent: my-agent Disallow: / ``` ## Setting the number of concurrent requests To improve the speed of the crawl the package concurrently crawls 10 urls by default. If you want to change that number you can use the `setConcurrency` method. ```php Crawler::create() ->setConcurrency(1) // now all urls will be crawled one by one ``` ## Defining Crawl Limits By default, the crawler continues until it has crawled every page it can find. This behavior might cause issues if you are working in an environment with limitations such as a serverless environment. The crawl behavior can be controlled with the following two options: - **Total Crawl Limit** (`setTotalCrawlLimit`): This limit defines the maximal count of URLs to crawl. - **Current Crawl Limit** (`setCurrentCrawlLimit`): This defines how many URLs are processed during the current crawl. Let's take a look at some examples to clarify the difference between these two methods. ### Example 1: Using the total crawl limit The `setTotalCrawlLimit` method allows to limit the total number of URLs to crawl, no matter often you call the crawler. ```php $queue = <your selection/implementation of a queue>; // Crawls 5 URLs and ends. Crawler::create() ->setCrawlQueue($queue) ->setTotalCrawlLimit(5) ->startCrawling($url); // Doesn't crawl further as the total limit is reached. Crawler::create() ->setCrawlQueue($queue) ->setTotalCrawlLimit(5) ->startCrawling($url); ``` ### Example 2: Using the current crawl limit The `setCurrentCrawlLimit` will set a limit on how many URls will be crawled per execution. This piece of code will process 5 pages with each execution, without a total limit of pages to crawl. ```php $queue = <your selection/implementation of a queue>; // Crawls 5 URLs and ends. Crawler::create() ->setCrawlQueue($queue) ->setCurrentCrawlLimit(5) ->startCrawling($url); // Crawls the next 5 URLs and ends. Crawler::create() ->setCrawlQueue($queue) ->setCurrentCrawlLimit(5) ->startCrawling($url); ``` ### Example 3: Combining the total and crawl limit Both limits can be combined to control the crawler: ```php $queue = <your selection/implementation of a queue>; // Crawls 5 URLs and ends. Crawler::create() ->setCrawlQueue($queue) ->setTotalCrawlLimit(10) ->setCurrentCrawlLimit(5) ->startCrawling($url); // Crawls the next 5 URLs and ends. Crawler::create() ->setCrawlQueue($queue) ->setTotalCrawlLimit(10) ->setCurrentCrawlLimit(5) ->startCrawling($url); // Doesn't crawl further as the total limit is reached. Crawler::create() ->setCrawlQueue($queue) ->setTotalCrawlLimit(10) ->setCurrentCrawlLimit(5) ->startCrawling($url); ``` ### Example 4: Crawling across requests You can use the `setCurrentCrawlLimit` to break up long running crawls. The following example demonstrates a (simplified) approach. It's made up of an initial request and any number of follow-up requests continuing the crawl. #### Initial Request To start crawling across different requests, you will need to create a new queue of your selected queue-driver. Start by passing the queue-instance to the crawler. The crawler will start filling the queue as pages are processed and new URLs are discovered. Serialize and store the queue reference after the crawler has finished (using the current crawl limit). ```php // Create a queue using your queue-driver. $queue = <your selection/implementation of a queue>; // Crawl the first set of URLs Crawler::create() ->setCrawlQueue($queue) ->setCurrentCrawlLimit(10) ->startCrawling($url); // Serialize and store your queue $serializedQueue = serialize($queue); ``` #### Subsequent Requests For any following requests you will need to unserialize your original queue and pass it to the crawler: ```php // Unserialize queue $queue = unserialize($serializedQueue); // Crawls the next set of URLs Crawler::create() ->setCrawlQueue($queue) ->setCurrentCrawlLimit(10) ->startCrawling($url); // Serialize and store your queue $serialized_queue = serialize($queue); ``` The behavior is based on the information in the queue. Only if the same queue-instance is passed in the behavior works as described. When a completely new queue is passed in, the limits of previous crawls -even for the same website- won't apply. An example with more details can be found [here](https://github.com/spekulatius/spatie-crawler-cached-queue-example). ## Setting the maximum crawl depth By default, the crawler continues until it has crawled every page of the supplied URL. If you want to limit the depth of the crawler you can use the `setMaximumDepth` method. ```php Crawler::create() ->setMaximumDepth(2) ``` ## Setting the maximum response size Most html pages are quite small. But the crawler could accidentally pick up on large files such as PDFs and MP3s. To keep memory usage low in such cases the crawler will only use the responses that are smaller than 2 MB. If, when streaming a response, it becomes larger than 2 MB, the crawler will stop streaming the response. An empty response body will be assumed. You can change the maximum response size. ```php // let's use a 3 MB maximum. Crawler::create() ->setMaximumResponseSize(1024 * 1024 * 3) ``` ## Add a delay between requests In some cases you might get rate-limited when crawling too aggressively. To circumvent this, you can use the `setDelayBetweenRequests()` method to add a pause between every request. This value is expressed in milliseconds. ```php Crawler::create() ->setDelayBetweenRequests(150) // After every page crawled, the crawler will wait for 150ms ``` ## Limiting which content-types to parse By default, every found page will be downloaded (up to `setMaximumResponseSize()` in size) and parsed for additional links. You can limit which content-types should be downloaded and parsed by setting the `setParseableMimeTypes()` with an array of allowed types. ```php Crawler::create() ->setParseableMimeTypes(['text/html', 'text/plain']) ``` This will prevent downloading the body of pages that have different mime types, like binary files, audio/video, ... that are unlikely to have links embedded in them. This feature mostly saves bandwidth. ## Using a custom crawl queue When crawling a site the crawler will put urls to be crawled in a queue. By default, this queue is stored in memory using the built-in `ArrayCrawlQueue`. When a site is very large you may want to store that queue elsewhere, maybe a database. In such cases, you can write your own crawl queue. A valid crawl queue is any class that implements the `Spatie\Crawler\CrawlQueues\CrawlQueue`-interface. You can pass your custom crawl queue via the `setCrawlQueue` method on the crawler. ```php Crawler::create() ->setCrawlQueue(<implementation of \Spatie\Crawler\CrawlQueues\CrawlQueue>) ``` Here - [ArrayCrawlQueue](https://github.com/spatie/crawler/blob/master/src/CrawlQueues/ArrayCrawlQueue.php) - [RedisCrawlQueue (third-party package)](https://github.com/repat/spatie-crawler-redis) - [CacheCrawlQueue for Laravel (third-party package)](https://github.com/spekulatius/spatie-crawler-toolkit-for-laravel) - [Laravel Model as Queue (third-party example app)](https://github.com/insign/spatie-crawler-queue-with-laravel-model) ## Change the default base url scheme By default, the crawler will set the base url scheme to `http` if none. You have the ability to change that with `setDefaultScheme`. ```php Crawler::create() ->setDefaultScheme('https') ``` ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Testing First, install the Puppeteer dependency, or your tests will fail. ``` npm install puppeteer ``` To run the tests you'll have to start the included node based server first in a separate terminal window. ```bash cd tests/server npm install node server.js ``` With the server running, you can start testing. ```bash composer test ``` ## Security If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. ## Postcardware You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium. We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). ## Credits - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. crawler/composer.json 0000644 00000002336 15105603711 0010730 0 ustar 00 { "name": "spatie/crawler", "description": "Crawl all internal links found on a website", "keywords": [ "spatie", "crawler", "link", "website" ], "homepage": "https://github.com/spatie/crawler", "license": "MIT", "authors": [ { "name": "Freek Van der Herten", "email": "freek@spatie.be" } ], "require": { "php": "^8.0", "guzzlehttp/guzzle": "^7.3", "guzzlehttp/psr7": "^2.0", "illuminate/collections": "^8.38|^9.0|^10.0", "nicmart/tree": "^0.3.0", "spatie/browsershot": "^3.45", "spatie/robots-txt": "^2.0", "symfony/dom-crawler": "^5.2|^6.0" }, "require-dev": { "pestphp/pest": "^1.21", "phpunit/phpunit": "^9.5" }, "config": { "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true, "phpstan/extension-installer": true } }, "autoload": { "psr-4": { "Spatie\\Crawler\\": "src" } }, "autoload-dev": { "psr-4": { "Spatie\\Crawler\\Test\\": "tests" } }, "scripts": { "test": "phpunit" } } crawler/LICENSE.md 0000644 00000002102 15105603711 0007601 0 ustar 00 The MIT License (MIT) Copyright (c) Spatie bvba <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. backtrace/src/Frame.php 0000644 00000003141 15105603711 0011013 0 ustar 00 <?php namespace Spatie\Backtrace; class Frame { /** @var string */ public $file; /** @var int */ public $lineNumber; /** @var array|null */ public $arguments = null; /** @var bool */ public $applicationFrame; /** @var string|null */ public $method; /** @var string|null */ public $class; public function __construct( string $file, int $lineNumber, ?array $arguments, string $method = null, string $class = null, bool $isApplicationFrame = false ) { $this->file = $file; $this->lineNumber = $lineNumber; $this->arguments = $arguments; $this->method = $method; $this->class = $class; $this->applicationFrame = $isApplicationFrame; } public function getSnippet(int $lineCount): array { return (new CodeSnippet()) ->surroundingLine($this->lineNumber) ->snippetLineCount($lineCount) ->get($this->file); } public function getSnippetAsString(int $lineCount): string { return (new CodeSnippet()) ->surroundingLine($this->lineNumber) ->snippetLineCount($lineCount) ->getAsString($this->file); } public function getSnippetProperties(int $lineCount): array { $snippet = $this->getSnippet($lineCount); return array_map(function (int $lineNumber) use ($snippet) { return [ 'line_number' => $lineNumber, 'text' => $snippet[$lineNumber], ]; }, array_keys($snippet)); } } backtrace/src/Arguments/ArgumentReducers.php 0000644 00000005444 15105603711 0015215 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\ArrayArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\BaseTypeArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\ClosureArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\DateTimeArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\DateTimeZoneArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\EnumArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\MinimalArrayArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\SensitiveParameterArrayReducer; use Spatie\Backtrace\Arguments\Reducers\StdClassArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\StringableArgumentReducer; use Spatie\Backtrace\Arguments\Reducers\SymphonyRequestArgumentReducer; class ArgumentReducers { /** @var array<int, ArgumentReducer> */ public $argumentReducers = []; /** * @param array<ArgumentReducer|class-string<ArgumentReducer>> $argumentReducers */ public static function create(array $argumentReducers): self { return new self(array_map( function ($argumentReducer) { /** @var $argumentReducer ArgumentReducer|class-string<ArgumentReducer> */ return $argumentReducer instanceof ArgumentReducer ? $argumentReducer : new $argumentReducer(); }, $argumentReducers )); } public static function default(array $extra = []): self { return new self(static::defaultReducers($extra)); } public static function minimal(array $extra = []): self { return new self(static::minimalReducers($extra)); } /** * @param array<int, ArgumentReducer> $argumentReducers */ protected function __construct(array $argumentReducers) { $this->argumentReducers = $argumentReducers; } protected static function defaultReducers(array $extra = []): array { return array_merge($extra, [ new BaseTypeArgumentReducer(), new ArrayArgumentReducer(), new StdClassArgumentReducer(), new EnumArgumentReducer(), new ClosureArgumentReducer(), new SensitiveParameterArrayReducer(), new DateTimeArgumentReducer(), new DateTimeZoneArgumentReducer(), new SymphonyRequestArgumentReducer(), new StringableArgumentReducer(), ]); } protected static function minimalReducers(array $extra = []): array { return array_merge($extra, [ new BaseTypeArgumentReducer(), new MinimalArrayArgumentReducer(), new EnumArgumentReducer(), new ClosureArgumentReducer(), new SensitiveParameterArrayReducer(), ]); } } backtrace/src/Arguments/ReducedArgument/VariadicReducedArgument.php 0000644 00000000771 15105603711 0021533 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; use Exception; class VariadicReducedArgument extends ReducedArgument { public function __construct(array $value) { foreach ($value as $key => $item) { if (! $item instanceof ReducedArgument) { throw new Exception('VariadicReducedArgument must be an array of ReducedArgument'); } $value[$key] = $item->value; } parent::__construct($value, 'array'); } } backtrace/src/Arguments/ReducedArgument/UnReducedArgument.php 0000644 00000000647 15105603711 0020375 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; class UnReducedArgument implements ReducedArgumentContract { /** @var self|null */ private static $instance = null; private function __construct() { } public static function create(): self { if (self::$instance !== null) { return self::$instance; } return self::$instance = new self(); } } backtrace/src/Arguments/ReducedArgument/TruncatedReducedArgument.php 0000644 00000000172 15105603711 0021735 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; class TruncatedReducedArgument extends ReducedArgument { } backtrace/src/Arguments/ReducedArgument/ReducedArgument.php 0000644 00000000652 15105603711 0020066 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; class ReducedArgument implements ReducedArgumentContract { /** @var mixed */ public $value; /** @var string */ public $originalType; /** * @param mixed $value */ public function __construct( $value, string $originalType ) { $this->originalType = $originalType; $this->value = $value; } } backtrace/src/Arguments/ReducedArgument/ReducedArgumentContract.php 0000644 00000000145 15105603711 0021561 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\ReducedArgument; interface ReducedArgumentContract { } backtrace/src/Arguments/Reducers/StdClassArgumentReducer.php 0000644 00000001020 15105603711 0020231 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use stdClass; class StdClassArgumentReducer extends ArrayArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof stdClass) { return UnReducedArgument::create(); } return parent::reduceArgument((array) $argument, stdClass::class); } } backtrace/src/Arguments/Reducers/EnumArgumentReducer.php 0000644 00000001207 15105603711 0017424 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use UnitEnum; class EnumArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof UnitEnum) { return UnReducedArgument::create(); } return new ReducedArgument( get_class($argument).'::'.$argument->name, get_class($argument), ); } } backtrace/src/Arguments/Reducers/MinimalArrayArgumentReducer.php 0000644 00000001142 15105603711 0021103 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class MinimalArrayArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if(! is_array($argument)) { return UnReducedArgument::create(); } return new ReducedArgument( 'array (size='.count($argument).')', 'array' ); } } backtrace/src/Arguments/Reducers/DateTimeZoneArgumentReducer.php 0000644 00000001202 15105603711 0021043 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use DateTimeZone; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class DateTimeZoneArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof DateTimeZone) { return UnReducedArgument::create(); } return new ReducedArgument( $argument->getName(), get_class($argument), ); } } backtrace/src/Arguments/Reducers/SymphonyRequestArgumentReducer.php 0000644 00000001267 15105603711 0021725 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Symfony\Component\HttpFoundation\Request; class SymphonyRequestArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if(! $argument instanceof Request) { return UnReducedArgument::create(); } return new ReducedArgument( "{$argument->getMethod()} {$argument->getUri()}", get_class($argument), ); } } backtrace/src/Arguments/Reducers/SensitiveParameterArrayReducer.php 0000644 00000001312 15105603711 0021623 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use SensitiveParameterValue; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class SensitiveParameterArrayReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof SensitiveParameterValue) { return UnReducedArgument::create(); } return new ReducedArgument( 'SensitiveParameterValue('.get_debug_type($argument->getValue()).')', get_class($argument) ); } } backtrace/src/Arguments/Reducers/DateTimeArgumentReducer.php 0000644 00000001226 15105603711 0020215 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use DateTimeInterface; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class DateTimeArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof DateTimeInterface) { return UnReducedArgument::create(); } return new ReducedArgument( $argument->format('d M Y H:i:s e'), get_class($argument), ); } } backtrace/src/Arguments/Reducers/ClosureArgumentReducer.php 0000644 00000001715 15105603711 0020140 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Closure; use ReflectionFunction; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class ClosureArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof Closure) { return UnReducedArgument::create(); } $reflection = new ReflectionFunction($argument); if ($reflection->getFileName() && $reflection->getStartLine() && $reflection->getEndLine()) { return new ReducedArgument( "{$reflection->getFileName()}:{$reflection->getStartLine()}-{$reflection->getEndLine()}", 'Closure' ); } return new ReducedArgument("{$reflection->getFileName()}", 'Closure'); } } backtrace/src/Arguments/Reducers/BaseTypeArgumentReducer.php 0000644 00000001305 15105603711 0020233 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class BaseTypeArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (is_int($argument) || is_float($argument) || is_bool($argument) || is_string($argument) || $argument === null ) { return new ReducedArgument($argument, get_debug_type($argument)); } return UnReducedArgument::create(); } } backtrace/src/Arguments/Reducers/ArrayArgumentReducer.php 0000644 00000003170 15105603711 0017577 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ArgumentReducers; use Spatie\Backtrace\Arguments\ReduceArgumentPayloadAction; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\TruncatedReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; class ArrayArgumentReducer implements ReducedArgumentContract { /** @var int */ protected $maxArraySize = 25; /** @var \Spatie\Backtrace\Arguments\ReduceArgumentPayloadAction */ protected $reduceArgumentPayloadAction; public function __construct() { $this->reduceArgumentPayloadAction = new ReduceArgumentPayloadAction(ArgumentReducers::minimal()); } public function execute($argument): ReducedArgumentContract { if (! is_array($argument)) { return UnReducedArgument::create(); } return $this->reduceArgument($argument, 'array'); } protected function reduceArgument(array $argument, string $originalType): ReducedArgumentContract { foreach ($argument as $key => $value) { $argument[$key] = $this->reduceArgumentPayloadAction->reduce( $value, true )->value; } if (count($argument) > $this->maxArraySize) { return new TruncatedReducedArgument( array_slice($argument, 0, $this->maxArraySize), 'array' ); } return new ReducedArgument($argument, $originalType); } } backtrace/src/Arguments/Reducers/ArgumentReducer.php 0000644 00000000415 15105603711 0016577 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; interface ArgumentReducer { /** * @param mixed $argument */ public function execute($argument): ReducedArgumentContract; } backtrace/src/Arguments/Reducers/StringableArgumentReducer.php 0000644 00000001172 15105603711 0020613 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments\Reducers; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgumentContract; use Spatie\Backtrace\Arguments\ReducedArgument\UnReducedArgument; use Stringable; class StringableArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof Stringable) { return UnReducedArgument::create(); } return new ReducedArgument( (string) $argument, get_class($argument), ); } } backtrace/src/Arguments/ProvidedArgument.php 0000644 00000006111 15105603711 0015205 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use ReflectionParameter; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; use Spatie\Backtrace\Arguments\ReducedArgument\TruncatedReducedArgument; class ProvidedArgument { /** @var string */ public $name; /** @var bool */ public $passedByReference = false; /** @var bool */ public $isVariadic = false; /** @var bool */ public $hasDefaultValue = false; /** @var mixed */ public $defaultValue = null; /** @var bool */ public $defaultValueUsed = false; /** @var bool */ public $truncated = false; /** @var mixed */ public $reducedValue = null; /** @var string|null */ public $originalType = null; public static function fromReflectionParameter(ReflectionParameter $parameter): self { return new self( $parameter->getName(), $parameter->isPassedByReference(), $parameter->isVariadic(), $parameter->isDefaultValueAvailable(), $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null, ); } public static function fromNonReflectableParameter( int $index ): self { return new self( "arg{$index}", false, ); } public function __construct( string $name, bool $passedByReference = false, bool $isVariadic = false, bool $hasDefaultValue = false, $defaultValue = null, bool $defaultValueUsed = false, bool $truncated = false, $reducedValue = null, ?string $originalType = null ) { $this->originalType = $originalType; $this->reducedValue = $reducedValue; $this->truncated = $truncated; $this->defaultValueUsed = $defaultValueUsed; $this->defaultValue = $defaultValue; $this->hasDefaultValue = $hasDefaultValue; $this->isVariadic = $isVariadic; $this->passedByReference = $passedByReference; $this->name = $name; if ($this->isVariadic) { $this->defaultValue = []; } } public function setReducedArgument( ReducedArgument $reducedArgument ): self { $this->reducedValue = $reducedArgument->value; $this->originalType = $reducedArgument->originalType; if ($reducedArgument instanceof TruncatedReducedArgument) { $this->truncated = true; } return $this; } public function defaultValueUsed(): self { $this->defaultValueUsed = true; $this->originalType = get_debug_type($this->defaultValue); return $this; } public function toArray(): array { return [ 'name' => $this->name, 'value' => $this->defaultValueUsed ? $this->defaultValue : $this->reducedValue, 'original_type' => $this->originalType, 'passed_by_reference' => $this->passedByReference, 'is_variadic' => $this->isVariadic, 'truncated' => $this->truncated, ]; } } backtrace/src/Arguments/ReduceArgumentPayloadAction.php 0000644 00000002266 15105603711 0017317 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use Spatie\Backtrace\Arguments\ReducedArgument\ReducedArgument; class ReduceArgumentPayloadAction { /** @var \Spatie\Backtrace\Arguments\ArgumentReducers */ protected $argumentReducers; public function __construct( ArgumentReducers $argumentReducers ) { $this->argumentReducers = $argumentReducers; } public function reduce($argument, bool $includeObjectType = false): ReducedArgument { foreach ($this->argumentReducers->argumentReducers as $reducer) { $reduced = $reducer->execute($argument); if ($reduced instanceof ReducedArgument) { return $reduced; } } if (gettype($argument) === 'object' && $includeObjectType) { return new ReducedArgument( 'object ('.get_class($argument).')', get_debug_type($argument), ); } if (gettype($argument) === 'object') { return new ReducedArgument('object', get_debug_type($argument), ); } return new ReducedArgument( $argument, get_debug_type($argument), ); } } backtrace/src/Arguments/ReduceArgumentsAction.php 0000644 00000007234 15105603711 0016170 0 ustar 00 <?php namespace Spatie\Backtrace\Arguments; use ReflectionException; use ReflectionFunction; use ReflectionMethod; use ReflectionParameter; use Spatie\Backtrace\Arguments\ReducedArgument\VariadicReducedArgument; use Throwable; class ReduceArgumentsAction { /** @var ArgumentReducers */ protected $argumentReducers; /** @var ReduceArgumentPayloadAction */ protected $reduceArgumentPayloadAction; public function __construct( ArgumentReducers $argumentReducers ) { $this->argumentReducers = $argumentReducers; $this->reduceArgumentPayloadAction = new ReduceArgumentPayloadAction($argumentReducers); } public function execute( ?string $class, ?string $method, ?array $frameArguments ): ?array { try { if ($frameArguments === null) { return null; } $parameters = $this->getParameters($class, $method); if ($parameters === null) { $arguments = []; foreach ($frameArguments as $index => $argument) { $arguments[$index] = ProvidedArgument::fromNonReflectableParameter($index) ->setReducedArgument($this->reduceArgumentPayloadAction->reduce($argument)) ->toArray(); } return $arguments; } $arguments = array_map( function ($argument) { return $this->reduceArgumentPayloadAction->reduce($argument); }, $frameArguments, ); $argumentsCount = count($arguments); $hasVariadicParameter = false; foreach ($parameters as $index => $parameter) { if ($index + 1 > $argumentsCount) { $parameter->defaultValueUsed(); } elseif ($parameter->isVariadic) { $parameter->setReducedArgument(new VariadicReducedArgument(array_slice($arguments, $index))); $hasVariadicParameter = true; } else { $parameter->setReducedArgument($arguments[$index]); } $parameters[$index] = $parameter->toArray(); } if ($this->moreArgumentsProvidedThanParameters($arguments, $parameters, $hasVariadicParameter)) { for ($i = count($parameters); $i < count($arguments); $i++) { $parameters[$i] = ProvidedArgument::fromNonReflectableParameter(count($parameters)) ->setReducedArgument($arguments[$i]) ->toArray(); } } return $parameters; } catch (Throwable $e) { return null; } } /** @return null|Array<\Spatie\Backtrace\Arguments\ProvidedArgument> */ protected function getParameters( ?string $class, ?string $method ): ?array { try { $reflection = $class !== null ? new ReflectionMethod($class, $method) : new ReflectionFunction($method); } catch (ReflectionException $e) { return null; } return array_map( function (ReflectionParameter $reflectionParameter) { return ProvidedArgument::fromReflectionParameter($reflectionParameter); }, $reflection->getParameters(), ); } protected function moreArgumentsProvidedThanParameters( array $arguments, array $parameters, bool $hasVariadicParameter ): bool { return count($arguments) > count($parameters) && ! $hasVariadicParameter; } } backtrace/src/File.php 0000644 00000001335 15105603711 0010643 0 ustar 00 <?php namespace Spatie\Backtrace; use SplFileObject; class File { /** @var \SplFileObject */ protected $file; public function __construct(string $path) { $this->file = new SplFileObject($path); } public function numberOfLines(): int { $this->file->seek(PHP_INT_MAX); return $this->file->key() + 1; } public function getLine(int $lineNumber = null): string { if (is_null($lineNumber)) { return $this->getNextLine(); } $this->file->seek($lineNumber - 1); return $this->file->current(); } public function getNextLine(): string { $this->file->next(); return $this->file->current(); } } backtrace/src/CodeSnippet.php 0000644 00000003777 15105603711 0012215 0 ustar 00 <?php namespace Spatie\Backtrace; use RuntimeException; class CodeSnippet { /** @var int */ protected $surroundingLine = 1; /** @var int */ protected $snippetLineCount = 9; public function surroundingLine(int $surroundingLine): self { $this->surroundingLine = $surroundingLine; return $this; } public function snippetLineCount(int $snippetLineCount): self { $this->snippetLineCount = $snippetLineCount; return $this; } public function get(string $fileName): array { if (! file_exists($fileName)) { return []; } try { $file = new File($fileName); [$startLineNumber, $endLineNumber] = $this->getBounds($file->numberOfLines()); $code = []; $line = $file->getLine($startLineNumber); $currentLineNumber = $startLineNumber; while ($currentLineNumber <= $endLineNumber) { $code[$currentLineNumber] = rtrim(substr($line, 0, 250)); $line = $file->getNextLine(); $currentLineNumber++; } return $code; } catch (RuntimeException $exception) { return []; } } public function getAsString(string $fileName): string { $snippet = $this->get($fileName); $snippetStrings = array_map(function (string $line, string $number) { return "{$number} {$line}"; }, $snippet, array_keys($snippet)); return implode(PHP_EOL, $snippetStrings); } protected function getBounds(int $totalNumberOfLineInFile): array { $startLine = max($this->surroundingLine - floor($this->snippetLineCount / 2), 1); $endLine = $startLine + ($this->snippetLineCount - 1); if ($endLine > $totalNumberOfLineInFile) { $endLine = $totalNumberOfLineInFile; $startLine = max($endLine - ($this->snippetLineCount - 1), 1); } return [$startLine, $endLine]; } } backtrace/src/Backtrace.php 0000644 00000015103 15105603711 0011641 0 ustar 00 <?php namespace Spatie\Backtrace; use Closure; use Spatie\Backtrace\Arguments\ArgumentReducers; use Spatie\Backtrace\Arguments\ReduceArgumentsAction; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; use Throwable; class Backtrace { /** @var bool */ protected $withArguments = false; /** @var bool */ protected $reduceArguments = false; /** @var array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null */ protected $argumentReducers = null; /** @var bool */ protected $withObject = false; /** @var string|null */ protected $applicationPath; /** @var int */ protected $offset = 0; /** @var int */ protected $limit = 0; /** @var \Closure|null */ protected $startingFromFrameClosure = null; /** @var \Throwable|null */ protected $throwable = null; public static function create(): self { return new static(); } public static function createForThrowable(Throwable $throwable): self { return (new static())->forThrowable($throwable); } protected function forThrowable(Throwable $throwable): self { $this->throwable = $throwable; return $this; } public function withArguments( bool $withArguments = true ): self { $this->withArguments = $withArguments; return $this; } /** * @param array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null $argumentReducers * * @return $this */ public function reduceArguments( $argumentReducers = null ): self { $this->reduceArguments = true; $this->argumentReducers = $argumentReducers; return $this; } public function withObject(): self { $this->withObject = true; return $this; } public function applicationPath(string $applicationPath): self { $this->applicationPath = rtrim($applicationPath, '/'); return $this; } public function offset(int $offset): self { $this->offset = $offset; return $this; } public function limit(int $limit): self { $this->limit = $limit; return $this; } public function startingFromFrame(Closure $startingFromFrameClosure) { $this->startingFromFrameClosure = $startingFromFrameClosure; return $this; } /** * @return \Spatie\Backtrace\Frame[] */ public function frames(): array { $rawFrames = $this->getRawFrames(); return $this->toFrameObjects($rawFrames); } public function firstApplicationFrameIndex(): ?int { foreach ($this->frames() as $index => $frame) { if ($frame->applicationFrame) { return $index; } } return null; } protected function getRawFrames(): array { if ($this->throwable) { return $this->throwable->getTrace(); } $options = null; if (! $this->withArguments) { $options = $options | DEBUG_BACKTRACE_IGNORE_ARGS; } if ($this->withObject()) { $options = $options | DEBUG_BACKTRACE_PROVIDE_OBJECT; } $limit = $this->limit; if ($limit !== 0) { $limit += 3; } return debug_backtrace($options, $limit); } /** * @return \Spatie\Backtrace\Frame[] */ protected function toFrameObjects(array $rawFrames): array { $currentFile = $this->throwable ? $this->throwable->getFile() : ''; $currentLine = $this->throwable ? $this->throwable->getLine() : 0; $arguments = $this->withArguments ? [] : null; $frames = []; $reduceArgumentsAction = new ReduceArgumentsAction($this->resolveArgumentReducers()); foreach ($rawFrames as $rawFrame) { $frames[] = new Frame( $currentFile, $currentLine, $arguments, $rawFrame['function'] ?? null, $rawFrame['class'] ?? null, $this->isApplicationFrame($currentFile) ); $arguments = $this->withArguments ? $rawFrame['args'] ?? null : null; if ($this->reduceArguments) { $arguments = $reduceArgumentsAction->execute( $rawFrame['class'] ?? null, $rawFrame['function'] ?? null, $arguments ); } $currentFile = $rawFrame['file'] ?? 'unknown'; $currentLine = $rawFrame['line'] ?? 0; } $frames[] = new Frame( $currentFile, $currentLine, [], '[top]' ); $frames = $this->removeBacktracePackageFrames($frames); if ($closure = $this->startingFromFrameClosure) { $frames = $this->startAtFrameFromClosure($frames, $closure); } $frames = array_slice($frames, $this->offset, $this->limit === 0 ? PHP_INT_MAX : $this->limit); return array_values($frames); } protected function isApplicationFrame(string $frameFilename): bool { $relativeFile = str_replace('\\', DIRECTORY_SEPARATOR, $frameFilename); if (! empty($this->applicationPath)) { $relativeFile = array_reverse(explode($this->applicationPath ?? '', $frameFilename, 2))[0]; } if (strpos($relativeFile, DIRECTORY_SEPARATOR.'vendor') === 0) { return false; } return true; } protected function removeBacktracePackageFrames(array $frames): array { return $this->startAtFrameFromClosure($frames, function (Frame $frame) { return $frame->class !== static::class; }); } /** * @param \Spatie\Backtrace\Frame[] $frames * @param \Closure $closure * * @return array */ protected function startAtFrameFromClosure(array $frames, Closure $closure): array { foreach ($frames as $i => $frame) { $foundStartingFrame = $closure($frame); if ($foundStartingFrame) { return $frames; } unset($frames[$i]); } return $frames; } protected function resolveArgumentReducers(): ArgumentReducers { if ($this->argumentReducers === null) { return ArgumentReducers::default(); } if ($this->argumentReducers instanceof ArgumentReducers) { return $this->argumentReducers; } return ArgumentReducers::create($this->argumentReducers); } } backtrace/README.md 0000644 00000014545 15105603711 0007752 0 ustar 00 # A better PHP backtrace [](https://packagist.org/packages/spatie/backtrace)  [](https://packagist.org/packages/spatie/backtrace) To get the backtrace in PHP you can use the `debug_backtrace` function. By default, it can be hard to work with. The reported function name for a frame is skewed: it belongs to the previous frame. Also, options need to be passed using a bitmask. This package provides a better way than `debug_backtrace` to work with a back trace. Here's an example: ```php // returns an array with `Spatie\Backtrace\Frame` instances $frames = Spatie\Backtrace\Backtrace::create()->frames(); $firstFrame = $frames[0]; $firstFrame->file; // returns the file name $firstFrame->lineNumber; // returns the line number $firstFrame->class; // returns the class name ``` ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/backtrace.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/backtrace) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation You can install the package via composer: ```bash composer require spatie/backtrace ``` ## Usage This is how you can create a backtrace instance: ```php $backtrace = Spatie\Backtrace\Backtrace::create(); ``` ### Getting the frames To get all the frames you can call `frames`. ```php $frames = $backtrace->frames(); // contains an array with `Spatie\Backtrace\Frame` instances ``` A `Spatie\Backtrace\Frame` has these properties: - `file`: the name of the file - `lineNumber`: the line number - `arguments`: the arguments used for this frame. Will be `null` if `withArguments` was not used. - `class`: the class name for this frame. Will be `null` if the frame concerns a function. - `method`: the method used in this frame - `applicationFrame`: contains `true` is this frame belongs to your application, and `false` if it belongs to a file in the vendor directory ### Collecting arguments For performance reasons, the frames of the back trace will not contain the arguments of the called functions. If you want to add those use the `withArguments` method. ```php $backtrace = Spatie\Backtrace\Backtrace::create()->withArguments(); ``` #### Reducing arguments For viewing purposes, arguments can be reduced to a string: ```php $backtrace = Spatie\Backtrace\Backtrace::create()->withArguments()->reduceArguments(); ``` By default, some typical types will be reduced to a string. You can define your own reduction algorithm per type by implementing an `ArgumentReducer`: ```php class DateTimeWithOtherFormatArgumentReducer implements ArgumentReducer { public function execute($argument): ReducedArgumentContract { if (! $argument instanceof DateTimeInterface) { return UnReducedArgument::create(); } return new ReducedArgument( $argument->format('d/m/y H:i'), get_class($argument), ); } } ``` This is a copy of the built-in argument reducer for `DateTimeInterface` where we've updated the format. An `UnReducedArgument` object is returned when the argument is not of the expected type. A `ReducedArgument` object is returned with the reduced value of the argument and the original type of the argument. The reducer can be used as such: ```php $backtrace = Spatie\Backtrace\Backtrace::create()->withArguments()->reduceArguments( Spatie\Backtrace\Arguments\ArgumentReducers::default([ new DateTimeWithOtherFormatArgumentReducer() ]) ); ``` Which will first execute the new reducer and then the default ones. ### Setting the application path You can use the `applicationPath` to pass the base path of your app. This value will be used to determine whether a frame is an application frame, or a vendor frame. Here's an example using a Laravel specific function. ```php $backtrace = Spatie\Backtrace\Backtrace::create()->applicationPath(base_path()); ``` ### Getting a certain part of a trace If you only want to have the frames starting from a particular frame in the backtrace you can use the `startingFromFrame` method: ```php use Spatie\Backtrace\Backtrace; use Spatie\Backtrace\Frame; $frames = Backtrace::create() ->startingFromFrame(function (Frame $frame) { return $frame->class === MyClass::class; }) ->frames(); ``` With this code, all frames before the frame that concerns `MyClass` will have been filtered out. Alternatively, you can use the `offset` method, which will skip the given number of frames. In this example the first 2 frames will not end up in `$frames`. ```php $frames = Spatie\Backtrace\Backtrace::create() ->offset(2) ->frames(); ``` ### Limiting the number of frames To only get a specific number of frames use the `limit` function. In this example, we'll only get the first two frames. ```php $frames = Spatie\Backtrace\Backtrace::create() ->limit(2) ->frames(); ``` ### Getting a backtrace for a throwable Here's how you can get a backtrace for a throwable. ```php $frames = Spatie\Backtrace\Backtrace::createForThrowable($throwable) ``` Because we will use the backtrace that is already available the throwable, the frames will always contain the arguments used. ## Testing ``` bash composer test ``` ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security Vulnerabilities Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits - [Freek Van de Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. backtrace/composer.json 0000644 00000002644 15105603711 0011212 0 ustar 00 { "name": "spatie/backtrace", "description": "A better backtrace", "keywords": [ "spatie", "backtrace" ], "homepage": "https://github.com/spatie/backtrace", "license": "MIT", "authors": [ { "name": "Freek Van de Herten", "email": "freek@spatie.be", "homepage": "https://spatie.be", "role": "Developer" } ], "require": { "php": "^7.3|^8.0" }, "require-dev": { "ext-json": "*", "phpunit/phpunit": "^9.3", "spatie/phpunit-snapshot-assertions": "^4.2", "symfony/var-dumper": "^5.1" }, "autoload": { "psr-4": { "Spatie\\Backtrace\\": "src" } }, "autoload-dev": { "psr-4": { "Spatie\\Backtrace\\Tests\\": "tests" } }, "scripts": { "psalm": "vendor/bin/psalm", "test": "vendor/bin/phpunit", "test-coverage": "vendor/bin/phpunit --coverage-html coverage", "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" }, "config": { "sort-packages": true }, "minimum-stability": "dev", "prefer-stable": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/spatie" }, { "type": "other", "url": "https://spatie.be/open-source/support-us" } ] } backtrace/LICENSE.md 0000644 00000002102 15105603711 0010061 0 ustar 00 The MIT License (MIT) Copyright (c) Spatie bvba <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. flare-client-php/src/helpers.php 0000644 00000001246 15105603711 0012642 0 ustar 00 <?php if (! function_exists('array_merge_recursive_distinct')) { /** * @param array<int|string, mixed> $array1 * @param array<int|string, mixed> $array2 * * @return array<int|string, mixed> */ function array_merge_recursive_distinct(array &$array1, array &$array2): array { $merged = $array1; foreach ($array2 as $key => &$value) { if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { $merged[$key] = array_merge_recursive_distinct($merged[$key], $value); } else { $merged[$key] = $value; } } return $merged; } } flare-client-php/src/Frame.php 0000644 00000001361 15105603711 0012230 0 ustar 00 <?php namespace Spatie\FlareClient; use Spatie\Backtrace\Frame as SpatieFrame; class Frame { public static function fromSpatieFrame( SpatieFrame $frame, ): self { return new self($frame); } public function __construct( protected SpatieFrame $frame, ) { } public function toArray(): array { return [ 'file' => $this->frame->file, 'line_number' => $this->frame->lineNumber, 'method' => $this->frame->method, 'class' => $this->frame->class, 'code_snippet' => $this->frame->getSnippet(30), 'arguments' => $this->frame->arguments, 'application_frame' => $this->frame->applicationFrame, ]; } } flare-client-php/src/Contracts/ProvidesFlareContext.php 0000644 00000000261 15105603711 0017246 0 ustar 00 <?php namespace Spatie\FlareClient\Contracts; interface ProvidesFlareContext { /** * @return array<int|string, mixed> */ public function context(): array; } flare-client-php/src/Enums/MessageLevels.php 0000644 00000000323 15105603711 0015021 0 ustar 00 <?php namespace Spatie\FlareClient\Enums; class MessageLevels { const INFO = 'info'; const DEBUG = 'debug'; const WARNING = 'warning'; const ERROR = 'error'; const CRITICAL = 'critical'; } flare-client-php/src/Concerns/UsesTime.php 0000644 00000000633 15105603711 0014507 0 ustar 00 <?php namespace Spatie\FlareClient\Concerns; use Spatie\FlareClient\Time\SystemTime; use Spatie\FlareClient\Time\Time; trait UsesTime { public static Time $time; public static function useTime(Time $time): void { self::$time = $time; } public function getCurrentTime(): int { $time = self::$time ?? new SystemTime(); return $time->getCurrentTime(); } } flare-client-php/src/Concerns/HasContext.php 0000644 00000002501 15105603711 0015025 0 ustar 00 <?php namespace Spatie\FlareClient\Concerns; trait HasContext { protected ?string $messageLevel = null; protected ?string $stage = null; /** * @var array<string, mixed> */ protected array $userProvidedContext = []; public function stage(?string $stage): self { $this->stage = $stage; return $this; } public function messageLevel(?string $messageLevel): self { $this->messageLevel = $messageLevel; return $this; } /** * @param string $groupName * @param mixed $default * * @return array<int, mixed> */ public function getGroup(string $groupName = 'context', $default = []): array { return $this->userProvidedContext[$groupName] ?? $default; } public function context(string $key, mixed $value): self { return $this->group('context', [$key => $value]); } /** * @param string $groupName * @param array<string, mixed> $properties * * @return $this */ public function group(string $groupName, array $properties): self { $group = $this->userProvidedContext[$groupName] ?? []; $this->userProvidedContext[$groupName] = array_merge_recursive_distinct( $group, $properties ); return $this; } } flare-client-php/src/View.php 0000644 00000002642 15105603711 0012113 0 ustar 00 <?php namespace Spatie\FlareClient; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper; class View { protected string $file; /** @var array<string, mixed> */ protected array $data = []; /** * @param string $file * @param array<string, mixed> $data */ public function __construct(string $file, array $data = []) { $this->file = $file; $this->data = $data; } /** * @param string $file * @param array<string, mixed> $data * * @return self */ public static function create(string $file, array $data = []): self { return new self($file, $data); } protected function dumpViewData(mixed $variable): string { $cloner = new VarCloner(); $dumper = new HtmlDumper(); $dumper->setDumpHeader(''); $output = fopen('php://memory', 'r+b'); if (! $output) { return ''; } $dumper->dump($cloner->cloneVar($variable)->withMaxDepth(1), $output, [ 'maxDepth' => 1, 'maxStringLength' => 160, ]); return (string)stream_get_contents($output, -1, 0); } /** @return array<string, mixed> */ public function toArray(): array { return [ 'file' => $this->file, 'data' => array_map([$this, 'dumpViewData'], $this->data), ]; } } flare-client-php/src/Time/Time.php 0000644 00000000151 15105603711 0012766 0 ustar 00 <?php namespace Spatie\FlareClient\Time; interface Time { public function getCurrentTime(): int; } flare-client-php/src/Time/SystemTime.php 0000644 00000000330 15105603711 0014172 0 ustar 00 <?php namespace Spatie\FlareClient\Time; use DateTimeImmutable; class SystemTime implements Time { public function getCurrentTime(): int { return (new DateTimeImmutable())->getTimestamp(); } } flare-client-php/src/Solutions/ReportSolution.php 0000644 00000002220 15105603711 0016200 0 ustar 00 <?php namespace Spatie\FlareClient\Solutions; use Spatie\Ignition\Contracts\RunnableSolution; use Spatie\Ignition\Contracts\Solution as SolutionContract; class ReportSolution { protected SolutionContract $solution; public function __construct(SolutionContract $solution) { $this->solution = $solution; } public static function fromSolution(SolutionContract $solution): self { return new self($solution); } /** * @return array<string, mixed> */ public function toArray(): array { $isRunnable = ($this->solution instanceof RunnableSolution); return [ 'class' => get_class($this->solution), 'title' => $this->solution->getSolutionTitle(), 'description' => $this->solution->getSolutionDescription(), 'links' => $this->solution->getDocumentationLinks(), /** @phpstan-ignore-next-line */ 'action_description' => $isRunnable ? $this->solution->getSolutionActionDescription() : null, 'is_runnable' => $isRunnable, 'ai_generated' => $this->solution->aiGenerated ?? false, ]; } } flare-client-php/src/Context/RequestContextProvider.php 0000644 00000007324 15105603711 0017337 0 ustar 00 <?php namespace Spatie\FlareClient\Context; use RuntimeException; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Mime\Exception\InvalidArgumentException; use Throwable; class RequestContextProvider implements ContextProvider { protected ?Request $request; public function __construct(Request $request = null) { $this->request = $request ?? Request::createFromGlobals(); } /** * @return array<string, mixed> */ public function getRequest(): array { return [ 'url' => $this->request->getUri(), 'ip' => $this->request->getClientIp(), 'method' => $this->request->getMethod(), 'useragent' => $this->request->headers->get('User-Agent'), ]; } /** * @return array<int, mixed> */ protected function getFiles(): array { if (is_null($this->request->files)) { return []; } return $this->mapFiles($this->request->files->all()); } /** * @param array<int, mixed> $files * * @return array<string, string> */ protected function mapFiles(array $files): array { return array_map(function ($file) { if (is_array($file)) { return $this->mapFiles($file); } if (! $file instanceof UploadedFile) { return; } try { $fileSize = $file->getSize(); } catch (RuntimeException $e) { $fileSize = 0; } try { $mimeType = $file->getMimeType(); } catch (InvalidArgumentException $e) { $mimeType = 'undefined'; } return [ 'pathname' => $file->getPathname(), 'size' => $fileSize, 'mimeType' => $mimeType, ]; }, $files); } /** * @return array<string, mixed> */ public function getSession(): array { try { $session = $this->request->getSession(); } catch (Throwable $exception) { $session = []; } return $session ? $this->getValidSessionData($session) : []; } protected function getValidSessionData($session): array { if (! method_exists($session, 'all')) { return []; } try { json_encode($session->all()); } catch (Throwable $e) { return []; } return $session->all(); } /** * @return array<int|string, mixed */ public function getCookies(): array { return $this->request->cookies->all(); } /** * @return array<string, mixed> */ public function getHeaders(): array { /** @var array<string, list<string|null>> $headers */ $headers = $this->request->headers->all(); return array_filter( array_map( fn (array $header) => $header[0], $headers ) ); } /** * @return array<string, mixed> */ public function getRequestData(): array { return [ 'queryString' => $this->request->query->all(), 'body' => $this->request->request->all(), 'files' => $this->getFiles(), ]; } /** @return array<string, mixed> */ public function toArray(): array { return [ 'request' => $this->getRequest(), 'request_data' => $this->getRequestData(), 'headers' => $this->getHeaders(), 'cookies' => $this->getCookies(), 'session' => $this->getSession(), ]; } } flare-client-php/src/Context/ConsoleContextProvider.php 0000644 00000001034 15105603711 0017301 0 ustar 00 <?php namespace Spatie\FlareClient\Context; class ConsoleContextProvider implements ContextProvider { /** * @var array<string, mixed> */ protected array $arguments = []; /** * @param array<string, mixed> $arguments */ public function __construct(array $arguments = []) { $this->arguments = $arguments; } /** * @return array<int|string, mixed> */ public function toArray(): array { return [ 'arguments' => $this->arguments, ]; } } flare-client-php/src/Context/ContextProviderDetector.php 0000644 00000000221 15105603711 0017445 0 ustar 00 <?php namespace Spatie\FlareClient\Context; interface ContextProviderDetector { public function detectCurrentContext(): ContextProvider; } flare-client-php/src/Context/BaseContextProviderDetector.php 0000644 00000001273 15105603711 0020250 0 ustar 00 <?php namespace Spatie\FlareClient\Context; class BaseContextProviderDetector implements ContextProviderDetector { public function detectCurrentContext(): ContextProvider { if ($this->runningInConsole()) { return new ConsoleContextProvider($_SERVER['argv'] ?? []); } return new RequestContextProvider(); } protected function runningInConsole(): bool { if (isset($_ENV['APP_RUNNING_IN_CONSOLE'])) { return $_ENV['APP_RUNNING_IN_CONSOLE'] === 'true'; } if (isset($_ENV['FLARE_FAKE_WEB_REQUEST'])) { return false; } return in_array(php_sapi_name(), ['cli', 'phpdb']); } } flare-client-php/src/Context/ContextProvider.php 0000644 00000000252 15105603711 0015757 0 ustar 00 <?php namespace Spatie\FlareClient\Context; interface ContextProvider { /** * @return array<int, string|mixed> */ public function toArray(): array; } flare-client-php/src/Glows/Glow.php 0000644 00000002363 15105603711 0013204 0 ustar 00 <?php namespace Spatie\FlareClient\Glows; use Spatie\FlareClient\Concerns\UsesTime; use Spatie\FlareClient\Enums\MessageLevels; class Glow { use UsesTime; protected string $name; /** @var array<int, mixed> */ protected array $metaData = []; protected string $messageLevel; protected float $microtime; /** * @param string $name * @param string $messageLevel * @param array<int, mixed> $metaData * @param float|null $microtime */ public function __construct( string $name, string $messageLevel = MessageLevels::INFO, array $metaData = [], ?float $microtime = null ) { $this->name = $name; $this->messageLevel = $messageLevel; $this->metaData = $metaData; $this->microtime = $microtime ?? microtime(true); } /** * @return array{time: int, name: string, message_level: string, meta_data: array, microtime: float} */ public function toArray(): array { return [ 'time' => $this->getCurrentTime(), 'name' => $this->name, 'message_level' => $this->messageLevel, 'meta_data' => $this->metaData, 'microtime' => $this->microtime, ]; } } flare-client-php/src/Glows/GlowRecorder.php 0000644 00000001026 15105603711 0014665 0 ustar 00 <?php namespace Spatie\FlareClient\Glows; class GlowRecorder { const GLOW_LIMIT = 30; /** @var array<int, Glow> */ protected array $glows = []; public function record(Glow $glow): void { $this->glows[] = $glow; $this->glows = array_slice($this->glows, static::GLOW_LIMIT * -1, static::GLOW_LIMIT); } /** @return array<int, Glow> */ public function glows(): array { return $this->glows; } public function reset(): void { $this->glows = []; } } flare-client-php/src/Report.php 0000644 00000025375 15105603711 0012464 0 ustar 00 <?php namespace Spatie\FlareClient; use ErrorException; use Spatie\Backtrace\Arguments\ArgumentReducers; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; use Spatie\Backtrace\Backtrace; use Spatie\Backtrace\Frame as SpatieFrame; use Spatie\FlareClient\Concerns\HasContext; use Spatie\FlareClient\Concerns\UsesTime; use Spatie\FlareClient\Context\ContextProvider; use Spatie\FlareClient\Contracts\ProvidesFlareContext; use Spatie\FlareClient\Glows\Glow; use Spatie\FlareClient\Solutions\ReportSolution; use Spatie\Ignition\Contracts\Solution; use Spatie\LaravelIgnition\Exceptions\ViewException; use Throwable; class Report { use UsesTime; use HasContext; protected Backtrace $stacktrace; protected string $exceptionClass = ''; protected string $message = ''; /** @var array<int, array{time: int, name: string, message_level: string, meta_data: array, microtime: float}> */ protected array $glows = []; /** @var array<int, array<int|string, mixed>> */ protected array $solutions = []; /** @var array<int, string> */ public array $documentationLinks = []; protected ContextProvider $context; protected ?string $applicationPath = null; protected ?string $applicationVersion = null; /** @var array<int|string, mixed> */ protected array $userProvidedContext = []; /** @var array<int|string, mixed> */ protected array $exceptionContext = []; protected ?Throwable $throwable = null; protected string $notifierName = 'Flare Client'; protected ?string $languageVersion = null; protected ?string $frameworkVersion = null; protected ?int $openFrameIndex = null; protected string $trackingUuid; protected ?View $view; public static ?string $fakeTrackingUuid = null; /** @param array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null $argumentReducers */ public static function createForThrowable( Throwable $throwable, ContextProvider $context, ?string $applicationPath = null, ?string $version = null, null|array|ArgumentReducers $argumentReducers = null, bool $withStackTraceArguments = true, ): self { $stacktrace = Backtrace::createForThrowable($throwable) ->withArguments($withStackTraceArguments) ->reduceArguments($argumentReducers) ->applicationPath($applicationPath ?? ''); return (new self()) ->setApplicationPath($applicationPath) ->throwable($throwable) ->useContext($context) ->exceptionClass(self::getClassForThrowable($throwable)) ->message($throwable->getMessage()) ->stackTrace($stacktrace) ->exceptionContext($throwable) ->setApplicationVersion($version); } protected static function getClassForThrowable(Throwable $throwable): string { /** @phpstan-ignore-next-line */ if ($throwable::class === ViewException::class) { /** @phpstan-ignore-next-line */ if ($previous = $throwable->getPrevious()) { return get_class($previous); } } return get_class($throwable); } /** @param array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null $argumentReducers */ public static function createForMessage( string $message, string $logLevel, ContextProvider $context, ?string $applicationPath = null, null|array|ArgumentReducers $argumentReducers = null, bool $withStackTraceArguments = true, ): self { $stacktrace = Backtrace::create() ->withArguments($withStackTraceArguments) ->reduceArguments($argumentReducers) ->applicationPath($applicationPath ?? ''); return (new self()) ->setApplicationPath($applicationPath) ->message($message) ->useContext($context) ->exceptionClass($logLevel) ->stacktrace($stacktrace) ->openFrameIndex($stacktrace->firstApplicationFrameIndex()); } public function __construct() { $this->trackingUuid = self::$fakeTrackingUuid ?? $this->generateUuid(); } public function trackingUuid(): string { return $this->trackingUuid; } public function exceptionClass(string $exceptionClass): self { $this->exceptionClass = $exceptionClass; return $this; } public function getExceptionClass(): string { return $this->exceptionClass; } public function throwable(Throwable $throwable): self { $this->throwable = $throwable; return $this; } public function getThrowable(): ?Throwable { return $this->throwable; } public function message(string $message): self { $this->message = $message; return $this; } public function getMessage(): string { return $this->message; } public function stacktrace(Backtrace $stacktrace): self { $this->stacktrace = $stacktrace; return $this; } public function getStacktrace(): Backtrace { return $this->stacktrace; } public function notifierName(string $notifierName): self { $this->notifierName = $notifierName; return $this; } public function languageVersion(string $languageVersion): self { $this->languageVersion = $languageVersion; return $this; } public function frameworkVersion(string $frameworkVersion): self { $this->frameworkVersion = $frameworkVersion; return $this; } public function useContext(ContextProvider $request): self { $this->context = $request; return $this; } public function openFrameIndex(?int $index): self { $this->openFrameIndex = $index; return $this; } public function setApplicationPath(?string $applicationPath): self { $this->applicationPath = $applicationPath; return $this; } public function getApplicationPath(): ?string { return $this->applicationPath; } public function setApplicationVersion(?string $applicationVersion): self { $this->applicationVersion = $applicationVersion; return $this; } public function getApplicationVersion(): ?string { return $this->applicationVersion; } public function view(?View $view): self { $this->view = $view; return $this; } public function addGlow(Glow $glow): self { $this->glows[] = $glow->toArray(); return $this; } public function addSolution(Solution $solution): self { $this->solutions[] = ReportSolution::fromSolution($solution)->toArray(); return $this; } /** * @param array<int, string> $documentationLinks * * @return $this */ public function addDocumentationLinks(array $documentationLinks): self { $this->documentationLinks = $documentationLinks; return $this; } /** * @param array<int|string, mixed> $userProvidedContext * * @return $this */ public function userProvidedContext(array $userProvidedContext): self { $this->userProvidedContext = $userProvidedContext; return $this; } /** * @return array<int|string, mixed> */ public function allContext(): array { $context = $this->context->toArray(); $context = array_merge_recursive_distinct($context, $this->exceptionContext); return array_merge_recursive_distinct($context, $this->userProvidedContext); } protected function exceptionContext(Throwable $throwable): self { if ($throwable instanceof ProvidesFlareContext) { $this->exceptionContext = $throwable->context(); } return $this; } /** * @return array<int|string, mixed> */ protected function stracktraceAsArray(): array { return array_map( fn (SpatieFrame $frame) => Frame::fromSpatieFrame($frame)->toArray(), $this->cleanupStackTraceForError($this->stacktrace->frames()), ); } /** * @param array<SpatieFrame> $frames * * @return array<SpatieFrame> */ protected function cleanupStackTraceForError(array $frames): array { if ($this->throwable === null || get_class($this->throwable) !== ErrorException::class) { return $frames; } $firstErrorFrameIndex = null; $restructuredFrames = array_values(array_slice($frames, 1)); // remove the first frame where error was created foreach ($restructuredFrames as $index => $frame) { if ($frame->file === $this->throwable->getFile()) { $firstErrorFrameIndex = $index; break; } } if ($firstErrorFrameIndex === null) { return $frames; } $restructuredFrames[$firstErrorFrameIndex]->arguments = null; // Remove error arguments return array_values(array_slice($restructuredFrames, $firstErrorFrameIndex)); } /** * @return array<string, mixed> */ public function toArray(): array { return [ 'notifier' => $this->notifierName ?? 'Flare Client', 'language' => 'PHP', 'framework_version' => $this->frameworkVersion, 'language_version' => $this->languageVersion ?? phpversion(), 'exception_class' => $this->exceptionClass, 'seen_at' => $this->getCurrentTime(), 'message' => $this->message, 'glows' => $this->glows, 'solutions' => $this->solutions, 'documentation_links' => $this->documentationLinks, 'stacktrace' => $this->stracktraceAsArray(), 'context' => $this->allContext(), 'stage' => $this->stage, 'message_level' => $this->messageLevel, 'open_frame_index' => $this->openFrameIndex, 'application_path' => $this->applicationPath, 'application_version' => $this->applicationVersion, 'tracking_uuid' => $this->trackingUuid, ]; } /* * Found on https://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid/15875555#15875555 */ protected function generateUuid(): string { // Generate 16 bytes (128 bits) of random data or use the data passed into the function. $data = random_bytes(16); // Set version to 0100 $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // Set bits 6-7 to 10 $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // Output the 36 character UUID. return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } } flare-client-php/src/Api.php 0000644 00000003506 15105603711 0011712 0 ustar 00 <?php namespace Spatie\FlareClient; use Exception; use Spatie\FlareClient\Http\Client; use Spatie\FlareClient\Truncation\ReportTrimmer; class Api { protected Client $client; protected bool $sendReportsImmediately = false; /** @var array<int, Report> */ protected array $queue = []; public function __construct(Client $client) { $this->client = $client; register_shutdown_function([$this, 'sendQueuedReports']); } public function sendReportsImmediately(): self { $this->sendReportsImmediately = true; return $this; } public function report(Report $report): void { try { $this->sendReportsImmediately ? $this->sendReportToApi($report) : $this->addReportToQueue($report); } catch (Exception $e) { // } } public function sendTestReport(Report $report): self { $this->sendReportToApi($report); return $this; } protected function addReportToQueue(Report $report): self { $this->queue[] = $report; return $this; } public function sendQueuedReports(): void { try { foreach ($this->queue as $report) { $this->sendReportToApi($report); } } catch (Exception $e) { // } finally { $this->queue = []; } } protected function sendReportToApi(Report $report): void { $payload = $this->truncateReport($report->toArray()); $this->client->post('reports', $payload); } /** * @param array<int|string, mixed> $payload * * @return array<int|string, mixed> */ protected function truncateReport(array $payload): array { return (new ReportTrimmer())->trim($payload); } } flare-client-php/src/Truncation/TruncationStrategy.php 0000644 00000000365 15105603711 0017200 0 ustar 00 <?php namespace Spatie\FlareClient\Truncation; interface TruncationStrategy { /** * @param array<int|string, mixed> $payload * * @return array<int|string, mixed> */ public function execute(array $payload): array; } flare-client-php/src/Truncation/TrimStringsStrategy.php 0000644 00000002240 15105603711 0017331 0 ustar 00 <?php namespace Spatie\FlareClient\Truncation; class TrimStringsStrategy extends AbstractTruncationStrategy { /** * @return array<int, int> */ public static function thresholds(): array { return [1024, 512, 256]; } /** * @param array<int|string, mixed> $payload * * @return array<int|string, mixed> */ public function execute(array $payload): array { foreach (static::thresholds() as $threshold) { if (! $this->reportTrimmer->needsToBeTrimmed($payload)) { break; } $payload = $this->trimPayloadString($payload, $threshold); } return $payload; } /** * @param array<int|string, mixed> $payload * @param int $threshold * * @return array<int|string, mixed> */ protected function trimPayloadString(array $payload, int $threshold): array { array_walk_recursive($payload, function (&$value) use ($threshold) { if (is_string($value) && strlen($value) > $threshold) { $value = substr($value, 0, $threshold); } }); return $payload; } } flare-client-php/src/Truncation/AbstractTruncationStrategy.php 0000644 00000000443 15105603711 0020661 0 ustar 00 <?php namespace Spatie\FlareClient\Truncation; abstract class AbstractTruncationStrategy implements TruncationStrategy { protected ReportTrimmer $reportTrimmer; public function __construct(ReportTrimmer $reportTrimmer) { $this->reportTrimmer = $reportTrimmer; } } flare-client-php/src/Truncation/TrimStackFrameArgumentsStrategy.php 0000644 00000000531 15105603711 0021607 0 ustar 00 <?php namespace Spatie\FlareClient\Truncation; class TrimStackFrameArgumentsStrategy implements TruncationStrategy { public function execute(array $payload): array { for ($i = 0; $i < count($payload['stacktrace']); $i++) { $payload['stacktrace'][$i]['arguments'] = null; } return $payload; } } flare-client-php/src/Truncation/ReportTrimmer.php 0000644 00000002405 15105603711 0016137 0 ustar 00 <?php namespace Spatie\FlareClient\Truncation; class ReportTrimmer { protected static int $maxPayloadSize = 524288; /** @var array<int, class-string<\Spatie\FlareClient\Truncation\TruncationStrategy>> */ protected array $strategies = [ TrimStringsStrategy::class, TrimStackFrameArgumentsStrategy::class, TrimContextItemsStrategy::class, ]; /** * @param array<int|string, mixed> $payload * * @return array<int|string, mixed> */ public function trim(array $payload): array { foreach ($this->strategies as $strategy) { if (! $this->needsToBeTrimmed($payload)) { break; } $payload = (new $strategy($this))->execute($payload); } return $payload; } /** * @param array<int|string, mixed> $payload * * @return bool */ public function needsToBeTrimmed(array $payload): bool { return strlen((string)json_encode($payload)) > self::getMaxPayloadSize(); } public static function getMaxPayloadSize(): int { return self::$maxPayloadSize; } public static function setMaxPayloadSize(int $maxPayloadSize): void { self::$maxPayloadSize = $maxPayloadSize; } } flare-client-php/src/Truncation/TrimContextItemsStrategy.php 0000644 00000002662 15105603711 0020336 0 ustar 00 <?php namespace Spatie\FlareClient\Truncation; class TrimContextItemsStrategy extends AbstractTruncationStrategy { /** * @return array<int, int> */ public static function thresholds(): array { return [100, 50, 25, 10]; } /** * @param array<int|string, mixed> $payload * * @return array<int|string, mixed> */ public function execute(array $payload): array { foreach (static::thresholds() as $threshold) { if (! $this->reportTrimmer->needsToBeTrimmed($payload)) { break; } $payload['context'] = $this->iterateContextItems($payload['context'], $threshold); } return $payload; } /** * @param array<int|string, mixed> $contextItems * @param int $threshold * * @return array<int|string, mixed> */ protected function iterateContextItems(array $contextItems, int $threshold): array { array_walk($contextItems, [$this, 'trimContextItems'], $threshold); return $contextItems; } protected function trimContextItems(mixed &$value, mixed $key, int $threshold): mixed { if (is_array($value)) { if (count($value) > $threshold) { $value = array_slice($value, $threshold * -1, $threshold); } array_walk($value, [$this, 'trimContextItems'], $threshold); } return $value; } } flare-client-php/src/FlareMiddleware/AddDocumentationLinks.php 0000644 00000002503 15105603711 0020447 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use ArrayObject; use Closure; use Spatie\FlareClient\Report; class AddDocumentationLinks implements FlareMiddleware { protected ArrayObject $documentationLinkResolvers; public function __construct(ArrayObject $documentationLinkResolvers) { $this->documentationLinkResolvers = $documentationLinkResolvers; } public function handle(Report $report, Closure $next) { if (! $throwable = $report->getThrowable()) { return $next($report); } $links = $this->getLinks($throwable); if (count($links)) { $report->addDocumentationLinks($links); } return $next($report); } /** @return array<int, string> */ protected function getLinks(\Throwable $throwable): array { $allLinks = []; foreach ($this->documentationLinkResolvers as $resolver) { $resolvedLinks = $resolver($throwable); if (is_null($resolvedLinks)) { continue; } if (is_string($resolvedLinks)) { $resolvedLinks = [$resolvedLinks]; } foreach ($resolvedLinks as $link) { $allLinks[] = $link; } } return array_values(array_unique($allLinks)); } } flare-client-php/src/FlareMiddleware/CensorRequestHeaders.php 0000644 00000001264 15105603711 0020325 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Spatie\FlareClient\Report; class CensorRequestHeaders implements FlareMiddleware { protected array $headers = []; public function __construct(array $headers) { $this->headers = $headers; } public function handle(Report $report, $next) { $context = $report->allContext(); foreach ($this->headers as $header) { $header = strtolower($header); if (isset($context['headers'][$header])) { $context['headers'][$header] = '<CENSORED>'; } } $report->userProvidedContext($context); return $next($report); } } flare-client-php/src/FlareMiddleware/AddNotifierName.php 0000644 00000000521 15105603711 0017213 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Spatie\FlareClient\Report; class AddNotifierName implements FlareMiddleware { public const NOTIFIER_NAME = 'Flare Client'; public function handle(Report $report, $next) { $report->notifierName(static::NOTIFIER_NAME); return $next($report); } } flare-client-php/src/FlareMiddleware/CensorRequestBodyFields.php 0000644 00000001275 15105603711 0021000 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Spatie\FlareClient\Report; class CensorRequestBodyFields implements FlareMiddleware { protected array $fieldNames = []; public function __construct(array $fieldNames) { $this->fieldNames = $fieldNames; } public function handle(Report $report, $next) { $context = $report->allContext(); foreach ($this->fieldNames as $fieldName) { if (isset($context['request_data']['body'][$fieldName])) { $context['request_data']['body'][$fieldName] = '<CENSORED>'; } } $report->userProvidedContext($context); return $next($report); } } flare-client-php/src/FlareMiddleware/AddGitInformation.php 0000644 00000004073 15105603711 0017572 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Closure; use Spatie\FlareClient\Report; use Symfony\Component\Process\Process; use Throwable; class AddGitInformation { protected ?string $baseDir = null; public function handle(Report $report, Closure $next) { try { $this->baseDir = $this->getGitBaseDirectory(); if (! $this->baseDir) { return $next($report); } $report->group('git', [ 'hash' => $this->hash(), 'message' => $this->message(), 'tag' => $this->tag(), 'remote' => $this->remote(), 'isDirty' => ! $this->isClean(), ]); } catch (Throwable) { } return $next($report); } protected function hash(): ?string { return $this->command("git log --pretty=format:'%H' -n 1") ?: null; } protected function message(): ?string { return $this->command("git log --pretty=format:'%s' -n 1") ?: null; } protected function tag(): ?string { return $this->command('git describe --tags --abbrev=0') ?: null; } protected function remote(): ?string { return $this->command('git config --get remote.origin.url') ?: null; } protected function isClean(): bool { return empty($this->command('git status -s')); } protected function getGitBaseDirectory(): ?string { /** @var Process $process */ $process = Process::fromShellCommandline("echo $(git rev-parse --show-toplevel)")->setTimeout(1); $process->run(); if (! $process->isSuccessful()) { return null; } $directory = trim($process->getOutput()); if (! file_exists($directory)) { return null; } return $directory; } protected function command($command) { $process = Process::fromShellCommandline($command, $this->baseDir)->setTimeout(1); $process->run(); return trim($process->getOutput()); } } flare-client-php/src/FlareMiddleware/AddSolutions.php 0000644 00000001472 15105603711 0016640 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Closure; use Spatie\FlareClient\Report; use Spatie\Ignition\Contracts\SolutionProviderRepository; class AddSolutions implements FlareMiddleware { protected SolutionProviderRepository $solutionProviderRepository; public function __construct(SolutionProviderRepository $solutionProviderRepository) { $this->solutionProviderRepository = $solutionProviderRepository; } public function handle(Report $report, Closure $next) { if ($throwable = $report->getThrowable()) { $solutions = $this->solutionProviderRepository->getSolutionsForThrowable($throwable); foreach ($solutions as $solution) { $report->addSolution($solution); } } return $next($report); } } flare-client-php/src/FlareMiddleware/AddEnvironmentInformation.php 0000644 00000000537 15105603711 0021354 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Closure; use Spatie\FlareClient\Report; class AddEnvironmentInformation implements FlareMiddleware { public function handle(Report $report, Closure $next) { $report->group('env', [ 'php_version' => phpversion(), ]); return $next($report); } } flare-client-php/src/FlareMiddleware/FlareMiddleware.php 0000644 00000000274 15105603711 0017256 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Closure; use Spatie\FlareClient\Report; interface FlareMiddleware { public function handle(Report $report, Closure $next); } flare-client-php/src/FlareMiddleware/AddGlows.php 0000644 00000001107 15105603711 0015727 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; namespace Spatie\FlareClient\FlareMiddleware; use Closure; use Spatie\FlareClient\Glows\GlowRecorder; use Spatie\FlareClient\Report; class AddGlows implements FlareMiddleware { protected GlowRecorder $recorder; public function __construct(GlowRecorder $recorder) { $this->recorder = $recorder; } public function handle(Report $report, Closure $next) { foreach ($this->recorder->glows() as $glow) { $report->addGlow($glow); } return $next($report); } } flare-client-php/src/FlareMiddleware/RemoveRequestIp.php 0000644 00000000557 15105603711 0017332 0 ustar 00 <?php namespace Spatie\FlareClient\FlareMiddleware; use Spatie\FlareClient\Report; class RemoveRequestIp implements FlareMiddleware { public function handle(Report $report, $next) { $context = $report->allContext(); $context['request']['ip'] = null; $report->userProvidedContext($context); return $next($report); } } flare-client-php/src/Flare.php 0000644 00000027714 15105603711 0012241 0 ustar 00 <?php namespace Spatie\FlareClient; use Error; use ErrorException; use Exception; use Illuminate\Contracts\Container\Container; use Illuminate\Pipeline\Pipeline; use Spatie\Backtrace\Arguments\ArgumentReducers; use Spatie\Backtrace\Arguments\Reducers\ArgumentReducer; use Spatie\FlareClient\Concerns\HasContext; use Spatie\FlareClient\Context\BaseContextProviderDetector; use Spatie\FlareClient\Context\ContextProviderDetector; use Spatie\FlareClient\Enums\MessageLevels; use Spatie\FlareClient\FlareMiddleware\AddEnvironmentInformation; use Spatie\FlareClient\FlareMiddleware\AddGlows; use Spatie\FlareClient\FlareMiddleware\CensorRequestBodyFields; use Spatie\FlareClient\FlareMiddleware\FlareMiddleware; use Spatie\FlareClient\FlareMiddleware\RemoveRequestIp; use Spatie\FlareClient\Glows\Glow; use Spatie\FlareClient\Glows\GlowRecorder; use Spatie\FlareClient\Http\Client; use Throwable; class Flare { use HasContext; protected Client $client; protected Api $api; /** @var array<int, FlareMiddleware|class-string<FlareMiddleware>> */ protected array $middleware = []; protected GlowRecorder $recorder; protected ?string $applicationPath = null; protected ContextProviderDetector $contextDetector; protected $previousExceptionHandler = null; /** @var null|callable */ protected $previousErrorHandler = null; /** @var null|callable */ protected $determineVersionCallable = null; protected ?int $reportErrorLevels = null; /** @var null|callable */ protected $filterExceptionsCallable = null; /** @var null|callable */ protected $filterReportsCallable = null; protected ?string $stage = null; protected ?string $requestId = null; protected ?Container $container = null; /** @var array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null */ protected null|array|ArgumentReducers $argumentReducers = null; protected bool $withStackFrameArguments = true; public static function make( string $apiKey = null, ContextProviderDetector $contextDetector = null ): self { $client = new Client($apiKey); return new self($client, $contextDetector); } public function setApiToken(string $apiToken): self { $this->client->setApiToken($apiToken); return $this; } public function apiTokenSet(): bool { return $this->client->apiTokenSet(); } public function setBaseUrl(string $baseUrl): self { $this->client->setBaseUrl($baseUrl); return $this; } public function setStage(?string $stage): self { $this->stage = $stage; return $this; } public function sendReportsImmediately(): self { $this->api->sendReportsImmediately(); return $this; } public function determineVersionUsing(callable $determineVersionCallable): self { $this->determineVersionCallable = $determineVersionCallable; return $this; } public function reportErrorLevels(int $reportErrorLevels): self { $this->reportErrorLevels = $reportErrorLevels; return $this; } public function filterExceptionsUsing(callable $filterExceptionsCallable): self { $this->filterExceptionsCallable = $filterExceptionsCallable; return $this; } public function filterReportsUsing(callable $filterReportsCallable): self { $this->filterReportsCallable = $filterReportsCallable; return $this; } /** @param array<class-string<ArgumentReducer>|ArgumentReducer>|ArgumentReducers|null $argumentReducers */ public function argumentReducers(null|array|ArgumentReducers $argumentReducers): self { $this->argumentReducers = $argumentReducers; return $this; } public function withStackFrameArguments(bool $withStackFrameArguments = true): self { $this->withStackFrameArguments = $withStackFrameArguments; return $this; } public function version(): ?string { if (! $this->determineVersionCallable) { return null; } return ($this->determineVersionCallable)(); } /** * @param \Spatie\FlareClient\Http\Client $client * @param \Spatie\FlareClient\Context\ContextProviderDetector|null $contextDetector * @param array<int, FlareMiddleware> $middleware */ public function __construct( Client $client, ContextProviderDetector $contextDetector = null, array $middleware = [], ) { $this->client = $client; $this->recorder = new GlowRecorder(); $this->contextDetector = $contextDetector ?? new BaseContextProviderDetector(); $this->middleware = $middleware; $this->api = new Api($this->client); $this->registerDefaultMiddleware(); } /** @return array<int, FlareMiddleware|class-string<FlareMiddleware>> */ public function getMiddleware(): array { return $this->middleware; } public function setContextProviderDetector(ContextProviderDetector $contextDetector): self { $this->contextDetector = $contextDetector; return $this; } public function setContainer(Container $container): self { $this->container = $container; return $this; } public function registerFlareHandlers(): self { $this->registerExceptionHandler(); $this->registerErrorHandler(); return $this; } public function registerExceptionHandler(): self { /** @phpstan-ignore-next-line */ $this->previousExceptionHandler = set_exception_handler([$this, 'handleException']); return $this; } public function registerErrorHandler(): self { $this->previousErrorHandler = set_error_handler([$this, 'handleError']); return $this; } protected function registerDefaultMiddleware(): self { return $this->registerMiddleware([ new AddGlows($this->recorder), new AddEnvironmentInformation(), ]); } /** * @param FlareMiddleware|array<FlareMiddleware>|class-string<FlareMiddleware>|callable $middleware * * @return $this */ public function registerMiddleware($middleware): self { if (! is_array($middleware)) { $middleware = [$middleware]; } $this->middleware = array_merge($this->middleware, $middleware); return $this; } /** * @return array<int,FlareMiddleware|class-string<FlareMiddleware>> */ public function getMiddlewares(): array { return $this->middleware; } /** * @param string $name * @param string $messageLevel * @param array<int, mixed> $metaData * * @return $this */ public function glow( string $name, string $messageLevel = MessageLevels::INFO, array $metaData = [] ): self { $this->recorder->record(new Glow($name, $messageLevel, $metaData)); return $this; } public function handleException(Throwable $throwable): void { $this->report($throwable); if ($this->previousExceptionHandler && is_callable($this->previousExceptionHandler)) { call_user_func($this->previousExceptionHandler, $throwable); } } /** * @return mixed */ public function handleError(mixed $code, string $message, string $file = '', int $line = 0) { $exception = new ErrorException($message, 0, $code, $file, $line); $this->report($exception); if ($this->previousErrorHandler) { return call_user_func( $this->previousErrorHandler, $message, $code, $file, $line ); } } public function applicationPath(string $applicationPath): self { $this->applicationPath = $applicationPath; return $this; } public function report(Throwable $throwable, callable $callback = null, Report $report = null): ?Report { if (! $this->shouldSendReport($throwable)) { return null; } $report ??= $this->createReport($throwable); if (! is_null($callback)) { call_user_func($callback, $report); } $this->recorder->reset(); $this->sendReportToApi($report); return $report; } protected function shouldSendReport(Throwable $throwable): bool { if (isset($this->reportErrorLevels) && $throwable instanceof Error) { return (bool) ($this->reportErrorLevels & $throwable->getCode()); } if (isset($this->reportErrorLevels) && $throwable instanceof ErrorException) { return (bool) ($this->reportErrorLevels & $throwable->getSeverity()); } if ($this->filterExceptionsCallable && $throwable instanceof Exception) { return (bool) (call_user_func($this->filterExceptionsCallable, $throwable)); } return true; } public function reportMessage(string $message, string $logLevel, callable $callback = null): void { $report = $this->createReportFromMessage($message, $logLevel); if (! is_null($callback)) { call_user_func($callback, $report); } $this->sendReportToApi($report); } public function sendTestReport(Throwable $throwable): void { $this->api->sendTestReport($this->createReport($throwable)); } protected function sendReportToApi(Report $report): void { if ($this->filterReportsCallable) { if (! call_user_func($this->filterReportsCallable, $report)) { return; } } try { $this->api->report($report); } catch (Exception $exception) { } } public function reset(): void { $this->api->sendQueuedReports(); $this->userProvidedContext = []; $this->recorder->reset(); } protected function applyAdditionalParameters(Report $report): void { $report ->stage($this->stage) ->messageLevel($this->messageLevel) ->setApplicationPath($this->applicationPath) ->userProvidedContext($this->userProvidedContext); } public function anonymizeIp(): self { $this->registerMiddleware(new RemoveRequestIp()); return $this; } /** * @param array<int, string> $fieldNames * * @return $this */ public function censorRequestBodyFields(array $fieldNames): self { $this->registerMiddleware(new CensorRequestBodyFields($fieldNames)); return $this; } public function createReport(Throwable $throwable): Report { $report = Report::createForThrowable( $throwable, $this->contextDetector->detectCurrentContext(), $this->applicationPath, $this->version(), $this->argumentReducers, $this->withStackFrameArguments ); return $this->applyMiddlewareToReport($report); } public function createReportFromMessage(string $message, string $logLevel): Report { $report = Report::createForMessage( $message, $logLevel, $this->contextDetector->detectCurrentContext(), $this->applicationPath, $this->argumentReducers, $this->withStackFrameArguments ); return $this->applyMiddlewareToReport($report); } protected function applyMiddlewareToReport(Report $report): Report { $this->applyAdditionalParameters($report); $middleware = array_map(function ($singleMiddleware) { return is_string($singleMiddleware) ? new $singleMiddleware : $singleMiddleware; }, $this->middleware); $report = (new Pipeline()) ->send($report) ->through($middleware) ->then(fn ($report) => $report); return $report; } } flare-client-php/src/Http/Exceptions/InvalidData.php 0000644 00000000411 15105603711 0016411 0 ustar 00 <?php namespace Spatie\FlareClient\Http\Exceptions; use Spatie\FlareClient\Http\Response; class InvalidData extends BadResponseCode { public static function getMessageForResponse(Response $response): string { return 'Invalid data found'; } } flare-client-php/src/Http/Exceptions/MissingParameter.php 0000644 00000000403 15105603711 0017504 0 ustar 00 <?php namespace Spatie\FlareClient\Http\Exceptions; use Exception; class MissingParameter extends Exception { public static function create(string $parameterName): self { return new self("`$parameterName` is a required parameter"); } } flare-client-php/src/Http/Exceptions/BadResponse.php 0000644 00000000653 15105603711 0016446 0 ustar 00 <?php namespace Spatie\FlareClient\Http\Exceptions; use Exception; use Spatie\FlareClient\Http\Response; class BadResponse extends Exception { public Response $response; public static function createForResponse(Response $response): self { $exception = new self("Could not perform request because: {$response->getError()}"); $exception->response = $response; return $exception; } } flare-client-php/src/Http/Exceptions/NotFound.php 0000644 00000000375 15105603711 0015776 0 ustar 00 <?php namespace Spatie\FlareClient\Http\Exceptions; use Spatie\FlareClient\Http\Response; class NotFound extends BadResponseCode { public static function getMessageForResponse(Response $response): string { return 'Not found'; } } flare-client-php/src/Http/Exceptions/BadResponseCode.php 0000644 00000001436 15105603711 0017241 0 ustar 00 <?php namespace Spatie\FlareClient\Http\Exceptions; use Exception; use Spatie\FlareClient\Http\Response; class BadResponseCode extends Exception { public Response $response; /** * @var array<int, mixed> */ public array $errors = []; public static function createForResponse(Response $response): self { $exception = new self(static::getMessageForResponse($response)); $exception->response = $response; $bodyErrors = isset($response->getBody()['errors']) ? $response->getBody()['errors'] : []; $exception->errors = $bodyErrors; return $exception; } public static function getMessageForResponse(Response $response): string { return "Response code {$response->getHttpResponseCode()} returned"; } } flare-client-php/src/Http/Client.php 0000644 00000013665 15105603711 0013345 0 ustar 00 <?php namespace Spatie\FlareClient\Http; use Spatie\FlareClient\Http\Exceptions\BadResponseCode; use Spatie\FlareClient\Http\Exceptions\InvalidData; use Spatie\FlareClient\Http\Exceptions\MissingParameter; use Spatie\FlareClient\Http\Exceptions\NotFound; class Client { protected ?string $apiToken; protected ?string $baseUrl; protected int $timeout; protected $lastRequest = null; public function __construct( ?string $apiToken = null, string $baseUrl = 'https://reporting.flareapp.io/api', int $timeout = 10 ) { $this->apiToken = $apiToken; if (! $baseUrl) { throw MissingParameter::create('baseUrl'); } $this->baseUrl = $baseUrl; if (! $timeout) { throw MissingParameter::create('timeout'); } $this->timeout = $timeout; } public function setApiToken(string $apiToken): self { $this->apiToken = $apiToken; return $this; } public function apiTokenSet(): bool { return ! empty($this->apiToken); } public function setBaseUrl(string $baseUrl): self { $this->baseUrl = $baseUrl; return $this; } /** * @param string $url * @param array $arguments * * @return array|false */ public function get(string $url, array $arguments = []) { return $this->makeRequest('get', $url, $arguments); } /** * @param string $url * @param array $arguments * * @return array|false */ public function post(string $url, array $arguments = []) { return $this->makeRequest('post', $url, $arguments); } /** * @param string $url * @param array $arguments * * @return array|false */ public function patch(string $url, array $arguments = []) { return $this->makeRequest('patch', $url, $arguments); } /** * @param string $url * @param array $arguments * * @return array|false */ public function put(string $url, array $arguments = []) { return $this->makeRequest('put', $url, $arguments); } /** * @param string $method * @param array $arguments * * @return array|false */ public function delete(string $method, array $arguments = []) { return $this->makeRequest('delete', $method, $arguments); } /** * @param string $httpVerb * @param string $url * @param array $arguments * * @return array */ protected function makeRequest(string $httpVerb, string $url, array $arguments = []) { $queryString = http_build_query([ 'key' => $this->apiToken, ]); $fullUrl = "{$this->baseUrl}/{$url}?{$queryString}"; $headers = [ 'x-api-token: '.$this->apiToken, ]; $response = $this->makeCurlRequest($httpVerb, $fullUrl, $headers, $arguments); if ($response->getHttpResponseCode() === 422) { throw InvalidData::createForResponse($response); } if ($response->getHttpResponseCode() === 404) { throw NotFound::createForResponse($response); } if ($response->getHttpResponseCode() !== 200 && $response->getHttpResponseCode() !== 204) { throw BadResponseCode::createForResponse($response); } return $response->getBody(); } public function makeCurlRequest(string $httpVerb, string $fullUrl, array $headers = [], array $arguments = []): Response { $curlHandle = $this->getCurlHandle($fullUrl, $headers); switch ($httpVerb) { case 'post': curl_setopt($curlHandle, CURLOPT_POST, true); $this->attachRequestPayload($curlHandle, $arguments); break; case 'get': curl_setopt($curlHandle, CURLOPT_URL, $fullUrl.'&'.http_build_query($arguments)); break; case 'delete': curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE'); break; case 'patch': curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PATCH'); $this->attachRequestPayload($curlHandle, $arguments); break; case 'put': curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT'); $this->attachRequestPayload($curlHandle, $arguments); break; } $body = json_decode(curl_exec($curlHandle), true); $headers = curl_getinfo($curlHandle); $error = curl_error($curlHandle); return new Response($headers, $body, $error); } protected function attachRequestPayload(&$curlHandle, array $data) { $encoded = json_encode($data); $this->lastRequest['body'] = $encoded; curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $encoded); } /** * @param string $fullUrl * @param array $headers * * @return resource */ protected function getCurlHandle(string $fullUrl, array $headers = []) { $curlHandle = curl_init(); curl_setopt($curlHandle, CURLOPT_URL, $fullUrl); curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array_merge([ 'Accept: application/json', 'Content-Type: application/json', ], $headers)); curl_setopt($curlHandle, CURLOPT_USERAGENT, 'Laravel/Flare API 1.0'); curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->timeout); curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); curl_setopt($curlHandle, CURLOPT_ENCODING, ''); curl_setopt($curlHandle, CURLINFO_HEADER_OUT, true); curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 1); return $curlHandle; } } flare-client-php/src/Http/Response.php 0000644 00000001535 15105603711 0013716 0 ustar 00 <?php namespace Spatie\FlareClient\Http; class Response { protected mixed $headers; protected mixed $body; protected mixed $error; public function __construct(mixed $headers, mixed $body, mixed $error) { $this->headers = $headers; $this->body = $body; $this->error = $error; } public function getHeaders(): mixed { return $this->headers; } public function getBody(): mixed { return $this->body; } public function hasBody(): bool { return $this->body != false; } public function getError(): mixed { return $this->error; } public function getHttpResponseCode(): ?int { if (! isset($this->headers['http_code'])) { return null; } return (int) $this->headers['http_code']; } } flare-client-php/README.md 0000644 00000003400 15105603711 0011151 0 ustar 00 # Send PHP errors to Flare [](https://packagist.org/packages/spatie/flare-client-php) [](https://github.com/spatie/flare-client-php/actions/workflows/run-tests.yml) [](https://github.com/spatie/flare-client-php/actions/workflows/phpstan.yml) [](https://packagist.org/packages/spatie/flare-client-php) This repository contains the PHP client to send errors and exceptions to [Flare](https://flareapp.io). The client can be installed using composer and works for PHP 8.0 and above. Using Laravel? You probably want to use [Ignition for Laravel](https://github.com/spatie/laravel-ignition). It comes with a beautiful error page and has the Flare client built in.  ## Documentation You can find the documentation of this package at [the docs of Flare](https://flareapp.io/docs/flare/general/welcome-to-flare). ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Testing ``` bash composer test ``` ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security If you discover any security related issues, please email support@flareapp.io instead of using the issue tracker. ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. flare-client-php/composer.json 0000644 00000003522 15105603711 0012421 0 ustar 00 { "name": "spatie/flare-client-php", "description": "Send PHP errors to Flare", "keywords": [ "spatie", "flare", "exception", "reporting" ], "homepage": "https://github.com/spatie/flare-client-php", "license": "MIT", "require": { "php": "^8.0", "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0", "spatie/backtrace": "^1.5.2", "symfony/http-foundation": "^5.2|^6.0|^7.0", "symfony/mime": "^5.2|^6.0|^7.0", "symfony/process": "^5.2|^6.0|^7.0", "symfony/var-dumper": "^5.2|^6.0|^7.0", "nesbot/carbon": "^2.62.1" }, "require-dev": { "dms/phpunit-arraysubset-asserts": "^0.5.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "spatie/phpunit-snapshot-assertions": "^4.0|^5.0", "pestphp/pest": "^1.20|^2.0" }, "autoload": { "psr-4": { "Spatie\\FlareClient\\": "src" }, "files": [ "src/helpers.php" ] }, "autoload-dev": { "psr-4": { "Spatie\\FlareClient\\Tests\\": "tests" } }, "scripts": { "analyse": "vendor/bin/phpstan analyse", "baseline": "vendor/bin/phpstan analyse --generate-baseline", "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes", "test": "vendor/bin/pest", "test-coverage": "vendor/bin/phpunit --coverage-html coverage" }, "config": { "sort-packages": true, "allow-plugins": { "pestphp/pest-plugin": true, "phpstan/extension-installer": true } }, "prefer-stable": true, "minimum-stability": "dev", "extra": { "branch-alias": { "dev-main": "1.3.x-dev" } } } flare-client-php/LICENSE.md 0000644 00000002102 15105603711 0011274 0 ustar 00 The MIT License (MIT) Copyright (c) Facade <info@facade.company> 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. laravel-sitemap/src/Contracts/Sitemapable.php 0000644 00000000236 15105603711 0015320 0 ustar 00 <?php namespace Spatie\Sitemap\Contracts; use Spatie\Sitemap\Tags\Url; interface Sitemapable { public function toSitemapTag(): Url | string | array; } laravel-sitemap/src/SitemapServiceProvider.php 0000644 00000001207 15105603711 0015567 0 ustar 00 <?php namespace Spatie\Sitemap; use Spatie\Crawler\Crawler; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; class SitemapServiceProvider extends PackageServiceProvider { public function configurePackage(Package $package): void { $package ->name('laravel-sitemap') ->hasConfigFile() ->hasViews(); } public function packageRegistered(): void { $this->app->when(SitemapGenerator::class) ->needs(Crawler::class) ->give(static fn (): Crawler => Crawler::create(config('sitemap.guzzle_options'))); } } laravel-sitemap/src/Crawler/Observer.php 0000644 00000003151 15105603711 0014317 0 ustar 00 <?php namespace Spatie\Sitemap\Crawler; use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; use Spatie\Crawler\CrawlObservers\CrawlObserver; class Observer extends CrawlObserver { /** @var callable */ protected $hasCrawled; public function __construct(callable $hasCrawled) { $this->hasCrawled = $hasCrawled; } /** * Called when the crawler will crawl the url. * * @param \Psr\Http\Message\UriInterface $url */ public function willCrawl(UriInterface $url): void { } /** * Called when the crawl has ended. */ public function finishedCrawling(): void { } /** * Called when the crawler has crawled the given url successfully. * * @param \Psr\Http\Message\UriInterface $url * @param \Psr\Http\Message\ResponseInterface $response * @param \Psr\Http\Message\UriInterface|null $foundOnUrl */ public function crawled( UriInterface $url, ResponseInterface $response, ?UriInterface $foundOnUrl = null ): void { ($this->hasCrawled)($url, $response); } /** * Called when the crawler had a problem crawling the given url. * * @param \Psr\Http\Message\UriInterface $url * @param \GuzzleHttp\Exception\RequestException $requestException * @param \Psr\Http\Message\UriInterface|null $foundOnUrl */ public function crawlFailed( UriInterface $url, RequestException $requestException, ?UriInterface $foundOnUrl = null ): void { } } laravel-sitemap/src/Crawler/Profile.php 0000644 00000000670 15105603711 0014133 0 ustar 00 <?php namespace Spatie\Sitemap\Crawler; use Psr\Http\Message\UriInterface; use Spatie\Crawler\CrawlProfiles\CrawlProfile; class Profile extends CrawlProfile { /** @var callable */ protected $callback; public function shouldCrawlCallback(callable $callback): void { $this->callback = $callback; } public function shouldCrawl(UriInterface $url): bool { return ($this->callback)($url); } } laravel-sitemap/src/SitemapIndex.php 0000644 00000003531 15105603711 0013525 0 ustar 00 <?php namespace Spatie\Sitemap; use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\Support\Responsable; use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Storage; use Spatie\Sitemap\Tags\Sitemap; use Spatie\Sitemap\Tags\Tag; class SitemapIndex implements Responsable, Renderable { /** @var \Spatie\Sitemap\Tags\Sitemap[] */ protected array $tags = []; public static function create(): static { return new static(); } public function add(string | Sitemap $tag): static { if (is_string($tag)) { $tag = Sitemap::create($tag); } $this->tags[] = $tag; return $this; } public function getSitemap(string $url): ?Sitemap { return collect($this->tags)->first(function (Tag $tag) use ($url) { return $tag->getType() === 'sitemap' && $tag->url === $url; }); } public function hasSitemap(string $url): bool { return (bool) $this->getSitemap($url); } public function render(): string { $tags = $this->tags; return view('sitemap::sitemapIndex/index') ->with(compact('tags')) ->render(); } public function writeToFile(string $path): static { file_put_contents($path, $this->render()); return $this; } public function writeToDisk(string $disk, string $path): static { Storage::disk($disk)->put($path, $this->render()); return $this; } /** * Create an HTTP response that represents the object. * * @param \Illuminate\Http\Request $request * @return \Symfony\Component\HttpFoundation\Response */ public function toResponse($request) { return Response::make($this->render(), 200, [ 'Content-Type' => 'text/xml', ]); } } laravel-sitemap/src/SitemapGenerator.php 0000644 00000012512 15105603711 0014403 0 ustar 00 <?php namespace Spatie\Sitemap; use Closure; use GuzzleHttp\Psr7\Uri; use Illuminate\Support\Collection; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; use Spatie\Browsershot\Browsershot; use Spatie\Crawler\Crawler; use Spatie\Crawler\CrawlProfiles\CrawlProfile; use Spatie\Sitemap\Crawler\Observer; use Spatie\Sitemap\Crawler\Profile; use Spatie\Sitemap\Tags\Url; class SitemapGenerator { protected Collection $sitemaps; protected Uri $urlToBeCrawled; protected Crawler $crawler; /** @var callable */ protected $shouldCrawl; /** @var callable */ protected $hasCrawled; protected int $concurrency = 10; protected bool | int $maximumTagsPerSitemap = false; protected ?int $maximumCrawlCount = null; public static function create(string $urlToBeCrawled): static { return app(static::class)->setUrl($urlToBeCrawled); } public function __construct(Crawler $crawler) { $this->crawler = $crawler; $this->sitemaps = new Collection([new Sitemap]); $this->hasCrawled = fn (Url $url, ResponseInterface $response = null) => $url; } public function configureCrawler(Closure $closure): static { call_user_func_array($closure, [$this->crawler]); return $this; } public function setConcurrency(int $concurrency): static { $this->concurrency = $concurrency; return $this; } public function setMaximumCrawlCount(int $maximumCrawlCount): static { $this->maximumCrawlCount = $maximumCrawlCount; return $this; } public function maxTagsPerSitemap(int $maximumTagsPerSitemap = 50000): static { $this->maximumTagsPerSitemap = $maximumTagsPerSitemap; return $this; } public function setUrl(string $urlToBeCrawled): static { $this->urlToBeCrawled = new Uri($urlToBeCrawled); if ($this->urlToBeCrawled->getPath() === '') { $this->urlToBeCrawled = $this->urlToBeCrawled->withPath('/'); } return $this; } public function shouldCrawl(callable $shouldCrawl): static { $this->shouldCrawl = $shouldCrawl; return $this; } public function hasCrawled(callable $hasCrawled): static { $this->hasCrawled = $hasCrawled; return $this; } public function getSitemap(): Sitemap { if (config('sitemap.execute_javascript')) { $this->crawler->executeJavaScript(); } if (config('sitemap.chrome_binary_path')) { $this->crawler ->setBrowsershot((new Browsershot)->setChromePath(config('sitemap.chrome_binary_path'))) ->acceptNofollowLinks(); } if (! is_null($this->maximumCrawlCount)) { $this->crawler->setTotalCrawlLimit($this->maximumCrawlCount); } $this->crawler ->setCrawlProfile($this->getCrawlProfile()) ->setCrawlObserver($this->getCrawlObserver()) ->setConcurrency($this->concurrency) ->startCrawling($this->urlToBeCrawled); return $this->sitemaps->first(); } public function writeToFile(string $path): static { $sitemap = $this->getSitemap(); if ($this->maximumTagsPerSitemap) { $sitemap = SitemapIndex::create(); $format = str_replace('.xml', '_%d.xml', $path); // Parses each sub-sitemaps, writes and push them into the sitemap index $this->sitemaps->each(function (Sitemap $item, int $key) use ($sitemap, $format) { $path = sprintf($format, $key); $item->writeToFile(sprintf($format, $key)); $sitemap->add(last(explode('public', $path))); }); } $sitemap->writeToFile($path); return $this; } protected function getCrawlProfile(): CrawlProfile { $shouldCrawl = function (UriInterface $url) { if ($url->getHost() !== $this->urlToBeCrawled->getHost()) { return false; } if (! is_callable($this->shouldCrawl)) { return true; } return ($this->shouldCrawl)($url); }; $profileClass = config('sitemap.crawl_profile', Profile::class); $profile = new $profileClass($this->urlToBeCrawled); if (method_exists($profile, 'shouldCrawlCallback')) { $profile->shouldCrawlCallback($shouldCrawl); } return $profile; } protected function getCrawlObserver(): Observer { $performAfterUrlHasBeenCrawled = function (UriInterface $crawlerUrl, ResponseInterface $response = null) { $sitemapUrl = ($this->hasCrawled)(Url::create((string) $crawlerUrl), $response); if ($this->shouldStartNewSitemapFile()) { $this->sitemaps->push(new Sitemap); } if ($sitemapUrl) { $this->sitemaps->last()->add($sitemapUrl); } }; return new Observer($performAfterUrlHasBeenCrawled); } protected function shouldStartNewSitemapFile(): bool { if (! $this->maximumTagsPerSitemap) { return false; } $currentNumberOfTags = count($this->sitemaps->last()->getTags()); return $currentNumberOfTags >= $this->maximumTagsPerSitemap; } } laravel-sitemap/src/Sitemap.php 0000644 00000004504 15105603711 0012536 0 ustar 00 <?php namespace Spatie\Sitemap; use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\Support\Responsable; use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Storage; use Spatie\Sitemap\Contracts\Sitemapable; use Spatie\Sitemap\Tags\Tag; use Spatie\Sitemap\Tags\Url; class Sitemap implements Responsable, Renderable { /** @var \Spatie\Sitemap\Tags\Url[] */ protected array $tags = []; public static function create(): static { return new static(); } public function add(string | Url | Sitemapable | iterable $tag): static { if (is_object($tag) && array_key_exists(Sitemapable::class, class_implements($tag))) { $tag = $tag->toSitemapTag(); } if (is_iterable($tag)) { foreach ($tag as $item) { $this->add($item); } return $this; } if (is_string($tag)) { $tag = Url::create($tag); } if (! in_array($tag, $this->tags)) { $this->tags[] = $tag; } return $this; } public function getTags(): array { return $this->tags; } public function getUrl(string $url): ?Url { return collect($this->tags)->first(function (Tag $tag) use ($url) { return $tag->getType() === 'url' && $tag->url === $url; }); } public function hasUrl(string $url): bool { return (bool) $this->getUrl($url); } public function render(): string { $tags = collect($this->tags)->unique('url')->filter(); return view('sitemap::sitemap') ->with(compact('tags')) ->render(); } public function writeToFile(string $path): static { file_put_contents($path, $this->render()); return $this; } public function writeToDisk(string $disk, string $path): static { Storage::disk($disk)->put($path, $this->render()); return $this; } /** * Create an HTTP response that represents the object. * * @param \Illuminate\Http\Request $request * @return \Symfony\Component\HttpFoundation\Response */ public function toResponse($request) { return Response::make($this->render(), 200, [ 'Content-Type' => 'text/xml', ]); } } laravel-sitemap/src/Tags/Url.php 0000644 00000006646 15105603711 0012605 0 ustar 00 <?php namespace Spatie\Sitemap\Tags; use Carbon\Carbon; use DateTimeInterface; class Url extends Tag { const CHANGE_FREQUENCY_ALWAYS = 'always'; const CHANGE_FREQUENCY_HOURLY = 'hourly'; const CHANGE_FREQUENCY_DAILY = 'daily'; const CHANGE_FREQUENCY_WEEKLY = 'weekly'; const CHANGE_FREQUENCY_MONTHLY = 'monthly'; const CHANGE_FREQUENCY_YEARLY = 'yearly'; const CHANGE_FREQUENCY_NEVER = 'never'; public string $url; public Carbon $lastModificationDate; public string $changeFrequency; public float $priority = 0.8; /** @var \Spatie\Sitemap\Tags\Alternate[] */ public array $alternates = []; /** @var \Spatie\Sitemap\Tags\Image[] */ public array $images = []; /** @var \Spatie\Sitemap\Tags\Video[] */ public array $videos = []; /** @var \Spatie\Sitemap\Tags\News[] */ public array $news = []; public static function create(string $url): static { return new static($url); } public function __construct(string $url) { $this->url = $url; $this->changeFrequency = static::CHANGE_FREQUENCY_DAILY; } public function setUrl(string $url = ''): static { $this->url = $url; return $this; } public function setLastModificationDate(DateTimeInterface $lastModificationDate): static { $this->lastModificationDate = Carbon::instance($lastModificationDate); return $this; } public function setChangeFrequency(string $changeFrequency): static { $this->changeFrequency = $changeFrequency; return $this; } public function setPriority(float $priority): static { $this->priority = max(0, min($priority, 1)); return $this; } public function addAlternate(string $url, string $locale = ''): static { $this->alternates[] = new Alternate($url, $locale); return $this; } public function addImage( string $url, string $caption = '', string $geo_location = '', string $title = '', string $license = '' ): static { $this->images[] = new Image($url, $caption, $geo_location, $title, $license); return $this; } public function addVideo(string $thumbnailLoc, string $title, string $description, $contentLoc = null, $playerLoc = null, array $options = [], array $allow = [], array $deny = [], array $tags = []): static { $this->videos[] = new Video($thumbnailLoc, $title, $description, $contentLoc, $playerLoc, $options, $allow, $deny, $tags); return $this; } public function addNews(string $name, string $language, string $title, DateTimeInterface $publicationDate, array $options = []): static { $this->news[] = new News($name, $language, $title, $publicationDate, $options); return $this; } public function path(): string { return parse_url($this->url, PHP_URL_PATH) ?? ''; } public function segments(?int $index = null): array|string|null { $segments = collect(explode('/', $this->path())) ->filter(function ($value) { return $value !== ''; }) ->values() ->toArray(); if (! is_null($index)) { return $this->segment($index); } return $segments; } public function segment(int $index): ?string { return $this->segments()[$index - 1] ?? null; } } laravel-sitemap/src/Tags/Alternate.php 0000644 00000001157 15105603711 0013752 0 ustar 00 <?php namespace Spatie\Sitemap\Tags; class Alternate { public string $locale; public string $url; public static function create(string $url, string $locale = ''): static { return new static($url, $locale); } public function __construct(string $url, $locale = '') { $this->setUrl($url); $this->setLocale($locale); } public function setLocale(string $locale = ''): static { $this->locale = $locale; return $this; } public function setUrl(string $url = ''): static { $this->url = $url; return $this; } } laravel-sitemap/src/Tags/News.php 0000644 00000003256 15105603711 0012751 0 ustar 00 <?php namespace Spatie\Sitemap\Tags; use Carbon\Carbon; use DateTimeInterface; class News { public const OPTION_ACCESS_SUB = 'Subscription'; public const OPTION_ACCESS_REG = 'Registration'; public const OPTION_GENRES_PR = 'PressRelease'; public const OPTION_GENRES_SATIRE = 'Satire'; public const OPTION_GENRES_BLOG = 'Blog'; public const OPTION_GENRES_OPED = 'OpEd'; public const OPTION_GENRES_OPINION = 'Opinion'; public const OPTION_GENRES_UG = 'UserGenerated'; public string $name; public string $language; public string $title; public Carbon $publicationDate; public ?array $options; public function __construct( string $name, string $language, string $title, DateTimeInterface $publicationDate, array $options = [] ) { $this ->setName($name) ->setLanguage($language) ->setTitle($title) ->setPublicationDate($publicationDate) ->setOptions($options); } public function setName(string $name): self { $this->name = $name; return $this; } public function setLanguage(string $language): self { $this->language = $language; return $this; } public function setTitle(string $title): self { $this->title = $title; return $this; } public function setPublicationDate(DateTimeInterface $publicationDate): self { $this->publicationDate = Carbon::instance($publicationDate); return $this; } public function setOptions(array $options): self { $this->options = $options; return $this; } } laravel-sitemap/src/Tags/Video.php 0000644 00000004777 15105603711 0013114 0 ustar 00 <?php namespace Spatie\Sitemap\Tags; class Video { public const OPTION_PLATFORM_WEB = 'web'; public const OPTION_PLATFORM_MOBILE = 'mobile'; public const OPTION_PLATFORM_TV = 'tv'; public const OPTION_NO = "no"; public const OPTION_YES = "yes"; public string $thumbnailLoc; public string $title; public string $description; public ?string $contentLoc; public ?string $playerLoc; public array $options; public array $allow; public array $deny; public array $tags; public function __construct(string $thumbnailLoc, string $title, string $description, string $contentLoc = null, string|array $playerLoc = null, array $options = [], array $allow = [], array $deny = [], array $tags = []) { if ($contentLoc === null && $playerLoc === null) { // https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps throw new \Exception("It's required to provide either a Content Location or Player Location"); } $this->setThumbnailLoc($thumbnailLoc) ->setTitle($title) ->setDescription($description) ->setContentLoc($contentLoc) ->setPlayerLoc($playerLoc) ->setOptions($options) ->setAllow($allow) ->setDeny($deny) ->setTags($tags); } public function setThumbnailLoc(string $thumbnailLoc): self { $this->thumbnailLoc = $thumbnailLoc; return $this; } public function setTitle(string $title): self { $this->title = $title; return $this; } public function setDescription(string $description): self { $this->description = $description; return $this; } public function setContentLoc(?string $contentLoc): self { $this->contentLoc = $contentLoc; return $this; } public function setPlayerLoc(?string $playerLoc): self { $this->playerLoc = $playerLoc; return $this; } public function setOptions(?array $options): self { $this->options = $options; return $this; } public function setAllow(array $allow): self { $this->allow = $allow; return $this; } public function setDeny(array $deny): self { $this->deny = $deny; return $this; } public function setTags(array $tags): self { $this->tags = array_slice($tags, 0, 32); // maximum 32 tags allowed return $this; } } laravel-sitemap/src/Tags/Tag.php 0000644 00000000255 15105603711 0012544 0 ustar 00 <?php namespace Spatie\Sitemap\Tags; abstract class Tag { public function getType(): string { return mb_strtolower(class_basename(static::class)); } } laravel-sitemap/src/Tags/Image.php 0000644 00000002617 15105603711 0013057 0 ustar 00 <?php namespace Spatie\Sitemap\Tags; class Image { public string $url; public string $caption; public string $geo_location; public string $title; public string $license; public static function create(string $url, string $caption = '', string $geo_location = '', string $title = '', string $license = ''): static { return new static($url, $caption, $geo_location, $title, $license); } public function __construct(string $url, string $caption = '', string $geo_location = '', string $title = '', string $license = '') { $this->setUrl($url); $this->setCaption($caption); $this->setGeoLocation($geo_location); $this->setTitle($title); $this->setLicense($license); } public function setUrl(string $url = ''): static { $this->url = $url; return $this; } public function setCaption(string $caption = ''): static { $this->caption = $caption; return $this; } public function setGeoLocation(string $geo_location = ''): static { $this->geo_location = $geo_location; return $this; } public function setTitle(string $title = ''): static { $this->title = $title; return $this; } public function setLicense(string $license = ''): static { $this->license = $license; return $this; } } laravel-sitemap/src/Tags/Sitemap.php 0000644 00000001520 15105603711 0013427 0 ustar 00 <?php namespace Spatie\Sitemap\Tags; use Carbon\Carbon; use DateTimeInterface; class Sitemap extends Tag { public string $url; public Carbon $lastModificationDate; public static function create(string $url): static { return new static($url); } public function __construct(string $url) { $this->url = $url; $this->lastModificationDate = Carbon::now(); } public function setUrl(string $url = ''): static { $this->url = $url; return $this; } public function setLastModificationDate(DateTimeInterface $lastModificationDate): static { $this->lastModificationDate = Carbon::instance($lastModificationDate); return $this; } public function path(): string { return parse_url($this->url, PHP_URL_PATH) ?? ''; } } laravel-sitemap/README.md 0000644 00000043111 15105603711 0011110 0 ustar 00 # Generate sitemaps with ease [](https://packagist.org/packages/spatie/laravel-sitemap) [](LICENSE.md) [](https://github.com/spatie/laravel-sitemap/actions/workflows/run-tests.yml) [](https://github.com/spatie/laravel-sitemap/actions/workflows/php-cs-fixer.yml) [](https://packagist.org/packages/spatie/laravel-sitemap) This package can generate a sitemap without you having to add urls to it manually. This works by crawling your entire site. ```php use Spatie\Sitemap\SitemapGenerator; SitemapGenerator::create('https://example.com')->writeToFile($path); ``` You can also create your sitemap manually: ```php use Carbon\Carbon; use Spatie\Sitemap\Sitemap; use Spatie\Sitemap\Tags\Url; Sitemap::create() ->add(Url::create('/home') ->setLastModificationDate(Carbon::yesterday()) ->setChangeFrequency(Url::CHANGE_FREQUENCY_YEARLY) ->setPriority(0.1)) ->add(...) ->writeToFile($path); ``` Or you can have the best of both worlds by generating a sitemap and then adding more links to it: ```php SitemapGenerator::create('https://example.com') ->getSitemap() ->add(Url::create('/extra-page') ->setLastModificationDate(Carbon::yesterday()) ->setChangeFrequency(Url::CHANGE_FREQUENCY_YEARLY) ->setPriority(0.1)) ->add(...) ->writeToFile($path); ``` You can also control the maximum depth of the sitemap: ```php SitemapGenerator::create('https://example.com') ->configureCrawler(function (Crawler $crawler) { $crawler->setMaximumDepth(3); }) ->writeToFile($path); ``` The generator has [the ability to execute JavaScript](https://github.com/spatie/laravel-sitemap#executing-javascript) on each page so links injected into the dom by JavaScript will be crawled as well. You can also use one of your available filesystem disks to write the sitemap to. ```php SitemapGenerator::create('https://example.com')->getSitemap()->writeToDisk('public', 'sitemap.xml'); ``` You can also add your models directly by implementing the `\Spatie\Sitemap\Contracts\Sitemapable` interface. ```php use Spatie\Sitemap\Contracts\Sitemapable; use Spatie\Sitemap\Tags\Url; class Post extends Model implements Sitemapable { public function toSitemapTag(): Url | string | array { return route('blog.post.show', $this); return Url::create(route('blog.post.show', $this)) ->setLastModificationDate(Carbon::create($this->updated_at)) ->setChangeFrequency(Url::CHANGE_FREQUENCY_YEARLY) ->setPriority(0.1); } } ``` Now you can add a single post model to the sitemap or even a whole collection. ```php use Spatie\Sitemap\Sitemap; Sitemap::create() ->add($post) ->add(Post::all()); ``` This way you can add all your pages super fast without the need to crawl them all. ## Support us [<img src="https://github-ads.s3.eu-central-1.amazonaws.com/laravel-sitemap.jpg?t=1" width="419px" />](https://spatie.be/github-ad-click/laravel-sitemap) We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). ## Installation First, install the package via composer: ``` bash composer require spatie/laravel-sitemap ``` The package will automatically register itself. If you want to update your sitemap automatically and frequently you need to perform [some extra steps](https://github.com/spatie/laravel-sitemap#generating-the-sitemap-frequently). ## Configuration You can override the default options for the crawler. First publish the configuration: ```bash php artisan vendor:publish --provider="Spatie\Sitemap\SitemapServiceProvider" --tag=sitemap-config ``` This will copy the default config to `config/sitemap.php` where you can edit it. ```php use GuzzleHttp\RequestOptions; use Spatie\Sitemap\Crawler\Profile; return [ /* * These options will be passed to GuzzleHttp\Client when it is created. * For in-depth information on all options see the Guzzle docs: * * http://docs.guzzlephp.org/en/stable/request-options.html */ 'guzzle_options' => [ /* * Whether or not cookies are used in a request. */ RequestOptions::COOKIES => true, /* * The number of seconds to wait while trying to connect to a server. * Use 0 to wait indefinitely. */ RequestOptions::CONNECT_TIMEOUT => 10, /* * The timeout of the request in seconds. Use 0 to wait indefinitely. */ RequestOptions::TIMEOUT => 10, /* * Describes the redirect behavior of a request. */ RequestOptions::ALLOW_REDIRECTS => false, ], /* * The sitemap generator can execute JavaScript on each page so it will * discover links that are generated by your JS scripts. This feature * is powered by headless Chrome. */ 'execute_javascript' => false, /* * The package will make an educated guess as to where Google Chrome is installed. * You can also manually pass it's location here. */ 'chrome_binary_path' => '', /* * The sitemap generator uses a CrawlProfile implementation to determine * which urls should be crawled for the sitemap. */ 'crawl_profile' => Profile::class, ]; ``` ## Usage ### Generating a sitemap The easiest way is to crawl the given domain and generate a sitemap with all found links. The destination of the sitemap should be specified by `$path`. ```php SitemapGenerator::create('https://example.com')->writeToFile($path); ``` The generated sitemap will look similar to this: ```xml <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://example.com</loc> <lastmod>2016-01-01T00:00:00+00:00</lastmod> <changefreq>daily</changefreq> <priority>0.8</priority> </url> <url> <loc>https://example.com/page</loc> <lastmod>2016-01-01T00:00:00+00:00</lastmod> <changefreq>daily</changefreq> <priority>0.8</priority> </url> ... </urlset> ``` ### Customizing the sitemap generator #### Define a custom Crawl Profile You can create a custom crawl profile by implementing the `Spatie\Crawler\CrawlProfiles\CrawlProfile` interface and by customizing the `shouldCrawl()` method for full control over what url/domain/sub-domain should be crawled: ```php use Spatie\Crawler\CrawlProfiles\CrawlProfile; use Psr\Http\Message\UriInterface; class CustomCrawlProfile extends CrawlProfile { public function shouldCrawl(UriInterface $url): bool { if ($url->getHost() !== 'localhost') { return false; } return $url->getPath() === '/'; } } ``` and register your `CustomCrawlProfile::class` in `config/sitemap.php`. ```php return [ ... /* * The sitemap generator uses a CrawlProfile implementation to determine * which urls should be crawled for the sitemap. */ 'crawl_profile' => CustomCrawlProfile::class, ]; ``` #### Changing properties To change the `lastmod`, `changefreq` and `priority` of the contact page: ```php use Carbon\Carbon; use Spatie\Sitemap\SitemapGenerator; use Spatie\Sitemap\Tags\Url; SitemapGenerator::create('https://example.com') ->hasCrawled(function (Url $url) { if ($url->segment(1) === 'contact') { $url->setPriority(0.9) ->setLastModificationDate(Carbon::create('2016', '1', '1')); } return $url; }) ->writeToFile($sitemapPath); ``` #### Leaving out some links If you don't want a crawled link to appear in the sitemap, just don't return it in the callable you pass to `hasCrawled `. ```php use Spatie\Sitemap\SitemapGenerator; use Spatie\Sitemap\Tags\Url; SitemapGenerator::create('https://example.com') ->hasCrawled(function (Url $url) { if ($url->segment(1) === 'contact') { return; } return $url; }) ->writeToFile($sitemapPath); ``` #### Preventing the crawler from crawling some pages You can also instruct the underlying crawler to not crawl some pages by passing a `callable` to `shouldCrawl`. **Note:** `shouldCrawl` will only work with the default crawl `Profile` or custom crawl profiles that implement a `shouldCrawlCallback` method. ```php use Spatie\Sitemap\SitemapGenerator; use Psr\Http\Message\UriInterface; SitemapGenerator::create('https://example.com') ->shouldCrawl(function (UriInterface $url) { // All pages will be crawled, except the contact page. // Links present on the contact page won't be added to the // sitemap unless they are present on a crawlable page. return strpos($url->getPath(), '/contact') === false; }) ->writeToFile($sitemapPath); ``` #### Configuring the crawler The crawler itself can be [configured](https://github.com/spatie/crawler#usage) to do a few different things. You can configure the crawler used by the sitemap generator, for example: to ignore robot checks; like so. ```php SitemapGenerator::create('http://localhost:4020') ->configureCrawler(function (Crawler $crawler) { $crawler->ignoreRobots(); }) ->writeToFile($file); ``` #### Limiting the amount of pages crawled You can limit the amount of pages crawled by calling `setMaximumCrawlCount` ```php use Spatie\Sitemap\SitemapGenerator; SitemapGenerator::create('https://example.com') ->setMaximumCrawlCount(500) // only the 500 first pages will be crawled ... ``` #### Executing Javascript The sitemap generator can execute JavaScript on each page so it will discover links that are generated by your JS scripts. You can enable this feature by setting `execute_javascript` in the config file to `true`. Under the hood, [headless Chrome](https://github.com/spatie/browsershot) is used to execute JavaScript. Here are some pointers on [how to install it on your system](https://github.com/spatie/browsershot#requirements). The package will make an educated guess as to where Chrome is installed on your system. You can also manually pass the location of the Chrome binary to `executeJavaScript()`. #### Manually adding links You can manually add links to a sitemap: ```php use Spatie\Sitemap\SitemapGenerator; use Spatie\Sitemap\Tags\Url; SitemapGenerator::create('https://example.com') ->getSitemap() // here we add one extra link, but you can add as many as you'd like ->add(Url::create('/extra-page')->setPriority(0.5)) ->writeToFile($sitemapPath); ``` #### Adding alternates to links Multilingual sites may have several alternate versions of the same page (one per language). Based on the previous example adding an alternate can be done as follows: ```php use Spatie\Sitemap\SitemapGenerator; use Spatie\Sitemap\Tags\Url; SitemapGenerator::create('https://example.com') ->getSitemap() // here we add one extra link, but you can add as many as you'd like ->add(Url::create('/extra-page')->setPriority(0.5)->addAlternate('/extra-pagina', 'nl')) ->writeToFile($sitemapPath); ``` Note the ```addAlternate``` function which takes an alternate URL and the locale it belongs to. #### Adding images to links Urls can also have images. See also https://developers.google.com/search/docs/advanced/sitemaps/image-sitemaps ```php use Spatie\Sitemap\Sitemap; use Spatie\Sitemap\Tags\Url; Sitemap::create() // here we add an image to a URL ->add(Url::create('https://example.com')->addImage('https://example.com/images/home.jpg', 'Home page image')) ->writeToFile($sitemapPath); ``` #### Adding videos to links As well as images, videos can be wrapped by URL tags. See https://developers.google.com/search/docs/crawling-indexing/sitemaps/video-sitemaps You can set required attributes like so: ```php use Spatie\Sitemap\Sitemap; use Spatie\Sitemap\Tags\Url; Sitemap::create() ->add( Url::create('https://example.com') ->addVideo('https://example.com/images/thumbnail.jpg', 'Video title', 'Video Description', 'https://example.com/videos/source.mp4', 'https://example.com/video/123') ) ->writeToFile($sitemapPath); ``` If you want to pass the optional parameters like `family_friendly`, `live`, or `platform`: ```php use Spatie\Sitemap\Sitemap; use Spatie\Sitemap\Tags\Url; use Spatie\Sitemap\Tags\Video; $options = ['family_friendly' => Video::OPTION_YES, 'live' => Video::OPTION_NO]; $allowOptions = ['platform' => Video::OPTION_PLATFORM_MOBILE]; $denyOptions = ['restriction' => 'CA']; Sitemap::create() ->add( Url::create('https://example.com') ->addVideo('https://example.com/images/thumbnail.jpg', 'Video title', 'Video Description', 'https://example.com/videos/source.mp4', 'https://example.com/video/123', $options, $allowOptions, $denyOptions) ) ->writeToFile($sitemapPath); ``` ### Manually creating a sitemap You can also create a sitemap fully manual: ```php use Carbon\Carbon; Sitemap::create() ->add('/page1') ->add('/page2') ->add(Url::create('/page3')->setLastModificationDate(Carbon::create('2016', '1', '1'))) ->writeToFile($sitemapPath); ``` ### Creating a sitemap index You can create a sitemap index: ```php use Spatie\Sitemap\SitemapIndex; SitemapIndex::create() ->add('/pages_sitemap.xml') ->add('/posts_sitemap.xml') ->writeToFile($sitemapIndexPath); ``` You can pass a `Spatie\Sitemap\Tags\Sitemap` object to manually set the `lastModificationDate` property. ```php use Spatie\Sitemap\SitemapIndex; use Spatie\Sitemap\Tags\Sitemap; SitemapIndex::create() ->add('/pages_sitemap.xml') ->add(Sitemap::create('/posts_sitemap.xml') ->setLastModificationDate(Carbon::yesterday())) ->writeToFile($sitemapIndexPath); ``` the generated sitemap index will look similar to this: ```xml <?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>http://www.example.com/pages_sitemap.xml</loc> <lastmod>2016-01-01T00:00:00+00:00</lastmod> </sitemap> <sitemap> <loc>http://www.example.com/posts_sitemap.xml</loc> <lastmod>2015-12-31T00:00:00+00:00</lastmod> </sitemap> </sitemapindex> ``` ### Create a sitemap index with sub-sequent sitemaps You can call the `maxTagsPerSitemap` method to generate a sitemap that only contains the given amount of tags ```php use Spatie\Sitemap\SitemapGenerator; SitemapGenerator::create('https://example.com') ->maxTagsPerSitemap(20000) ->writeToFile(public_path('sitemap.xml')); ``` ## Generating the sitemap frequently Your site will probably be updated from time to time. In order to let your sitemap reflect these changes, you can run the generator periodically. The easiest way of doing this is to make use of Laravel's default scheduling capabilities. You could set up an artisan command much like this one: ```php namespace App\Console\Commands; use Illuminate\Console\Command; use Spatie\Sitemap\SitemapGenerator; class GenerateSitemap extends Command { /** * The console command name. * * @var string */ protected $signature = 'sitemap:generate'; /** * The console command description. * * @var string */ protected $description = 'Generate the sitemap.'; /** * Execute the console command. * * @return mixed */ public function handle() { // modify this to your own needs SitemapGenerator::create(config('app.url')) ->writeToFile(public_path('sitemap.xml')); } } ``` That command should then be scheduled in the console kernel. ```php // app/Console/Kernel.php protected function schedule(Schedule $schedule) { ... $schedule->command('sitemap:generate')->daily(); ... } ``` ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. ## Testing First start the test server in a separate terminal session: ``` bash cd tests/server ./start_server.sh ``` With the server running you can execute the tests: ``` bash $ composer test ``` ## Contributing Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. ## Security If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. ## Credits - [Freek Van der Herten](https://github.com/freekmurze) - [All Contributors](../../contributors) ## Support us Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie). All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff. ## License The MIT License (MIT). Please see [License File](LICENSE.md) for more information. laravel-sitemap/resources/views/sitemapIndex/index.blade.php 0000644 00000000353 15105603711 0020321 0 ustar 00 <?= '<'.'?'.'xml version="1.0" encoding="UTF-8"?>'."\n" ?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> @foreach($tags as $tag) @include('sitemap::sitemapIndex/' . $tag->getType()) @endforeach </sitemapindex> laravel-sitemap/resources/views/sitemapIndex/sitemap.blade.php 0000644 00000000352 15105603711 0020653 0 ustar 00 <sitemap> @if (! empty($tag->url)) <loc>{{ url($tag->url) }}</loc> @endif @if (! empty($tag->lastModificationDate)) <lastmod>{{ $tag->lastModificationDate->format(DateTime::ATOM) }}</lastmod> @endif </sitemap> laravel-sitemap/resources/views/news.blade.php 0000644 00000000651 15105603711 0015535 0 ustar 00 <news:news> <news:publication> <news:name>{{ $news->name }}</news:name> <news:language>{{ $news->language }}</news:language> </news:publication> <news:title>{{ $news->title }}</news:title> <news:publication_date>{{ $news->publicationDate->toW3cString() }}</news:publication_date> @foreach($news->options as $tag => $value) <news:{{$tag}}>{{$value}}</news:{{$tag}}> @endforeach </news:news>