From f4f0b9a831d95dba785dd6ab81f6eca3330002e5 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 28 Jun 2025 12:45:19 +1000 Subject: [PATCH 01/21] Import intervention/image --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 3cc1215dc..0ed10028e 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "php": "^8.0.2", "composer/composer": "^2.0.0", "doctrine/dbal": "^2.13.3|^3.1.4", + "intervention/image": "^3.10", "linkorb/jsmin-php": "~1.0", "wikimedia/less.php": "~5.2", "scssphp/scssphp": "~1.0", From f340082b58ac1ba03cb1f1473bfd2bc6abd9f232 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Tue, 19 Aug 2025 18:51:40 +1000 Subject: [PATCH 02/21] Refactor resizer to use intervention/image --- src/Resize/BrokenImage.php | 37 --- src/Resize/Resizer.php | 595 ++++--------------------------------- 2 files changed, 54 insertions(+), 578 deletions(-) delete mode 100644 src/Resize/BrokenImage.php diff --git a/src/Resize/BrokenImage.php b/src/Resize/BrokenImage.php deleted file mode 100644 index 851c67e04..000000000 --- a/src/Resize/BrokenImage.php +++ /dev/null @@ -1,37 +0,0 @@ -mime = $file->getMimeType(); // Open up the file - $this->image = $this->originalImage = $this->openImage($file); - - // Get width and height of our image - $this->orientation = $this->getOrientation($file); + $this->image = $this->openImage($file); // Get width and height of our image - $this->width = $this->getWidth(); - $this->height = $this->getHeight(); + $this->width = $this->image->width(); + $this->height = $this->image->height(); // Set default options $this->setOptions([]); @@ -105,55 +106,12 @@ public static function open($file): Resizer } /** - * retainImageTransparency manipulates an image resource in order to keep - * transparency for PNG and GIF files. + * setOptions sets resizer options */ - protected function retainImageTransparency($img) - { - if (!$img) { - return; - } - - if ($this->mime === 'image/gif') { - $alphaColor = ['red' => 0, 'green' => 0, 'blue' => 0]; - $alphaIndex = imagecolortransparent($img); - - if ($alphaIndex >= 0) { - $alphaColor = imagecolorsforindex($img, $alphaIndex); - } - - $alphaIndex = imagecolorallocatealpha($img, $alphaColor['red'], $alphaColor['green'], $alphaColor['blue'], 127); - imagefill($img, 0, 0, $alphaIndex); - imagecolortransparent($img, $alphaIndex); - } - elseif ($this->mime === 'image/png' || $this->mime === 'image/webp') { - imagealphablending($img, false); - imagesavealpha($img, true); - } - } - - /** - * reset the image back to the original. - */ - public function reset(): Resizer - { - $this->image = $this->originalImage; - - return $this; - } - - /** - * setOptions sets resizer options. Available options are: - * - mode: Either exact, portrait, landscape, auto, fit or crop. - * - offset: The offset of the crop = [ left, top ] - * - sharpen: Sharpen image, from 0 - 100 (default: 0) - * - interlace: Interlace image, Boolean: false (disabled: default), true (enabled) - * - quality: Image quality, from 0 - 100 (default: 90) - */ - public function setOptions(array $options): Resizer + public function setOptions(array $options): static { $this->options = array_merge([ - 'mode' => 'crop', + 'mode' => 'auto', 'offset' => [0, 0], 'sharpen' => 0, 'interlace' => false, @@ -163,532 +121,87 @@ public function setOptions(array $options): Resizer return $this; } - /** - * setOption sets an individual resizer option - * @param string $option Option name to set - * @param mixed $value Option value to set - */ - protected function setOption($option, $value): Resizer - { - $this->options[$option] = $value; - - return $this; - } - /** * getOption gets an individual resizer option - * @param string $option Option name to get - * @return mixed Depends on the option + * @param string $option */ protected function getOption($option) { - return array_get($this->options, $option); + return $this->options[$option] ?? null; } /** - * getOrientation receives the image's exif orientation + * openImage opens a file, detect its mime-type and create an image resource from it * @param \Symfony\Component\HttpFoundation\File\File $file - * @return int|null + * @return mixed */ - protected function getOrientation($file) + protected function openImage($file): ImageInterface { $filePath = $file->getPathname(); - if ($this->mime !== 'image/jpeg' || !function_exists('exif_read_data')) { - return null; - } - - // Reading the exif data is prone to fail due to bad data - $exif = @exif_read_data($filePath); - if (!isset($exif['Orientation'])) { - return null; - } - - // Only take care of spin orientations, no mirrored - if (!in_array($exif['Orientation'], [1, 3, 6, 8], true)) { - return null; - } - - return $exif['Orientation']; - } - - /** - * getWidth receives the image's width while respecting - * the exif orientation - * @return int - */ - protected function getWidth() - { - switch ($this->orientation) { - case 6: - case 8: - return imagesy($this->image); - - case 1: - case 3: - default: - return imagesx($this->image); - } - } - - /** - * getHeight receives the image's height while respecting - * the exif orientation - * @return int - */ - protected function getHeight() - { - switch ($this->orientation) { - case 6: - case 8: - return imagesx($this->image); - - case 1: - case 3: - default: - return imagesy($this->image); - } - } - - /** - * getRotatedOriginal receives the original but rotated image - * according to exif orientation - * @return \GdImage - */ - protected function getRotatedOriginal() - { - switch ($this->orientation) { - case 6: - $angle = 270.0; - break; - - case 8: - $angle = 90.0; - break; - - case 3: - $angle = 180.0; - break; - - case 1: - default: - return $this->image; - } - - $bgcolor = imagecolorallocate($this->image, 0, 0, 0); - - return imagerotate($this->image, $angle, $bgcolor); - } - - /** - * resize and/or crop an image - * @param int $newWidth The width of the image - * @param int $newHeight The height of the image - * @param array $options A set of resizing options - */ - public function resize($newWidth, $newHeight, $options = []): Resizer - { - $this->setOptions($options); - - /* - * Sanitize input - */ - $newWidth = (int) $newWidth; - $newHeight = (int) $newHeight; - - if (!$newWidth && !$newHeight) { - $newWidth = $this->width; - $newHeight = $this->height; - } - elseif (!$newWidth) { - $newWidth = $this->getSizeByFixedHeight($newHeight); - } - elseif (!$newHeight) { - $newHeight = $this->getSizeByFixedWidth($newWidth); - } - - // Get optimal width and height - based on supplied mode. - list($optimalWidth, $optimalHeight) = $this->getDimensions($newWidth, $newHeight); - - // Get the rotated the original image according to exif orientation - $rotatedOriginal = $this->getRotatedOriginal(); - - if ($this->mime === 'image/gif') { - // Use imagescale() for GIFs, as it produces better results - $imageResized = imagescale($rotatedOriginal, (int) $optimalWidth, (int) $optimalHeight, IMG_NEAREST_NEIGHBOUR); - $this->retainImageTransparency($imageResized); - } - else { - // Resample - create image canvas of x, y size - $imageResized = imagecreatetruecolor((int) $optimalWidth, (int) $optimalHeight); - $this->retainImageTransparency($imageResized); - - // Create the new image - imagecopyresampled( - $imageResized, - $rotatedOriginal, - 0, - 0, - 0, - 0, - (int) $optimalWidth, - (int) $optimalHeight, - (int) $this->width, - (int) $this->height - ); - } - - $this->image = $imageResized; - - // Apply sharpness - if ($sharpen = $this->getOption('sharpen')) { - $this->sharpen($sharpen); - } - - // If mode is crop: find center and use for the cropping. - if ($this->getOption('mode') === 'crop') { - $offset = $this->getOption('offset'); - $cropStartX = ($optimalWidth / 2) - ($newWidth / 2) - $offset[0]; - $cropStartY = ($optimalHeight / 2) - ($newHeight / 2) - $offset[1]; - $this->crop($cropStartX, $cropStartY, $newWidth, $newHeight); - } - - return $this; - } - - /** - * sharpen the image across a scale of 0 - 100 - * @param int $sharpness - */ - public function sharpen($sharpness): Resizer - { - if ($sharpness <= 0 || $sharpness > 100) { - return $this; - } - - $image = $this->image; - - // Normalize sharpening value - $kernelCenter = exp((80 - ((float) $sharpness)) / 18) + 9; - - $matrix = [ - [-1, -1, -1], - [-1, $kernelCenter, -1], - [-1, -1, -1], - ]; + $driver = new \Intervention\Image\Drivers\Gd\Driver; - $divisor = array_sum(array_map('array_sum', $matrix)); + $manager = new \Intervention\Image\ImageManager($driver); - imageconvolution($image, $matrix, $divisor, 0); - - $this->image = $image; - - return $this; + return $manager->read($filePath); } /** - * Crops an image from its center - * @param int $cropStartX Start on X axis - * @param int $cropStartY Start on Y axis - * @param int $newWidth The new width - * @param int $newHeight The new height - * @param int $srcWidth Source area width. - * @param int $srcHeight Source area height. + * reset the image back to the original. */ - public function crop($cropStartX, $cropStartY, $newWidth, $newHeight, $srcWidth = null, $srcHeight = null): Resizer + public function reset(): static { - $image = $this->image; - - if ($srcWidth === null) { - $srcWidth = $newWidth; - } - if ($srcHeight === null) { - $srcHeight = $newHeight; - } - - // Create a new canvas - $imageResized = imagecreatetruecolor((int) $newWidth, (int) $newHeight); - $this->retainImageTransparency($imageResized); - - // Crop the image to the requested size - imagecopyresampled( - $imageResized, - $image, - 0, - 0, - (int) $cropStartX, - (int) $cropStartY, - (int) $newWidth, - (int) $newHeight, - (int) $srcWidth, - (int) $srcHeight - ); - - $this->image = $imageResized; + $this->image = $this->openImage($this->file); return $this; } /** * save the image based on its file type. - * @param string $savePath Where to save the image + * @param string $savePath */ public function save($savePath) { - $image = $this->image; - - $imageQuality = $this->getOption('quality'); - - // Apply boundaries to quality (0-100) - $imageQuality = max(min($imageQuality, 100), 0); - - if ($this->getOption('interlace')) { - imageinterlace($image, true); - } - - // Determine the image type from the destination file - $extension = pathinfo($savePath, PATHINFO_EXTENSION) ?: $this->extension; - - // Create and save an image based on it's extension - switch (strtolower($extension)) { - case 'jpg': - case 'jpeg': - // Check JPG support is enabled - if (imagetypes() & IMG_JPG) { - $width = imagesx($image); - $height = imagesy($image); - - $imageCanvas = imagecreatetruecolor($width, $height); - $white = imagecolorallocate($imageCanvas, 255, 255, 255); - imagefill($imageCanvas, 0, 0, $white); - imagecopy($imageCanvas, $image, 0, 0, 0, 0, $width, $height); - imagejpeg($imageCanvas, $savePath, $imageQuality); - } - break; - - case 'gif': - // Check GIF support is enabled - if (imagetypes() & IMG_GIF) { - imagegif($image, $savePath); - } - break; - - case 'png': - // Scale quality from 0-100 to 0-9 - $scaleQuality = round(($imageQuality / 100) * 9); - - // Invert quality setting as 0 is best, not 9 - $invertScaleQuality = 9 - $scaleQuality; - - // Check PNG support is enabled - if (imagetypes() & IMG_PNG) { - imagepng($image, $savePath, $invertScaleQuality); - } - break; - - case 'webp': - // Check WEBP support is enabled - if (imagetypes() & IMG_WEBP) { - imagewebp($image, $savePath, $imageQuality); - } - break; - - default: - throw new Exception(sprintf( - 'Invalid image type: %s. Accepted types: jpg, gif, png, webp.', - $extension - )); - } - - // Remove the resource for the resized image - imagedestroy($image); - } - - /** - * openImage opens a file, detect its mime-type and create an image resource from it - * @param \Symfony\Component\HttpFoundation\File\File $file File instance - * @return mixed - */ - protected function openImage($file) - { - $filePath = $file->getPathname(); - - switch ($this->mime) { - case 'image/jpeg': - $img = @imagecreatefromjpeg($filePath); - break; - case 'image/gif': - $img = @imagecreatefromgif($filePath); - break; - case 'image/png': - $img = @imagecreatefrompng($filePath); - $this->retainImageTransparency($img); - break; - case 'image/webp': - $img = @imagecreatefromwebp($filePath); - $this->retainImageTransparency($img); - break; - default: - throw new Exception("Invalid mime type: {$this->mime}. Accepted types: image/jpeg, image/gif, image/png, image/webp."); - } - - if ($img === false) { - throw new Exception("Resizer failed opening the file for reading ({$this->mime})."); - } - - return $img; - } - - /** - * getDimensions returns the image dimensions based on the option that was chosen. - * @param int $newWidth The width of the image - * @param int $newHeight The height of the image - * @return array - * @throws Exception Thrown for invalid dimension string - */ - protected function getDimensions($newWidth, $newHeight) - { - $mode = $this->getOption('mode'); - - switch ($mode) { - case 'exact': - return [$newWidth, $newHeight]; - - case 'portrait': - return [$this->getSizeByFixedHeight($newHeight), $newHeight]; - - case 'landscape': - return [$newWidth, $this->getSizeByFixedWidth($newWidth)]; - - case 'auto': - return $this->getSizeByAuto($newWidth, $newHeight); - - case 'crop': - return $this->getOptimalCrop($newWidth, $newHeight); - - case 'fit': - return $this->getSizeByFit($newWidth, $newHeight); - - default: - throw new Exception('Invalid dimension type. Accepted types: exact, portrait, landscape, auto, crop, fit.'); - } + $this->image->save($savePath); } /** - * getSizeByFixedHeight returns the width based on the image height - * @param int $newHeight The height of the image - * @return int + * resize and/or crop an image, specifying the new width and height of the + * destination image. + * @param int $width + * @param int $height + * @param array $options */ - protected function getSizeByFixedHeight($newHeight) + public function resize($width, $height, $options = []): static { - $ratio = $this->width / $this->height; - return $newHeight * $ratio; - } + $this->setOptions($options); - /** - * Returns the height based on the image width - * @param int $newWidth The width of the image - * @return int - */ - protected function getSizeByFixedWidth($newWidth) - { - $ratio = $this->height / $this->width; - return $newWidth * $ratio; - } + $mode = $this->options['mode'] ?? 'auto'; - /** - * getSizeByAuto checks to see if an image is portrait or landscape and resizes accordingly. - * @param int $newWidth The width of the image - * @param int $newHeight The height of the image - */ - protected function getSizeByAuto($newWidth, $newHeight): array - { - // Less than 1 pixel height and width? (revert to original) - if ($newWidth <= 1 && $newHeight <= 1) { - $newWidth = $this->width; - $newHeight = $this->height; - } - elseif ($newWidth <= 1) { - $newWidth = $this->getSizeByFixedHeight($newHeight); + if ($mode === 'exact') { + $this->image->resize($width, $height); } - // Less than 1 pixel height? (portrait) - elseif ($newHeight <= 1) { - $newHeight = $this->getSizeByFixedWidth($newWidth); - } - - // Image to be resized is wider (landscape) - if ($this->height < $this->width) { - $optimalWidth = $newWidth; - $optimalHeight = $this->getSizeByFixedWidth($newWidth); + elseif ($mode === 'crop') { + $this->image->crop( + $width, + $height, + $this->options['offset'][0] ?? 0, + $this->options['offset'][1] ?? 0 + ); } - // Image to be resized is taller (portrait) - elseif ($this->height > $this->width) { - $optimalWidth = $this->getSizeByFixedHeight($newHeight); - $optimalHeight = $newHeight; + elseif ($mode === 'cover' || $mode === 'fit') { + $this->image->cover($width, $height); } - // Image to be resized is a square - else { - if ($newHeight < $newWidth) { - $optimalWidth = $newWidth; - $optimalHeight = $this->getSizeByFixedWidth($newWidth); - } - elseif ($newHeight > $newWidth) { - $optimalWidth = $this->getSizeByFixedHeight($newHeight); - $optimalHeight = $newHeight; - } - else { - // Square being resized to a square - $optimalWidth = $newWidth; - $optimalHeight = $newHeight; - } + elseif ($mode === 'auto') { + $this->image->scale($width, $height); } - - return [$optimalWidth, $optimalHeight]; - } - - /** - * getOptimalCrop attempts to find the best way to crop. Whether crop is based on the - * image being portrait or landscape. - * @param int $newWidth The width of the image - * @param int $newHeight The height of the image - */ - protected function getOptimalCrop($newWidth, $newHeight): array - { - $heightRatio = $this->height / $newHeight; - $widthRatio = $this->width / $newWidth; - - if ($heightRatio < $widthRatio) { - $optimalRatio = $heightRatio; + elseif ($mode === 'portrait') { + $this->image->scale(null, $height); } - else { - $optimalRatio = $widthRatio; + elseif ($mode === 'landscape') { + $this->image->scale($width, null); } - $optimalHeight = round($this->height / $optimalRatio); - $optimalWidth = round($this->width / $optimalRatio); - - return [$optimalWidth, $optimalHeight]; - } - - /** - * getSizeByFit fits the image inside a bounding box using maximum width - * and height constraints. - * @param int $maxWidth The maximum width of the image - * @param int $maxHeight The maximum height of the image - */ - protected function getSizeByFit($maxWidth, $maxHeight): array - { - // Calculate the scaling ratios in order to get the target width and height - $ratioW = $maxWidth / $this->width; - $ratioH = $maxHeight / $this->height; - - // Select the ratio which makes the image fit inside the constraints - $effectiveRatio = min($ratioW, $ratioH); - - // Calculate the final width and height according to this ratio - $optimalWidth = round($this->width * $effectiveRatio); - $optimalHeight = round($this->height * $effectiveRatio); - - return [$optimalWidth, $optimalHeight]; + return $this; } } From 19e4688c8c7b64f767dd6773e0f07c594283faff Mon Sep 17 00:00:00 2001 From: Dheia Date: Sat, 6 Sep 2025 05:15:43 +0300 Subject: [PATCH 03/21] Update ComposerManager.php (#643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem Description and Solution for "Class core.composer does not exist" in October CMS v4 Problem In October CMS version v4 (built on Laravel 12), an issue occurred when loading certain plugins that contained their own vendor folder and composer.json file. The system was throwing the following error: ``` ReflectionException: Class "core.composer" does not exist ``` This error occurred because the system was trying to access the core.composer object from the Laravel Container at a time when it wasn't yet available, particularly during the initial system initialization process. Root Cause The error was occurring in the instance() method within the October\Rain\Composer\ComposerManager class, which contained the following code: ```php public static function instance(): static { return App::make('core.composer'); } ``` In previous versions of October CMS, the core.composer object was registered in the container synchronously with class loading. However, in v4 with Laravel 12, the service provider loading mechanism changed, causing timing conflicts. Solution The instance() method was modified to first check if the object exists in the container, and if not, create a new instance instead: ```php public static function instance(): static { try { return App::make('core.composer'); } catch (\Illuminate\Contracts\Container\BindingResolutionException $e) { // If not available in the container, create a new instance return new static(); } } ``` Required Changes 1. Modify the file: plugins/october/rain/src/Composer/ComposerManager.php 2. Update the instance() method as shown above 3. Ensure the Illuminate\Contracts\Container\BindingResolutionException exception is imported at the top of the file Additional Notes · This solution maintains compatibility with previous versions of October CMS · It doesn't affect the current functionality of the system · It solves the problem without requiring major changes to the system architecture · It works as a preventive solution even when the core.composer object isn't registered in the container This solution has been tested on October CMS v4 and has proven effective in resolving the issue without causing any side effects. --- src/Composer/ComposerManager.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Composer/ComposerManager.php b/src/Composer/ComposerManager.php index 15a9a6b8a..872a961a3 100644 --- a/src/Composer/ComposerManager.php +++ b/src/Composer/ComposerManager.php @@ -41,7 +41,11 @@ public function __construct() */ public static function instance(): static { - return App::make('core.composer'); + try { + return App::make('core.composer'); + } catch (\Illuminate\Contracts\Container\BindingResolutionException $e) { + return new static(); + } } /** From b4b48d74d4b8af7d89f49e137b338fbce107e682 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Tue, 21 Oct 2025 06:35:14 +1100 Subject: [PATCH 04/21] Formatting, remove combine, stick with assetic --- src/Assetic/Factory/Resource/FileResource.php | 12 ++ src/Assetic/Filter/CssMinFilter.php | 4 +- src/Assetic/Filter/FilterCollection.php | 26 ++- src/Assetic/Filter/JSqueezeFilter.php | 32 +++- src/Assetic/Filter/JavascriptImporter.php | 3 +- src/Assetic/Filter/ScssphpFilter.php | 50 +++++- src/Assetic/Filter/StylesheetMinify.php | 6 + src/Combine/Combiner.php | 50 ------ src/Combine/CombinerServiceProvider.php | 34 ---- src/Combine/JavascriptMinify.php | 30 ---- src/Combine/JsCompile.php | 157 ------------------ src/Combine/LessCompile.php | 44 ----- src/Combine/ScssCompile.php | 41 ----- src/Combine/StylesheetMinify.php | 56 ------- 14 files changed, 119 insertions(+), 426 deletions(-) delete mode 100644 src/Combine/Combiner.php delete mode 100644 src/Combine/CombinerServiceProvider.php delete mode 100644 src/Combine/JavascriptMinify.php delete mode 100644 src/Combine/JsCompile.php delete mode 100644 src/Combine/LessCompile.php delete mode 100644 src/Combine/ScssCompile.php delete mode 100644 src/Combine/StylesheetMinify.php diff --git a/src/Assetic/Factory/Resource/FileResource.php b/src/Assetic/Factory/Resource/FileResource.php index 9bc720f46..bf7b73cbe 100644 --- a/src/Assetic/Factory/Resource/FileResource.php +++ b/src/Assetic/Factory/Resource/FileResource.php @@ -7,6 +7,9 @@ */ class FileResource implements ResourceInterface { + /** + * @var string path + */ protected $path; /** @@ -19,16 +22,25 @@ public function __construct($path) $this->path = $path; } + /** + * isFresh + */ public function isFresh($timestamp) { return file_exists($this->path) && filemtime($this->path) <= $timestamp; } + /** + * getContent + */ public function getContent() { return file_exists($this->path) ? file_get_contents($this->path) : ''; } + /** + * __toString + */ public function __toString() { return $this->path; diff --git a/src/Assetic/Filter/CssMinFilter.php b/src/Assetic/Filter/CssMinFilter.php index dbea87ff3..6dd952ae9 100644 --- a/src/Assetic/Filter/CssMinFilter.php +++ b/src/Assetic/Filter/CssMinFilter.php @@ -15,8 +15,8 @@ class CssMinFilter implements FilterInterface public function __construct() { - $this->filters = array(); - $this->plugins = array(); + $this->filters = []; + $this->plugins = []; } public function setFilters(array $filters) diff --git a/src/Assetic/Filter/FilterCollection.php b/src/Assetic/Filter/FilterCollection.php index d5dc7fbc1..27654b03f 100644 --- a/src/Assetic/Filter/FilterCollection.php +++ b/src/Assetic/Filter/FilterCollection.php @@ -10,8 +10,14 @@ */ class FilterCollection implements FilterInterface, \IteratorAggregate, \Countable { - private $filters = array(); + /** + * @var array filters + */ + protected $filters = []; + /** + * __construct + */ public function __construct($filters = array()) { foreach ($filters as $filter) { @@ -36,16 +42,25 @@ public function ensure(FilterInterface $filter) } } + /** + * all + */ public function all() { return $this->filters; } + /** + * clear + */ public function clear() { $this->filters = array(); } + /** + * filterLoad + */ public function filterLoad(AssetInterface $asset) { foreach ($this->filters as $filter) { @@ -53,6 +68,9 @@ public function filterLoad(AssetInterface $asset) } } + /** + * filterDump + */ public function filterDump(AssetInterface $asset) { foreach ($this->filters as $filter) { @@ -60,11 +78,17 @@ public function filterDump(AssetInterface $asset) } } + /** + * getIterator + */ public function getIterator(): Traversable { return new \ArrayIterator($this->filters); } + /** + * count + */ public function count(): int { return count($this->filters); diff --git a/src/Assetic/Filter/JSqueezeFilter.php b/src/Assetic/Filter/JSqueezeFilter.php index ef0214483..991378e51 100644 --- a/src/Assetic/Filter/JSqueezeFilter.php +++ b/src/Assetic/Filter/JSqueezeFilter.php @@ -11,12 +11,34 @@ */ class JSqueezeFilter implements FilterInterface { - private $singleLine = true; - private $keepImportantComments = true; - private $className; - private $specialVarRx = false; - private $defaultRx; + /** + * @var mixed singleLine + */ + protected $singleLine = true; + /** + * @var bool keepImportantComments + */ + protected $keepImportantComments = true; + + /** + * @var mixed className + */ + protected $className; + + /** + * @var bool specialVarRx + */ + protected $specialVarRx = false; + + /** + * @var mixed defaultRx + */ + protected $defaultRx; + + /** + * __construct + */ public function __construct() { // JSqueeze is namespaced since 2.x, this works with both 1.x and 2.x diff --git a/src/Assetic/Filter/JavascriptImporter.php b/src/Assetic/Filter/JavascriptImporter.php index 478fe0b30..afde433a9 100644 --- a/src/Assetic/Filter/JavascriptImporter.php +++ b/src/Assetic/Filter/JavascriptImporter.php @@ -7,7 +7,7 @@ /** * JavascriptImporter importer JS Filter - * Class used to import referenced javascript files. + * Class used to import referenced javascript files, inside comments. * * =include library/jquery.js; * =require library/jquery.js; @@ -17,7 +17,6 @@ */ class JavascriptImporter implements FilterInterface { - /** * @var string Location of where the processed JS script resides. */ diff --git a/src/Assetic/Filter/ScssphpFilter.php b/src/Assetic/Filter/ScssphpFilter.php index bb2bc3244..fea92fbbc 100644 --- a/src/Assetic/Filter/ScssphpFilter.php +++ b/src/Assetic/Filter/ScssphpFilter.php @@ -20,41 +20,77 @@ */ class ScssphpFilter implements DependencyExtractorInterface { - private $importPaths = []; - private $customFunctions = []; - private $formatter; - private $variables = []; + /** + * @var array importPaths + */ + protected $importPaths = []; + + /** + * @var array customFunctions + */ + protected $customFunctions = []; + + /** + * @var mixed formatter + */ + protected $formatter; + /** + * @var array variables + */ + protected $variables = []; + + /** + * setFormatter + */ public function setFormatter($formatter) { $this->formatter = $formatter; } + /** + * setVariables + */ public function setVariables(array $variables) { $this->variables = $variables; } + /** + * addVariable + */ public function addVariable($variable) { $this->variables[] = $variable; } + /** + * setImportPaths + */ public function setImportPaths(array $paths) { $this->importPaths = $paths; } + /** + * addImportPath + */ public function addImportPath($path) { $this->importPaths[] = $path; } + /** + * registerFunction + */ public function registerFunction($name, $callable) { $this->customFunctions[$name] = $callable; } + /** + * filterLoad + */ public function filterLoad(AssetInterface $asset) { $sc = new Compiler(); @@ -101,10 +137,16 @@ public function filterLoad(AssetInterface $asset) $asset->setContent($result->getCss()); } + /** + * filterDump + */ public function filterDump(AssetInterface $asset) { } + /** + * getChildren + */ public function getChildren(AssetFactory $factory, $content, $loadPath = null) { $sc = new Compiler(); diff --git a/src/Assetic/Filter/StylesheetMinify.php b/src/Assetic/Filter/StylesheetMinify.php index c4da45a65..f2c81b811 100644 --- a/src/Assetic/Filter/StylesheetMinify.php +++ b/src/Assetic/Filter/StylesheetMinify.php @@ -12,10 +12,16 @@ */ class StylesheetMinify implements FilterInterface { + /** + * filterLoad + */ public function filterLoad(AssetInterface $asset) { } + /** + * filterDump + */ public function filterDump(AssetInterface $asset) { $asset->setContent($this->minify($asset->getContent())); diff --git a/src/Combine/Combiner.php b/src/Combine/Combiner.php deleted file mode 100644 index 5754c228d..000000000 --- a/src/Combine/Combiner.php +++ /dev/null @@ -1,50 +0,0 @@ -minify($text); - } - - /** - * compileLess - */ - public function compileLess(array $text, $options = []) - { - return (new LessCompile)->compile($text, $options); - } - - /** - * compileScss - */ - public function compileScss(array $text, $options = []) - { - return (new ScssCompile)->compile($text, $options); - } - - /** - * minifyJs - */ - public function minifyJs(array $text, $options = []) - { - return (new JavascriptMinify)->minify($text); - } - - /** - * compileJs - */ - public function compileJs(array $text, $options = []) - { - return (new JsCompile)->compile($text, $options); - } -} diff --git a/src/Combine/CombinerServiceProvider.php b/src/Combine/CombinerServiceProvider.php deleted file mode 100644 index 3e62b066f..000000000 --- a/src/Combine/CombinerServiceProvider.php +++ /dev/null @@ -1,34 +0,0 @@ -app->singleton('combiner', function ($app) { - return new Combiner; - }); - } - - /** - * provides the returned services. - * @return array - */ - public function provides() - { - return [ - 'combiner', - ]; - } -} diff --git a/src/Combine/JavascriptMinify.php b/src/Combine/JavascriptMinify.php deleted file mode 100644 index fe34957ca..000000000 --- a/src/Combine/JavascriptMinify.php +++ /dev/null @@ -1,30 +0,0 @@ -minify(file_get_contents($path)); - } -} diff --git a/src/Combine/JsCompile.php b/src/Combine/JsCompile.php deleted file mode 100644 index c89125056..000000000 --- a/src/Combine/JsCompile.php +++ /dev/null @@ -1,157 +0,0 @@ - null - ], $options)); - - if (!$basePath) { - throw new Exception('You must specify a base path'); - } - - $this->basePath = $basePath; - $this->includedFiles = []; - - return $this->parse($js); - } - - /** - * compileFile - */ - public function compileFile($path, $options = []) - { - return $this->compile(file_get_contents($path), $options); - } - - /** - * Process JS imports inside a string of javascript - * @param $content string JS code to process. - * @return string Processed JS. - */ - protected function parse($content) - { - $imported = ''; - - // Look for: /* comments */ - if (!preg_match_all('@/\*(.*)\*/@msU', $content, $matches)) { - return $content; - } - - foreach ($matches[1] as $macro) { - // Look for: =include something - if (!preg_match_all('/=([^\\s]*)\\s(.*)\n/', $macro, $matches2)) { - continue; - } - - foreach ($matches2[1] as $index => $macroName) { - $method = 'directive' . ucfirst(strtolower($macroName)); - - if (method_exists($this, $method)) { - $imported .= $this->$method($matches2[2][$index]); - } - } - } - - return $imported . $content; - } - - /** - * directiveInclude to process script includes - */ - protected function directiveInclude($data, $required = false) - { - $require = explode(',', $data); - $result = ""; - - foreach ($require as $script) { - $script = trim($script); - - if (!File::extension($script)) { - $script = $script . '.js'; - } - - $scriptPath = realpath($this->basePath . '/' . $script); - if (!File::isFile($scriptPath)) { - $errorMsg = sprintf("File '%s' not found.", $script); - if ($required) { - throw new RuntimeException($errorMsg); - } - - $result .= '/* ' . $errorMsg . ' */' . PHP_EOL; - continue; - } - - // Exclude duplicates - if (in_array($script, $this->includedFiles)) { - continue; - } - - $this->includedFiles[] = $script; - - // Nested parsing - $oldScriptPath = $this->basePath; - $this->basePath = dirname($scriptPath); - $content = File::get($scriptPath); - $content = $this->parse($content) . PHP_EOL; - $this->basePath = $oldScriptPath; - - // Parse in "magic constants" - $content = str_replace( - ['__DATE__', '__FILE__'], - [date("D M j G:i:s T Y"), $script], - $content - ); - - $result .= $content; - } - - return $result; - } - - /** - * directiveRequire to process mandatory script includes - */ - protected function directiveRequire($data) - { - return $this->directiveInclude($data, true); - } - - /** - * directiveDefine to define and replace variables - */ - protected function directiveDefine($data) - { - if (preg_match('@([^\\s]*)\\s+(.*)@', $data, $matches)) { - // str_replace($matches[1], $matches[2], $context); - $this->definedVars[] = [$matches[1], $matches[2]]; - } - - return ''; - } -} diff --git a/src/Combine/LessCompile.php b/src/Combine/LessCompile.php deleted file mode 100644 index 55a9481a2..000000000 --- a/src/Combine/LessCompile.php +++ /dev/null @@ -1,44 +0,0 @@ - null, - 'compress' => false, - ], $options)); - - $parser = new Less_Parser([ - 'compress' => (bool) $compress - ]); - - $parser->parse($less); - - // Set the LESS variables after parsing to override them - if ($vars) { - $parser->ModifyVars($vars); - } - - return $parser->getCss(); - } - - /** - * compileFile - */ - public function compileFile($path, $options = []) - { - return $this->compile(file_get_contents($path), $options); - } -} diff --git a/src/Combine/ScssCompile.php b/src/Combine/ScssCompile.php deleted file mode 100644 index 8174518be..000000000 --- a/src/Combine/ScssCompile.php +++ /dev/null @@ -1,41 +0,0 @@ - null, - 'compress' => false, // @todo - ], $options)); - - $parser = new Compiler(); - - if ($vars) { - $parser->addVariables($vars); - } - - $result = $parser->compileString($scss); - - return $result->getCss(); - } - - /** - * compileFile - */ - public function compileFile($path, $options = []) - { - return $this->compile(file_get_contents($path), $options); - } -} diff --git a/src/Combine/StylesheetMinify.php b/src/Combine/StylesheetMinify.php deleted file mode 100644 index dcb0d3c9d..000000000 --- a/src/Combine/StylesheetMinify.php +++ /dev/null @@ -1,56 +0,0 @@ -, but not after !*/ - $css = preg_replace('/(,|:|;|\{|}|[^!]\*\/|>) /', '$1', $css); - - // Remove space before , ; { } > - $css = preg_replace('/ (,|;|\{|}|>)/', '$1', $css); - - // Remove newline before } > - $css = preg_replace('/(\r\n|\r|\n)(})/', '$2', $css); - - // Remove trailing zeros from float numbers preceded by : or a white-space - // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px - $css = preg_replace('/((?minify(file_get_contents($path)); - } -} From 6fbd545b9d8158462bfede5d69e0b0f8803a1b1e Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Tue, 21 Oct 2025 06:35:43 +1100 Subject: [PATCH 05/21] Remove unused files --- .../Factory/Resource/DirectoryResource.php | 73 ------------------- .../DirectoryResourceFilterIterator.php | 36 --------- .../Resource/DirectoryResourceIterator.php | 18 ----- src/Assetic/Factory/Resource/FileResource.php | 48 ------------ .../Resource/IteratorResourceInterface.php | 10 --- .../Factory/Resource/ResourceInterface.php | 32 -------- 6 files changed, 217 deletions(-) delete mode 100644 src/Assetic/Factory/Resource/DirectoryResource.php delete mode 100644 src/Assetic/Factory/Resource/DirectoryResourceFilterIterator.php delete mode 100644 src/Assetic/Factory/Resource/DirectoryResourceIterator.php delete mode 100644 src/Assetic/Factory/Resource/FileResource.php delete mode 100644 src/Assetic/Factory/Resource/IteratorResourceInterface.php delete mode 100644 src/Assetic/Factory/Resource/ResourceInterface.php diff --git a/src/Assetic/Factory/Resource/DirectoryResource.php b/src/Assetic/Factory/Resource/DirectoryResource.php deleted file mode 100644 index 4dd87a8c1..000000000 --- a/src/Assetic/Factory/Resource/DirectoryResource.php +++ /dev/null @@ -1,73 +0,0 @@ - - */ -class DirectoryResource implements IteratorResourceInterface -{ - private $path; - private $pattern; - - /** - * Constructor. - * - * @param string $path A directory path - * @param string $pattern A filename pattern - */ - public function __construct($path, $pattern = null) - { - if (DIRECTORY_SEPARATOR != substr($path, -1)) { - $path .= DIRECTORY_SEPARATOR; - } - - $this->path = $path; - $this->pattern = $pattern; - } - - public function isFresh($timestamp) - { - if (!is_dir($this->path) || filemtime($this->path) > $timestamp) { - return false; - } - - foreach ($this as $resource) { - if (!$resource->isFresh($timestamp)) { - return false; - } - } - - return true; - } - - /** - * Returns the combined content of all inner resources. - */ - public function getContent() - { - $content = array(); - foreach ($this as $resource) { - $content[] = $resource->getContent(); - } - - return implode("\n", $content); - } - - public function __toString() - { - return $this->path; - } - - public function getIterator() - { - return is_dir($this->path) - ? new DirectoryResourceIterator($this->getInnerIterator()) - : new \EmptyIterator(); - } - - protected function getInnerIterator() - { - return new DirectoryResourceFilterIterator(new \RecursiveDirectoryIterator($this->path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS), $this->pattern); - } -} diff --git a/src/Assetic/Factory/Resource/DirectoryResourceFilterIterator.php b/src/Assetic/Factory/Resource/DirectoryResourceFilterIterator.php deleted file mode 100644 index 11a85be03..000000000 --- a/src/Assetic/Factory/Resource/DirectoryResourceFilterIterator.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @access private - */ -class DirectoryResourceFilterIterator extends \RecursiveFilterIterator -{ - protected $pattern; - - public function __construct(\RecursiveDirectoryIterator $iterator, $pattern = null) - { - parent::__construct($iterator); - - $this->pattern = $pattern; - } - - public function accept() - { - $file = $this->current(); - $name = $file->getBasename(); - - if ($file->isDir()) { - return '.' != $name[0]; - } - - return null === $this->pattern || 0 < preg_match($this->pattern, $name); - } - - public function getChildren() - { - return new self(new \RecursiveDirectoryIterator($this->current()->getPathname(), \RecursiveDirectoryIterator::FOLLOW_SYMLINKS), $this->pattern); - } -} diff --git a/src/Assetic/Factory/Resource/DirectoryResourceIterator.php b/src/Assetic/Factory/Resource/DirectoryResourceIterator.php deleted file mode 100644 index bc8da7d6d..000000000 --- a/src/Assetic/Factory/Resource/DirectoryResourceIterator.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @access private - */ -class DirectoryResourceIterator extends \RecursiveIteratorIterator -{ - /** - * current - */ - public function current() - { - return new FileResource(parent::current()->getPathname()); - } -} diff --git a/src/Assetic/Factory/Resource/FileResource.php b/src/Assetic/Factory/Resource/FileResource.php deleted file mode 100644 index bf7b73cbe..000000000 --- a/src/Assetic/Factory/Resource/FileResource.php +++ /dev/null @@ -1,48 +0,0 @@ - - */ -class FileResource implements ResourceInterface -{ - /** - * @var string path - */ - protected $path; - - /** - * __construct - * - * @param string $path The path to a file - */ - public function __construct($path) - { - $this->path = $path; - } - - /** - * isFresh - */ - public function isFresh($timestamp) - { - return file_exists($this->path) && filemtime($this->path) <= $timestamp; - } - - /** - * getContent - */ - public function getContent() - { - return file_exists($this->path) ? file_get_contents($this->path) : ''; - } - - /** - * __toString - */ - public function __toString() - { - return $this->path; - } -} diff --git a/src/Assetic/Factory/Resource/IteratorResourceInterface.php b/src/Assetic/Factory/Resource/IteratorResourceInterface.php deleted file mode 100644 index 83cdd6478..000000000 --- a/src/Assetic/Factory/Resource/IteratorResourceInterface.php +++ /dev/null @@ -1,10 +0,0 @@ - - */ -interface IteratorResourceInterface extends ResourceInterface, \IteratorAggregate -{ -} diff --git a/src/Assetic/Factory/Resource/ResourceInterface.php b/src/Assetic/Factory/Resource/ResourceInterface.php deleted file mode 100644 index 623911f45..000000000 --- a/src/Assetic/Factory/Resource/ResourceInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -interface ResourceInterface -{ - /** - * Checks if a timestamp represents the latest resource. - * - * @param integer $timestamp A UNIX timestamp - * - * @return Boolean True if the timestamp is up to date - */ - public function isFresh($timestamp); - - /** - * Returns the content of the resource. - * - * @return string The content - */ - public function getContent(); - - /** - * Returns a unique string for the current resource. - * - * @return string A unique string to identity the current resource - */ - public function __toString(); -} From e2432ebe5c848c6ffb664ffa1be5ebc83e4c53d4 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 29 Nov 2025 20:23:51 +1100 Subject: [PATCH 06/21] Model compatibility with Laravel ValidationException --- src/Database/ModelException.php | 6 +++--- src/Exception/ValidationException.php | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Database/ModelException.php b/src/Database/ModelException.php index cab434d91..4027b139f 100644 --- a/src/Database/ModelException.php +++ b/src/Database/ModelException.php @@ -16,13 +16,13 @@ class ModelException extends ValidationException protected $model; /** - * __construct recevies the troublesome model + * __construct receives the troublesome model */ public function __construct(Model $model) { + parent::__construct($model->errors()); + $this->model = $model; - $this->errors = $model->errors(); - $this->evalErrors(); } /** diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php index da2644165..7117be879 100644 --- a/src/Exception/ValidationException.php +++ b/src/Exception/ValidationException.php @@ -3,6 +3,7 @@ use Validator as ValidatorFacade; use Illuminate\Validation\ValidationException as ValidationExceptionBase; use Illuminate\Validation\Validator; +use Illuminate\Support\MessageBag; use InvalidArgumentException; /** @@ -55,6 +56,10 @@ protected function resolveToValidator($validation) $validator = ValidatorFacade::make([], []); $validator->errors()->merge($validation); } + elseif ($validation instanceof MessageBag) { + $validator = ValidatorFacade::make([], []); + $validator->errors()->merge($validation->messages()); + } if (!$validator instanceof Validator) { throw new InvalidArgumentException('ValidationException constructor requires instance of Validator or array'); From 7bf0222bbe42fe0cce0ae41bfaa89f7dfde8c950 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sun, 30 Nov 2025 09:50:49 +1100 Subject: [PATCH 07/21] Fixes setFieldPrefix usage --- src/Exception/ValidationException.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php index 7117be879..764fe4db2 100644 --- a/src/Exception/ValidationException.php +++ b/src/Exception/ValidationException.php @@ -36,8 +36,6 @@ public function __construct($validation) { parent::__construct($this->resolveToValidator($validation)); - $this->errors = $this->validator->errors(); - $this->evalErrors(); } @@ -75,12 +73,12 @@ protected function evalErrors() { $this->fields = []; - foreach ($this->errors->getMessages() as $field => $messages) { + foreach ($this->errors() as $field => $messages) { $fieldName = implode('.', array_merge($this->fieldPrefix, [$field])); $this->fields[$fieldName] = (array) $messages; } - $this->message = $this->errors->first(); + $this->message = $this->getErrors()->first(); } /** @@ -89,11 +87,11 @@ protected function evalErrors() */ public function getErrors() { - return $this->errors; + return $this->validator->errors(); } /** - * getFields returns invalid fields + * @deprecated use ->errors() */ public function getFields() { @@ -108,5 +106,7 @@ public function setFieldPrefix(array $prefix) $this->fieldPrefix = array_filter($prefix, 'strlen'); $this->evalErrors(); + + $this->validator = $this->resolveToValidator($this->fields); } } From 2129ef7f779327fbd705b8a93eb272b6c5932064 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 3 Dec 2025 06:27:53 +1100 Subject: [PATCH 08/21] Implement AjaxExceptionInterface --- composer.json | 8 +++++++- src/Exception/AjaxException.php | 12 +++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 989fae620..2c3a2a1ab 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "php": "^8.0.2", "composer/composer": "^2.0.0", "composer/installers": "^1 || ^2", + "larajax/larajax": "dev-main", "doctrine/dbal": "^2.13.3|^3.1.4", "intervention/image": "^3.10", "jaybizzle/crawler-detect": "^1.3", @@ -67,5 +68,10 @@ } }, "minimum-stability": "dev", - "prefer-stable": true + "prefer-stable": true, + "config": { + "allow-plugins": { + "composer/installers": true + } + } } diff --git a/src/Exception/AjaxException.php b/src/Exception/AjaxException.php index 91478897e..8255c47cd 100644 --- a/src/Exception/AjaxException.php +++ b/src/Exception/AjaxException.php @@ -1,5 +1,7 @@ contents[$key] = $val; } + + /** + * toAjaxData + */ + public function toAjaxData(): array + { + return (array) $this->contents; + } } From 0721425705f96255f0cdcc83a9d50f799853bd43 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Thu, 11 Dec 2025 17:58:24 +1100 Subject: [PATCH 09/21] Formatting --- src/Assetic/Asset/AssetCache.php | 38 +++++------ src/Assetic/Asset/AssetCollection.php | 67 ++++++++++--------- .../Asset/AssetCollectionInterface.php | 16 ++--- src/Assetic/Asset/AssetInterface.php | 40 +++++------ src/Assetic/Asset/BaseAsset.php | 55 ++++++++------- src/Assetic/Asset/FileAsset.php | 6 +- src/Assetic/Asset/GlobAsset.php | 16 ++--- src/Assetic/Asset/HttpAsset.php | 18 ++--- src/Assetic/Asset/StringAsset.php | 15 +++-- src/Assetic/AssetManager.php | 12 ++-- src/Assetic/AssetWriter.php | 8 +-- src/Assetic/AsseticServiceProvider.php | 12 ++-- src/Assetic/Combiner.php | 20 +++--- src/Assetic/Factory/AssetFactory.php | 14 ++-- src/Assetic/Filter/BaseCssFilter.php | 16 ++--- src/Assetic/Filter/CssImportFilter.php | 10 +-- src/Assetic/Filter/CssRewriteFilter.php | 8 +-- src/Assetic/Filter/FilterCollection.php | 14 ++-- src/Assetic/Filter/FilterInterface.php | 4 +- src/Assetic/Filter/JavascriptImporter.php | 14 ++-- src/Assetic/Filter/StylesheetMinify.php | 10 +-- src/Assetic/FilterManager.php | 10 +-- src/Assetic/Util/CssUtils.php | 17 +++-- src/Assetic/Util/VarUtils.php | 15 ++--- 24 files changed, 232 insertions(+), 223 deletions(-) diff --git a/src/Assetic/Asset/AssetCache.php b/src/Assetic/Asset/AssetCache.php index 5100a37f7..5dd15689e 100644 --- a/src/Assetic/Asset/AssetCache.php +++ b/src/Assetic/Asset/AssetCache.php @@ -12,12 +12,12 @@ class AssetCache implements AssetInterface { /** - * @var mixed asset + * @var AssetInterface asset */ protected $asset; /** - * @var mixed cache + * @var CacheInterface cache */ protected $cache; @@ -33,7 +33,7 @@ public function __construct(AssetInterface $asset, CacheInterface $cache) /** * ensureFilter */ - public function ensureFilter(FilterInterface $filter) + public function ensureFilter(FilterInterface $filter): void { $this->asset->ensureFilter($filter); } @@ -41,7 +41,7 @@ public function ensureFilter(FilterInterface $filter) /** * getFilters */ - public function getFilters() + public function getFilters(): array { return $this->asset->getFilters(); } @@ -49,7 +49,7 @@ public function getFilters() /** * clearFilters */ - public function clearFilters() + public function clearFilters(): void { $this->asset->clearFilters(); } @@ -57,7 +57,7 @@ public function clearFilters() /** * load */ - public function load(?FilterInterface $additionalFilter = null) + public function load(?FilterInterface $additionalFilter = null): void { $cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'load'); if ($this->cache->has($cacheKey)) { @@ -73,7 +73,7 @@ public function load(?FilterInterface $additionalFilter = null) /** * dump */ - public function dump(?FilterInterface $additionalFilter = null) + public function dump(?FilterInterface $additionalFilter = null): string { $cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'dump'); if ($this->cache->has($cacheKey)) { @@ -89,7 +89,7 @@ public function dump(?FilterInterface $additionalFilter = null) /** * getContent */ - public function getContent() + public function getContent(): ?string { return $this->asset->getContent(); } @@ -97,7 +97,7 @@ public function getContent() /** * setContent */ - public function setContent($content) + public function setContent(?string $content): void { $this->asset->setContent($content); } @@ -105,7 +105,7 @@ public function setContent($content) /** * getSourceRoot */ - public function getSourceRoot() + public function getSourceRoot(): ?string { return $this->asset->getSourceRoot(); } @@ -113,7 +113,7 @@ public function getSourceRoot() /** * getSourcePath */ - public function getSourcePath() + public function getSourcePath(): ?string { return $this->asset->getSourcePath(); } @@ -121,7 +121,7 @@ public function getSourcePath() /** * getSourceDirectory */ - public function getSourceDirectory() + public function getSourceDirectory(): ?string { return $this->asset->getSourceDirectory(); } @@ -129,7 +129,7 @@ public function getSourceDirectory() /** * getTargetPath */ - public function getTargetPath() + public function getTargetPath(): ?string { return $this->asset->getTargetPath(); } @@ -137,7 +137,7 @@ public function getTargetPath() /** * setTargetPath */ - public function setTargetPath($targetPath) + public function setTargetPath(?string $targetPath): void { $this->asset->setTargetPath($targetPath); } @@ -145,7 +145,7 @@ public function setTargetPath($targetPath) /** * getLastModified */ - public function getLastModified() + public function getLastModified(): ?int { return $this->asset->getLastModified(); } @@ -153,7 +153,7 @@ public function getLastModified() /** * getVars */ - public function getVars() + public function getVars(): array { return $this->asset->getVars(); } @@ -161,7 +161,7 @@ public function getVars() /** * setValues */ - public function setValues(array $values) + public function setValues(array $values): void { $this->asset->setValues($values); } @@ -169,7 +169,7 @@ public function setValues(array $values) /** * getValues */ - public function getValues() + public function getValues(): array { return $this->asset->getValues(); } @@ -190,7 +190,7 @@ public function getValues() * * @return string A key for identifying the current asset */ - protected static function getCacheKey(AssetInterface $asset, ?FilterInterface $additionalFilter = null, $salt = '') + protected static function getCacheKey(AssetInterface $asset, ?FilterInterface $additionalFilter = null, string $salt = ''): string { if ($additionalFilter) { $asset = clone $asset; diff --git a/src/Assetic/Asset/AssetCollection.php b/src/Assetic/Asset/AssetCollection.php index 0bd21604d..74b15f7a6 100644 --- a/src/Assetic/Asset/AssetCollection.php +++ b/src/Assetic/Asset/AssetCollection.php @@ -18,42 +18,42 @@ class AssetCollection implements IteratorAggregate, AssetCollectionInterface { /** - * @var mixed assets + * @var array assets */ protected $assets; /** - * @var mixed filters + * @var FilterCollection filters */ protected $filters; /** - * @var mixed sourceRoot + * @var string|null sourceRoot */ protected $sourceRoot; /** - * @var mixed targetPath + * @var string|null targetPath */ protected $targetPath; /** - * @var mixed content + * @var string|null content */ protected $content; /** - * @var mixed clones + * @var SplObjectStorage clones */ protected $clones; /** - * @var mixed vars + * @var array vars */ protected $vars; /** - * @var mixed values + * @var array values */ protected $values; @@ -65,7 +65,7 @@ class AssetCollection implements IteratorAggregate, AssetCollectionInterface * @param string $sourceRoot The root directory * @param array $vars */ - public function __construct($assets = [], $filters = [], $sourceRoot = null, array $vars = []) + public function __construct(array $assets = [], array $filters = [], ?string $sourceRoot = null, array $vars = []) { $this->assets = []; foreach ($assets as $asset) { @@ -91,7 +91,7 @@ public function __clone() /** * all */ - public function all() + public function all(): array { return $this->assets; } @@ -99,7 +99,7 @@ public function all() /** * add */ - public function add(AssetInterface $asset) + public function add(AssetInterface $asset): void { $this->assets[] = $asset; } @@ -107,7 +107,7 @@ public function add(AssetInterface $asset) /** * removeLeaf */ - public function removeLeaf(AssetInterface $needle, $graceful = false) + public function removeLeaf(AssetInterface $needle, bool $graceful = false): bool { foreach ($this->assets as $i => $asset) { $clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null; @@ -132,11 +132,11 @@ public function removeLeaf(AssetInterface $needle, $graceful = false) /** * replaceLeaf */ - public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, $graceful = false) + public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, bool $graceful = false): bool { foreach ($this->assets as $i => $asset) { $clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null; - if (in_array($needle, array($asset, $clone), true)) { + if (in_array($needle, [$asset, $clone], true)) { unset($this->clones[$asset]); $this->assets[$i] = $replacement; @@ -158,7 +158,7 @@ public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, /** * ensureFilter */ - public function ensureFilter(FilterInterface $filter) + public function ensureFilter(FilterInterface $filter): void { $this->filters->ensure($filter); } @@ -166,7 +166,7 @@ public function ensureFilter(FilterInterface $filter) /** * getFilters */ - public function getFilters() + public function getFilters(): array { return $this->filters->all(); } @@ -174,7 +174,7 @@ public function getFilters() /** * clearFilters */ - public function clearFilters() + public function clearFilters(): void { $this->filters->clear(); $this->clones = new SplObjectStorage(); @@ -183,7 +183,7 @@ public function clearFilters() /** * load */ - public function load(?FilterInterface $additionalFilter = null) + public function load(?FilterInterface $additionalFilter = null): void { // loop through leaves and load each asset $parts = []; @@ -198,7 +198,7 @@ public function load(?FilterInterface $additionalFilter = null) /** * dump */ - public function dump(?FilterInterface $additionalFilter = null) + public function dump(?FilterInterface $additionalFilter = null): string { // loop through leaves and dump each asset $parts = []; @@ -212,7 +212,7 @@ public function dump(?FilterInterface $additionalFilter = null) /** * getContent */ - public function getContent() + public function getContent(): ?string { return $this->content; } @@ -220,7 +220,7 @@ public function getContent() /** * setContent */ - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } @@ -228,7 +228,7 @@ public function setContent($content) /** * getSourceRoot */ - public function getSourceRoot() + public function getSourceRoot(): ?string { return $this->sourceRoot; } @@ -236,25 +236,28 @@ public function getSourceRoot() /** * getSourcePath */ - public function getSourcePath() + public function getSourcePath(): ?string { + return null; } /** * getSourceDirectory returns the first available source directory, useful * when extracting imports and a singular collection is returned */ - public function getSourceDirectory() + public function getSourceDirectory(): ?string { foreach ($this as $asset) { return $asset->getSourceDirectory(); } + + return null; } /** * getTargetPath */ - public function getTargetPath() + public function getTargetPath(): ?string { return $this->targetPath; } @@ -262,7 +265,7 @@ public function getTargetPath() /** * setTargetPath */ - public function setTargetPath($targetPath) + public function setTargetPath(?string $targetPath): void { $this->targetPath = $targetPath; } @@ -270,12 +273,12 @@ public function setTargetPath($targetPath) /** * getLastModified returns the highest last-modified value of all assets in the current collection. * - * @return integer|null A UNIX timestamp + * @return int|null A UNIX timestamp */ - public function getLastModified() + public function getLastModified(): ?int { if (!count($this->assets)) { - return; + return null; } $mtime = 0; @@ -300,7 +303,7 @@ public function getIterator(): Traversable /** * getVars */ - public function getVars() + public function getVars(): array { return $this->vars; } @@ -308,7 +311,7 @@ public function getVars() /** * setValues */ - public function setValues(array $values) + public function setValues(array $values): void { $this->values = $values; @@ -320,7 +323,7 @@ public function setValues(array $values) /** * getValues */ - public function getValues() + public function getValues(): array { return $this->values; } diff --git a/src/Assetic/Asset/AssetCollectionInterface.php b/src/Assetic/Asset/AssetCollectionInterface.php index 895f62454..e300b0f07 100644 --- a/src/Assetic/Asset/AssetCollectionInterface.php +++ b/src/Assetic/Asset/AssetCollectionInterface.php @@ -12,37 +12,37 @@ interface AssetCollectionInterface extends AssetInterface, \Traversable * * @return array An array of AssetInterface objects */ - public function all(); + public function all(): array; /** * Adds an asset to the current collection. * * @param AssetInterface $asset An asset */ - public function add(AssetInterface $asset); + public function add(AssetInterface $asset): void; /** * Removes a leaf. * * @param AssetInterface $leaf The leaf to remove - * @param Boolean $graceful Whether the failure should return false or throw an exception + * @param bool $graceful Whether the failure should return false or throw an exception * - * @return Boolean Whether the asset has been found + * @return bool Whether the asset has been found * * @throws \InvalidArgumentException If the asset cannot be found */ - public function removeLeaf(AssetInterface $leaf, $graceful = false); + public function removeLeaf(AssetInterface $leaf, bool $graceful = false): bool; /** * Replaces an existing leaf with a new one. * * @param AssetInterface $needle The current asset to replace * @param AssetInterface $replacement The new asset - * @param Boolean $graceful Whether the failure should return false or throw an exception + * @param bool $graceful Whether the failure should return false or throw an exception * - * @return Boolean Whether the asset has been found + * @return bool Whether the asset has been found * * @throws \InvalidArgumentException If the asset cannot be found */ - public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, $graceful = false); + public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, bool $graceful = false): bool; } diff --git a/src/Assetic/Asset/AssetInterface.php b/src/Assetic/Asset/AssetInterface.php index f69ee9fdd..0ff32a382 100644 --- a/src/Assetic/Asset/AssetInterface.php +++ b/src/Assetic/Asset/AssetInterface.php @@ -14,19 +14,19 @@ interface AssetInterface * * @param FilterInterface $filter A filter */ - public function ensureFilter(FilterInterface $filter); + public function ensureFilter(FilterInterface $filter): void; /** * Returns an array of filters currently applied. * * @return array An array of filters */ - public function getFilters(); + public function getFilters(): array; /** * Clears all filters from the current asset. */ - public function clearFilters(); + public function clearFilters(): void; /** * Loads the asset into memory and applies load filters. @@ -35,7 +35,7 @@ public function clearFilters(); * * @param FilterInterface $additionalFilter An additional filter */ - public function load(?FilterInterface $additionalFilter = null); + public function load(?FilterInterface $additionalFilter = null): void; /** * Applies dump filters and returns the asset as a string. @@ -51,23 +51,23 @@ public function load(?FilterInterface $additionalFilter = null); * * @return string The filtered content of the current asset */ - public function dump(?FilterInterface $additionalFilter = null); + public function dump(?FilterInterface $additionalFilter = null): string; /** * Returns the loaded content of the current asset. * - * @return string The content + * @return string|null The content */ - public function getContent(); + public function getContent(): ?string; /** * Sets the content of the current asset. * * Filters can use this method to change the content of the asset. * - * @param string $content The asset content + * @param string|null $content The asset content */ - public function setContent($content); + public function setContent(?string $content): void; /** * Returns an absolute path or URL to the source asset's root directory. @@ -83,7 +83,7 @@ public function setContent($content); * * @return string|null The asset's root */ - public function getSourceRoot(); + public function getSourceRoot(): ?string; /** * Returns the relative path for the source asset. @@ -99,7 +99,7 @@ public function getSourceRoot(); * * @return string|null The source asset path */ - public function getSourcePath(); + public function getSourcePath(): ?string; /** * Returns the asset's source directory. @@ -109,47 +109,47 @@ public function getSourcePath(); * * @return string|null The asset's source directory */ - public function getSourceDirectory(); + public function getSourceDirectory(): ?string; /** * Returns the URL for the current asset. * * @return string|null A web URL where the asset will be dumped */ - public function getTargetPath(); + public function getTargetPath(): ?string; /** * Sets the URL for the current asset. * - * @param string $targetPath A web URL where the asset will be dumped + * @param string|null $targetPath A web URL where the asset will be dumped */ - public function setTargetPath($targetPath); + public function setTargetPath(?string $targetPath): void; /** * Returns the time the current asset was last modified. * - * @return integer|null A UNIX timestamp + * @return int|null A UNIX timestamp */ - public function getLastModified(); + public function getLastModified(): ?int; /** * Returns an array of variable names for this asset. * * @return array */ - public function getVars(); + public function getVars(): array; /** * Sets the values for the asset's variables. * * @param array $values */ - public function setValues(array $values); + public function setValues(array $values): void; /** * Returns the current values for this asset. * * @return array an array of strings */ - public function getValues(); + public function getValues(): array; } diff --git a/src/Assetic/Asset/BaseAsset.php b/src/Assetic/Asset/BaseAsset.php index 93888e26e..4d2ed4e7c 100644 --- a/src/Assetic/Asset/BaseAsset.php +++ b/src/Assetic/Asset/BaseAsset.php @@ -1,6 +1,5 @@ filters = new FilterCollection($filters); $this->sourceRoot = $sourceRoot; @@ -76,7 +75,7 @@ public function __construct($filters = array(), $sourceRoot = null, $sourcePath $this->sourceDir = dirname("$sourceRoot/$sourcePath"); } $this->vars = $vars; - $this->values = array(); + $this->values = []; $this->loaded = false; } @@ -85,17 +84,17 @@ public function __clone() $this->filters = clone $this->filters; } - public function ensureFilter(FilterInterface $filter) + public function ensureFilter(FilterInterface $filter): void { $this->filters->ensure($filter); } - public function getFilters() + public function getFilters(): array { return $this->filters->all(); } - public function clearFilters() + public function clearFilters(): void { $this->filters->clear(); } @@ -106,7 +105,7 @@ public function clearFilters() * @param string $content The asset content * @param FilterInterface $additionalFilter An additional filter */ - protected function doLoad($content, ?FilterInterface $additionalFilter = null) + protected function doLoad(?string $content, ?FilterInterface $additionalFilter = null): void { $filter = clone $this->filters; if ($additionalFilter) { @@ -122,7 +121,7 @@ protected function doLoad($content, ?FilterInterface $additionalFilter = null) $this->loaded = true; } - public function dump(?FilterInterface $additionalFilter = null) + public function dump(?FilterInterface $additionalFilter = null): string { if (!$this->loaded) { $this->load(); @@ -136,40 +135,40 @@ public function dump(?FilterInterface $additionalFilter = null) $asset = clone $this; $filter->filterDump($asset); - return $asset->getContent(); + return $asset->getContent() ?? ''; } - public function getContent() + public function getContent(): ?string { return $this->content; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getSourceRoot() + public function getSourceRoot(): ?string { return $this->sourceRoot; } - public function getSourcePath() + public function getSourcePath(): ?string { return $this->sourcePath; } - public function getSourceDirectory() + public function getSourceDirectory(): ?string { return $this->sourceDir; } - public function getTargetPath() + public function getTargetPath(): ?string { return $this->targetPath; } - public function setTargetPath($targetPath) + public function setTargetPath(?string $targetPath): void { if ($this->vars) { foreach ($this->vars as $var) { @@ -182,12 +181,12 @@ public function setTargetPath($targetPath) $this->targetPath = $targetPath; } - public function getVars() + public function getVars(): array { return $this->vars; } - public function setValues(array $values) + public function setValues(array $values): void { foreach ($values as $var => $v) { if (!in_array($var, $this->vars, true)) { @@ -199,7 +198,7 @@ public function setValues(array $values) $this->loaded = false; } - public function getValues() + public function getValues(): array { return $this->values; } diff --git a/src/Assetic/Asset/FileAsset.php b/src/Assetic/Asset/FileAsset.php index 5e3c68c3a..65d9234be 100644 --- a/src/Assetic/Asset/FileAsset.php +++ b/src/Assetic/Asset/FileAsset.php @@ -29,7 +29,7 @@ class FileAsset extends BaseAsset * * @throws InvalidArgumentException If the supplied root doesn't match the source when guessing the path */ - public function __construct($source, $filters = [], $sourceRoot = null, $sourcePath = null, array $vars = []) + public function __construct(string $source, array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null, array $vars = []) { if ($sourceRoot === null) { $sourceRoot = dirname($source); @@ -53,7 +53,7 @@ public function __construct($source, $filters = [], $sourceRoot = null, $sourceP /** * load */ - public function load(?FilterInterface $additionalFilter = null) + public function load(?FilterInterface $additionalFilter = null): void { $source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues()); @@ -67,7 +67,7 @@ public function load(?FilterInterface $additionalFilter = null) /** * getLastModified */ - public function getLastModified() + public function getLastModified(): ?int { $source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues()); diff --git a/src/Assetic/Asset/GlobAsset.php b/src/Assetic/Asset/GlobAsset.php index b0be9faf8..7ed5fd585 100644 --- a/src/Assetic/Asset/GlobAsset.php +++ b/src/Assetic/Asset/GlobAsset.php @@ -12,12 +12,12 @@ class GlobAsset extends AssetCollection { /** - * @var mixed globs + * @var array globs */ protected $globs; /** - * @var mixed initialized + * @var bool initialized */ protected $initialized; @@ -29,7 +29,7 @@ class GlobAsset extends AssetCollection * @param string $root The root directory * @param array $vars */ - public function __construct($globs, $filters = [], $root = null, array $vars = []) + public function __construct($globs, array $filters = [], ?string $root = null, array $vars = []) { $this->globs = (array) $globs; $this->initialized = false; @@ -40,7 +40,7 @@ public function __construct($globs, $filters = [], $root = null, array $vars = [ /** * all */ - public function all() + public function all(): array { if (!$this->initialized) { $this->initialize(); @@ -52,7 +52,7 @@ public function all() /** * load */ - public function load(?FilterInterface $additionalFilter = null) + public function load(?FilterInterface $additionalFilter = null): void { if (!$this->initialized) { $this->initialize(); @@ -64,7 +64,7 @@ public function load(?FilterInterface $additionalFilter = null) /** * dump */ - public function dump(?FilterInterface $additionalFilter = null) + public function dump(?FilterInterface $additionalFilter = null): string { if (!$this->initialized) { $this->initialize(); @@ -76,7 +76,7 @@ public function dump(?FilterInterface $additionalFilter = null) /** * getLastModified */ - public function getLastModified() + public function getLastModified(): ?int { if (!$this->initialized) { $this->initialize(); @@ -100,7 +100,7 @@ public function getIterator(): Traversable /** * setValues */ - public function setValues(array $values) + public function setValues(array $values): void { parent::setValues($values); $this->initialized = false; diff --git a/src/Assetic/Asset/HttpAsset.php b/src/Assetic/Asset/HttpAsset.php index 23490a009..32132ec35 100644 --- a/src/Assetic/Asset/HttpAsset.php +++ b/src/Assetic/Asset/HttpAsset.php @@ -13,12 +13,12 @@ class HttpAsset extends BaseAsset { /** - * @var mixed sourceUrl + * @var string sourceUrl */ protected $sourceUrl; /** - * @var mixed ignoreErrors + * @var bool ignoreErrors */ protected $ignoreErrors; @@ -32,7 +32,7 @@ class HttpAsset extends BaseAsset * * @throws InvalidArgumentException If the first argument is not an URL */ - public function __construct($sourceUrl, $filters = [], $ignoreErrors = false, array $vars = array()) + public function __construct(string $sourceUrl, array $filters = [], bool $ignoreErrors = false, array $vars = []) { if (strpos($sourceUrl, '//') === 0) { $sourceUrl = 'http:'.$sourceUrl; @@ -44,8 +44,8 @@ public function __construct($sourceUrl, $filters = [], $ignoreErrors = false, ar $this->sourceUrl = $sourceUrl; $this->ignoreErrors = $ignoreErrors; - list($scheme, $url) = explode('://', $sourceUrl, 2); - list($host, $path) = explode('/', $url, 2); + [$scheme, $url] = explode('://', $sourceUrl, 2); + [$host, $path] = explode('/', $url, 2); parent::__construct($filters, $scheme.'://'.$host, $path, $vars); } @@ -69,16 +69,18 @@ public function load(?FilterInterface $additionalFilter = null) /** * getLastModified */ - public function getLastModified() + public function getLastModified(): ?int { - if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(array('http' => array('method' => 'HEAD'))))) { + if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(['http' => ['method' => 'HEAD']]))) { foreach ($http_response_header as $header) { if (stripos($header, 'Last-Modified: ') === 0) { - list(, $mtime) = explode(':', $header, 2); + [, $mtime] = explode(':', $header, 2); return strtotime(trim($mtime)); } } } + + return null; } } diff --git a/src/Assetic/Asset/StringAsset.php b/src/Assetic/Asset/StringAsset.php index 9540815e9..faae3b25c 100644 --- a/src/Assetic/Asset/StringAsset.php +++ b/src/Assetic/Asset/StringAsset.php @@ -9,7 +9,14 @@ */ class StringAsset extends BaseAsset { + /** + * @var string string + */ private $string; + + /** + * @var int|null lastModified + */ private $lastModified; /** @@ -20,7 +27,7 @@ class StringAsset extends BaseAsset * @param string $sourceRoot The source asset root directory * @param string $sourcePath The source asset path */ - public function __construct($content, $filters = [], $sourceRoot = null, $sourcePath = null) + public function __construct(string $content, array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null) { $this->string = $content; @@ -30,7 +37,7 @@ public function __construct($content, $filters = [], $sourceRoot = null, $source /** * load */ - public function load(?FilterInterface $additionalFilter = null) + public function load(?FilterInterface $additionalFilter = null): void { $this->doLoad($this->string, $additionalFilter); } @@ -38,7 +45,7 @@ public function load(?FilterInterface $additionalFilter = null) /** * setLastModified */ - public function setLastModified($lastModified) + public function setLastModified(?int $lastModified): void { $this->lastModified = $lastModified; } @@ -46,7 +53,7 @@ public function setLastModified($lastModified) /** * getLastModified */ - public function getLastModified() + public function getLastModified(): ?int { return $this->lastModified; } diff --git a/src/Assetic/AssetManager.php b/src/Assetic/AssetManager.php index 46be4e4ed..28be568ae 100644 --- a/src/Assetic/AssetManager.php +++ b/src/Assetic/AssetManager.php @@ -22,7 +22,7 @@ class AssetManager * @return AssetInterface The asset * @throws InvalidArgumentException If there is no asset by that name */ - public function get($name) + public function get(string $name): AssetInterface { if (!isset($this->assets[$name])) { throw new InvalidArgumentException(sprintf('There is no "%s" asset.', $name)); @@ -35,9 +35,9 @@ public function get($name) * has checks if the current asset manager has a certain asset. * * @param string $name an asset name - * @return Boolean True if the asset has been set, false if not + * @return bool True if the asset has been set, false if not */ - public function has($name) + public function has(string $name): bool { return isset($this->assets[$name]); } @@ -49,7 +49,7 @@ public function has($name) * @param AssetInterface $asset The asset * @throws InvalidArgumentException If the asset name is invalid */ - public function set($name, AssetInterface $asset) + public function set(string $name, AssetInterface $asset): void { if (!ctype_alnum(str_replace('_', '', $name))) { throw new InvalidArgumentException(sprintf('The name "%s" is invalid.', $name)); @@ -63,7 +63,7 @@ public function set($name, AssetInterface $asset) * * @return array An array of asset names */ - public function getNames() + public function getNames(): array { return array_keys($this->assets); } @@ -71,7 +71,7 @@ public function getNames() /** * clear clears all assets. */ - public function clear() + public function clear(): void { $this->assets = []; } diff --git a/src/Assetic/AssetWriter.php b/src/Assetic/AssetWriter.php index 1a909151a..b95b62aa2 100644 --- a/src/Assetic/AssetWriter.php +++ b/src/Assetic/AssetWriter.php @@ -30,7 +30,7 @@ class AssetWriter * @param array $values * @throws InvalidArgumentException */ - public function __construct($dir, array $values = array()) + public function __construct(string $dir, array $values = []) { foreach ($values as $var => $vals) { foreach ($vals as $value) { @@ -47,7 +47,7 @@ public function __construct($dir, array $values = array()) /** * writeManagerAssets */ - public function writeManagerAssets(AssetManager $am) + public function writeManagerAssets(AssetManager $am): void { foreach ($am->getNames() as $name) { $this->writeAsset($am->get($name)); @@ -57,7 +57,7 @@ public function writeManagerAssets(AssetManager $am) /** * writeAsset */ - public function writeAsset(AssetInterface $asset) + public function writeAsset(AssetInterface $asset): void { foreach (VarUtils::getCombinations($asset->getVars(), $this->values) as $combination) { $asset->setValues($combination); @@ -76,7 +76,7 @@ public function writeAsset(AssetInterface $asset) /** * write */ - protected static function write($path, $contents) + protected static function write(string $path, string $contents): void { if (!is_dir($dir = dirname($path)) && false === @mkdir($dir, 0755, true)) { throw new RuntimeException('Unable to create directory '.$dir); diff --git a/src/Assetic/AsseticServiceProvider.php b/src/Assetic/AsseticServiceProvider.php index ce97c02d7..8b7dfe6a1 100644 --- a/src/Assetic/AsseticServiceProvider.php +++ b/src/Assetic/AsseticServiceProvider.php @@ -5,13 +5,16 @@ /** * AsseticServiceProvider + * + * @package october/assetic + * @author Alexey Bobkov, Samuel Georges */ class AsseticServiceProvider extends ServiceProvider implements DeferrableProvider { /** - * register the service provider. + * Register the service provider. */ - public function register() + public function register(): void { $this->app->singleton('assetic', function ($app) { $combiner = new Combiner; @@ -22,10 +25,9 @@ public function register() } /** - * provides the returned services. - * @return array + * Provides the returned services. */ - public function provides() + public function provides(): array { return [ 'assetic', diff --git a/src/Assetic/Combiner.php b/src/Assetic/Combiner.php index a0aa3a01a..0a9a82f7e 100644 --- a/src/Assetic/Combiner.php +++ b/src/Assetic/Combiner.php @@ -39,7 +39,7 @@ class Combiner /** * parse */ - public function parse(array $assets, $options = []) + public function parse(array $assets, array $options = []): string { return $this->prepareCombiner($assets, $options)->dump(); } @@ -47,14 +47,12 @@ public function parse(array $assets, $options = []) /** * prepareCombiner before dumping */ - public function prepareCombiner(array $assets, $options = []) + public function prepareCombiner(array $assets, array $options = []): AssetCollection { - extract(array_merge([ - 'targetPath' => null, - 'production' => false, - 'useCache' => true, - 'deepHashKey' => null - ], $options)); + $targetPath = $options['targetPath'] ?? null; + $production = $options['production'] ?? false; + $useCache = $options['useCache'] ?? true; + $deepHashKey = $options['deepHashKey'] ?? null; if ($deepHashKey !== null) { $this->setDeepHashKeyOnFilters($deepHashKey); @@ -102,7 +100,7 @@ public function prepareCombiner(array $assets, $options = []) /** * registerDefaultFilters */ - public function registerDefaultFilters() + public function registerDefaultFilters(): void { // Default JavaScript filters $this->registerFilter('js', new \October\Rain\Assetic\Filter\JavascriptImporter); @@ -121,7 +119,7 @@ public function registerDefaultFilters() /** * setStoragePath */ - public function setStoragePath($path) + public function setStoragePath(?string $path): void { $this->storagePath = $path; } @@ -129,7 +127,7 @@ public function setStoragePath($path) /** * setLocalPath */ - public function setLocalPath($path) + public function setLocalPath(?string $path): void { $this->localPath = $path; } diff --git a/src/Assetic/Factory/AssetFactory.php b/src/Assetic/Factory/AssetFactory.php index 73f132567..d2c178b48 100644 --- a/src/Assetic/Factory/AssetFactory.php +++ b/src/Assetic/Factory/AssetFactory.php @@ -148,11 +148,11 @@ public function setFilterManager(FilterManager $fm) public function createAsset($inputs = [], $filters = [], array $options = []) { if (!is_array($inputs)) { - $inputs = array($inputs); + $inputs = [$inputs]; } if (!is_array($filters)) { - $filters = array($filters); + $filters = [$filters]; } if (!isset($options['output'])) { @@ -168,11 +168,11 @@ public function createAsset($inputs = [], $filters = [], array $options = []) } if (!isset($options['root'])) { - $options['root'] = array($this->root); + $options['root'] = [$this->root]; } else { if (!is_array($options['root'])) { - $options['root'] = array($options['root']); + $options['root'] = [$options['root']]; } $options['root'][] = $this->root; @@ -189,7 +189,7 @@ public function createAsset($inputs = [], $filters = [], array $options = []) foreach ($inputs as $input) { if (is_array($input)) { // nested formula - $asset->add(call_user_func_array(array($this, 'createAsset'), $input)); + $asset->add($this->createAsset(...$input)); } else { $asset->add($this->parseInput($input, $options)); @@ -240,9 +240,9 @@ public function createAsset($inputs = [], $filters = [], array $options = []) /** * generateAssetName */ - public function generateAssetName($inputs, $filters, $options = []) + public function generateAssetName($inputs, $filters, $options = []): string { - foreach (array_diff(array_keys($options), array('output', 'debug', 'root')) as $key) { + foreach (array_diff(array_keys($options), ['output', 'debug', 'root']) as $key) { unset($options[$key]); } diff --git a/src/Assetic/Filter/BaseCssFilter.php b/src/Assetic/Filter/BaseCssFilter.php index 823d80018..1d1fd3fba 100644 --- a/src/Assetic/Filter/BaseCssFilter.php +++ b/src/Assetic/Filter/BaseCssFilter.php @@ -12,32 +12,32 @@ abstract class BaseCssFilter implements FilterInterface /** * @see CssUtils::filterReferences() */ - protected function filterReferences($content, $callback, $limit = -1, &$count = 0) + protected function filterReferences(string $content, callable $callback): string { - return CssUtils::filterReferences($content, $callback, $limit, $count); + return CssUtils::filterReferences($content, $callback); } /** * @see CssUtils::filterUrls() */ - protected function filterUrls($content, $callback, $limit = -1, &$count = 0) + protected function filterUrls(string $content, callable $callback): string { - return CssUtils::filterUrls($content, $callback, $limit, $count); + return CssUtils::filterUrls($content, $callback); } /** * @see CssUtils::filterImports() */ - protected function filterImports($content, $callback, $limit = -1, &$count = 0, $includeUrl = true) + protected function filterImports(string $content, callable $callback, bool $includeUrl = true): string { - return CssUtils::filterImports($content, $callback, $limit, $count, $includeUrl); + return CssUtils::filterImports($content, $callback, $includeUrl); } /** * @see CssUtils::filterIEFilters() */ - protected function filterIEFilters($content, $callback, $limit = -1, &$count = 0) + protected function filterIEFilters(string $content, callable $callback): string { - return CssUtils::filterIEFilters($content, $callback, $limit, $count); + return CssUtils::filterIEFilters($content, $callback); } } diff --git a/src/Assetic/Filter/CssImportFilter.php b/src/Assetic/Filter/CssImportFilter.php index 0e41097b3..95afd69a4 100644 --- a/src/Assetic/Filter/CssImportFilter.php +++ b/src/Assetic/Filter/CssImportFilter.php @@ -15,12 +15,12 @@ class CssImportFilter extends BaseCssFilter implements HashableInterface, DependencyExtractorInterface { /** - * @var mixed importFilter + * @var FilterInterface|null importFilter */ protected $importFilter; /** - * @var string lastHash + * @var string|null lastHash */ protected $lastHash; @@ -52,13 +52,13 @@ public function filterLoad(AssetInterface $asset) // Absolute if (strpos($matches['url'], '://') !== false) { - list($importScheme, $tmp) = explode('://', $matches['url'], 2); - list($importHost, $importPath) = explode('/', $tmp, 2); + [$importScheme, $tmp] = explode('://', $matches['url'], 2); + [$importHost, $importPath] = explode('/', $tmp, 2); $importRoot = $importScheme.'://'.$importHost; } // Protocol-relative elseif (strpos($matches['url'], '//') === 0) { - list($importHost, $importPath) = explode('/', substr($matches['url'], 2), 2); + [$importHost, $importPath] = explode('/', substr($matches['url'], 2), 2); $importRoot = '//'.$importHost; } // Root-relative diff --git a/src/Assetic/Filter/CssRewriteFilter.php b/src/Assetic/Filter/CssRewriteFilter.php index 1465e5f67..62b19281c 100644 --- a/src/Assetic/Filter/CssRewriteFilter.php +++ b/src/Assetic/Filter/CssRewriteFilter.php @@ -12,14 +12,14 @@ class CssRewriteFilter extends BaseCssFilter /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { } /** * filterDump */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { $sourceBase = $asset->getSourceRoot(); $sourcePath = $asset->getSourcePath(); @@ -31,8 +31,8 @@ public function filterDump(AssetInterface $asset) // Learn how to get from the target back to the source if (strpos($sourceBase, '://') !== false) { - list($scheme, $url) = explode('://', $sourceBase.'/'.$sourcePath, 2); - list($host, $path) = explode('/', $url, 2); + [$scheme, $url] = explode('://', $sourceBase.'/'.$sourcePath, 2); + [$host, $path] = explode('/', $url, 2); $host = $scheme.'://'.$host.'/'; $path = false === strpos($path, '/') ? '' : dirname($path); diff --git a/src/Assetic/Filter/FilterCollection.php b/src/Assetic/Filter/FilterCollection.php index 27654b03f..a67431998 100644 --- a/src/Assetic/Filter/FilterCollection.php +++ b/src/Assetic/Filter/FilterCollection.php @@ -18,7 +18,7 @@ class FilterCollection implements FilterInterface, \IteratorAggregate, \Countabl /** * __construct */ - public function __construct($filters = array()) + public function __construct(array $filters = []) { foreach ($filters as $filter) { $this->ensure($filter); @@ -31,7 +31,7 @@ public function __construct($filters = array()) * If the supplied filter is another filter collection, each of its * filters will be checked. */ - public function ensure(FilterInterface $filter) + public function ensure(FilterInterface $filter): void { if ($filter instanceof \Traversable) { foreach ($filter as $f) { @@ -45,7 +45,7 @@ public function ensure(FilterInterface $filter) /** * all */ - public function all() + public function all(): array { return $this->filters; } @@ -53,15 +53,15 @@ public function all() /** * clear */ - public function clear() + public function clear(): void { - $this->filters = array(); + $this->filters = []; } /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { foreach ($this->filters as $filter) { $filter->filterLoad($asset); @@ -71,7 +71,7 @@ public function filterLoad(AssetInterface $asset) /** * filterDump */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { foreach ($this->filters as $filter) { $filter->filterDump($asset); diff --git a/src/Assetic/Filter/FilterInterface.php b/src/Assetic/Filter/FilterInterface.php index 3cbbc6da1..fb4daead5 100644 --- a/src/Assetic/Filter/FilterInterface.php +++ b/src/Assetic/Filter/FilterInterface.php @@ -14,12 +14,12 @@ interface FilterInterface * * @param AssetInterface $asset An asset */ - public function filterLoad(AssetInterface $asset); + public function filterLoad(AssetInterface $asset): void; /** * Filters an asset just before it's dumped. * * @param AssetInterface $asset An asset */ - public function filterDump(AssetInterface $asset); + public function filterDump(AssetInterface $asset): void; } diff --git a/src/Assetic/Filter/JavascriptImporter.php b/src/Assetic/Filter/JavascriptImporter.php index afde433a9..f8efcff1b 100644 --- a/src/Assetic/Filter/JavascriptImporter.php +++ b/src/Assetic/Filter/JavascriptImporter.php @@ -40,14 +40,14 @@ class JavascriptImporter implements FilterInterface /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { } /** * filterDump */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { $this->scriptPath = dirname($asset->getSourceRoot() . '/' . $asset->getSourcePath()); $this->scriptFile = basename($asset->getSourcePath()); @@ -57,10 +57,10 @@ public function filterDump(AssetInterface $asset) /** * Process JS imports inside a string of javascript - * @param $content string JS code to process. + * @param string $content JS code to process. * @return string Processed JS. */ - protected function parse($content) + protected function parse(string $content): string { $macros = []; $imported = ''; @@ -91,7 +91,7 @@ protected function parse($content) /** * Directive to process script includes */ - protected function directiveInclude($data, $required = false) + protected function directiveInclude(string $data, bool $required = false): string { $require = explode(',', $data); $result = ""; @@ -156,7 +156,7 @@ protected function directiveInclude($data, $required = false) /** * Directive to process mandatory script includes */ - protected function directiveRequire($data) + protected function directiveRequire(string $data): string { return $this->directiveInclude($data, true); } @@ -164,7 +164,7 @@ protected function directiveRequire($data) /** * Directive to define and replace variables */ - protected function directiveDefine($data) + protected function directiveDefine(string $data): string { if (preg_match('@([^\\s]*)\\s+(.*)@', $data, $matches)) { // str_replace($matches[1], $matches[2], $context); diff --git a/src/Assetic/Filter/StylesheetMinify.php b/src/Assetic/Filter/StylesheetMinify.php index f2c81b811..329be65c4 100644 --- a/src/Assetic/Filter/StylesheetMinify.php +++ b/src/Assetic/Filter/StylesheetMinify.php @@ -15,24 +15,24 @@ class StylesheetMinify implements FilterInterface /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { } /** * filterDump */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { $asset->setContent($this->minify($asset->getContent())); } /** - * minify CSS - * @var $css string CSS code to minify. + * Minify CSS + * @param string $css CSS code to minify. * @return string Minified CSS. */ - protected function minify($css) + protected function minify(string $css): string { // Normalize whitespace in a smart way $css = preg_replace('/\s{2,}/', ' ', $css); diff --git a/src/Assetic/FilterManager.php b/src/Assetic/FilterManager.php index 866d28441..cce50ebd7 100644 --- a/src/Assetic/FilterManager.php +++ b/src/Assetic/FilterManager.php @@ -19,7 +19,7 @@ class FilterManager /** * set */ - public function set($alias, FilterInterface $filter) + public function set(string $alias, FilterInterface $filter): void { $this->checkName($alias); @@ -29,7 +29,7 @@ public function set($alias, FilterInterface $filter) /** * get */ - public function get($alias) + public function get(string $alias): FilterInterface { if (!isset($this->filters[$alias])) { throw new InvalidArgumentException(sprintf('There is no "%s" filter.', $alias)); @@ -41,7 +41,7 @@ public function get($alias) /** * has */ - public function has($alias) + public function has(string $alias): bool { return isset($this->filters[$alias]); } @@ -49,7 +49,7 @@ public function has($alias) /** * getNames */ - public function getNames() + public function getNames(): array { return array_keys($this->filters); } @@ -59,7 +59,7 @@ public function getNames() * @param string $name An asset name candidate * @throws InvalidArgumentException If the asset name is invalid */ - protected function checkName($name) + protected function checkName(string $name): void { if (!ctype_alnum(str_replace('_', '', $name))) { throw new InvalidArgumentException(sprintf('The name "%s" is invalid.', $name)); diff --git a/src/Assetic/Util/CssUtils.php b/src/Assetic/Util/CssUtils.php index 3ca73ebbf..2f940666c 100644 --- a/src/Assetic/Util/CssUtils.php +++ b/src/Assetic/Util/CssUtils.php @@ -1,6 +1,5 @@ $vals) { if (!in_array($var, $vars, true)) { continue; @@ -55,7 +54,7 @@ public static function getCombinations(array $vars, array $values) for ($i = array_product($nbValues), $c = $i * 2; $i < $c; $i++) { $k = $i; - $combination = array(); + $combination = []; foreach ($vars as $var) { $combination[$var] = $values[$var][$k % $nbValues[$var]]; From b87773d57250f42d84924e12a9a5f8c4727e1100 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Thu, 11 Dec 2025 18:11:57 +1100 Subject: [PATCH 10/21] Clean up unused files --- src/Assetic/Cache/ArrayCache.php | 47 ----------------- src/Assetic/Exception/Exception.php | 10 ---- src/Assetic/Exception/FilterException.php | 43 ---------------- src/Assetic/Filter/CallablesFilter.php | 51 ------------------ src/Assetic/Filter/CssCacheBustingFilter.php | 54 -------------------- 5 files changed, 205 deletions(-) delete mode 100644 src/Assetic/Cache/ArrayCache.php delete mode 100644 src/Assetic/Exception/Exception.php delete mode 100644 src/Assetic/Exception/FilterException.php delete mode 100644 src/Assetic/Filter/CallablesFilter.php delete mode 100644 src/Assetic/Filter/CssCacheBustingFilter.php diff --git a/src/Assetic/Cache/ArrayCache.php b/src/Assetic/Cache/ArrayCache.php deleted file mode 100644 index 892fe728a..000000000 --- a/src/Assetic/Cache/ArrayCache.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class ArrayCache implements CacheInterface -{ - private $cache = array(); - - /** - * @see CacheInterface::has() - */ - public function has($key) - { - return isset($this->cache[$key]); - } - - /** - * @see CacheInterface::get() - */ - public function get($key) - { - if (!$this->has($key)) { - throw new \RuntimeException('There is no cached value for '.$key); - } - - return $this->cache[$key]; - } - - /** - * @see CacheInterface::set() - */ - public function set($key, $value) - { - $this->cache[$key] = $value; - } - - /** - * @see CacheInterface::remove() - */ - public function remove($key) - { - unset($this->cache[$key]); - } -} diff --git a/src/Assetic/Exception/Exception.php b/src/Assetic/Exception/Exception.php deleted file mode 100644 index 2f106977b..000000000 --- a/src/Assetic/Exception/Exception.php +++ /dev/null @@ -1,10 +0,0 @@ - - */ -interface Exception -{ -} diff --git a/src/Assetic/Exception/FilterException.php b/src/Assetic/Exception/FilterException.php deleted file mode 100644 index 005403105..000000000 --- a/src/Assetic/Exception/FilterException.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -class FilterException extends \RuntimeException implements Exception -{ - private $originalMessage; - private $input; - - public function __construct($message, $code = 0, ?\Exception $previous = null) - { - parent::__construct($message, $code, $previous); - - $this->originalMessage = $message; - } - - public function setInput($input) - { - $this->input = $input; - $this->updateMessage(); - - return $this; - } - - public function getInput() - { - return $this->input; - } - - private function updateMessage() - { - $message = $this->originalMessage; - - if (!empty($this->input)) { - $message .= "\n\nInput:\n".$this->input; - } - - $this->message = $message; - } -} diff --git a/src/Assetic/Filter/CallablesFilter.php b/src/Assetic/Filter/CallablesFilter.php deleted file mode 100644 index 07dd1b7fe..000000000 --- a/src/Assetic/Filter/CallablesFilter.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class CallablesFilter implements FilterInterface, DependencyExtractorInterface -{ - private $loader; - private $dumper; - private $extractor; - - /** - * @param callable|null $loader - * @param callable|null $dumper - * @param callable|null $extractor - */ - public function __construct($loader = null, $dumper = null, $extractor = null) - { - $this->loader = $loader; - $this->dumper = $dumper; - $this->extractor = $extractor; - } - - public function filterLoad(AssetInterface $asset) - { - if (null !== $callable = $this->loader) { - $callable($asset); - } - } - - public function filterDump(AssetInterface $asset) - { - if (null !== $callable = $this->dumper) { - $callable($asset); - } - } - - public function getChildren(AssetFactory $factory, $content, $loadPath = null) - { - if (null !== $callable = $this->extractor) { - return $callable($factory, $content, $loadPath); - } - - return array(); - } -} diff --git a/src/Assetic/Filter/CssCacheBustingFilter.php b/src/Assetic/Filter/CssCacheBustingFilter.php deleted file mode 100644 index 1d8d73fab..000000000 --- a/src/Assetic/Filter/CssCacheBustingFilter.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ -class CssCacheBustingFilter extends BaseCssFilter -{ - private $version; - private $format = '%s?%s'; - - public function setVersion($version) - { - $this->version = $version; - } - - public function setFormat($versionFormat) - { - $this->format = $versionFormat; - } - - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - if (!$this->version) { - return; - } - - $version = $this->version; - $format = $this->format; - - $asset->setContent($this->filterReferences( - $asset->getContent(), - function ($matches) use ($version, $format) { - if (0 === strpos($matches['url'], 'data:')) { - return $matches[0]; - } - - return str_replace( - $matches['url'], - sprintf($format, $matches['url'], $version), - $matches[0] - ); - } - )); - } -} From dd576997f070079a7164dc9bcbe69047581242bc Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Thu, 11 Dec 2025 18:15:18 +1100 Subject: [PATCH 11/21] Fixes return types --- src/Assetic/Filter/CssImportFilter.php | 4 ++-- src/Assetic/Filter/CssMinFilter.php | 4 ++-- src/Assetic/Filter/JSMinFilter.php | 4 ++-- src/Assetic/Filter/JSqueezeFilter.php | 4 ++-- src/Assetic/Filter/LessCompiler.php | 4 ++-- src/Assetic/Filter/LessphpFilter.php | 4 ++-- src/Assetic/Filter/ScssCompiler.php | 2 +- src/Assetic/Filter/ScssphpFilter.php | 4 ++-- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Assetic/Filter/CssImportFilter.php b/src/Assetic/Filter/CssImportFilter.php index 95afd69a4..217f16353 100644 --- a/src/Assetic/Filter/CssImportFilter.php +++ b/src/Assetic/Filter/CssImportFilter.php @@ -37,7 +37,7 @@ public function __construct(?FilterInterface $importFilter = null) /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { $importFilter = $this->importFilter; $sourceRoot = $asset->getSourceRoot(); @@ -107,7 +107,7 @@ public function filterLoad(AssetInterface $asset) /** * filterDump */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { } diff --git a/src/Assetic/Filter/CssMinFilter.php b/src/Assetic/Filter/CssMinFilter.php index 6dd952ae9..9b341d6ca 100644 --- a/src/Assetic/Filter/CssMinFilter.php +++ b/src/Assetic/Filter/CssMinFilter.php @@ -39,11 +39,11 @@ public function setPlugin($name, $value) $this->plugins[$name] = $value; } - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { } - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { $filters = $this->filters; $plugins = $this->plugins; diff --git a/src/Assetic/Filter/JSMinFilter.php b/src/Assetic/Filter/JSMinFilter.php index 668070643..19ba8e609 100644 --- a/src/Assetic/Filter/JSMinFilter.php +++ b/src/Assetic/Filter/JSMinFilter.php @@ -17,7 +17,7 @@ class JSMinFilter implements FilterInterface /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { } @@ -25,7 +25,7 @@ public function filterLoad(AssetInterface $asset) * filterDump will use JSMin to minify the asset and checks the filename * for "min.js" to issues arising from double minification. */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { $contents = $asset->getContent(); diff --git a/src/Assetic/Filter/JSqueezeFilter.php b/src/Assetic/Filter/JSqueezeFilter.php index 991378e51..70c4053ae 100644 --- a/src/Assetic/Filter/JSqueezeFilter.php +++ b/src/Assetic/Filter/JSqueezeFilter.php @@ -72,11 +72,11 @@ public function keepImportantComments($bool) $this->keepImportantComments = (bool) $bool; } - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { } - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { $parser = new $this->className(); $asset->setContent($parser->squeeze( diff --git a/src/Assetic/Filter/LessCompiler.php b/src/Assetic/Filter/LessCompiler.php index 1db6ff61d..91951a5ec 100644 --- a/src/Assetic/Filter/LessCompiler.php +++ b/src/Assetic/Filter/LessCompiler.php @@ -38,7 +38,7 @@ public function setPresets(array $presets) /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { $parser = new Less_Parser(); @@ -56,7 +56,7 @@ public function filterLoad(AssetInterface $asset) /** * filterDump */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { } diff --git a/src/Assetic/Filter/LessphpFilter.php b/src/Assetic/Filter/LessphpFilter.php index defb0fae1..15522a887 100644 --- a/src/Assetic/Filter/LessphpFilter.php +++ b/src/Assetic/Filter/LessphpFilter.php @@ -76,7 +76,7 @@ public function setPreserveComments($preserveComments) $this->preserveComments = $preserveComments; } - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { $lc = new \lessc(); if ($dir = $asset->getSourceDirectory()) { @@ -111,7 +111,7 @@ public function registerFunction($name, $callable) $this->customFunctions[$name] = $callable; } - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { } diff --git a/src/Assetic/Filter/ScssCompiler.php b/src/Assetic/Filter/ScssCompiler.php index ded0189bb..a3d32e503 100644 --- a/src/Assetic/Filter/ScssCompiler.php +++ b/src/Assetic/Filter/ScssCompiler.php @@ -49,7 +49,7 @@ public function addVariable($variable) $this->variables[] = $variable; } - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { parent::setVariables($this->variables); parent::filterLoad($asset); diff --git a/src/Assetic/Filter/ScssphpFilter.php b/src/Assetic/Filter/ScssphpFilter.php index fea92fbbc..d2f682cfe 100644 --- a/src/Assetic/Filter/ScssphpFilter.php +++ b/src/Assetic/Filter/ScssphpFilter.php @@ -91,7 +91,7 @@ public function registerFunction($name, $callable) /** * filterLoad */ - public function filterLoad(AssetInterface $asset) + public function filterLoad(AssetInterface $asset): void { $sc = new Compiler(); @@ -140,7 +140,7 @@ public function filterLoad(AssetInterface $asset) /** * filterDump */ - public function filterDump(AssetInterface $asset) + public function filterDump(AssetInterface $asset): void { } From 1a11083671956cc08e74351d7392a60b7ab7d53b Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 13 Dec 2025 11:47:14 +1100 Subject: [PATCH 12/21] Fixes tests --- tests/Assetic/MockAsset.php | 41 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/Assetic/MockAsset.php b/tests/Assetic/MockAsset.php index fafc2c386..4a08c194d 100644 --- a/tests/Assetic/MockAsset.php +++ b/tests/Assetic/MockAsset.php @@ -18,69 +18,78 @@ public function __construct(string $content = '') $this->content = $content; } - public function ensureFilter(FilterInterface $filter) + public function ensureFilter(FilterInterface $filter): void { } - public function getFilters() + public function getFilters(): array { + return []; } - public function clearFilters() + public function clearFilters(): void { } - public function load(?FilterInterface $additionalFilter = null) + public function load(?FilterInterface $additionalFilter = null): void { } - public function dump(?FilterInterface $additionalFilter = null) + public function dump(?FilterInterface $additionalFilter = null): string { + return $this->content ?? ''; } - public function getContent() + public function getContent(): ?string { return $this->content; } - public function setContent($content) + public function setContent(?string $content): void { $this->content = $content; } - public function getSourceRoot() + public function getSourceRoot(): ?string { + return null; } - public function getSourcePath() + public function getSourcePath(): ?string { + return null; } - public function getSourceDirectory() + public function getSourceDirectory(): ?string { + return null; } - public function getTargetPath() + public function getTargetPath(): ?string { + return null; } - public function setTargetPath($targetPath) + public function setTargetPath(?string $targetPath): void { } - public function getLastModified() + public function getLastModified(): ?int { + return null; } - public function getVars() + public function getVars(): array { + return []; } - public function setValues(array $values) + public function setValues(array $values): void { } - public function getValues() + public function getValues(): array { + return []; } } From 1a97728a18187a44a82b14357b06c8d9c72a565e Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 17 Dec 2025 17:54:48 +1100 Subject: [PATCH 13/21] Fixes backward compatibility --- src/Resize/Resizer.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Resize/Resizer.php b/src/Resize/Resizer.php index 564fbbc28..1158ea418 100644 --- a/src/Resize/Resizer.php +++ b/src/Resize/Resizer.php @@ -182,12 +182,18 @@ public function resize($width, $height, $options = []): static $this->image->resize($width, $height); } elseif ($mode === 'crop') { - $this->image->crop( - $width, - $height, - $this->options['offset'][0] ?? 0, - $this->options['offset'][1] ?? 0 - ); + // Backward compatibility + if (!array_key_exists('offset', $options)) { + $this->image->cover($width, $height); + } + else { + $this->image->crop( + $width, + $height, + $this->options['offset'][0] ?? 0, + $this->options['offset'][1] ?? 0 + ); + } } elseif ($mode === 'cover' || $mode === 'fit') { $this->image->cover($width, $height); From dfb98f7fb65da18ae96d2b1f40b8ea393fac497b Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Fri, 26 Dec 2025 12:59:08 +1100 Subject: [PATCH 14/21] Vite global --- globals/Vite.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 globals/Vite.php diff --git a/globals/Vite.php b/globals/Vite.php new file mode 100644 index 000000000..00f3c57ea --- /dev/null +++ b/globals/Vite.php @@ -0,0 +1,8 @@ + Date: Tue, 30 Dec 2025 16:33:12 +1100 Subject: [PATCH 15/21] Target Larajax ^2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2c3a2a1ab..2ba9c90c5 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "php": "^8.0.2", "composer/composer": "^2.0.0", "composer/installers": "^1 || ^2", - "larajax/larajax": "dev-main", + "larajax/larajax": "^2.0", "doctrine/dbal": "^2.13.3|^3.1.4", "intervention/image": "^3.10", "jaybizzle/crawler-detect": "^1.3", From 27afc46e8b348a7c2df92e5ba63bd68ce353b068 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 31 Dec 2025 13:53:28 +0100 Subject: [PATCH 16/21] Update tests to match php 8.5 --- .github/workflows/tests.yml | 2 +- src/Html/HtmlBuilder.php | 2 +- src/Translation/Translator.php | 2 +- tests/Html/HtmlBuilderTest.php | 10 ++++++---- tests/Mail/MailerTest.php | 1 - tests/Parse/MarkdownTest.php | 4 ++-- tests/Parse/SyntaxFieldParserTest.php | 3 --- tests/Scaffold/ScaffoldBaseTest.php | 3 --- tests/TestCase.php | 1 - 9 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39ef9cc4a..9357dda8e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: strategy: max-parallel: 6 matrix: - phpVersions: ['8.2', '8.3', '8.4'] + phpVersions: ['8.2', '8.3', '8.4', '8.5'] fail-fast: false name: PHP ${{ matrix.phpVersions }} steps: diff --git a/src/Html/HtmlBuilder.php b/src/Html/HtmlBuilder.php index e83f1bf41..0db1f3125 100644 --- a/src/Html/HtmlBuilder.php +++ b/src/Html/HtmlBuilder.php @@ -444,7 +444,7 @@ public static function limit($html, $maxLength = 100, $end = '...') break; } - if ($tag[0] === '&' || ord($tag) >= 0x80) { + if ($tag[0] === '&' || ord($tag[0]) >= 0x80) { $result .= $tag; $printedLength++; } diff --git a/src/Translation/Translator.php b/src/Translation/Translator.php index cf6be3b46..fd1f3445a 100644 --- a/src/Translation/Translator.php +++ b/src/Translation/Translator.php @@ -45,7 +45,7 @@ public function get($key, array $replace = [], $locale = null, $fallback = true) // Laravel notes that with JSON translations, there is no usage of a fallback language. // The key is the translation. Here we extend the technology to add fallback support. - if ($fallback && $line === null) { + if ($fallback && $line === null && $this->fallback !== null) { $this->load('*', '*', $this->fallback); $line = $this->loaded['*']['*'][$this->fallback][$key] ?? null; } diff --git a/tests/Html/HtmlBuilderTest.php b/tests/Html/HtmlBuilderTest.php index 5d7bb9ef3..e432e545f 100644 --- a/tests/Html/HtmlBuilderTest.php +++ b/tests/Html/HtmlBuilderTest.php @@ -21,15 +21,17 @@ public function testLimit() $result = with(new HtmlBuilder)->limit("

