├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── composer.json ├── config └── route-model-autobinding.php ├── phpcs.xml.dist ├── phpunit.xml ├── src ├── Autobinder.php ├── CaseTypes.php ├── Commands │ ├── CacheRouteModels.php │ └── ClearCachedRouteModels.php └── RouteModelAutobindingServiceProvider.php └── tests ├── Feature ├── AutobinderCacheTest.php ├── AutobinderCaseTest.php ├── AutobinderTest.php └── Commands │ ├── AutobinderCacheCommandTest.php │ └── AutobinderClearCacheCommandTest.php ├── MocksInstances.php ├── TestCase.php └── resources ├── MyPackage └── src │ └── Models │ ├── Sub │ ├── Package.php │ └── Scope.php │ ├── Thing.php │ └── plain.php ├── app ├── Models │ ├── BaseModel.php │ ├── NoModel.php │ └── SomethingInherited.php └── User.php ├── bootstrap └── cache │ └── .gitignore ├── cache.php ├── composer.json └── modules └── MyModule └── Models ├── Address.php └── HasSomething.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://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 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.yml] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | vendor 4 | tests/files/ 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.4 6 | - nightly 7 | 8 | cache: 9 | directories: 10 | - $HOME/.composer/cache 11 | 12 | env: 13 | matrix: 14 | - LARAVEL_VERSION="^7.0" COMPOSER_FLAGS="--prefer-lowest" 15 | - LARAVEL_VERSION="^7.0" COMPOSER_FLAGS="--prefer-stable" 16 | - LARAVEL_VERSION="^8.0" COMPOSER_FLAGS="--prefer-lowest" 17 | - LARAVEL_VERSION="^8.0" COMPOSER_FLAGS="--prefer-stable" 18 | - LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-lowest" MINIMUM_STABILITY="dev" 19 | - LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-stable" MINIMUM_STABILITY="dev" 20 | 21 | matrix: 22 | allow_failures: 23 | - php: nightly 24 | - env: LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-lowest" MINIMUM_STABILITY="dev" 25 | - env: LARAVEL_VERSION="dev-master" ORCHESTRA_VERSION="dev-master" COMPOSER_FLAGS="--prefer-stable" MINIMUM_STABILITY="dev" 26 | fast_finish: true 27 | 28 | before_install: 29 | - composer validate --strict 30 | - travis_retry composer self-update 31 | - if [[ -n ${MINIMUM_STABILITY} ]]; then composer config minimum-stability ${MINIMUM_STABILITY}; echo "Minimum stability set to ${MINIMUM_STABILITY}"; else echo "Minimum stability left unchanged"; fi 32 | - if [[ -n ${ORCHESTRA_VERSION} ]]; then composer require orchestra/testbench=${ORCHESTRA_VERSION} --dev --no-update; else echo "orchestra/testbench version requirement left unchanged"; fi 33 | - composer require laravel/framework=${LARAVEL_VERSION} --no-update 34 | 35 | install: 36 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist 37 | 38 | script: 39 | - vendor/bin/phpunit 40 | 41 | notifications: 42 | email: 43 | on_failure: change 44 | on_success: never 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `sebastiaanluca/laravel-route-model-autobinding` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | ## 5.0.0 (2020-10-19) 8 | 9 | ### Added 10 | 11 | - Added support for Laravel 8 12 | 13 | ### Removed 14 | 15 | - Dropped support for Laravel 6 16 | 17 | ## 4.0.0 (2020-04-24) 18 | 19 | ### Added 20 | 21 | - Added support for Laravel 7 22 | 23 | ### Removed 24 | 25 | - Dropped support for Laravel 5 26 | - Dropped support for PHP 7.2 27 | 28 | ## 3.0.0 (2019-09-06) 29 | 30 | ### Added 31 | 32 | - Added support for Laravel 6.0 33 | 34 | ## 2.0.1 (2019-03-01) 35 | 36 | ### Fixed 37 | 38 | - Replaced global Laravel helpers with static variants 39 | 40 | ## 2.0.0 (2019-03-01) 41 | 42 | ### Added 43 | 44 | - Added support for Laravel 5.8 45 | 46 | ### Removed 47 | 48 | - Removed support for Laravel 5.7 and lower 49 | 50 | ## 1.1.0 (2018-09-04) 51 | 52 | ### Added 53 | 54 | - Run tests against Laravel 5.7 55 | 56 | ## 1.0.1 (2018-08-06) 57 | 58 | ### Fixed 59 | 60 | - Fixed service provider auto-discovery 61 | 62 | ## 1.0.0 (2018-08-06) 63 | 64 | ### Added 65 | 66 | - Added automatic route model binding 67 | - Added cache command 68 | - Added clear cache command 69 | 70 | ### Fixed 71 | 72 | - Ignore traits 73 | - Ignore interfaces 74 | - Ignore non-Eloquent classes 75 | - Ignore abstract classes 76 | - Ignore plain PHP files 77 | - Include models that inherit Eloquent model higher up the chain 78 | - Read root namespace directories instead of Models/ 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | It's simple, really: 4 | 5 | - Everyone is welcome to contribute 6 | - Use common sense at all times 7 | - Be open to other opinions and constructive criticism 8 | - Be friendly 9 | 10 | Feel like someone's in violation of this? [Contact me directly][link-author-email]. 11 | 12 | [link-author-email]: mailto:hello@sebastiaanluca.com 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are very welcome and will be fully credited. 4 | 5 | We accept contributions via pull requests. 6 | 7 | ## Pull request guidelines 8 | 9 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Don't go crazy with the formatting. 10 | 11 | - **Add tests** - Your patch won't be accepted if it doesn't have tests. Don't worry though! Feel free to submit a PR without, we'll help you along the way. 12 | 13 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 14 | 15 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 16 | 17 | - **Create feature branches** - Don't ask us to pull from your master branch unless it only contains the PR code. 18 | 19 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 20 | 21 | - **Send coherent history** - Make sure each commit in your pull request is somewhat meaningful and contains related changes. Don't go overboard by changing a dozen files and doing everything in a single commit. 22 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Make it clear if the issue is a **bug**, an **enhancement** or just a **question**. The easiest way to indicate this is to prefix the title, e.g. `[Question] I have a question`. 4 | 5 | Provide a detailed description of the change or addition you are proposing. Include some screenshots or code examples if possible. 6 | 7 | ### Your environment 8 | 9 | If you're reporting a bug or asking a specific question, include as many relevant details about your environment so we can reproduce it. The more, the better. 10 | 11 | - Package version or last commit 12 | - Operating system and version 13 | - PHP version 14 | - Related package versions 15 | - … 16 | 17 | ## Context 18 | 19 | Why is this change important to you? How would you use it? How can it benefit other users? 20 | 21 | ## Possible implementation 22 | 23 | Not obligatory, but suggest an idea for implementing addition or change. 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2018 (until present) Sebastiaan Luca 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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Type 2 | 3 | What kind of pull request is this? Put an `x` in all the boxes that apply: 4 | 5 | - [ ] Bug fix (non-breaking change which fixes an issue) 6 | - [ ] New feature (non-breaking change which adds functionality) 7 | - [ ] Extend feature (non-breaking change which extends existing functionality) 8 | - [ ] Change feature (non-breaking change which either changes or refactors existing functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 10 | 11 | ## What does it change? 12 | 13 | Describe your changes in detail. 14 | 15 | ## Why this PR? 16 | 17 | Why is this change required? What problem does it solve? 18 | 19 | ## How has this been tested? 20 | 21 | Please describe in detail how you tested your changes (or are planning on testing them). 22 | 23 | ## Checklist 24 | 25 | To facilitate merging your change and the approval of this PR, please make sure you've reviewed and applied the following: 26 | 27 | - This PR addresses exactly one issue 28 | - All changes were made in a fork of this project (preferably also in a separate branch) 29 | - It follows the code style of this project 30 | - Tests were added to cover the changes 31 | - All previously existing tests still pass 32 | - If the change to the code requires a change to the documentation, it has been updated accordingly 33 | 34 | If you're unsure about any of these, don't hesitate to ask. We're here to help! 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automatically bind Eloquent models as route segment variables 2 | 3 | [![Latest stable release][version-badge]][link-packagist] 4 | [![Software license][license-badge]](LICENSE.md) 5 | [![Build status][travis-badge]][link-travis] 6 | [![Total downloads][downloads-badge]][link-packagist] 7 | [![Total stars][stars-badge]][link-github] 8 | 9 | [![Read my blog][blog-link-badge]][link-blog] 10 | [![View my other packages and projects][packages-link-badge]][link-packages] 11 | [![Follow @sebastiaanluca on Twitter][twitter-profile-badge]][link-twitter] 12 | [![Share this package on Twitter][twitter-share-badge]][link-twitter-share] 13 | 14 | **Immediately start using models in your routes without having to worry about maintaining any list or map.** 15 | 16 | Medium to large Laravel applications can have *a lot* of models. If you're a heavy user of [route model binding](https://laravel.com/docs/5.6/routing#route-model-binding) to automatically retrieve and inject model instances in your controllers, that means you have to *manually* register dozens or hundreds of models in your route service provider. You then need to do the same for each new, changed, or deleted model which makes this nifty feature hard to maintain and easy to forget. 17 | 18 | This package solves the issue of manually having to register each model by doing all the grunt work for you. It reads your `composer.json` PSR-4 autoload section and scans all their model directories for usable Eloquent models. It then explicitly binds each model into the router as a route segment variable using a case type of your choosing (see [configuring casing](#casing)). 19 | 20 | ### Example 21 | 22 | Get rid of this boilerplate code: 23 | 24 | ```php 25 | public function boot() : void 26 | { 27 | Route::model('user', \App\Users\User::class); 28 | Route::model('order', \App\Orders\Order::class); 29 | Route::model('shoppingCart', \App\Carts\Cart::class); 30 | Route::model('Item', \App\Item::class); 31 | Route::model('Admin', \App\Users\Admin::class); 32 | … (repeat dozens of times) 33 | } 34 | ``` 35 | 36 | And just do this (for any Eloquent model in your application): 37 | 38 | ```php 39 | Route::get('profile/{user}', ShowProfile::class); 40 | Route::get('orders/{order}', ShowOrder::class); 41 | Route::get('carts/{shoppingCart}', ShowShoppingCart::class); 42 | … 43 | ``` 44 | 45 | ## Table of contents 46 | 47 | - [Requirements](#requirements) 48 | - [How to install](#how-to-install) 49 | - [How to use](#how-to-use) 50 | - [Defining model namespaces](#defining-model-namespaces) 51 | - [Route segment variables](#route-segment-variables) 52 | - [Caching bindings for production](#caching-bindings-for-production) 53 | - [Configuration](#configuration) 54 | - [Casing](#casing) 55 | - [License](#license) 56 | - [Change log](#change-log) 57 | - [Testing](#testing) 58 | - [Contributing](#contributing) 59 | - [Security](#security) 60 | - [Credits](#credits) 61 | - [About](#about) 62 | 63 | ## Requirements 64 | 65 | - PHP 7.3 or higher 66 | - Laravel 7.0 or higher 67 | 68 | ## How to install 69 | 70 | Via Composer: 71 | 72 | ```bash 73 | composer require sebastiaanluca/laravel-route-model-autobinding 74 | ``` 75 | 76 | ## How to use 77 | 78 | ### Defining model namespaces 79 | 80 | Laravel route model autobinding uses your `composer.json` PSR-4 autoload section to know which namespaces and paths to scan. In any new Laravel project, the default `App\\` namespace is already in place, so for most projects no additional setup required. If you have other namespaces registered like local modules or (dev) packages, those will be scanned too. 81 | 82 | ```json 83 | { 84 | "autoload": { 85 | "psr-4": { 86 | "App\\": "app/", 87 | "MyModule\\": "modules/MyModule/", 88 | "MyPackage\\": "MyPackage/src/" 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | Furthermore it filters out traits, abstract classes, helper files, and other unusable items to only bind valid Eloquent models. 95 | 96 | ### Route segment variables 97 | 98 | After installing the package, you can immediately get to work using the aliased Eloquent models in your routes: 99 | 100 | ```php 101 | Route::get('profile/{user}', ShowProfile::class); 102 | Route::get('orders/{order}', ShowOrder::class); 103 | Route::get('carts/{shoppingCart}', ShowShoppingCart::class); 104 | … 105 | ``` 106 | 107 | Besides scanning and aliasing your models for you, this package alters no native Laravel functionality. Therefore, see the Laravel documentation on how to use [route model injection](https://laravel.com/docs/5.6/routing#route-model-binding). 108 | 109 | ### Caching bindings in production 110 | 111 | To cache all bindings and speed up your application in production, add the cache command to your deploy scripts: 112 | 113 | ``` 114 | php artisan autobinding:cache 115 | ``` 116 | 117 | This scans all your current models and writes a static cache file to the `bootstrap/cache` directory. Upon subsequent framework booting, it reads the cache file instead of scanning and aliasing on-the-fly. 118 | 119 | Note that this thus **disables runtime scanning**, meaning new models will not be recognized and changes to existing models will not be reflected (not very handy during development). You can however still change the case type in the configuration file, as the binding happens in a later stage. 120 | 121 | To clear the cache file, run: 122 | 123 | ``` 124 | php artisan autobinding:clear 125 | ``` 126 | 127 | ### Configuration 128 | 129 | Run 130 | 131 | ``` 132 | php artisan vendor:publish 133 | ``` 134 | 135 | and select 136 | 137 | ``` 138 | laravel-route-model-autobinding (configuration) 139 | ``` 140 | 141 | to publish the configuration file. 142 | 143 | #### Casing 144 | 145 | By default, the case type for aliasing models is set to *camel case*. You can change this to use camel, snake, or studly casing. 146 | 147 | See `\SebastiaanLuca\RouteModelAutobinding\CaseTypes` for possible options. 148 | 149 | Camel case (default): 150 | 151 | ```php 152 | Route::get('carts/{shoppingCart}', ShowShoppingCart::class); 153 | ``` 154 | 155 | Snake case: 156 | 157 | ```php 158 | Route::get('carts/{shopping_cart}', ShowShoppingCart::class); 159 | ``` 160 | 161 | Studly case: 162 | 163 | ```php 164 | Route::get('carts/{ShoppingCart}', ShowShoppingCart::class); 165 | ``` 166 | 167 | The case type can still be changed after caching your models. 168 | 169 | ## License 170 | 171 | This package operates under the MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information. 172 | 173 | ## Change log 174 | 175 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 176 | 177 | ## Testing 178 | 179 | ```bash 180 | composer install 181 | composer test 182 | ``` 183 | 184 | ## Contributing 185 | 186 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE OF CONDUCT](CODE_OF_CONDUCT.md) for details. 187 | 188 | ## Security 189 | 190 | If you discover any security related issues, please email [hello@sebastiaanluca.com][link-author-email] instead of using the issue tracker. 191 | 192 | ## Credits 193 | 194 | - [Sebastiaan Luca][link-github-profile] 195 | - [All Contributors][link-contributors] 196 | 197 | ## About 198 | 199 | My name is Sebastiaan and I'm a freelance back-end developer specializing in building custom Laravel applications. Check out my [portfolio][link-portfolio] for more information, [my blog][link-blog] for the latest tips and tricks, and my other [packages][link-packages] to kick-start your next project. 200 | 201 | Have a project that could use some guidance? Send me an e-mail at [hello@sebastiaanluca.com][link-author-email]! 202 | 203 | [version-badge]: https://img.shields.io/packagist/v/sebastiaanluca/laravel-route-model-autobinding.svg?label=stable 204 | [license-badge]: https://img.shields.io/badge/license-MIT-informational.svg 205 | [travis-badge]: https://img.shields.io/travis/sebastiaanluca/laravel-route-model-autobinding/master.svg 206 | [downloads-badge]: https://img.shields.io/packagist/dt/sebastiaanluca/laravel-route-model-autobinding.svg?color=brightgreen 207 | [stars-badge]: https://img.shields.io/github/stars/sebastiaanluca/laravel-route-model-autobinding.svg?color=brightgreen 208 | 209 | [blog-link-badge]: https://img.shields.io/badge/link-blog-lightgrey.svg 210 | [packages-link-badge]: https://img.shields.io/badge/link-other_packages-lightgrey.svg 211 | [twitter-profile-badge]: https://img.shields.io/twitter/follow/sebastiaanluca.svg?style=social 212 | [twitter-share-badge]: https://img.shields.io/twitter/url/http/shields.io.svg?style=social 213 | 214 | [link-github]: https://github.com/sebastiaanluca/laravel-route-model-autobinding 215 | [link-packagist]: https://packagist.org/packages/sebastiaanluca/laravel-route-model-autobinding 216 | [link-travis]: https://travis-ci.org/sebastiaanluca/laravel-route-model-autobinding 217 | [link-twitter-share]: https://twitter.com/intent/tweet?text=Automatically%20bind%20Eloquent%20models%20as%20route%20segment%20variables.%20Via%20@sebastiaanluca%20https://github.com/sebastiaanluca/laravel-route-model-autobinding 218 | [link-contributors]: ../../contributors 219 | 220 | [link-portfolio]: https://www.sebastiaanluca.com 221 | [link-blog]: https://blog.sebastiaanluca.com 222 | [link-packages]: https://packagist.org/packages/sebastiaanluca 223 | [link-twitter]: https://twitter.com/sebastiaanluca 224 | [link-github-profile]: https://github.com/sebastiaanluca 225 | [link-author-email]: mailto:hello@sebastiaanluca.com 226 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sebastiaanluca/laravel-route-model-autobinding", 3 | "type": "library", 4 | "description": "Automatically bind Eloquent models to be used as route segments.", 5 | "keywords": [ 6 | "bind", 7 | "eloquent", 8 | "laravel", 9 | "model", 10 | "php", 11 | "route" 12 | ], 13 | "homepage": "https://github.com/sebastiaanluca/laravel-route-model-autobinding", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Sebastiaan Luca", 18 | "email": "hello@sebastiaanluca.com", 19 | "homepage": "https://www.sebastiaanluca.com", 20 | "role": "Author" 21 | } 22 | ], 23 | "require": { 24 | "php": "^7.3", 25 | "laravel/framework": "^7.0|^8.0" 26 | }, 27 | "require-dev": { 28 | "kint-php/kint": "^3.3", 29 | "mockery/mockery": "^1.3", 30 | "orchestra/testbench": "^5.1|^6.0", 31 | "phpunit/phpunit": "^8.5" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "SebastiaanLuca\\RouteModelAutobinding\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "SebastiaanLuca\\RouteModelAutobinding\\Tests\\": "tests", 41 | "App\\": "tests/resources/app/", 42 | "MyModule\\": "tests/resources/modules/MyModule/", 43 | "MyPackage\\": "tests/resources/MyPackage/src/" 44 | } 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "extra": { 50 | "laravel": { 51 | "providers": [ 52 | "SebastiaanLuca\\RouteModelAutobinding\\RouteModelAutobindingServiceProvider" 53 | ] 54 | } 55 | }, 56 | "scripts": { 57 | "composer-validate": "@composer validate --no-check-all --strict --ansi", 58 | "test": "vendor/bin/phpunit", 59 | "test-lowest": [ 60 | "composer update --prefer-lowest --prefer-dist --no-interaction --ansi", 61 | "@test" 62 | ], 63 | "test-stable": [ 64 | "composer update --prefer-stable --prefer-dist --no-interaction --ansi", 65 | "@test" 66 | ], 67 | "check": [ 68 | "@composer-validate", 69 | "@test" 70 | ] 71 | }, 72 | "minimum-stability": "dev", 73 | "prefer-stable": true 74 | } 75 | -------------------------------------------------------------------------------- /config/route-model-autobinding.php: -------------------------------------------------------------------------------- 1 | CaseTypes::CAMEL_CASE, 13 | 14 | ]; 15 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | ./tests 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 20 | 21 | ./tests/Feature 22 | 23 | 24 | ./tests/Unit 25 | 26 | 27 | 28 | 29 | src/ 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Autobinder.php: -------------------------------------------------------------------------------- 1 | router = $routes; 27 | } 28 | 29 | /** 30 | * Scan all model directories and automatically bind Eloquent models as route segment variables. 31 | * 32 | * @return void 33 | */ 34 | public function bindAll() : void 35 | { 36 | if ($this->useCache()) { 37 | return; 38 | } 39 | 40 | $models = $this->getModels(); 41 | 42 | $this->bindRouteModels($models); 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function getModels() : array 49 | { 50 | $config = $this->getComposerConfig(); 51 | $paths = $this->getModelPaths($config); 52 | 53 | if (count($paths) === 0) { 54 | return []; 55 | } 56 | 57 | return $this->scan($paths); 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getCachePath() : string 64 | { 65 | return base_path('bootstrap/cache/autobinding.php'); 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | protected function useCache() : bool 72 | { 73 | if (! file_exists($cache = $this->getCachePath())) { 74 | return false; 75 | } 76 | 77 | $this->bindRouteModels(include $cache); 78 | 79 | return true; 80 | } 81 | 82 | /** 83 | * @return array 84 | */ 85 | protected function getComposerConfig() : array 86 | { 87 | $composer = file_get_contents(base_path('composer.json')); 88 | 89 | return json_decode($composer, true, JSON_UNESCAPED_SLASHES); 90 | } 91 | 92 | /** 93 | * @param array $config 94 | * 95 | * @return array 96 | */ 97 | protected function getModelPaths(array $config) : array 98 | { 99 | $paths = Arr::get($config, 'autoload.psr-4'); 100 | 101 | $paths = collect($paths) 102 | ->unique() 103 | ->mapWithKeys(function (string $path, string $namespace) { 104 | return [$namespace => base_path(rtrim($path, '/'))]; 105 | }) 106 | ->filter(function (string $path) { 107 | return is_dir($path); 108 | }); 109 | 110 | return $paths->toArray(); 111 | } 112 | 113 | /** 114 | * @param array $paths 115 | * 116 | * @return array 117 | */ 118 | protected function scan(array $paths) : array 119 | { 120 | $models = []; 121 | 122 | foreach ($paths as $namespace => $path) { 123 | foreach ((new Finder)->in($path)->files() as $file) { 124 | $name = str_replace( 125 | ['/', '.php'], 126 | ['\\', ''], 127 | Str::after($file->getPathname(), $path . DIRECTORY_SEPARATOR) 128 | ); 129 | 130 | $model = $namespace . $name; 131 | 132 | if (! class_exists($model)) { 133 | continue; 134 | } 135 | 136 | $reflection = new ReflectionClass($model); 137 | 138 | if ($reflection->isAbstract() || ! is_subclass_of($model, Model::class)) { 139 | continue; 140 | } 141 | 142 | $models[] = $model; 143 | } 144 | } 145 | 146 | return $models; 147 | } 148 | 149 | /** 150 | * @param array $models 151 | * 152 | * @return void 153 | */ 154 | protected function bindRouteModels(array $models) : void 155 | { 156 | foreach ($models as $model) { 157 | $this->router->model( 158 | $this->getModelAlias($model), 159 | $model 160 | ); 161 | } 162 | } 163 | 164 | /** 165 | * @param string $model 166 | * 167 | * @return string 168 | */ 169 | protected function getModelAlias(string $model) : string 170 | { 171 | $basename = class_basename($model); 172 | 173 | switch (config('route-model-autobinding.case')) { 174 | case CaseTypes::SNAKE_CASE: 175 | return Str::snake($basename); 176 | 177 | case CaseTypes::STUDLY_CASE: 178 | return Str::studly($basename); 179 | 180 | case CaseTypes::CAMEL_CASE: 181 | default: 182 | return Str::camel($basename); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/CaseTypes.php: -------------------------------------------------------------------------------- 1 | binder = $binder; 41 | } 42 | 43 | /** 44 | * Execute the console command. 45 | * 46 | * @return void 47 | */ 48 | public function handle() : void 49 | { 50 | $models = $this->binder->getModels(); 51 | 52 | $cache = $this->binder->getCachePath(); 53 | 54 | file_put_contents( 55 | $cache, 56 | 'info('Route model bindings cached!'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Commands/ClearCachedRouteModels.php: -------------------------------------------------------------------------------- 1 | binder = $binder; 41 | } 42 | 43 | /** 44 | * Execute the console command. 45 | * 46 | * @return void 47 | */ 48 | public function handle() : void 49 | { 50 | @unlink($this->binder->getCachePath()); 51 | 52 | $this->info('Route model bindings cleared!'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/RouteModelAutobindingServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 21 | $this->getConfigurationPath(), 22 | $this->getShortPackageName() 23 | ); 24 | 25 | $this->commands( 26 | CacheRouteModels::class, 27 | ClearCachedRouteModels::class 28 | ); 29 | } 30 | 31 | /** 32 | * Bootstrap the application services. 33 | * 34 | * @return void 35 | */ 36 | public function boot() : void 37 | { 38 | $this->registerPublishableResources(); 39 | 40 | app(Autobinder::class)->bindAll(); 41 | } 42 | 43 | /** 44 | * @return void 45 | */ 46 | private function registerPublishableResources() : void 47 | { 48 | $this->publishes([ 49 | $this->getConfigurationPath() => config_path($this->getShortPackageName() . '.php'), 50 | ], $this->getPackageName() . ' (configuration)'); 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | private function getConfigurationPath() : string 57 | { 58 | return __DIR__ . '/../config/' . $this->getShortPackageName() . '.php'; 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | private function getShortPackageName() : string 65 | { 66 | return 'route-model-autobinding'; 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | private function getPackageName() : string 73 | { 74 | return 'laravel-route-model-autobinding'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Feature/AutobinderCacheTest.php: -------------------------------------------------------------------------------- 1 | getRouter(); 24 | 25 | $router->shouldReceive('model')->with('user', 'App\\User'); 26 | $router->shouldReceive('model')->with('somethingInherited', 'App\\Models\\SomethingInherited'); 27 | $router->shouldReceive('model')->with('address', 'MyModule\\Models\\Address'); 28 | $router->shouldReceive('model')->with('thing', 'MyPackage\\Models\\Thing'); 29 | $router->shouldReceive('model')->with('package', 'MyPackage\\Models\\Sub\\Package'); 30 | 31 | app(Autobinder::class)->bindAll(); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function it reads from cache when cached() : void 38 | { 39 | $cache = base_path('bootstrap/cache/autobinding.php'); 40 | 41 | $router = $this->getRouter(); 42 | 43 | $router->shouldReceive('model')->with('somethingCached', 'App\\SomethingCached'); 44 | $router->shouldReceive('model')->with('cachedUser', 'App\\Models\\CachedUser'); 45 | 46 | $copy = copy( 47 | base_path('cache.php'), 48 | $cache 49 | ); 50 | 51 | $this->assertTrue($copy); 52 | 53 | app(Autobinder::class)->bindAll(); 54 | 55 | unlink($cache); 56 | } 57 | 58 | /** 59 | * @return \Mockery\MockInterface 60 | */ 61 | private function getRouter() : MockInterface 62 | { 63 | return $this->mock( 64 | Router::class, 65 | [app(Dispatcher::class)] 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Feature/AutobinderCaseTest.php: -------------------------------------------------------------------------------- 1 | getRouter(); 25 | 26 | config()->set('route-model-autobinding.case', null); 27 | 28 | $router->shouldReceive('model')->with('user', 'App\\User'); 29 | $router->shouldReceive('model')->with('somethingInherited', 'App\\Models\\SomethingInherited'); 30 | $router->shouldReceive('model')->with('address', 'MyModule\\Models\\Address'); 31 | $router->shouldReceive('model')->with('thing', 'MyPackage\\Models\\Thing'); 32 | $router->shouldReceive('model')->with('package', 'MyPackage\\Models\\Sub\\Package'); 33 | 34 | app(Autobinder::class)->bindAll(); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function it binds all models using camel case() : void 41 | { 42 | $router = $this->getRouter(); 43 | 44 | config()->set('route-model-autobinding.case', CaseTypes::CAMEL_CASE); 45 | 46 | $router->shouldReceive('model')->with('user', 'App\\User'); 47 | $router->shouldReceive('model')->with('somethingInherited', 'App\\Models\\SomethingInherited'); 48 | $router->shouldReceive('model')->with('address', 'MyModule\\Models\\Address'); 49 | $router->shouldReceive('model')->with('thing', 'MyPackage\\Models\\Thing'); 50 | $router->shouldReceive('model')->with('package', 'MyPackage\\Models\\Sub\\Package'); 51 | 52 | app(Autobinder::class)->bindAll(); 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function it binds all models using snake case() : void 59 | { 60 | $router = $this->getRouter(); 61 | 62 | config()->set('route-model-autobinding.case', CaseTypes::SNAKE_CASE); 63 | 64 | $router->shouldReceive('model')->with('user', 'App\\User'); 65 | $router->shouldReceive('model')->with('something_inherited', 'App\\Models\\SomethingInherited'); 66 | $router->shouldReceive('model')->with('address', 'MyModule\\Models\\Address'); 67 | $router->shouldReceive('model')->with('thing', 'MyPackage\\Models\\Thing'); 68 | $router->shouldReceive('model')->with('package', 'MyPackage\\Models\\Sub\\Package'); 69 | 70 | app(Autobinder::class)->bindAll(); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function it binds all models using studly case() : void 77 | { 78 | $router = $this->getRouter(); 79 | 80 | config()->set('route-model-autobinding.case', CaseTypes::STUDLY_CASE); 81 | 82 | $router->shouldReceive('model')->with('User', 'App\\User'); 83 | $router->shouldReceive('model')->with('SomethingInherited', 'App\\Models\\SomethingInherited'); 84 | $router->shouldReceive('model')->with('Address', 'MyModule\\Models\\Address'); 85 | $router->shouldReceive('model')->with('Thing', 'MyPackage\\Models\\Thing'); 86 | $router->shouldReceive('model')->with('Package', 'MyPackage\\Models\\Sub\\Package'); 87 | 88 | app(Autobinder::class)->bindAll(); 89 | } 90 | 91 | /** 92 | * @return \Mockery\MockInterface 93 | */ 94 | private function getRouter() : MockInterface 95 | { 96 | return $this->mock( 97 | Router::class, 98 | [app(Dispatcher::class)] 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/Feature/AutobinderTest.php: -------------------------------------------------------------------------------- 1 | mock( 23 | Router::class, 24 | [app(Dispatcher::class)] 25 | ); 26 | 27 | $router->shouldReceive('model')->with('user', 'App\\User'); 28 | $router->shouldReceive('model')->with('somethingInherited', 'App\\Models\\SomethingInherited'); 29 | $router->shouldReceive('model')->with('address', 'MyModule\\Models\\Address'); 30 | $router->shouldReceive('model')->with('thing', 'MyPackage\\Models\\Thing'); 31 | $router->shouldReceive('model')->with('package', 'MyPackage\\Models\\Sub\\Package'); 32 | 33 | app(Autobinder::class)->bindAll(); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function it returns all models() : void 40 | { 41 | $models = app(Autobinder::class)->getModels(); 42 | 43 | $expected = [ 44 | 'App\\User', 45 | 'App\\Models\\SomethingInherited', 46 | 'MyModule\\Models\\Address', 47 | 'MyPackage\\Models\\Sub\\Package', 48 | 'MyPackage\\Models\\Thing', 49 | ]; 50 | 51 | $this->assertSameValues($expected, $models); 52 | } 53 | 54 | /** 55 | * @test 56 | */ 57 | public function it returns the cache path() : void 58 | { 59 | $path = app(Autobinder::class)->getCachePath(); 60 | 61 | $this->assertSame( 62 | base_path('bootstrap/cache/autobinding.php'), 63 | $path 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Feature/Commands/AutobinderCacheCommandTest.php: -------------------------------------------------------------------------------- 1 | assertFileNotExists($cache); 20 | 21 | $this->artisan('autobinding:cache'); 22 | 23 | $this->assertFileExists($cache); 24 | 25 | unlink($cache); 26 | } 27 | 28 | /** 29 | * @param \Illuminate\Foundation\Application $app 30 | * 31 | * @return array 32 | */ 33 | protected function getPackageProviders($app) : array 34 | { 35 | return [ 36 | RouteModelAutobindingServiceProvider::class, 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Feature/Commands/AutobinderClearCacheCommandTest.php: -------------------------------------------------------------------------------- 1 | assertFileExists($cache); 20 | 21 | $this->artisan('autobinding:clear'); 22 | 23 | $this->assertFileNotExists($cache); 24 | } 25 | 26 | /** 27 | * @param \Illuminate\Foundation\Application $app 28 | * 29 | * @return array 30 | */ 31 | protected function getPackageProviders($app) : array 32 | { 33 | return [ 34 | RouteModelAutobindingServiceProvider::class, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/MocksInstances.php: -------------------------------------------------------------------------------- 1 | app->instance($class, $mock); 28 | 29 | return $mock; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | setBasePath(__DIR__ . '/resources'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/resources/MyPackage/src/Models/Sub/Package.php: -------------------------------------------------------------------------------- 1 | 'App\\SomethingCached', 3 | 1 => 'App\\Models\\CachedUser', 4 | ]; 5 | -------------------------------------------------------------------------------- /tests/resources/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "psr-4": { 4 | "App\\": "app/", 5 | "MyModule\\": "modules/MyModule/", 6 | "MyPackage\\": "MyPackage/src/" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/resources/modules/MyModule/Models/Address.php: -------------------------------------------------------------------------------- 1 |