├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── spec └── Devfactory │ └── Minify │ └── Providers │ ├── JavaScriptSpec.php │ └── StyleSheetSpec.php └── src ├── Contracts └── MinifyInterface.php ├── Exceptions ├── CannotRemoveFileException.php ├── CannotSaveFileException.php ├── DirNotExistException.php ├── DirNotWritableException.php ├── FileNotExistException.php └── InvalidArgumentException.php ├── Facades └── MinifyFacade.php ├── Minify.php ├── MinifyServiceProvider.php ├── Providers ├── BaseProvider.php ├── JavaScript.php └── StyleSheet.php └── config └── minify.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at https://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request, release] 4 | 5 | jobs: 6 | tests: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-latest] 11 | php: [8.1, 8.2] 12 | 13 | runs-on: ${{ matrix.os }} 14 | 15 | name: PHP ${{ matrix.php }} – ${{ matrix.os }} 16 | 17 | permissions: 18 | checks: write 19 | pull-requests: write 20 | contents: read 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup PHP 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: "${{ matrix.php }}" 30 | 31 | - name: Get composer cache directory 32 | id: composer-cache 33 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 34 | 35 | - name: Cache dependencies 36 | uses: actions/cache@v3 37 | with: 38 | path: ${{ steps.composer-cache.outputs.dir }} 39 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 40 | restore-keys: ${{ runner.os }}-composer-${{ matrix.dependency-version }}- 41 | 42 | - name: Install dependencies 43 | run: composer update --no-interaction 44 | 45 | - name: Run tests (phpspec) 46 | run: ./vendor/bin/phpspec run --format=dot 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | 6 | .idea/ 7 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Cees van Egmond 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minify 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/DevFactoryCH/minify/tests.yml)](https://github.com/DevFactoryCH/minify/actions/workflows/tests.yml) 4 | [![Latest Stable Version](https://poser.pugx.org/devfactory/minify/v/stable.svg)](https://packagist.org/packages/devfactory/minify) 5 | [![Total Downloads](https://poser.pugx.org/devfactory/minify/downloads.svg)](https://packagist.org/packages/devfactory/minify) 6 | [![License](https://poser.pugx.org/devfactory/minify/license.svg)](https://packagist.org/packages/devfactory/minify) 7 | 8 | With this package you can minify your existing stylesheet and JavaScript files for Laravel 10. 9 | This process can be a little tough, this package simplifies and automates this process. 10 | 11 | For Laravel 5 - 9 please use version 1.x of this package. 12 | 13 | For Laravel 4 please use [ceesvanegmond/minify](https://github.com/ceesvanegmond/minify) 14 | 15 | ## Installation 16 | 17 | Begin by installing this package through Composer. 18 | 19 | 20 | ```json 21 | { 22 | "require": { 23 | "devfactory/minify": "^2.0" 24 | } 25 | } 26 | ``` 27 | 28 | After the package installation, the `MinifyServiceProvider` and `Minify` facade are automatically registered. 29 | You can use the `Minify` facade anywhere in your application. 30 | 31 | To publish the config file: 32 | 33 | ```shell 34 | php artisan vendor:publish --provider="Devfactory\Minify\MinifyServiceProvider" --tag="config" 35 | ``` 36 | 37 | 38 | ## Upgrade to v2 39 | Minify version 2 is PHP 8.1+ and Laravel 10+ only. 40 | 41 | ### Required upgrade changes 42 | If the [`Devfactory\Minify\Contracts\MinifyInterface`](src/Contracts/MinifyInterface.php) interface is implemented, 43 | make sure update your implementation according to the updated types and exceptions. 44 | 45 | If the [`Devfactory\Minify\Providers\BaseProvider`](src/Providers/BaseProvider.php) abstract class is used, 46 | make sure update your classes according to the updated types and exceptions. 47 | 48 | The method `Devfactory\Minify\Providers\StyleSheet#urlCorrection` has been renamed to `Devfactory\Minify\Providers\StyleSheet#getFileContentWithCorrectedUrls`. 49 | 50 | Rename the `minify.config.php` configuration file to `minify.php`. 51 | 52 | ## Usage 53 | ### Stylesheet 54 | 55 | ```php 56 | // app/views/hello.blade.php 57 | 58 | 59 | 60 | ... 61 | {!! Minify::stylesheet('/css/main.css') !!} 62 | // or by passing multiple files 63 | {!! Minify::stylesheet(['/css/main.css', '/css/bootstrap.css']) !!} 64 | // add custom attributes 65 | {!! Minify::stylesheet(['/css/main.css', '/css/bootstrap.css'], ['foo' => 'bar']) !!} 66 | // add full uri of the resource 67 | {!! Minify::stylesheet(['/css/main.css', '/css/bootstrap.css'])->withFullUrl() !!} 68 | {!! Minify::stylesheet(['//fonts.googleapis.com/css?family=Roboto']) !!} 69 | 70 | // minify and combine all stylesheet files in given folder 71 | {!! Minify::stylesheetDir('/css/') !!} 72 | // add custom attributes to minify and combine all stylesheet files in given folder 73 | {!! Minify::stylesheetDir('/css/', ['foo' => 'bar', 'defer' => true]) !!} 74 | // minify and combine all stylesheet files in given folder with full uri 75 | {!! Minify::stylesheetDir('/css/')->withFullUrl() !!} 76 | 77 | ... 78 | 79 | ``` 80 | 81 | ### Javascript 82 | 83 | ```php 84 | // app/views/hello.blade.php 85 | 86 | 87 | 88 | ... 89 | 90 | {!! Minify::javascript('/js/jquery.js') !!} 91 | // or by passing multiple files 92 | {!! Minify::javascript(['/js/jquery.js', '/js/jquery-ui.js']) !!} 93 | // add custom attributes 94 | {!! Minify::javascript(['/js/jquery.js', '/js/jquery-ui.js'], ['bar' => 'baz']) !!} 95 | // add full uri of the resource 96 | {!! Minify::javascript(['/js/jquery.js', '/js/jquery-ui.js'])->withFullUrl() !!} 97 | {!! Minify::javascript(['//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js']) !!} 98 | 99 | // minify and combine all javascript files in given folder 100 | {!! Minify::javascriptDir('/js/') !!} 101 | // add custom attributes to minify and combine all javascript files in given folder 102 | {!! Minify::javascriptDir('/js/', ['bar' => 'baz', 'async' => true]) !!} 103 | // minify and combine all javascript files in given folder with full uri 104 | {!! Minify::javascriptDir('/js/')->withFullUrl() !!} 105 | 106 | ``` 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devfactory/minify", 3 | "keywords": [ 4 | "minify", 5 | "laravel5" 6 | ], 7 | "description": "A package for minifying styles and javascript for laravel 5", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Da Costa Alcindo", 12 | "email": "alcindo.dacosta@devfactory.ch" 13 | } 14 | ], 15 | "require": { 16 | "php": "^8.1", 17 | "tedivm/jshrink": "~1.0", 18 | "natxet/cssmin": "3.*", 19 | "illuminate/filesystem": "^10.0|^11.0|^12.0", 20 | "illuminate/support": "^10.0|^11.0|^12.0" 21 | }, 22 | "require-dev": { 23 | "phpspec/phpspec": "^7.4", 24 | "mikey179/vfsstream": "^1.6" 25 | }, 26 | "autoload": { 27 | "psr-0": { 28 | "": "src/" 29 | }, 30 | "psr-4": { 31 | "Devfactory\\Minify\\": "src/" 32 | } 33 | }, 34 | "minimum-stability": "stable", 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "Devfactory\\Minify\\MinifyServiceProvider" 39 | ], 40 | "aliases": { 41 | "Minify": "Devfactory\\Minify\\Facades\\MinifyFacade" 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/Devfactory/Minify/Providers/JavaScriptSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(JavaScript::class); 18 | } 19 | 20 | function it_adds_one_file(): void 21 | { 22 | vfsStream::setup('js', null, [ 23 | '1.js' => 'a', 24 | ]); 25 | 26 | $this->add(VfsStream::url('js')); 27 | $this->shouldHaveCount(1); 28 | } 29 | 30 | function it_adds_multiple_files(): void 31 | { 32 | vfsStream::setup('root', null, [ 33 | '1.js' => 'a', 34 | '2.js' => 'b', 35 | ]); 36 | 37 | $this->add([ 38 | VfsStream::url('root/1.js'), 39 | VfsStream::url('root/2.js') 40 | ]); 41 | 42 | $this->shouldHaveCount(2); 43 | } 44 | 45 | function it_adds_custom_attributes(): void 46 | { 47 | $this->tag('file', ['foobar' => 'baz', 'defer' => true]) 48 | ->shouldReturn('' . PHP_EOL); 49 | } 50 | 51 | function it_adds_without_custom_attributes(): void 52 | { 53 | $this->tag('file') 54 | ->shouldReturn('' . PHP_EOL); 55 | } 56 | 57 | function it_throws_exception_when_file_not_exists(): void 58 | { 59 | $this->shouldThrow(FileNotExistException::class) 60 | ->duringAdd('foobar'); 61 | } 62 | 63 | function it_should_throw_exception_when_buildpath_not_exist(): void 64 | { 65 | $prophet = new Prophet; 66 | $file = $prophet->prophesize(Filesystem::class); 67 | $file->makeDirectory('dir_bar', 0775, true)->willReturn(false); 68 | 69 | $this->beConstructedWith(null, null, $file); 70 | $this->shouldThrow(DirNotExistException::class) 71 | ->duringMake('dir_bar'); 72 | } 73 | 74 | function it_should_throw_exception_when_buildpath_not_writable(): void 75 | { 76 | vfsStream::setup('js', 0555); 77 | 78 | $this->shouldThrow(DirNotWritableException::class) 79 | ->duringMake(vfsStream::url('js')); 80 | } 81 | 82 | function it_minifies_multiple_files(): void 83 | { 84 | vfsStream::setup('root', null, [ 85 | 'output' => [], 86 | '1.js' => 'a', 87 | '2.js' => 'b', 88 | ]); 89 | 90 | $this->add(vfsStream::url('root/1.js')); 91 | $this->add(vfsStream::url('root/2.js')); 92 | 93 | $this->make(vfsStream::url('root/output')); 94 | 95 | $this->getAppended()->shouldBe("a\nb\n"); 96 | 97 | $output = md5('vfs://root/1.js-vfs://root/2.js'); 98 | $filemtime = filemtime(vfsStream::url('root/1.js')) + filemtime(vfsStream::url('root/2.js')); 99 | $extension = '.js'; 100 | 101 | $this->getFilename()->shouldBe($output . $filemtime . $extension); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /spec/Devfactory/Minify/Providers/StyleSheetSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(StyleSheet::class); 17 | } 18 | 19 | function it_adds_one_file(): void 20 | { 21 | vfsStream::setup('css', null, [ 22 | '1.css' => 'a', 23 | ]); 24 | 25 | $this->add(VfsStream::url('css')); 26 | $this->shouldHaveCount(1); 27 | } 28 | 29 | function it_adds_multiple_files(): void 30 | { 31 | vfsStream::setup('root', null, [ 32 | '1.css' => 'a', 33 | '2.css' => 'b', 34 | ]); 35 | 36 | $this->add([ 37 | VfsStream::url('root/1.css'), 38 | VfsStream::url('root/2.css') 39 | ]); 40 | 41 | $this->shouldHaveCount(2); 42 | } 43 | 44 | function it_adds_custom_attributes(): void 45 | { 46 | $this->tag('file', ['foobar' => 'baz', 'defer' => true]) 47 | ->shouldReturn('' . PHP_EOL); 48 | } 49 | 50 | function it_adds_without_custom_attributes(): void 51 | { 52 | $this->tag('file') 53 | ->shouldReturn('' . PHP_EOL); 54 | } 55 | 56 | function it_throws_exception_when_file_not_exists(): void 57 | { 58 | $this->shouldThrow(FileNotExistException::class) 59 | ->duringAdd('foobar'); 60 | } 61 | 62 | function it_should_throw_exception_when_buildpath_not_exist(): void 63 | { 64 | $prophet = new Prophet; 65 | $file = $prophet->prophesize(Filesystem::class); 66 | $file->makeDirectory('dir_bar', 0775, true)->willReturn(false); 67 | 68 | $this->beConstructedWith(null, null, $file); 69 | $this->shouldThrow(DirNotExistException::class) 70 | ->duringMake('dir_bar'); 71 | } 72 | 73 | function it_should_throw_exception_when_buildpath_not_writable(): void 74 | { 75 | vfsStream::setup('css', 0555); 76 | 77 | $this->shouldThrow(DirNotWritableException::class) 78 | ->duringMake(vfsStream::url('css')); 79 | } 80 | 81 | function it_minifies_multiple_files(): void 82 | { 83 | vfsStream::setup('root', null, [ 84 | 'output' => [], 85 | '1.css' => 'a', 86 | '2.css' => 'b', 87 | ]); 88 | 89 | $this->add(vfsStream::url('root/1.css')); 90 | $this->add(vfsStream::url('root/2.css')); 91 | 92 | $this->make(vfsStream::url('root/output')); 93 | 94 | $this->getAppended()->shouldBe("a\nb\n"); 95 | 96 | $output = md5('vfs://root/1.css-vfs://root/2.css'); 97 | $filemtime = filemtime(vfsStream::url('root/1.css')) + filemtime(vfsStream::url('root/2.css')); 98 | $extension = '.css'; 99 | 100 | $this->getFilename()->shouldBe($output . $filemtime . $extension); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Contracts/MinifyInterface.php: -------------------------------------------------------------------------------- 1 | checkConfiguration($config); 40 | 41 | $this->config = $config; 42 | $this->environment = $environment; 43 | } 44 | 45 | public function initialiseJavascript(array $attributes): void 46 | { 47 | $this->provider = new JavaScript(public_path(), [ 48 | 'hash_salt' => $this->config['hash_salt'], 49 | 'disable_mtime' => $this->config['disable_mtime'] 50 | ]); 51 | $this->buildPath = $this->config['js_build_path']; 52 | $this->attributes = $attributes; 53 | $this->buildExtension = 'js'; 54 | } 55 | 56 | private function initialiseStylesheet(array $attributes): void 57 | { 58 | $this->provider = new StyleSheet(public_path(), [ 59 | 'hash_salt' => $this->config['hash_salt'], 60 | 'disable_mtime' => $this->config['disable_mtime'] 61 | ]); 62 | $this->buildPath = $this->config['css_build_path']; 63 | $this->attributes = $attributes; 64 | $this->buildExtension = 'css'; 65 | } 66 | 67 | /** 68 | * @throws DirNotExistException 69 | * @throws DirNotWritableException 70 | * @throws CannotSaveFileException 71 | * @throws CannotRemoveFileException 72 | * @throws FileNotExistException 73 | */ 74 | public function javascript(array|string $file, array $attributes = []): Minify 75 | { 76 | $this->initialiseJavascript($attributes); 77 | 78 | $this->process($file); 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * @throws DirNotExistException 85 | * @throws DirNotWritableException 86 | * @throws CannotSaveFileException 87 | * @throws CannotRemoveFileException 88 | * @throws FileNotExistException 89 | */ 90 | public function stylesheet(array|string $file, array $attributes = []): Minify 91 | { 92 | $this->initialiseStylesheet($attributes); 93 | 94 | $this->process($file); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * @throws DirNotExistException 101 | * @throws DirNotWritableException 102 | * @throws CannotSaveFileException 103 | * @throws CannotRemoveFileException 104 | * @throws FileNotExistException 105 | */ 106 | public function stylesheetDir(string $dir, array $attributes = []): string 107 | { 108 | $this->initialiseStylesheet($attributes); 109 | 110 | return $this->assetDirHelper('css', $dir); 111 | } 112 | 113 | /** 114 | * @throws DirNotExistException 115 | * @throws DirNotWritableException 116 | * @throws CannotSaveFileException 117 | * @throws CannotRemoveFileException 118 | * @throws FileNotExistException 119 | */ 120 | public function javascriptDir(string $dir, array $attributes = []): string 121 | { 122 | $this->initialiseJavascript($attributes); 123 | 124 | return $this->assetDirHelper('js', $dir); 125 | } 126 | 127 | /** 128 | * @throws DirNotExistException 129 | * @throws DirNotWritableException 130 | * @throws CannotSaveFileException 131 | * @throws CannotRemoveFileException 132 | * @throws FileNotExistException 133 | */ 134 | private function assetDirHelper(string $ext, string $dir): Minify 135 | { 136 | $files = []; 137 | 138 | $itr_obj = new RecursiveDirectoryIterator(public_path() . $dir); 139 | $itr_obj->setFlags(FilesystemIterator::SKIP_DOTS); 140 | $dir_obj = new RecursiveIteratorIterator($itr_obj); 141 | 142 | foreach ($dir_obj as $fileinfo) { 143 | if (!$fileinfo->isDir() && ($filename = $fileinfo->getFilename()) && (pathinfo($filename, PATHINFO_EXTENSION) == $ext) && (strlen($fileinfo->getFilename()) < 30)) { 144 | $files[] = str_replace(public_path(), '', $fileinfo); 145 | } 146 | } 147 | 148 | if (count($files) > 0) { 149 | if ($this->config['reverse_sort']) { 150 | rsort($files); 151 | } else { 152 | sort($files); 153 | } 154 | $this->process($files); 155 | } 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * @throws DirNotExistException 162 | * @throws DirNotWritableException 163 | * @throws CannotSaveFileException 164 | * @throws CannotRemoveFileException 165 | * @throws FileNotExistException 166 | */ 167 | private function process(array|string $file): void 168 | { 169 | $this->provider->add($file); 170 | 171 | if ($this->minifyForCurrentEnvironment() && $this->provider->make($this->buildPath)) { 172 | $this->provider->minify(); 173 | } 174 | 175 | $this->fullUrl = false; 176 | } 177 | 178 | protected function render(): string 179 | { 180 | $baseUrl = $this->fullUrl ? $this->getBaseUrl() : ''; 181 | if (!$this->minifyForCurrentEnvironment()) { 182 | return $this->provider->tags($baseUrl, $this->attributes); 183 | } 184 | 185 | if ($this->buildExtension == 'js') { 186 | $buildPath = $this->config['js_url_path'] ?? $this->buildPath; 187 | } else# if( $this->buildExtension == 'css') 188 | { 189 | $buildPath = $this->config['css_url_path'] ?? $this->buildPath; 190 | } 191 | 192 | $filename = $baseUrl . $buildPath . $this->provider->getFilename(); 193 | 194 | if ($this->onlyUrl) { 195 | return $filename; 196 | } 197 | 198 | return $this->provider->tag($filename, $this->attributes); 199 | } 200 | 201 | protected function minifyForCurrentEnvironment(): bool 202 | { 203 | return !in_array($this->environment, $this->config['ignore_environments']); 204 | } 205 | 206 | public function withFullUrl(): Minify 207 | { 208 | $this->fullUrl = true; 209 | 210 | return $this; 211 | } 212 | 213 | public function onlyUrl(): Minify 214 | { 215 | $this->onlyUrl = true; 216 | 217 | return $this; 218 | } 219 | 220 | public function __toString() 221 | { 222 | return $this->render(); 223 | } 224 | 225 | /** 226 | * @throws InvalidArgumentException 227 | */ 228 | private function checkConfiguration(array $config): void 229 | { 230 | if (!isset($config['css_build_path']) || !is_string($config['css_build_path'])) 231 | { 232 | throw new InvalidArgumentException('Missing css_build_path field'); 233 | } 234 | 235 | if (!isset($config['js_build_path']) || !is_string($config['js_build_path'])) 236 | { 237 | throw new InvalidArgumentException('Missing js_build_path field'); 238 | } 239 | 240 | if (!isset($config['ignore_environments']) || !is_array($config['ignore_environments'])) 241 | { 242 | throw new InvalidArgumentException('Missing ignore_environments field'); 243 | } 244 | } 245 | 246 | private function getBaseUrl(): string 247 | { 248 | if (is_null($this->config['base_url']) || (trim($this->config['base_url']) == '')) { 249 | return Request::root(); 250 | } else { 251 | return $this->config['base_url']; 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/MinifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 13 | __DIR__ . '/config/minify.php' => config_path('minify.php'), 14 | ], 'config'); 15 | } 16 | 17 | /** 18 | * Register the service provider. 19 | */ 20 | public function register(): void 21 | { 22 | // Register container binding 23 | $this->app->singleton('minify', function ($app) { 24 | return new Minify( 25 | [ 26 | 'css_build_path' => config('minify.css_build_path'), 27 | 'css_url_path' => config('minify.css_url_path'), 28 | 'js_build_path' => config('minify.js_build_path'), 29 | 'js_url_path' => config('minify.js_url_path'), 30 | 'ignore_environments' => config('minify.ignore_environments'), 31 | 'base_url' => config('minify.base_url'), 32 | 'reverse_sort' => config('minify.reverse_sort'), 33 | 'disable_mtime' => config('minify.disable_mtime'), 34 | 'hash_salt' => config('minify.hash_salt'), 35 | ], 36 | $app->environment() 37 | ); 38 | }); 39 | 40 | // Merge config with config from application 41 | $this->mergeConfigFrom( 42 | __DIR__ . '/config/minify.php', 'minify' 43 | ); 44 | } 45 | 46 | /** 47 | * Get the services provided by the provider. 48 | */ 49 | public function provides(): array 50 | { 51 | return ['minify']; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Providers/BaseProvider.php: -------------------------------------------------------------------------------- 1 | file = $file ?: new Filesystem; 34 | 35 | $this->publicPath = $publicPath ?: $_SERVER['DOCUMENT_ROOT']; 36 | 37 | if (!is_array($config)) 38 | { 39 | $this->disable_mtime = false; 40 | $this->hash_salt = ''; 41 | } 42 | else 43 | { 44 | $this->disable_mtime = $config['disable_mtime'] ?: false; 45 | $this->hash_salt = $config['hash_salt'] ?: ''; 46 | } 47 | 48 | $value = function($key) 49 | { 50 | return $_SERVER[$key] ?? ''; 51 | }; 52 | 53 | $this->headers = [ 54 | 'User-Agent' => $value('HTTP_USER_AGENT'), 55 | 'Accept' => $value('HTTP_ACCEPT'), 56 | 'Accept-Language' => $value('HTTP_ACCEPT_LANGUAGE'), 57 | 'Accept-Encoding' => 'identity', 58 | 'Connection' => 'close', 59 | ]; 60 | } 61 | 62 | /** 63 | * @throws DirNotWritableException 64 | * @throws DirNotExistException 65 | * @throws CannotRemoveFileException 66 | * @throws FileNotExistException 67 | */ 68 | public function make(string $outputDir): bool 69 | { 70 | $this->outputDir = $this->publicPath . $outputDir; 71 | 72 | $this->checkDirectory(); 73 | 74 | if ($this->checkExistingFiles()) 75 | { 76 | return false; 77 | } 78 | 79 | $this->removeOldFiles(); 80 | $this->appendFiles(); 81 | 82 | return true; 83 | } 84 | 85 | /** 86 | * @throws FileNotExistException 87 | */ 88 | public function add(array|string $file): void 89 | { 90 | if (is_array($file)) 91 | { 92 | foreach ($file as $value) $this->add($value); 93 | } 94 | else if ($this->checkExternalFile($file)) 95 | { 96 | $this->files[] = $file; 97 | } 98 | else { 99 | $file = $this->publicPath . $file; 100 | if (!file_exists($file)) 101 | { 102 | throw new FileNotExistException("File '{$file}' does not exist"); 103 | } 104 | 105 | $this->files[] = $file; 106 | } 107 | } 108 | 109 | public function tags(string $baseUrl, array $attributes): string 110 | { 111 | $html = ''; 112 | foreach($this->files as $file) 113 | { 114 | $file = $baseUrl . str_replace($this->publicPath, '', $file); 115 | $html .= $this->tag($file, $attributes); 116 | } 117 | 118 | return $html; 119 | } 120 | 121 | public function count(): int 122 | { 123 | return count($this->files); 124 | } 125 | 126 | /** 127 | * @throws FileNotExistException 128 | */ 129 | protected function appendFiles(): void 130 | { 131 | foreach ($this->files as $file) { 132 | $contents = $this->getFileContents($file); 133 | $this->appended .= $contents . "\n"; 134 | } 135 | } 136 | 137 | /** 138 | * @throws FileNotExistException 139 | */ 140 | protected function getFileContents(string $file): string 141 | { 142 | if ($this->checkExternalFile($file)) 143 | { 144 | if (str_starts_with($file, '//')) $file = 'http:' . $file; 145 | 146 | $headers = $this->headers; 147 | foreach ($headers as $key => $value) 148 | { 149 | $headers[$key] = $key . ': ' . $value; 150 | } 151 | $context = stream_context_create(['http' => [ 152 | 'ignore_errors' => true, 153 | 'header' => implode("\r\n", $headers), 154 | ]]); 155 | 156 | $http_response_header = ['']; 157 | $contents = file_get_contents($file, false, $context); 158 | 159 | if (!str_contains($http_response_header[0], '200')) 160 | { 161 | throw new FileNotExistException("File '{$file}' does not exist"); 162 | } 163 | } 164 | else 165 | { 166 | $contents = file_get_contents($file); 167 | } 168 | 169 | return $contents; 170 | } 171 | 172 | protected function getPublicFileDirectory(string $file): string 173 | { 174 | $publicPath = function_exists('public_path') ? public_path() : ''; 175 | $fileWithoutPublicPath = str_replace($publicPath, '', $file); 176 | 177 | return str_replace(basename($fileWithoutPublicPath), '', $fileWithoutPublicPath); 178 | } 179 | 180 | protected function checkExistingFiles(): bool 181 | { 182 | $this->buildMinifiedFilename(); 183 | 184 | return file_exists($this->outputDir . $this->filename); 185 | } 186 | 187 | /** 188 | * @throws DirNotWritableException 189 | * @throws DirNotExistException 190 | */ 191 | protected function checkDirectory(): void 192 | { 193 | if (!file_exists($this->outputDir)) 194 | { 195 | // Try to create the directory 196 | if (!$this->file->makeDirectory($this->outputDir, 0775, true)) { 197 | throw new DirNotExistException("Buildpath '{$this->outputDir}' does not exist"); 198 | } 199 | } 200 | 201 | if (!is_writable($this->outputDir)) 202 | { 203 | throw new DirNotWritableException("Buildpath '{$this->outputDir}' is not writable"); 204 | } 205 | } 206 | 207 | protected function checkExternalFile(string $file): bool 208 | { 209 | return preg_match('/^(https?:)?\/\//', $file); 210 | } 211 | 212 | protected function buildMinifiedFilename(): void 213 | { 214 | $this->filename = $this->getHashedFilename() . (($this->disable_mtime) ? '' : $this->countModificationTime()) . static::EXTENSION; 215 | } 216 | 217 | /** 218 | * Build an HTML attribute string from an array. 219 | */ 220 | protected function attributes(array $attributes): string 221 | { 222 | $html = []; 223 | foreach ($attributes as $key => $value) 224 | { 225 | $element = $this->attributeElement($key, $value); 226 | 227 | if ( ! is_null($element)) $html[] = $element; 228 | } 229 | 230 | $output = count($html) > 0 ? ' '.implode(' ', $html) : ''; 231 | 232 | return trim($output); 233 | } 234 | 235 | /** 236 | * Build a single attribute element. 237 | */ 238 | protected function attributeElement(mixed $key, mixed $value): mixed 239 | { 240 | if (is_numeric($key)) $key = $value; 241 | 242 | if(is_bool($value)) 243 | return $key; 244 | 245 | if ( ! is_null($value)) 246 | return $key.'="'.htmlentities($value, ENT_QUOTES, 'UTF-8', false).'"'; 247 | 248 | return null; 249 | } 250 | 251 | protected function getHashedFilename(): string 252 | { 253 | $publicPath = $this->publicPath; 254 | return md5(implode('-', array_map(function($file) use ($publicPath) { return str_replace($publicPath, '', $file); }, $this->files)) . $this->hash_salt); 255 | } 256 | 257 | protected function countModificationTime(): int 258 | { 259 | $time = 0; 260 | 261 | foreach ($this->files as $file) 262 | { 263 | if ($this->checkExternalFile($file)) 264 | { 265 | $userAgent = $this->headers['User-Agent'] ?? ''; 266 | $time += hexdec(substr(md5($file . $userAgent), 0, 8)); 267 | } 268 | else { 269 | $time += filemtime($file); 270 | } 271 | } 272 | 273 | return $time; 274 | } 275 | 276 | /** 277 | * @throws CannotRemoveFileException 278 | */ 279 | protected function removeOldFiles(): void 280 | { 281 | $pattern = $this->outputDir . $this->getHashedFilename() . '*'; 282 | $find = glob($pattern); 283 | 284 | if( is_array($find) && count($find) ) 285 | { 286 | foreach ($find as $file) 287 | { 288 | if ( ! unlink($file) ) { 289 | throw new CannotRemoveFileException("File '{$file}' cannot be removed"); 290 | } 291 | } 292 | } 293 | } 294 | 295 | /** 296 | * @throws CannotSaveFileException 297 | */ 298 | protected function put(mixed $minified): string 299 | { 300 | if(file_put_contents($this->outputDir . $this->filename, $minified) === false) 301 | { 302 | throw new CannotSaveFileException("File '{$this->outputDir}{$this->filename}' cannot be saved"); 303 | } 304 | 305 | return $this->filename; 306 | } 307 | 308 | public function getAppended(): string 309 | { 310 | return $this->appended; 311 | } 312 | 313 | public function getFilename(): string 314 | { 315 | return $this->filename; 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/Providers/JavaScript.php: -------------------------------------------------------------------------------- 1 | appended); 20 | 21 | return $this->put($minified); 22 | } 23 | 24 | public function tag(string $file, array $attributes = []): string 25 | { 26 | $attributes = ['src' => $file] + $attributes; 27 | 28 | return "" . PHP_EOL; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Providers/StyleSheet.php: -------------------------------------------------------------------------------- 1 | appended); 17 | 18 | return $this->put($minified->getMinified()); 19 | } 20 | 21 | public function tag(string $file, array $attributes = []): string 22 | { 23 | $attributes = ['href' => $file, 'rel' => 'stylesheet'] + $attributes; 24 | 25 | return "attributes($attributes)}>" . PHP_EOL; 26 | } 27 | 28 | /** 29 | * Override appendFiles to solve CSS URL path issue. 30 | * 31 | * @throws FileNotExistException 32 | */ 33 | protected function appendFiles(): void 34 | { 35 | foreach ($this->files as $file) { 36 | $fileContent = $this->getFileContentWithCorrectedUrls($file); 37 | $this->appended .= $fileContent . "\n"; 38 | } 39 | } 40 | 41 | /** 42 | * CSS URL path correction. 43 | * 44 | * @throws FileNotExistException 45 | */ 46 | public function getFileContentWithCorrectedUrls(string $file): string 47 | { 48 | $fileDirectory = $this->getPublicFileDirectory($file); 49 | $fileContent = $this->getFileContents($file); 50 | 51 | return $this->contentUrlCorrection($fileDirectory, $fileContent); 52 | } 53 | 54 | /** 55 | * CSS content URL path correction. 56 | */ 57 | private function contentUrlCorrection(string $fileDirectory, string $fileContent): string 58 | { 59 | $contentReplace = []; 60 | $contentReplaceWith = []; 61 | preg_match_all('/url\((\s)?([\"|\'])?(.*?)([\"|\'])?(\s)?\)/i', $fileContent, $matches, PREG_PATTERN_ORDER); 62 | if (!count($matches)) { 63 | return $fileContent; 64 | } 65 | foreach ($matches[0] as $match) { 66 | $contentReplace[] = $match; 67 | if (strpos($match, "'")) { 68 | $contentReplaceWith[] = str_replace('url(\'', 'url(\''.$fileDirectory, $match); 69 | } elseif (str_contains($match, '"')) { 70 | $contentReplaceWith[] = str_replace('url("', 'url("'.$fileDirectory, $match); 71 | } else { 72 | $contentReplaceWith[] = str_replace('url(', 'url('.$fileDirectory, $match); 73 | } 74 | } 75 | 76 | return str_replace($contentReplace, $contentReplaceWith, $fileContent); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/config/minify.php: -------------------------------------------------------------------------------- 1 | true, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | App environments to not minify 20 | |-------------------------------------------------------------------------- 21 | | 22 | | These environments will not be minified and all individual files are 23 | | returned 24 | | 25 | */ 26 | 27 | 'ignore_environments' => [ 28 | 'local', 29 | ], 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | CSS build path 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Minify is an extension that can minify your css files into one build file. 37 | | The css_builds_path property is the location where the built files are 38 | | stored. This is relative to your public path. Notice the trailing slash. 39 | | Note that this directory must be writeable. 40 | | 41 | */ 42 | 43 | 'css_build_path' => '/css/builds/', 44 | 'css_url_path' => '/css/builds/', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | JS build path 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Minify is an extension that can minify your js files into one build file. 52 | | The js_build_path property is the location where the built files are 53 | | stored. This is relative to your public path. Notice the trailing slash. 54 | | Note that this directory must be writeable. 55 | | 56 | */ 57 | 58 | 'js_build_path' => '/js/builds/', 59 | 'js_url_path' => '/js/builds/', 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Hash modification 64 | |-------------------------------------------------------------------------- 65 | | 66 | | You can disable usage of modification time (mtime) for hash build and 67 | | add additional salt (exp. commit hash) for hash build 68 | | 69 | */ 70 | 71 | 'disable_mtime' => false, 72 | 'hash_salt' => '', 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | Base URL 77 | |-------------------------------------------------------------------------- 78 | | 79 | | You can set the base URL for the links generated with the configuration 80 | | value. By default, if empty HTTP_HOST would be used. 81 | | 82 | */ 83 | 'base_url' => '' 84 | ]; 85 | --------------------------------------------------------------------------------