├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── UPGRADING.md ├── composer-unused.php ├── composer.json ├── deptrac.yaml ├── infection.json.dist ├── manifests ├── Bootstrap.json ├── ChartJS.json ├── DataTables.json ├── Dropzone.json ├── Featherlight.json ├── FontAwesome.json ├── SBAdmin2.json ├── SortableJS.json ├── TinyMCE.json └── jQuery.json ├── rector.php ├── roave-bc-check.yaml └── src ├── Asset.php ├── Bundle.php ├── Config ├── Assets.php └── Filters.php ├── Exceptions └── AssetsException.php ├── Filters └── AssetsFilter.php ├── Language └── en │ └── Assets.php ├── RouteBundle.php └── Test ├── AssetsTestTrait.php └── BundlesTestCase.php /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | files() 9 | ->in([ 10 | __DIR__ . '/src/', 11 | __DIR__ . '/tests/', 12 | ]) 13 | ->exclude('build') 14 | ->append([__FILE__]); 15 | 16 | $overrides = [ 17 | 'php_unit_data_provider_return_type' => false, 18 | 'php_unit_data_provider_name' => false, 19 | 'php_unit_data_provider_static' => false, 20 | ]; 21 | 22 | $options = [ 23 | 'finder' => $finder, 24 | 'cacheFile' => 'build/.php-cs-fixer.cache', 25 | ]; 26 | 27 | return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v3.0.0](https://github.com/tattersoftware/codeigniter4-assets/tree/v3.0.0) (2021-06-23) 4 | 5 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.3.0...v3.0.0) 6 | 7 | **Closed issues:** 8 | 9 | - Discussion: Route integration [\#16](https://github.com/tattersoftware/codeigniter4-assets/issues/16) 10 | - Feature: Asset Groups [\#15](https://github.com/tattersoftware/codeigniter4-assets/issues/15) 11 | - Feature: After filter [\#10](https://github.com/tattersoftware/codeigniter4-assets/issues/10) 12 | 13 | **Merged pull requests:** 14 | 15 | - Refactor [\#31](https://github.com/tattersoftware/codeigniter4-assets/pull/31) ([MGatner](https://github.com/MGatner)) 16 | 17 | ## [v2.3.0](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.3.0) (2021-06-14) 18 | 19 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.2.2...v2.3.0) 20 | 21 | **Closed issues:** 22 | 23 | - Discussion: Patches [\#17](https://github.com/tattersoftware/codeigniter4-assets/issues/17) 24 | - Feature: Manifest URLs [\#14](https://github.com/tattersoftware/codeigniter4-assets/issues/14) 25 | 26 | **Merged pull requests:** 27 | 28 | - Deprecate [\#27](https://github.com/tattersoftware/codeigniter4-assets/pull/27) ([MGatner](https://github.com/MGatner)) 29 | 30 | ## [v2.2.2](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.2.2) (2021-01-26) 31 | 32 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.2.1...v2.2.2) 33 | 34 | **Merged pull requests:** 35 | 36 | - PHP 8 + Toolkit [\#21](https://github.com/tattersoftware/codeigniter4-assets/pull/21) ([MGatner](https://github.com/MGatner)) 37 | 38 | ## [v2.2.1](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.2.1) (2020-09-29) 39 | 40 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.2...v2.2.1) 41 | 42 | **Implemented enhancements:** 43 | 44 | - disable dynamic parameter .js?v=1599757859 [\#18](https://github.com/tattersoftware/codeigniter4-assets/issues/18) 45 | 46 | **Merged pull requests:** 47 | 48 | - Development Tools [\#20](https://github.com/tattersoftware/codeigniter4-assets/pull/20) ([MGatner](https://github.com/MGatner)) 49 | - Config useTimestamps [\#19](https://github.com/tattersoftware/codeigniter4-assets/pull/19) ([MGatner](https://github.com/MGatner)) 50 | 51 | ## [v2.2](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.2) (2020-07-11) 52 | 53 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.1.3...v2.2) 54 | 55 | **Merged pull requests:** 56 | 57 | - Test GitHub Action [\#13](https://github.com/tattersoftware/codeigniter4-assets/pull/13) ([MGatner](https://github.com/MGatner)) 58 | - Tests refactor [\#12](https://github.com/tattersoftware/codeigniter4-assets/pull/12) ([MGatner](https://github.com/MGatner)) 59 | 60 | ## [v2.1.3](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.1.3) (2020-04-19) 61 | 62 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.1.2...v2.1.3) 63 | 64 | **Closed issues:** 65 | 66 | - Set the order for assets [\#11](https://github.com/tattersoftware/codeigniter4-assets/issues/11) 67 | - loading single js per page [\#9](https://github.com/tattersoftware/codeigniter4-assets/issues/9) 68 | - config Assets.php for jquery e other [\#8](https://github.com/tattersoftware/codeigniter4-assets/issues/8) 69 | - Configure routes dynamically [\#7](https://github.com/tattersoftware/codeigniter4-assets/issues/7) 70 | - {locale} support [\#5](https://github.com/tattersoftware/codeigniter4-assets/issues/5) 71 | - vendor js loaded after route assets [\#4](https://github.com/tattersoftware/codeigniter4-assets/issues/4) 72 | 73 | **Merged pull requests:** 74 | 75 | - URI Locale support [\#6](https://github.com/tattersoftware/codeigniter4-assets/pull/6) ([rodolfodn](https://github.com/rodolfodn)) 76 | 77 | ## [v2.1.2](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.1.2) (2019-11-16) 78 | 79 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.1.1...v2.1.2) 80 | 81 | ## [v2.1.1](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.1.1) (2019-10-31) 82 | 83 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.1...v2.1.1) 84 | 85 | **Closed issues:** 86 | 87 | - Bug: Duplicate manifest publishing [\#3](https://github.com/tattersoftware/codeigniter4-assets/issues/3) 88 | - Feature: Support for JavaScript files in head [\#2](https://github.com/tattersoftware/codeigniter4-assets/issues/2) 89 | - Bug: Namespaced routes not matching [\#1](https://github.com/tattersoftware/codeigniter4-assets/issues/1) 90 | 91 | ## [v2.1](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.1) (2019-10-07) 92 | 93 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.0.2...v2.1) 94 | 95 | ## [v2.0.2](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.0.2) (2019-07-02) 96 | 97 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.0.1...v2.0.2) 98 | 99 | ## [v2.0.1](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.0.1) (2019-04-03) 100 | 101 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v2.0.0...v2.0.1) 102 | 103 | ## [v2.0.0](https://github.com/tattersoftware/codeigniter4-assets/tree/v2.0.0) (2019-03-25) 104 | 105 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v1.1.0...v2.0.0) 106 | 107 | ## [v1.1.0](https://github.com/tattersoftware/codeigniter4-assets/tree/v1.1.0) (2019-03-19) 108 | 109 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/v1.0.0...v1.1.0) 110 | 111 | ## [v1.0.0](https://github.com/tattersoftware/codeigniter4-assets/tree/v1.0.0) (2019-03-14) 112 | 113 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-assets/compare/7872bbe4077ad841b21c95d42c010cca6c9de3fa...v1.0.0) 114 | 115 | 116 | 117 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Tatter Software 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tatter\Assets 2 | 3 | Asset handling for CodeIgniter 4 4 | 5 | [![](https://github.com/tattersoftware/codeigniter4-assets/workflows/PHPUnit/badge.svg)](https://github.com/tattersoftware/codeigniter4-assets/actions/workflows/test.yml) 6 | [![](https://github.com/tattersoftware/codeigniter4-assets/workflows/PHPStan/badge.svg)](https://github.com/tattersoftware/codeigniter4-assets/actions/workflows/analyze.yml) 7 | [![](https://github.com/tattersoftware/codeigniter4-assets/workflows/Deptrac/badge.svg)](https://github.com/tattersoftware/codeigniter4-assets/actions/workflows/inspect.yml) 8 | [![Coverage Status](https://coveralls.io/repos/github/tattersoftware/codeigniter4-assets/badge.svg?branch=develop)](https://coveralls.io/github/tattersoftware/codeigniter4-assets?branch=develop) 9 | 10 | ## Quick Start 11 | 12 | 1. Install with Composer: `> composer require tatter/assets` 13 | 2. Enable the `assets` filter in **app/Config/Filters.php** 14 | 3. Assign `$routes` to their assets in **app/Config/Assets.php** 15 | 16 | ## Features 17 | 18 | Provides automated asset loading for CSS and JavaScript files for CodeIgniter 4. 19 | 20 | ## Installation 21 | 22 | Install easily via Composer to take advantage of CodeIgniter 4's autoloading capabilities 23 | and always be up-to-date: 24 | * `> composer require tatter/assets` 25 | 26 | Or, install manually by downloading the source files and adding the directory to 27 | `app/Config/Autoload.php`. 28 | 29 | ## Configuration 30 | 31 | The library's default behavior can be overridden or augmented by its config file. Copy 32 | **examples/Assets.php** to **app/Config/Assets.php** and follow the instructions in the 33 | comments. If no config file is found the library will use its default. 34 | 35 | In order to use the `AssetsFilter` you must add apply it to your target routes. The filter 36 | does its own route matching so it is safe to apply it globally in **app/Config/Filters.php**. 37 | See [Controller Filters](https://codeigniter.com/user_guide/incoming/filters.html) for more 38 | info, or the **Example** section below. 39 | 40 | ## Usage 41 | 42 | If installed correctly CodeIgniter 4 will detect and autoload the library, config, and filter. 43 | 44 | ### Asset 45 | 46 | You may use the `Asset` class to build a tag for a single asset file: 47 | ```php 48 | '); 53 | echo view('main', ['asset' => $asset]); 54 | ``` 55 | ... then in your view file: 56 | 57 | ```php 58 | 59 | 60 | Hello World 61 | 62 | 63 | 64 | ... 65 | ``` 66 | 67 | The `Asset` class also comes with some named constructors to help you create the tag strings: 68 | * `createFromPath(string $path)` - Returns an `Asset` from a file relative to your config's `$directory`. 69 | * `createFromUri(string $uri, string $type = null)` - Returns an `Asset` from a remote URL, with an optional type (`css`, `js`, `img`; `null` to detect). 70 | 71 | Named constructors make the above example much easier: 72 | ```php 73 | 74 | 75 | Hello World 76 | 77 | 78 | 79 | ... 80 | ``` 81 | 82 | ### Bundle 83 | 84 | Typically a project will need more than one single asset. The `Bundle` class allows you to collect 85 | multiple `Asset`s into a single instance. Use the `head()` and `body()` methods to return the `Asset`s 86 | destined for each tag, formatted as blocks of tags. 87 | 88 | `Bundle`s can be created one of two ways. 89 | 90 | #### Class Properties 91 | 92 | Create your own `Bundle` class and use these properties to stage the assets you want it to have: 93 | * `$bundles`: Names of other `Bundle` classes to merge with. 94 | * `$paths`: Relative file paths to make into `Asset`s. 95 | * `$uris`: URLs to make into `Asset`s. 96 | * `$strings`: Direct strings to pass into an `Asset`. 97 | 98 | Example: 99 | ```php 100 | add(Asset::createFromPath('styles.css')) // Add individual Assets 139 | ->merge($someOtherBundle); // Or combine multiple Bundles 140 | 141 | // Create more complex Assets 142 | $source = ''; 143 | $inHead = true; // Force a JavaScript Asset to the tag 144 | $asset = new Asset($source, $inHead); 145 | } 146 | } 147 | ``` 148 | 149 | ### Filter 150 | 151 | If you configured the `AssetsFilter` (see above) to load for your routes, you must also associate 152 | the specific assets or bundles per route. Use the config ``$routes`` property, where the route 153 | pattern is the key and the values are arrays of file paths, URLs, or bundle class names. E.g.: 154 | 155 | ```php 156 | [ 164 | 'bootstrap/bootstrap.min.css', 165 | 'bootstrap/bootstrap.bundle.min.js', 166 | 'font-awesome/css/all.min.css', 167 | 'styles/main.css', 168 | ], 169 | 'files' => [ 170 | 'dropzone/dropzone.min.css', 171 | 'dropzone/dropzone.min.js', 172 | ], 173 | ]; 174 | } 175 | ``` 176 | 177 | If you apply the filter via your Routes config file you may also supply bundle class names 178 | as arguments to merge them with any other configured route bundles: 179 | ```php 180 | // **app/Config/Routes.php** 181 | $routes->add('files', 'Files::index', ['filter' => 'assets:\App\Filters\FilesFilter']); 182 | ``` 183 | 184 | ## Example 185 | 186 | You want to make a simple web app for browsing and uploading files, based on Bootstrap's 187 | frontend. Start your CodeIgniter 4 project, then add Bootstrap and DropzoneJS to handle 188 | the uploads: 189 | 190 | composer require twbs/bootstrap enyo/dropzone 191 | 192 | > Note: You will need to copy files from **vendor** to **public/assets/** to make them 193 | accessible, or use the framework's `Publisher` class to handle this for you. 194 | 195 | Add this module as well: 196 | 197 | composer require tatter/assets 198 | 199 | Edit your **Filters.php** config file to enable the `AssetsFilter` on all routes: 200 | 201 | ```php 202 | /** 203 | * List of filter aliases that are always 204 | * applied before and after every request. 205 | * 206 | * @var array 207 | */ 208 | public $globals = [ 209 | 'before' => [ 210 | // 'honeypot', 211 | // 'csrf', 212 | ], 213 | 'after' => [ 214 | 'assets' => ['except' => 'api/*'], 215 | ], 216 | ]; 217 | ``` 218 | 219 | Create a new `Bundle` to define your Bootstrap files in **app/Bundles/DropzoneJS.php**: 220 | 221 | ```php 222 | [ 241 | 'bootstrap/dist/css/bootstrap.min.css', 242 | 'bootstrap/dist/js/bootstrap.bundle.min.js', 243 | ], 244 | 'files/*' => [ 245 | \App\Bundles\DropzoneJS::class, 246 | ], 247 | 'upload' => [ 248 | \App\Bundles\DropzoneJS::class, 249 | ], 250 | ]; 251 | ``` 252 | 253 | > Note: We could have made a `Bundle` for Bootstrap as well but since they are only needed for one route this is just as easy. 254 | 255 | If you finished all that then your assets should be injected into your `` and `` tags accordingly. 256 | 257 | Your view file: 258 | ```html 259 | 260 | 261 | File Upload 262 | 263 | 264 |

Hello

265 |

Put your upload form here.

266 | 267 | 268 | ``` 269 | 270 | ... served as: 271 | ```html 272 | 273 | 274 | File Upload 275 | 276 | 277 | 278 | 279 | 280 |

Hello

281 |

Put your upload form here.

282 | 283 | 284 | 285 | 286 | 287 | ``` 288 | 289 | ## Vendor Classes 290 | 291 | This library includes two abstract class stubs to ease working with third-party assets. 292 | `VendorPublisher` is a wrapper for the framework's [Publisher Library](https://codeigniter.com/user_guide/libraries/publisher.html) 293 | primed for use with `Assets`, and `VendorBundle` is a specialized version of this library's 294 | `Bundle` primed to handle assets published via `VendorPublisher`. Together these two classes 295 | can take a lot of the work out of managing assets you include from external sources. 296 | 297 | Let's revisit the example above... Instead of copies the files into **public/assets/** ourselves 298 | (and re-copying every time there is an update) let's create a `VendorPublisher` to do that 299 | for us. In **app/Publishers/BootstrapPublisher.php**: 300 | ```php 301 | Note: Since these are external dependencies be sure to exclude them from your repo with your **.gitignore** file. 320 | 321 | Now lets use these assets. We can create a new `VendorBundle` and use the new `addPath()` 322 | method to access the same files we just published from Composer's vendor directory. 323 | In **app/Bundles/BootstrapBundle.php**: 324 | ```php 325 | addPath('bootstrap/bootstrap.min.css') 337 | ->addPath('bootstrap/bootstrap.bundle.min.js'); 338 | } 339 | } 340 | ``` 341 | 342 | Now add the new bundle to our **app/Config/Assets.php** routes: 343 | ```php 344 | public $routes = [ 345 | '*' => [\App\Bundles\BootstrapBundle::class], 346 | ]; 347 | ``` 348 | 349 | And we have hands-free Bootstrap updates from now on! 350 | 351 | ## Testing 352 | 353 | This library includes some PHPUnit extension classes in **src/Test/** to assist with testing 354 | Assets and Bundles. These are used to test the files from this library but are also available 355 | for your own libraries and projects to use. Simply extend the appropriate test case and add 356 | a data provider method with your class name and criteria to meet. See the test files in 357 | **tests/** for examples. 358 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The development team and community take all security issues seriously. **Please do not make public any uncovered flaws.** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Thank you for improving the security of our code! Any assistance in removing security flaws will be acknowledged. 8 | 9 | **Please report security flaws by emailing the development team directly: support@tattersoftware.com**. 10 | 11 | The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating 12 | the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the 13 | progress towards a fix and full announcement, and may ask for additional information or guidance. 14 | 15 | ## Disclosure Policy 16 | 17 | When the security team receives a security bug report, they will assign it to a primary handler. 18 | This person will coordinate the fix and release process, involving the following steps: 19 | 20 | - Confirm the problem and determine the affected versions. 21 | - Audit code to find any potential similar problems. 22 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. 23 | 24 | ## Comments on this Policy 25 | 26 | If you have suggestions on how this process could be improved please submit a Pull Request. 27 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Version 2 to 3 4 | *** 5 | 6 | > Note: This is a complete refactor! Please be sure to read the docs carefully before upgrading. 7 | 8 | * Minimum PHP version has been bumped to `7.4` to match the upcoming framework changes 9 | * All properties that can be typed have been 10 | * The services no longer exist; remove all references to `Services::Assets` and `Services::manifests` to avoid exceptions 11 | * This library no longer publishes Assets; convert Manifests to the framework's new [Publisher format](https://codeigniter.com/user_guide/libraries/publisher.html) 12 | * Many of the example Manifests now have an official Publisher equivalent at [Tatter\Frontend](https://github.com/tattersoftware/codeigniter4-frontend) 13 | * The `DirectoryHandler` (mapping public directories to routes) has no equivalent in `v3` so be sure to create explicit bundles and routes for any you were using 14 | * The view files have been removed and replaced by `AssetsFilter` to handle tag injection directly; read the docs on setting up the filter 15 | 16 | For an example of converting a `v2` JSON manifest to a `v3` framework Publisher compare these files: 17 | 18 | * https://github.com/tattersoftware/codeigniter4-assets/blob/267220d437786e0ddb9d7681745f5942d95c543b/manifests/FontAwesome.json 19 | * https://github.com/tattersoftware/codeigniter4-frontend/blob/ae61773f279333c3a606498364977a8cec45d303/src/Publishers/FontAwesomePublisher.php 20 | -------------------------------------------------------------------------------- /composer-unused.php: -------------------------------------------------------------------------------- 1 | addNamedFilter(NamedFilter::fromString('symfony/config')) 13 | // ->addPatternFilter(PatternFilter::fromString('/symfony-.*/')) 14 | ->setAdditionalFilesFor('codeigniter4/framework', [ 15 | ...Glob::glob(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers/*.php'), 16 | ]); 17 | }; 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tatter/assets", 3 | "type": "library", 4 | "description": "Asset publishing and loading for CodeIgniter 4", 5 | "keywords": [ 6 | "codeigniter", 7 | "codeigniter4", 8 | "assets", 9 | "loader", 10 | "css", 11 | "javascript" 12 | ], 13 | "homepage": "https://github.com/tattersoftware/codeigniter4-assets", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Matthew Gatner", 18 | "email": "mgatner@tattersoftware.com", 19 | "homepage": "https://tattersoftware.com", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^7.4 || ^8.0" 25 | }, 26 | "require-dev": { 27 | "codeigniter4/framework": "^4.1", 28 | "tatter/tools": "^2.0" 29 | }, 30 | "config": { 31 | "allow-plugins": { 32 | "phpstan/extension-installer": true 33 | } 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Tatter\\Assets\\": "src" 38 | }, 39 | "exclude-from-classmap": [ 40 | "**/Database/Migrations/**" 41 | ] 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Tests\\Support\\": "tests/_support" 46 | } 47 | }, 48 | "minimum-stability": "dev", 49 | "prefer-stable": true, 50 | "scripts": { 51 | "analyze": "phpstan analyze", 52 | "ci": [ 53 | "Composer\\Config::disableProcessTimeout", 54 | "@deduplicate", 55 | "@analyze", 56 | "@test", 57 | "@inspect", 58 | "rector process", 59 | "@style" 60 | ], 61 | "deduplicate": "phpcpd app/ src/", 62 | "inspect": "deptrac analyze --cache-file=build/deptrac.cache", 63 | "mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit", 64 | "retool": "retool", 65 | "style": "php-cs-fixer fix --verbose --ansi --using-cache=no", 66 | "test": "phpunit" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /deptrac.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - ./src/ 4 | - ./vendor/codeigniter4/framework/system/ 5 | exclude_files: 6 | - '#.*test.*#i' 7 | layers: 8 | - name: Model 9 | collectors: 10 | - type: bool 11 | must: 12 | - type: className 13 | regex: .*[A-Za-z]+Model$ 14 | must_not: 15 | - type: directory 16 | regex: vendor/.* 17 | - name: Vendor Model 18 | collectors: 19 | - type: bool 20 | must: 21 | - type: className 22 | regex: .*[A-Za-z]+Model$ 23 | - type: directory 24 | regex: vendor/.* 25 | - name: Controller 26 | collectors: 27 | - type: bool 28 | must: 29 | - type: className 30 | regex: .*\/Controllers\/.* 31 | must_not: 32 | - type: directory 33 | regex: vendor/.* 34 | - name: Vendor Controller 35 | collectors: 36 | - type: bool 37 | must: 38 | - type: className 39 | regex: .*\/Controllers\/.* 40 | - type: directory 41 | regex: vendor/.* 42 | - name: Config 43 | collectors: 44 | - type: bool 45 | must: 46 | - type: directory 47 | regex: src/Config/.* 48 | must_not: 49 | - type: className 50 | regex: .*Services 51 | - type: directory 52 | regex: vendor/.* 53 | - name: Vendor Config 54 | collectors: 55 | - type: bool 56 | must: 57 | - type: directory 58 | regex: vendor/.*/Config/.* 59 | must_not: 60 | - type: className 61 | regex: .*Services 62 | - name: Entity 63 | collectors: 64 | - type: bool 65 | must: 66 | - type: directory 67 | regex: src/Entities/.* 68 | must_not: 69 | - type: directory 70 | regex: vendor/.* 71 | - name: Vendor Entity 72 | collectors: 73 | - type: bool 74 | must: 75 | - type: directory 76 | regex: vendor/.*/Entities/.* 77 | - name: View 78 | collectors: 79 | - type: bool 80 | must: 81 | - type: directory 82 | regex: src/Views/.* 83 | must_not: 84 | - type: directory 85 | regex: vendor/.* 86 | - name: Vendor View 87 | collectors: 88 | - type: bool 89 | must: 90 | - type: directory 91 | regex: vendor/.*/Views/.* 92 | - name: Service 93 | collectors: 94 | - type: className 95 | regex: .*Services.* 96 | ruleset: 97 | Entity: 98 | - Config 99 | - Model 100 | - Service 101 | - Vendor Config 102 | - Vendor Entity 103 | - Vendor Model 104 | Config: 105 | - Service 106 | - Vendor Config 107 | Model: 108 | - Config 109 | - Entity 110 | - Service 111 | - Vendor Config 112 | - Vendor Entity 113 | - Vendor Model 114 | Service: 115 | - Config 116 | - Vendor Config 117 | 118 | # Ignore anything in the Vendor layers 119 | Vendor Model: 120 | - Config 121 | - Service 122 | - Vendor Config 123 | - Vendor Controller 124 | - Vendor Entity 125 | - Vendor Model 126 | - Vendor View 127 | Vendor Controller: 128 | - Service 129 | - Vendor Config 130 | - Vendor Controller 131 | - Vendor Entity 132 | - Vendor Model 133 | - Vendor View 134 | Vendor Config: 135 | - Config 136 | - Service 137 | - Vendor Config 138 | - Vendor Controller 139 | - Vendor Entity 140 | - Vendor Model 141 | - Vendor View 142 | Vendor Entity: 143 | - Service 144 | - Vendor Config 145 | - Vendor Controller 146 | - Vendor Entity 147 | - Vendor Model 148 | - Vendor View 149 | Vendor View: 150 | - Service 151 | - Vendor Config 152 | - Vendor Controller 153 | - Vendor Entity 154 | - Vendor Model 155 | - Vendor View 156 | skip_violations: 157 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src/" 5 | ], 6 | "excludes": [ 7 | "Config", 8 | "Database/Migrations", 9 | "Views" 10 | ] 11 | }, 12 | "logs": { 13 | "text": "build/infection.log" 14 | }, 15 | "mutators": { 16 | "@default": true 17 | }, 18 | "bootstrap": "vendor/codeigniter4/framework/system/Test/bootstrap.php" 19 | } 20 | -------------------------------------------------------------------------------- /manifests/Bootstrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "twbs/bootstrap/dist", 3 | "destination": "vendor/bootstrap", 4 | "resources": [ 5 | { 6 | "source": "css" 7 | }, 8 | { 9 | "source": "js" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /manifests/ChartJS.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "nnnick/chartjs/dist", 3 | "destination": "vendor/chartjs", 4 | "resources": [ 5 | { 6 | "source": ".", 7 | "recursive": true 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /manifests/DataTables.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "peekleon/datatables-all", 3 | "destination": "vendor/datatables", 4 | "resources": [ 5 | { 6 | "source": "media", 7 | "recursive": true 8 | }, 9 | { 10 | "source": "extensions/Buttons/css", 11 | "destination": "css", 12 | "filter": "#\\.css$#" 13 | }, 14 | { 15 | "source": "extensions/Buttons/js", 16 | "destination": "js", 17 | "filter": "#\\.js$#" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /manifests/Dropzone.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "enyo/dropzone/dist", 3 | "destination": "vendor/dropzone", 4 | "resources": [ 5 | { 6 | "source": ".", 7 | "recursive": true, 8 | "flatten": true 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /manifests/Featherlight.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "bower-asset/featherlight/release", 3 | "destination": "vendor/featherlight", 4 | "resources": [ 5 | { 6 | "source": "." 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /manifests/FontAwesome.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "fortawesome/font-awesome", 3 | "destination": "vendor/font-awesome", 4 | "resources": [ 5 | { 6 | "source": "css", 7 | "destination": "css" 8 | }, 9 | { 10 | "source": "webfonts", 11 | "destination": "webfonts" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /manifests/SBAdmin2.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "bower-asset/sbadmin2", 3 | "destination": "vendor/sbadmin2", 4 | "resources": [ 5 | { 6 | "source": "css" 7 | }, 8 | { 9 | "source": "js", 10 | "filter": "#\\.js$#" 11 | }, 12 | { 13 | "source": "vendor/jquery-easing", 14 | "filter": "#\\.js$#" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /manifests/SortableJS.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "npm-asset/sortablejs", 3 | "destination": "vendor/sortablejs", 4 | "resources": [ 5 | { 6 | "source": ".", 7 | "filter": "#\\.js$#" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /manifests/TinyMCE.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "tinymce/tinymce", 3 | "destination": "vendor/tinymce", 4 | "resources": [ 5 | { 6 | "source": ".", 7 | "filter": "#\\.js$#" 8 | }, 9 | { 10 | "source": "plugins", 11 | "destination": "plugins", 12 | "recursive": true 13 | }, 14 | { 15 | "source": "skins", 16 | "destination": "skins", 17 | "recursive": true 18 | }, 19 | { 20 | "source": "themes", 21 | "destination": "themes", 22 | "recursive": true 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /manifests/jQuery.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "components/jquery", 3 | "destination": "vendor/jquery", 4 | "resources": [ 5 | { 6 | "source": ".", 7 | "filter": "#^jquery.+#" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | sets([ 41 | SetList::DEAD_CODE, 42 | LevelSetList::UP_TO_PHP_74, 43 | PHPUnitSetList::PHPUNIT_CODE_QUALITY, 44 | PHPUnitSetList::PHPUNIT_90, 45 | ]); 46 | 47 | $rectorConfig->parallel(); 48 | 49 | // The paths to refactor (can also be supplied with CLI arguments) 50 | $rectorConfig->paths([ 51 | __DIR__ . '/src/', 52 | __DIR__ . '/tests/', 53 | ]); 54 | 55 | // Include Composer's autoload - required for global execution, remove if running locally 56 | $rectorConfig->autoloadPaths([ 57 | __DIR__ . '/vendor/autoload.php', 58 | ]); 59 | 60 | // Do you need to include constants, class aliases, or a custom autoloader? 61 | $rectorConfig->bootstrapFiles([ 62 | realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php', 63 | ]); 64 | 65 | if (is_file(__DIR__ . '/phpstan.neon.dist')) { 66 | $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist'); 67 | } 68 | 69 | // Set the target version for refactoring 70 | $rectorConfig->phpVersion(PhpVersion::PHP_74); 71 | 72 | // Auto-import fully qualified class names 73 | $rectorConfig->importNames(); 74 | 75 | // Are there files or rules you need to skip? 76 | $rectorConfig->skip([ 77 | __DIR__ . '/src/Views', 78 | 79 | JsonThrowOnErrorRector::class, 80 | StringifyStrNeedlesRector::class, 81 | YieldDataProviderRector::class, 82 | 83 | // Note: requires php 8 84 | RemoveUnusedPromotedPropertyRector::class, 85 | AnnotationWithValueToAttributeRector::class, 86 | 87 | // May load view files directly when detecting classes 88 | StringClassNameToClassConstantRector::class, 89 | ]); 90 | 91 | // auto import fully qualified class names 92 | $rectorConfig->importNames(); 93 | 94 | $rectorConfig->rule(SimplifyUselessVariableRector::class); 95 | $rectorConfig->rule(RemoveAlwaysElseRector::class); 96 | $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); 97 | $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); 98 | $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); 99 | $rectorConfig->rule(SimplifyStrposLowerRector::class); 100 | $rectorConfig->rule(CombineIfRector::class); 101 | $rectorConfig->rule(SimplifyIfReturnBoolRector::class); 102 | $rectorConfig->rule(InlineIfToExplicitIfRector::class); 103 | $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); 104 | $rectorConfig->rule(ShortenElseIfRector::class); 105 | $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); 106 | $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); 107 | $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); 108 | $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); 109 | $rectorConfig->rule(SimplifyRegexPatternRector::class); 110 | $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); 111 | $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); 112 | $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); 113 | $rectorConfig 114 | ->ruleWithConfiguration(TypedPropertyFromAssignsRector::class, [ 115 | /** 116 | * The INLINE_PUBLIC value is default to false to avoid BC break, if you use for libraries and want to preserve BC break, you don't need to configure it, as it included in LevelSetList::UP_TO_PHP_74 117 | * Set to true for projects that allow BC break 118 | */ 119 | TypedPropertyFromAssignsRector::INLINE_PUBLIC => false, 120 | ]); 121 | $rectorConfig->rule(StringClassNameToClassConstantRector::class); 122 | $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class); 123 | $rectorConfig->rule(CompleteDynamicPropertiesRector::class); 124 | }; 125 | -------------------------------------------------------------------------------- /roave-bc-check.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#\[BC\] SKIPPED: .+ could not be found in the located source#' 4 | -------------------------------------------------------------------------------- /src/Asset.php: -------------------------------------------------------------------------------- 1 | uri = rtrim(self::$config->uri, '/\\') . '/'; 58 | self::$config->directory = rtrim(self::$config->directory, '/\\') . DIRECTORY_SEPARATOR; 59 | self::$config->vendor = rtrim(self::$config->vendor, '/\\') . DIRECTORY_SEPARATOR; 60 | } 61 | 62 | return self::$config; 63 | } 64 | 65 | /** 66 | * Changes the configuration. Should only be used during testing. 67 | * 68 | * @internal 69 | */ 70 | public static function useConfig(?AssetsConfig $config) 71 | { 72 | self::$config = $config; 73 | 74 | // If a new config was supplied then use it with Factories, otherwise reset to the "vanilla" version 75 | Factories::injectMock('config', 'Assets', $config ?? new AssetsConfig()); 76 | } 77 | 78 | // -------------------------------------------------------------------- 79 | // Named Constructors 80 | // -------------------------------------------------------------------- 81 | 82 | /** 83 | * Creates a new Asset from a local file. 84 | * 85 | * @param string $path File path relative to the configured directory 86 | */ 87 | public static function createFromPath(string $path): self 88 | { 89 | $config = self::config(); 90 | $path = ltrim($path, '/\\'); 91 | $file = new File($config->directory . $path, true); 92 | 93 | // Build the URI 94 | $uri = $config->uri . $path; 95 | 96 | // Append a timestamp if requested 97 | if ($config->useTimestamps) { 98 | $uri .= '?v=' . $file->getMTime(); 99 | } 100 | 101 | return self::createFromUri($uri); 102 | } 103 | 104 | /** 105 | * Creates a new Asset from a remote file. 106 | * Note that the framework's link_tag() does not support integrity and crossorigin 107 | * fields, so most CDN assets should be created directly. 108 | * 109 | * @param string|null $type One of: 'css', 'js', 'img'; or null to detect from extension 110 | */ 111 | public static function createFromUri(string $uri, ?string $type = null): self 112 | { 113 | helper(['html']); 114 | 115 | if ($type === null) { 116 | $extension = pathinfo(strtok($uri, '?'), PATHINFO_EXTENSION); // Query safe 117 | 118 | // Check for one of the numerous image extension 119 | $type = in_array($extension, self::IMAGE_EXTENSIONS, true) ? 'img' : $extension; 120 | } 121 | 122 | if ($type === 'css') { 123 | return new self(link_tag($uri)); 124 | } 125 | if ($type === 'js') { 126 | return new self(script_tag($uri), false); 127 | } 128 | if ($type === 'img') { 129 | $alt = ucfirst(pathinfo($uri, PATHINFO_FILENAME)); // Query safe 130 | 131 | return new self(img($uri, false, ['alt' => $alt]), false); 132 | } 133 | 134 | throw AssetsException::forUnsupportedType($type); 135 | } 136 | 137 | // -------------------------------------------------------------------- 138 | // Class Methods 139 | // -------------------------------------------------------------------- 140 | 141 | public function __construct(string $tag, bool $head = true) 142 | { 143 | $this->tag = $tag; 144 | $this->head = $head; 145 | } 146 | 147 | public function __toString(): string 148 | { 149 | return $this->tag; 150 | } 151 | 152 | public function isHead(): bool 153 | { 154 | return $this->head; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Bundle.php: -------------------------------------------------------------------------------- 1 | or . 14 | */ 15 | abstract class Bundle 16 | { 17 | // -------------------------------------------------------------------- 18 | // Initial Assets 19 | // -------------------------------------------------------------------- 20 | 21 | /** 22 | * The Assets. 23 | * 24 | * @var Asset[] 25 | */ 26 | protected array $assets = []; 27 | 28 | /** 29 | * Bundle classes to merge with this Bundle. 30 | * 31 | * @var string[] 32 | */ 33 | protected array $bundles = []; 34 | 35 | /** 36 | * Paths to include in this Bundle. 37 | * 38 | * @var string[] 39 | */ 40 | protected array $paths = []; 41 | 42 | /** 43 | * URIs to include in this Bundle. 44 | * 45 | * @var string[] 46 | */ 47 | protected array $uris = []; 48 | 49 | /** 50 | * Strings to include in this Bundle. 51 | * 52 | * @var string[] 53 | */ 54 | protected array $strings = []; 55 | 56 | // -------------------------------------------------------------------- 57 | // Asset Handling 58 | // -------------------------------------------------------------------- 59 | 60 | /** 61 | * Processes the properties into Assets and calls define(). 62 | */ 63 | final public function __construct() 64 | { 65 | // Put child Bundles first so they are more likely to be overwritten 66 | foreach ($this->bundles as $bundle) { 67 | $this->merge(new $bundle()); 68 | } 69 | 70 | // Create Assets out of the remaining preoperties 71 | foreach ($this->uris as $uri) { 72 | $this->assets[] = Asset::createFromUri($uri); 73 | } 74 | 75 | foreach ($this->paths as $uri) { 76 | $this->assets[] = Asset::createFromPath($uri); 77 | } 78 | 79 | foreach ($this->strings as $string) { 80 | $this->assets[] = new Asset($string); 81 | } 82 | 83 | $this->define(); 84 | } 85 | 86 | /** 87 | * Applies any initial inputs after the constructor. 88 | * This method is a stub to be implemented by child classes. 89 | */ 90 | protected function define(): void 91 | { 92 | } 93 | 94 | /** 95 | * Appends an Asset to the list. 96 | * 97 | * @return $this 98 | */ 99 | final public function add(Asset $asset) 100 | { 101 | $this->assets[] = $asset; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Merges Assets from another Bundle. 108 | * 109 | * @return $this 110 | */ 111 | final public function merge(Bundle $bundle) 112 | { 113 | foreach ($bundle->getAssets() as $asset) { 114 | $this->add($asset); 115 | } 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Optimizes and returns the list of Assets. 122 | * 123 | * @return Asset[] 124 | */ 125 | final public function getAssets(): array 126 | { 127 | $this->assets = array_values(array_unique($this->assets)); // array_unique works on stringables 128 | 129 | return $this->assets; 130 | } 131 | 132 | // -------------------------------------------------------------------- 133 | // Display Methods 134 | // -------------------------------------------------------------------- 135 | 136 | /** 137 | * Concatenates Assets for the tag. 138 | */ 139 | final public function head(): string 140 | { 141 | return $this->toString(true); 142 | } 143 | 144 | /** 145 | * Concatenates Assets for the tag. 146 | */ 147 | final public function body(): string 148 | { 149 | return $this->toString(false); 150 | } 151 | 152 | /** 153 | * Concatenates all Assets. Probably never very useful. 154 | */ 155 | final public function __toString(): string 156 | { 157 | return $this->toString(); 158 | } 159 | 160 | /** 161 | * Concatenates Assets for the tag. 162 | * 163 | * @param bool|null $head Whether to filter on head/body tag; null returns both 164 | */ 165 | private function toString(?bool $head = null): string 166 | { 167 | $lines = []; 168 | 169 | foreach ($this->getAssets() as $asset) { 170 | if ($head === null || $head === $asset->isHead()) { 171 | $lines[] = (string) $asset; 172 | } 173 | } 174 | 175 | return implode(PHP_EOL, $lines); 176 | } 177 | 178 | // -------------------------------------------------------------------- 179 | // Caching 180 | // -------------------------------------------------------------------- 181 | 182 | /** 183 | * Prepares the bundle for caching. 184 | * 185 | * @return Asset[] 186 | */ 187 | public function __serialize(): array 188 | { 189 | return $this->getAssets(); 190 | } 191 | 192 | /** 193 | * Restores the bundle from cached version. 194 | * 195 | * @param Asset[] $data 196 | */ 197 | public function __unserialize(array $data): void 198 | { 199 | foreach ($data as $asset) { 200 | $this->add($asset); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Config/Assets.php: -------------------------------------------------------------------------------- 1 | [ 59 | * 'https://pagecdn.io/lib/cleave/1.6.0/cleave.min.js', 60 | * \App\Bundles\Bootstrap::class, 61 | * ], 62 | * 'admin/*' => [ 63 | * \Tatter\Frontend\Bundles\AdminLTE::class, 64 | * 'admin/login.js', 65 | * ], 66 | * ]; 67 | * 68 | * @var array 69 | */ 70 | public array $routes = []; 71 | 72 | /** 73 | * Gathers Assets and Bundles that match the relative URI path. 74 | * $uri may contain a wildcard (*) which will allow any valid character. 75 | * Based on URL Helper's "url_is()". 76 | * 77 | * @return string[] 78 | */ 79 | final public function getForRoute(string $uri): array 80 | { 81 | $uri = '/' . trim($uri, '/ '); 82 | $matched = []; 83 | 84 | foreach ($this->routes as $route => $items) { 85 | // Convert to a real regex 86 | $route = '/' . trim(str_replace('*', '(\S)*', $route), '/ '); 87 | 88 | if (preg_match("|^{$route}$|", $uri)) { 89 | $matched = array_merge($matched, $items); 90 | } 91 | } 92 | 93 | return array_values(array_unique($matched)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Config/Filters.php: -------------------------------------------------------------------------------- 1 | aliases['assets'] = AssetsFilter::class; 12 | -------------------------------------------------------------------------------- /src/Exceptions/AssetsException.php: -------------------------------------------------------------------------------- 1 | []|null $arguments Additional Bundle classes 33 | */ 34 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): ?ResponseInterface 35 | { 36 | // Ignore irrelevent responses 37 | if ($response instanceof RedirectResponse || empty($response->getBody())) { 38 | return null; 39 | } 40 | 41 | // Check CLI separately for coverage 42 | if (is_cli() && ENVIRONMENT !== 'testing') { 43 | return null; // @codeCoverageIgnore 44 | } 45 | 46 | // Only run on HTML content 47 | if (strpos($response->getHeaderLine('Content-Type'), 'html') === false) { 48 | return null; 49 | } 50 | 51 | // Determine the path 52 | $uri = $request->getUri(); 53 | $path = method_exists($uri, 'getRoutePath') 54 | ? $uri->getRoutePath() 55 | : ltrim($uri->getPath()); 56 | $bundle = RouteBundle::createFromRoute($path); 57 | 58 | // Check for additional Bundles specified in the arguments 59 | if (! empty($arguments)) { 60 | foreach ($arguments as $class) { 61 | $bundle->merge(new $class()); 62 | } 63 | } 64 | 65 | $headTags = $bundle->head(); 66 | $bodyTags = $bundle->body(); 67 | 68 | // Short circuit? 69 | if ($headTags === '' && $bodyTags === '') { 70 | return null; 71 | } 72 | 73 | $body = $response->getBody(); 74 | 75 | // Add any head content right before the closing head tag 76 | if ($headTags) { 77 | $body = str_replace('', $headTags . PHP_EOL . '', $body); 78 | } 79 | // Add any body content right before the closing body tag 80 | if ($bodyTags) { 81 | $body = str_replace('', $bodyTags . PHP_EOL . '', $body); 82 | } 83 | 84 | // Use the new body and return the updated Response 85 | return $response->setBody($body); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Language/en/Assets.php: -------------------------------------------------------------------------------- 1 | 'Unsupported file type: "{0}"', 7 | 'invalidConfigItem' => 'Invalid item detected in config: {0}', 8 | ]; 9 | -------------------------------------------------------------------------------- /src/RouteBundle.php: -------------------------------------------------------------------------------- 1 | getForRoute($uri)) { 21 | return new self(); 22 | } 23 | 24 | if ($config->useCache) { 25 | // Use the hash of these items for the cache key 26 | $key = 'assets-' . md5(serialize($items)); 27 | 28 | // If there's a cached version then return it 29 | if ($bundle = cache($key)) { 30 | return $bundle; 31 | } 32 | } 33 | 34 | $bundle = new self(); 35 | 36 | foreach ($items as $item) { 37 | if (! is_string($item)) { 38 | throw new InvalidArgumentException('Config $route items must be strings.'); 39 | } 40 | 41 | // Bundle 42 | if (is_a($item, Bundle::class, true)) { 43 | $bundle->merge(new $item()); 44 | } 45 | // URI 46 | elseif (filter_var($item, FILTER_VALIDATE_URL) !== false) { 47 | $bundle->add(Asset::createFromUri($item)); 48 | } 49 | // File path 50 | elseif (is_file($config->directory . '/' . ltrim($item))) { 51 | $bundle->add(Asset::createFromPath($item)); 52 | } 53 | // Failure 54 | else { 55 | throw AssetsException::forInvalidConfigItem($item); 56 | } 57 | } 58 | 59 | // Re-check the config in case one of the included items requested cache exemption 60 | // @phpstan-ignore-next-line 61 | if ($config->useCache && isset($key)) { 62 | cache()->save($key, $bundle); 63 | } 64 | 65 | return $bundle; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Test/AssetsTestTrait.php: -------------------------------------------------------------------------------- 1 | refreshVfs) || self::$root === null) { 37 | self::$root = vfsStream::setup('root'); 38 | } 39 | 40 | // Create the config 41 | $this->assets = new AssetsConfig(); 42 | $this->assets->directory = self::$root->url() . DIRECTORY_SEPARATOR; 43 | $this->assets->useTimestamps = false; // These make testing much harder 44 | 45 | Asset::useConfig($this->assets); 46 | 47 | // Add VFS as an allowed Publisher directory 48 | config('Publisher')->restrictions[$this->assets->directory] = '*'; 49 | } 50 | 51 | /** 52 | * Resets the VFS if $refreshVfs is truthy. 53 | */ 54 | protected function tearDownAssetsTestTrait(): void 55 | { 56 | if (! empty($this->refreshVfs)) { 57 | self::$root = null; 58 | self::$published = false; 59 | } 60 | 61 | Asset::useConfig(null); 62 | } 63 | 64 | /** 65 | * Publishes all files once so they are 66 | * available for bundles. 67 | */ 68 | protected function publishAll(): void 69 | { 70 | if (self::$published) { 71 | return; 72 | } 73 | 74 | foreach (Publisher::discover() as $publisher) { 75 | $publisher->publish(); 76 | } 77 | 78 | self::$published = true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Test/BundlesTestCase.php: -------------------------------------------------------------------------------- 1 | publishAll(); 21 | } 22 | 23 | /** 24 | * @dataProvider bundleProvider 25 | * 26 | * @param class-string $class 27 | * @param string[] $expectedHeadFiles 28 | * @param string[] $expectedBodyFiles 29 | */ 30 | public function testBundlesFiles(string $class, array $expectedHeadFiles, array $expectedBodyFiles): void 31 | { 32 | $bundle = new $class(); 33 | $head = $bundle->head(); 34 | $body = $bundle->body(); 35 | 36 | foreach ($expectedHeadFiles as $file) { 37 | $this->assertStringContainsString($file, $head); 38 | } 39 | 40 | foreach ($expectedBodyFiles as $file) { 41 | $this->assertStringContainsString($file, $body); 42 | } 43 | } 44 | 45 | /** 46 | * Returns an array of items to test with each item 47 | * as a triple of [string bundleClassName, string[] headFileNames, string[] bodyFileNames] 48 | */ 49 | abstract public function bundleProvider(): array; 50 | } 51 | --------------------------------------------------------------------------------