The quick brown fox jumped over the lazy dog

The quick brown fox jumped over the lazy dog

", 50); $this->assertEquals('

The quick brown fox jumped over the lazy dog

The qu...

', $result); - $result = with(new HtmlBuilder)->limit(trim(" + $input = str_replace("\r\n", "\n", trim("

The quick brown fox jumped over the lazy dog

The quick brown fox jumped over the lazy dog

- "), 60); + ")); + $result = with(new HtmlBuilder)->limit($input, 60); - $this->assertEquals(trim(' + $expected = str_replace("\r\n", "\n", trim('

The quick brown fox jumped over the lazy dog

The...

- '), $result); + ')); + $this->assertEquals($expected, $result); } public function testClean() diff --git a/tests/Mail/MailerTest.php b/tests/Mail/MailerTest.php index 387046ccc..28ddd2ab8 100644 --- a/tests/Mail/MailerTest.php +++ b/tests/Mail/MailerTest.php @@ -95,7 +95,6 @@ protected static function callProtectedMethod($object, $name, $params = []) $className = get_class($object); $class = new ReflectionClass($className); $method = $class->getMethod($name); - $method->setAccessible(true); return $method->invokeArgs($object, $params); } diff --git a/tests/Parse/MarkdownTest.php b/tests/Parse/MarkdownTest.php index 83d6c7444..72c325fe9 100644 --- a/tests/Parse/MarkdownTest.php +++ b/tests/Parse/MarkdownTest.php @@ -97,7 +97,7 @@ public function testParseNonHtml() $normal = $parser->parse($text); // Only accepting one node per line - $this->assertEquals($expected, $normal); + $this->assertEquals(str_replace("\r\n", "\n", $expected), $normal); } public function testParseMultilineHtml() @@ -149,6 +149,6 @@ public function testParseMultilineHtml() $normal = $parser->parse($text); - $this->assertEquals($expected, $normal); + $this->assertEquals(str_replace("\r\n", "\n", $expected), $normal); } } diff --git a/tests/Parse/SyntaxFieldParserTest.php b/tests/Parse/SyntaxFieldParserTest.php index 79a7c94e9..4787da571 100644 --- a/tests/Parse/SyntaxFieldParserTest.php +++ b/tests/Parse/SyntaxFieldParserTest.php @@ -322,7 +322,6 @@ protected static function callProtectedMethod($object, $name, $params = []) $className = get_class($object); $class = new ReflectionClass($className); $method = $class->getMethod($name); - $method->setAccessible(true); return $method->invokeArgs($object, $params); } @@ -331,7 +330,6 @@ public static function getProtectedProperty($object, $name) $className = get_class($object); $class = new ReflectionClass($className); $property = $class->getProperty($name); - $property->setAccessible(true); return $property->getValue($object); } @@ -340,7 +338,6 @@ public static function setProtectedProperty($object, $name, $value) $className = get_class($object); $class = new ReflectionClass($className); $property = $class->getProperty($name); - $property->setAccessible(true); return $property->setValue($object, $value); } } diff --git a/tests/Scaffold/ScaffoldBaseTest.php b/tests/Scaffold/ScaffoldBaseTest.php index 62975091d..55de50adb 100644 --- a/tests/Scaffold/ScaffoldBaseTest.php +++ b/tests/Scaffold/ScaffoldBaseTest.php @@ -25,7 +25,6 @@ protected static function callProtectedMethod($object, $name, $params = []) $className = get_class($object); $class = new ReflectionClass($className); $method = $class->getMethod($name); - $method->setAccessible(true); return $method->invokeArgs($object, $params); } @@ -34,7 +33,6 @@ public static function getProtectedProperty($object, $name) $className = get_class($object); $class = new ReflectionClass($className); $property = $class->getProperty($name); - $property->setAccessible(true); return $property->getValue($object); } @@ -43,7 +41,6 @@ public static function setProtectedProperty($object, $name, $value) $className = get_class($object); $class = new ReflectionClass($className); $property = $class->getProperty($name); - $property->setAccessible(true); return $property->setValue($object, $value); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 522796664..724c229b7 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -20,7 +20,6 @@ protected static function callProtectedMethod($object, $name, $params = []) $className = get_class($object); $class = new ReflectionClass($className); $method = $class->getMethod($name); - $method->setAccessible(true); return $method->invokeArgs($object, $params); } From a9f4317bd1b5bd65d59689fa72daf1a84786fc92 Mon Sep 17 00:00:00 2001 From: Samuel Date: Wed, 31 Dec 2025 14:04:16 +0100 Subject: [PATCH 17/21] Update ModelException.php --- src/Halcyon/Exception/ModelException.php | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Halcyon/Exception/ModelException.php b/src/Halcyon/Exception/ModelException.php index f1bee9879..8975efd1e 100644 --- a/src/Halcyon/Exception/ModelException.php +++ b/src/Halcyon/Exception/ModelException.php @@ -2,6 +2,8 @@ use October\Rain\Halcyon\Model; use October\Rain\Exception\ValidationException; +use Illuminate\Support\MessageBag; +use Exception; /** * ModelException used when validation fails, contains the invalid model for easy analysis @@ -16,16 +18,41 @@ class ModelException extends ValidationException */ protected $model; + /** + * @var MessageBag validationErrors + */ + protected $validationErrors; + /** * __construct receives the invalid model */ public function __construct(Model $model) { $this->model = $model; - $this->errors = $model->errors(); + $this->validationErrors = $model->errors(); + + // Bypass parent constructor to avoid Validator facade dependency + Exception::__construct($this->validationErrors->first()); + $this->evalErrors(); } + /** + * errors returns validation errors + */ + public function errors(): array + { + return $this->validationErrors->messages(); + } + + /** + * getErrors returns the message bag instance + */ + public function getErrors(): MessageBag + { + return $this->validationErrors; + } + /** * getModel returns the model with invalid attributes */ From 8a26416ad58ef05a6d6e5b7de2c5586b6229bbb9 Mon Sep 17 00:00:00 2001 From: Samuel Date: Sun, 4 Jan 2026 03:48:42 +0100 Subject: [PATCH 18/21] Add lazy loading prevention support to Model (#646) Override getRelationValue() to integrate with Laravel's lazy loading prevention system while using October Rain's property-based relation definitions (hasRelation instead of isRelation). Supports: - Model::preventLazyLoading() - $model->withRelationshipAutoloading() - Model::handleLazyLoadingViolationUsing() - attemptToAutoloadRelation() for automatic eager loading --- src/Database/Concerns/HasAttributes.php | 26 +++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Database/Concerns/HasAttributes.php b/src/Database/Concerns/HasAttributes.php index fd5361b21..a71693381 100644 --- a/src/Database/Concerns/HasAttributes.php +++ b/src/Database/Concerns/HasAttributes.php @@ -75,13 +75,35 @@ public function getAttribute($key) return $this->getAttributeValue($key); } + return $this->getRelationValue($key); + } + + /** + * getRelationValue gets a relationship value from a method. + * Overridden from {@link Eloquent} to implement recognition of the relation + * using October Rain's property-based relation definitions. + * @param string $key + * @return mixed + */ + public function getRelationValue($key) + { if ($this->relationLoaded($key)) { return $this->relations[$key]; } - if ($this->hasRelation($key)) { - return $this->getRelationshipFromMethod($key); + if (!$this->hasRelation($key)) { + return; + } + + if ($this->attemptToAutoloadRelation($key)) { + return $this->relations[$key]; } + + if ($this->preventsLazyLoading) { + $this->handleLazyLoadingViolation($key); + } + + return $this->getRelationshipFromMethod($key); } /** From ebecf6e9cafa6743c95ace4264b18348d3348533 Mon Sep 17 00:00:00 2001 From: Sam G Date: Sun, 4 Jan 2026 13:51:50 +1100 Subject: [PATCH 19/21] Check both October and Laravel --- src/Database/Concerns/HasAttributes.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Database/Concerns/HasAttributes.php b/src/Database/Concerns/HasAttributes.php index a71693381..e875fcd3e 100644 --- a/src/Database/Concerns/HasAttributes.php +++ b/src/Database/Concerns/HasAttributes.php @@ -91,7 +91,8 @@ public function getRelationValue($key) return $this->relations[$key]; } - if (!$this->hasRelation($key)) { + // Check both October and Laravel + if (!$this->hasRelation($key) && !$this->isRelation($key)) { return; } From 61b9080a5f6a29e234e9a39eded8a35f11fff06c Mon Sep 17 00:00:00 2001 From: Sam G Date: Sun, 4 Jan 2026 13:55:48 +1100 Subject: [PATCH 20/21] Adds new gateway client --- src/Installer/GatewayClient.php | 618 ++++++++++++++++++++++++++++++++ 1 file changed, 618 insertions(+) create mode 100644 src/Installer/GatewayClient.php diff --git a/src/Installer/GatewayClient.php b/src/Installer/GatewayClient.php new file mode 100644 index 000000000..9a29c77e1 --- /dev/null +++ b/src/Installer/GatewayClient.php @@ -0,0 +1,618 @@ +setCredentials('your-api-key', 'your-api-secret'); + * $projects = $client->listProjects(); + * + * // Project-level operations (uses license key or auth.json hash) + * $client = (new GatewayClient)->setProjectHash('your-license-key-or-hash'); + * $updates = $client->checkForUpdates(['plugins' => [...], 'themes' => [...]]); + * + * // Open operations (no auth) + * $client = new GatewayClient; + * $result = $client->validateLicense('XXXX-XXXX-XXXX-XXXX'); + * + */ +class GatewayClient +{ + /** + * @var string API base URL + */ + const API_BASE_URL = 'https://api.octobercms.com'; + + /** + * @var string API version prefix + */ + const API_VERSION = 'v1'; + + /** + * @var string|null apiKey for HMAC authentication + */ + protected $apiKey; + + /** + * @var string|null apiSecret for HMAC authentication + */ + protected $apiSecret; + + /** + * @var string|null projectHash for project-level authentication (license key or auth.json hash) + */ + protected $projectHash; + + /** + * @var int timeout in seconds + */ + protected $timeout = 30; + + /** + * @var array lastResponseHeaders + */ + protected $lastResponseHeaders = []; + + /** + * @var int lastStatusCode + */ + protected $lastStatusCode = 0; + + /** + * @var string|null lastErrorCode from API response + */ + protected $lastErrorCode; + + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + /** + * setCredentials for account-level authentication + */ + public function setCredentials(string $apiKey, string $apiSecret): self + { + $this->apiKey = $apiKey; + $this->apiSecret = $apiSecret; + return $this; + } + + /** + * setProjectHash for project-level authentication + * + * @param string $projectHash License key or auth.json hash (bind code) + */ + public function setProjectHash(string $projectHash): self + { + $this->projectHash = $projectHash; + return $this; + } + + /** + * setTimeout for requests + */ + public function setTimeout(int $seconds): self + { + $this->timeout = $seconds; + return $this; + } + + // ========================================================================= + // ACCOUNT-LEVEL ENDPOINTS (HMAC Auth Required) + // ========================================================================= + + /** + * createProject creates a new project + * + * @param string $name Project name + * @param array $options Optional: description + * @return array Project data including project_id and license_key + * @throws Exception + */ + public function createProject(string $name, array $options = []): array + { + return $this->requestWithHmac('projects/create', array_merge( + ['name' => $name], + $options + )); + } + + /** + * updateProject updates an existing project + * + * @param int $projectId Project ID + * @param array $options Optional: name, description + * @return array Updated project data + * @throws Exception + */ + public function updateProject(int $projectId, array $options = []): array + { + return $this->requestWithHmac('projects/update', array_merge( + ['project_id' => $projectId], + $options + )); + } + + /** + * deleteProject deletes a project + * + * @param int $projectId Project ID + * @return array Confirmation + * @throws Exception + */ + public function deleteProject(int $projectId): array + { + return $this->requestWithHmac('projects/delete', [ + 'project_id' => $projectId, + ]); + } + + /** + * listProjects returns all projects for the account + * + * @param int $limit Results per page (1-100) + * @param string|null $cursor Pagination cursor + * @return array Projects list with next_cursor + * @throws Exception + */ + public function listProjects(int $limit = 50, ?string $cursor = null): array + { + $params = ['limit' => $limit]; + if ($cursor !== null) { + $params['cursor'] = $cursor; + } + return $this->requestWithHmac('projects/list', $params); + } + + /** + * getProject returns detailed information about a project + * + * @param int $projectId Project ID + * @return array Project details + * @throws Exception + */ + public function getProject(int $projectId): array + { + return $this->requestWithHmac('projects/get', [ + 'project_id' => $projectId, + ]); + } + + /** + * lookupByDomain looks up a project by domain name + * + * @param string $domainName Domain to look up + * @return array Project ID + * @throws Exception + */ + public function lookupByDomain(string $domainName): array + { + return $this->requestWithHmac('licenses/domain', [ + 'domain_name' => $domainName, + ]); + } + + /** + * rotateLicense regenerates a license key + * + * @param int $projectId Project ID + * @return array New license_key and license_hash + * @throws Exception + */ + public function rotateLicense(int $projectId): array + { + return $this->requestWithHmac('licenses/rotate', [ + 'project_id' => $projectId, + ]); + } + + /** + * attachPackage attaches a package to a project + * + * @param int $projectId Project ID + * @param string $packageCode Package code (e.g., 'Author.PluginName') + * @param string $type Package type ('plugin' or 'theme') + * @return array Confirmation with attached, package, type + * @throws Exception + */ + public function attachPackage(int $projectId, string $packageCode, string $type): array + { + return $this->requestWithHmac('projects/packages/attach', [ + 'project_id' => $projectId, + 'package' => $packageCode, + 'type' => $type, + ]); + } + + /** + * detachPackage detaches a package from a project + * + * @param int $projectId Project ID + * @param string $packageCode Package code (e.g., 'Author.PluginName') + * @param string $type Package type ('plugin' or 'theme') + * @return array Confirmation with detached, package, type + * @throws Exception + */ + public function detachPackage(int $projectId, string $packageCode, string $type): array + { + return $this->requestWithHmac('projects/packages/detach', [ + 'project_id' => $projectId, + 'package' => $packageCode, + 'type' => $type, + ]); + } + + // ========================================================================= + // OPEN ENDPOINTS (No Auth Required) + // ========================================================================= + + /** + * validateLicense validates a license key (no authentication required) + * + * @param string $licenseKey License key to validate + * @return array License information + * @throws Exception + */ + public function validateLicense(string $licenseKey): array + { + return $this->request('licenses/validate', [ + 'license_key' => $licenseKey, + ]); + } + + /** + * ping performs a health check + * + * @return array Server status + * @throws Exception + */ + public function ping(): array + { + return $this->request('ping', [], 'GET'); + } + + // ========================================================================= + // PROJECT-LEVEL ENDPOINTS + // ========================================================================= + + /** + * getProjectDetail returns project details (project-level auth) + * + * @param string|null $projectHash Optional project hash override + * @return array Project details + * @throws Exception + */ + public function getProjectDetail(?string $projectHash = null): array + { + return $this->requestWithProject('project/detail', [ + 'id' => $projectHash ?? $this->projectHash, + ]); + } + + /** + * checkForUpdates checks for available updates (project-level auth) + * + * @param array $options plugins (assoc array code=>version), themes, version, build + * @return array Available updates + * @throws Exception + */ + public function checkForUpdates(array $options = []): array + { + $params = []; + + if (isset($options['plugins'])) { + $params['plugins'] = base64_encode(json_encode($options['plugins'])); + } + if (isset($options['themes'])) { + $params['themes'] = base64_encode(json_encode($options['themes'])); + } + if (isset($options['version'])) { + $params['version'] = $options['version']; + } + if (isset($options['build'])) { + $params['build'] = $options['build']; + } + + return $this->requestWithProject('project/check', $params); + } + + /** + * getPackages returns multiple package details + * + * @param array $codes Package codes + * @param string $type Package type (plugin or theme) + * @return array Package information + * @throws Exception + */ + public function getPackages(array $codes, string $type = 'plugin'): array + { + return $this->request('package/details', [ + 'names' => $codes, + 'type' => $type, + ]); + } + + /** + * getPackage returns single package details + * + * @param string $code Package code + * @param string $type Package type + * @return array Package information with requirements + * @throws Exception + */ + public function getPackage(string $code, string $type = 'plugin'): array + { + return $this->requestWithProject('package/detail', [ + 'name' => $code, + 'type' => $type, + ]); + } + + /** + * getPackageContent returns package documentation content + * + * @param string $code Package code + * @param string $type Package type + * @return array Package info with HTML content + * @throws Exception + */ + public function getPackageContent(string $code, string $type = 'plugin'): array + { + return $this->requestWithProject('package/content', [ + 'name' => $code, + 'type' => $type, + ]); + } + + /** + * browsePackages returns a paginated list of packages + * + * @param int $page Page number + * @param string $type Package type + * @param int|null $version Compatibility version filter + * @return array Paginated package list + * @throws Exception + */ + public function browsePackages(int $page = 1, string $type = 'plugin', ?int $version = null): array + { + $params = [ + 'page' => $page, + 'type' => $type, + ]; + if ($version !== null) { + $params['version'] = $version; + } + return $this->request('package/browse', $params); + } + + /** + * searchPackages searches for packages + * + * @param string $query Search query + * @param string|null $type Package type filter + * @return array Matching packages + * @throws Exception + */ + public function searchPackages(string $query, ?string $type = null): array + { + $params = ['query' => $query]; + if ($type !== null) { + $params['type'] = $type; + } + return $this->request('package/search', $params); + } + + /** + * getInstallDetail returns installation details + * + * @return array Core and package hashes + * @throws Exception + */ + public function getInstallDetail(): array + { + return $this->requestWithProject('install/detail', []); + } + + // ========================================================================= + // HTTP CLIENT + // ========================================================================= + + /** + * requestWithHmac makes a request with HMAC authentication + */ + protected function requestWithHmac(string $endpoint, array $params): array + { + if (!$this->apiKey || !$this->apiSecret) { + throw new Exception('API credentials required for this operation'); + } + + // Add nonce + $params['nonce'] = (string) round(microtime(true) * 1000); + + // Build query string for signing + $queryString = http_build_query($params, '', '&'); + + // Compute signature + $signature = base64_encode( + hash_hmac('sha512', $queryString, base64_decode($this->apiSecret), true) + ); + + $headers = [ + 'Rest-Key: ' . $this->apiKey, + 'Rest-Sign: ' . $signature, + ]; + + return $this->request($endpoint, $params, 'POST', $headers); + } + + /** + * requestWithProject makes a request with project-level authentication + */ + protected function requestWithProject(string $endpoint, array $params): array + { + $headers = []; + + if ($this->projectHash) { + $headers[] = 'php-auth-pw: ' . $this->projectHash; + $params['project'] = $this->projectHash; + } + + return $this->request($endpoint, $params, 'POST', $headers); + } + + /** + * request makes an HTTP request + * + * @param string $endpoint API endpoint + * @param array $params Request parameters + * @param string $method HTTP method + * @param array $headers Additional headers + * @return array Decoded response + * @throws Exception + */ + protected function request(string $endpoint, array $params = [], string $method = 'POST', array $headers = []): array + { + $url = self::API_BASE_URL . '/' . self::API_VERSION . '/' . ltrim($endpoint, '/'); + + // Default headers + $headers = array_merge([ + 'Content-Type: application/x-www-form-urlencoded', + 'Accept: application/json', + ], $headers); + + // Initialize cURL + $ch = curl_init(); + + if ($method === 'GET') { + if (!empty($params)) { + $url .= '?' . http_build_query($params); + } + } + else { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params, '', '&')); + } + + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_HEADER => true, + CURLOPT_SSL_VERIFYPEER => true, + ]); + + $response = curl_exec($ch); + + if ($response === false) { + $error = curl_error($ch); + throw new Exception('cURL error: ' . $error); + } + + $this->lastStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + + // Parse headers and body + $headerString = substr($response, 0, $headerSize); + $body = substr($response, $headerSize); + + $this->lastResponseHeaders = $this->parseHeaders($headerString); + $this->lastErrorCode = null; + + // Decode JSON response + $data = json_decode($body, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception('Invalid JSON response: ' . $body); + } + + // Check for error responses + if ($this->lastStatusCode >= 400) { + $this->lastErrorCode = $data['error'] ?? 'unknown_error'; + $message = $data['message'] ?? 'An error occurred'; + throw new Exception($message, $this->lastStatusCode); + } + + return $data; + } + + /** + * parseHeaders extracts headers from response + */ + protected function parseHeaders(string $headerString): array + { + $headers = []; + foreach (explode("\r\n", $headerString) as $line) { + if (strpos($line, ':') !== false) { + list($key, $value) = explode(':', $line, 2); + $headers[trim($key)] = trim($value); + } + } + return $headers; + } + + /** + * getLastResponseSignature returns the last response signature for verification + */ + public function getLastResponseSignature(): ?string + { + return $this->lastResponseHeaders['Rest-Sign'] ?? null; + } + + /** + * getLastStatusCode returns the last HTTP status code + */ + public function getLastStatusCode(): int + { + return $this->lastStatusCode; + } + + /** + * getRetryAfter returns the Retry-After header value (for rate limiting) + */ + public function getRetryAfter(): ?int + { + $value = $this->lastResponseHeaders['Retry-After'] ?? null; + return $value !== null ? (int) $value : null; + } + + /** + * getLastErrorCode returns the API error code from the last failed request + */ + public function getLastErrorCode(): ?string + { + return $this->lastErrorCode; + } + + /** + * isRateLimited checks if the last error was a rate limit error + */ + public function isRateLimited(): bool + { + return $this->lastErrorCode === 'rate_limited' || $this->lastStatusCode === 429; + } + + /** + * isAuthError checks if the last error was an authentication error + */ + public function isAuthError(): bool + { + return $this->lastErrorCode === 'invalid_auth' || $this->lastStatusCode === 401; + } + + /** + * isValidationError checks if the last error was a validation error + */ + public function isValidationError(): bool + { + return $this->lastErrorCode === 'validation_error' || $this->lastStatusCode === 422; + } +} From 436cf8f71b3715ecc704b2d766002b2c28745c01 Mon Sep 17 00:00:00 2001 From: Sam G Date: Sun, 4 Jan 2026 13:56:19 +1100 Subject: [PATCH 21/21] Bump version --- .gitignore | 3 ++- src/Installer/InstallManager.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 151a78eb1..859eccd37 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,9 @@ composer.lock # Editor files .idea .vscode +.claude # Other files .DS_Store php_errors.log -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache diff --git a/src/Installer/InstallManager.php b/src/Installer/InstallManager.php index 879b89352..54edbc8f8 100644 --- a/src/Installer/InstallManager.php +++ b/src/Installer/InstallManager.php @@ -21,7 +21,7 @@ class InstallManager /** * @var string WANT_VERSION is the default composer version string to use. */ - const WANT_VERSION = '^4.0'; + const WANT_VERSION = '^4.1'; /** * instance creates a new instance of this singleton