├── .gitignore ├── .travis.yml ├── phpunit.xml ├── composer.json ├── src ├── DingoServiceProvider.php ├── FastRouteDispatcher.php ├── RouteBindingServiceProvider.php └── BindingResolver.php ├── LICENSE ├── tests ├── IntegratedTest.php ├── FastRouteDispatcherTest.php └── BindingResolverTest.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | .vscode 4 | .phpunit.result.* 5 | 6 | # We ignore the lock file to allow tests to be done on the latest supported versions of Lumen and FastRoute 7 | composer.lock 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.4 6 | - 8.0 7 | - 8.1 8 | - 8.2 9 | - 8.3 10 | 11 | script: 12 | - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install --prefer-dist --no-interaction 13 | - phpunit 14 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmghv/lumen-route-binding", 3 | "description": "Route model binding for Lumen", 4 | "license": "MIT", 5 | "homepage": "https://github.com/mmghv/lumen-route-binding", 6 | "keywords": ["lumen", "route", "model", "binding", "bind", "route model binding", "fast-route"], 7 | "authors": [ 8 | { 9 | "name": "Mohamed Gharib", 10 | "email": "mmghv@coddest.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "~7.1|~8.0", 15 | "laravel/lumen-framework": "5 - 11", 16 | "nikic/fast-route": ">=0.4 <2.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "~7.2.0|~8.0|~9.0", 20 | "mockery/mockery": "~1.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "mmghv\\LumenRouteBinding\\": "src/" 25 | } 26 | }, 27 | "minimum-stability": "dev", 28 | "prefer-stable": true 29 | } 30 | -------------------------------------------------------------------------------- /src/DingoServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['dispatcher']; 21 | 22 | // Set routes resolver from dingo router. 23 | $dispatcher->setRoutesResolver(function() use ($routeCollector) { 24 | return $routeCollector->getData(); 25 | }); 26 | 27 | return $dispatcher; 28 | }; 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mohamed Gharib 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 | -------------------------------------------------------------------------------- /tests/IntegratedTest.php: -------------------------------------------------------------------------------- 1 | register('mmghv\LumenRouteBinding\RouteBindingServiceProvider'); 21 | 22 | // Get the binder instance 23 | $binder = $app->make('bindingResolver'); 24 | 25 | // Register a simple binding 26 | $binder->bind('wildcard', function ($val) { 27 | return "{$val} Resolved"; 28 | }); 29 | 30 | $router = isset($app->router) ? $app->router : $app; 31 | 32 | // Register a route with a wildcard 33 | $router->get('/{wildcard}', function ($wildcard) { 34 | return response($wildcard); 35 | }); 36 | 37 | // Dispatch the request 38 | $response = $app->handle(Request::create('/myWildcard', 'GET')); 39 | 40 | // Assert the binding is resolved 41 | $this->assertSame('myWildcard Resolved', $response->getContent(), '-> Response should be the wildcard value after been resolved!'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/FastRouteDispatcher.php: -------------------------------------------------------------------------------- 1 | routesResolver = $routesResolver; 39 | } 40 | 41 | /** 42 | * Set the binding Resolver used to handle route-model-binding. 43 | * 44 | * @param BindingResolver|null $bindingResolver 45 | */ 46 | public function setBindingResolver(BindingResolver $bindingResolver = null) 47 | { 48 | $this->bindingResolver = $bindingResolver; 49 | } 50 | 51 | /** 52 | * Dispatch the request after getting the routes data if not set at the instantiation. 53 | * 54 | * @param string $httpMethod 55 | * @param atring $uri 56 | * 57 | * @return integer 58 | */ 59 | public function dispatch($httpMethod, $uri) 60 | { 61 | // If routes resolver callback is set, call it to get the routes data 62 | if ($this->routesResolver) { 63 | list($this->staticRouteMap, $this->variableRouteData) = call_user_func($this->routesResolver); 64 | } 65 | 66 | // Pass the call to the parent fast-route dispatcher 67 | return parent::dispatch($httpMethod, $uri); 68 | } 69 | 70 | 71 | /** 72 | * Dispatch the route and it's variables, then resolve the route bindings. 73 | * 74 | * @param array $routeData 75 | * @param string $uri 76 | * 77 | * @return array 78 | */ 79 | protected function dispatchVariableRoute($routeData, $uri) 80 | { 81 | $routeInfo = parent::dispatchVariableRoute($routeData, $uri); 82 | 83 | if ($this->bindingResolver && isset($routeInfo[2])) { 84 | $routeInfo[2] = $this->bindingResolver->resolveBindings($routeInfo[2]); 85 | } 86 | 87 | return $routeInfo; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/RouteBindingServiceProvider.php: -------------------------------------------------------------------------------- 1 | binder = new BindingResolver([$this->app, 'make']); 32 | $this->app->instance('bindingResolver', $this->binder); 33 | 34 | // Create a new FastRoute dispatcher (our extended one) 35 | // with no routes data at the moment because routes file is not loaded yet 36 | $this->dispatcher = new FastRouteDispatcher(null); 37 | 38 | // Set binding resolver 39 | $this->dispatcher->setBindingResolver($this->binder); 40 | 41 | // Set routes resolver (will be called when request is dispatched and the routes file is loaded) 42 | $this->dispatcher->setRoutesResolver($this->getRoutesResolver()); 43 | 44 | // Set our dispatcher to be used by the application instead of the default one 45 | $this->app->setDispatcher($this->dispatcher); 46 | 47 | // Save the dispatcher in the container in case someone needs it later (you're welcome) 48 | $this->app->instance('dispatcher', $this->dispatcher); 49 | } 50 | 51 | /** 52 | * Get route resolver used to get routes data when the request is dispatched. 53 | * 54 | * @return \Closure 55 | */ 56 | protected function getRoutesResolver() 57 | { 58 | return function () { 59 | // Create fast-route collector 60 | $routeCollector = new RouteCollector(new RouteParser, new DataGenerator); 61 | 62 | // Get routes data from application 63 | foreach ($this->getRoutes() as $route) { 64 | $routeCollector->addRoute($route['method'], $route['uri'], $route['action']); 65 | } 66 | 67 | return $routeCollector->getData(); 68 | }; 69 | } 70 | 71 | /** 72 | * Get routes data. 73 | * 74 | * @return array 75 | */ 76 | protected function getRoutes() 77 | { 78 | // Support lumen < 5.5 by checking for the router property. 79 | $router = property_exists($this->app, 'router') ? $this->app->router : $this->app; 80 | 81 | return $router->getRoutes(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/FastRouteDispatcherTest.php: -------------------------------------------------------------------------------- 1 | routeData = []; 30 | 31 | // Stub the binding resolver 32 | $this->binder = m::mock('mmghv\LumenRouteBinding\BindingResolver'); 33 | 34 | // Create a new FastRoute dispatcher (our extended one, the class under test) 35 | $this->dispatcher = new FastRouteDispatcher(null); 36 | $this->assertBaseDispatcher('construct', null); 37 | 38 | $this->dispatcher->setBindingResolver($this->binder); 39 | } 40 | 41 | public function tearDown(): void 42 | { 43 | m::close(); 44 | 45 | // Assert that no unexpected calls found on the base dispatcher (similar to m::close()) 46 | $this->assertBaseDispatcher(null); 47 | } 48 | 49 | /** 50 | * Set routes resolver callback on the dispatcher. 51 | */ 52 | public function setRoutesResolver() 53 | { 54 | $this->dispatcher->setRoutesResolver(function () { 55 | return [null, $this->routeData]; 56 | }); 57 | } 58 | 59 | /** 60 | * Assert method calls on the base FastRoute dispatcher. 61 | * 62 | * @param string $method 63 | * @param ... $args 64 | */ 65 | public function assertBaseDispatcher($method = null, $args = null) 66 | { 67 | if (is_null($method)) { 68 | // Assert that no unexpected calls found on the base dispatcher 69 | $calls = $GLOBALS['baseDispacherAssertions']; 70 | if ($calls !== []) { 71 | $this->fail("-> Found unexpected call(s) on the base dispatcher : ".@var_export($calls, true)); 72 | } 73 | 74 | return; 75 | } 76 | 77 | $args = func_get_args(); 78 | $call = array_shift($GLOBALS['baseDispacherAssertions']); 79 | 80 | if ($call !== $args) { 81 | array_shift($args); 82 | $argsImplode = ''; 83 | foreach ($args as $arg) { 84 | $argsImplode .= ($argsImplode ? ', ' : '') . @var_export($arg, true); 85 | } 86 | 87 | $this->fail("-> Failed asserting that method [$method] is called on the base dispatcher with args ($argsImplode)\n-> The actual call is : ".@var_export($call, true)); 88 | } 89 | } 90 | 91 | public function testDispatcherWorksAsExpected() 92 | { 93 | // Assert no calls on the base dispatcher 94 | $this->assertBaseDispatcher(null); 95 | 96 | // Set the route data : 97 | // Note that we intendedly populate it with the routes data here 98 | // (after we setup the dispatcher) to make sure that the dispatcher 99 | // can be registered before the 'routes' file is loaded. 100 | $this->routeData = [ 101 | 'routes' => 'myRoutes', 102 | 'vars' => ['myVars'] 103 | ]; 104 | 105 | // Set the routes resolver 106 | $this->setRoutesResolver(); 107 | 108 | // Expect the binding resolver to be called with the route variables returned from the base dispatcher 109 | $this->binder->shouldReceive('resolveBindings')->once() 110 | ->with($this->routeData['vars'])->andReturn(['myVars resolved']); 111 | 112 | // Call the dispatcher (what the application will do) 113 | $result = $this->dispatcher->dispatch('httpMethod', 'uri'); 114 | 115 | // Assert that our dispatcher passed the 'dispatch' method to the base dispatcher 116 | $this->assertBaseDispatcher('dispatch', 'httpMethod', 'uri'); 117 | 118 | // Assert that the method 'dispatchVariableRoute' called on the base dispatcher with the correct data 119 | $this->assertBaseDispatcher('dispatchVariableRoute', $this->routeData, 'uri'); 120 | 121 | // Assert that our dispatcher replaced the variables with the resolved ones 122 | $this->assertSame($result, [1 => 'data', 2 => ['myVars resolved']], '-> Expected the dispatcher to replace the variables with the resolved ones!'); 123 | } 124 | 125 | public function testDispatcherCanAcceptRoutesOnConstructWithoutResolver() 126 | { 127 | // Assert no calls on the base dispatcher 128 | $this->assertBaseDispatcher(null); 129 | 130 | $this->routeData = [ 131 | 'routes' => 'myRoutes', 132 | 'vars' => ['myVars'] 133 | ]; 134 | 135 | // Pass routes data on construct and don't use routes resolver 136 | $this->dispatcher = new FastRouteDispatcher([null, $this->routeData]); 137 | $this->assertBaseDispatcher('construct', [null, $this->routeData]); 138 | 139 | $this->dispatcher->setBindingResolver($this->binder); 140 | 141 | // Expect the binding resolver to be called with the route variables returned from the base dispatcher 142 | $this->binder->shouldReceive('resolveBindings')->once() 143 | ->with($this->routeData['vars'])->andReturn(['myVars resolved']); 144 | 145 | // Call the dispatcher (what the application will do) 146 | $result = $this->dispatcher->dispatch('httpMethod', 'uri'); 147 | 148 | // Assert that our dispatcher passed the 'dispatch' method to the base dispatcher 149 | $this->assertBaseDispatcher('dispatch', 'httpMethod', 'uri'); 150 | 151 | // Assert that the method 'dispatchVariableRoute' called on the base dispatcher with the correct data 152 | $this->assertBaseDispatcher('dispatchVariableRoute', $this->routeData, 'uri'); 153 | 154 | // Assert that our dispatcher replaced the variables with the resolved ones 155 | $this->assertSame($result, [1 => 'data', 2 => ['myVars resolved']], '-> Expected the dispatcher to replace the variables with the resolved ones!'); 156 | } 157 | 158 | public function testDispatcherCanChangeRoutesWithResolver() 159 | { 160 | // Assert no calls on the base dispatcher 161 | $this->assertBaseDispatcher(null); 162 | 163 | $this->routeData = [ 164 | 'routes' => 'myRoutes', 165 | 'vars' => ['myVars'] 166 | ]; 167 | 168 | // Pass routes data on construct 169 | $this->dispatcher = new FastRouteDispatcher([null, $this->routeData]); 170 | $this->assertBaseDispatcher('construct', [null, $this->routeData]); 171 | 172 | $this->dispatcher->setBindingResolver($this->binder); 173 | 174 | $this->routeData = [ 175 | 'routes' => 'myRoutes2', 176 | 'vars' => ['myVars2'] 177 | ]; 178 | 179 | // Set the routes resolver to change the routes 180 | $this->setRoutesResolver(); 181 | 182 | $newRouteData = [ 183 | 'routes' => 'myRoutes3', 184 | 'vars' => ['myVars3'] 185 | ]; 186 | 187 | // Set a new routes resolver to change the routes again 188 | $this->dispatcher->setRoutesResolver(function() use ($newRouteData) { 189 | return [null, $newRouteData]; 190 | }); 191 | 192 | // Expect the binding resolver to be called with the last route variables set by the resolver 193 | $this->binder->shouldReceive('resolveBindings')->once() 194 | ->with($newRouteData['vars'])->andReturn(['myVars resolved']); 195 | 196 | // Call the dispatcher (what the application will do) 197 | $result = $this->dispatcher->dispatch('httpMethod', 'uri'); 198 | 199 | // Assert that our dispatcher passed the 'dispatch' method to the base dispatcher 200 | $this->assertBaseDispatcher('dispatch', 'httpMethod', 'uri'); 201 | 202 | // Assert that the method 'dispatchVariableRoute' called on the base dispatcher with the correct data 203 | $this->assertBaseDispatcher('dispatchVariableRoute', $newRouteData, 'uri'); 204 | 205 | // Assert that our dispatcher replaced the variables with the resolved ones 206 | $this->assertSame($result, [1 => 'data', 2 => ['myVars resolved']], '-> Expected the dispatcher to replace the variables with the resolved ones!'); 207 | } 208 | 209 | public function testDispatcherDoesNotCallTheResolverIfNoVarsPresent() 210 | { 211 | // Assert no calls on the base dispatcher 212 | $this->assertBaseDispatcher(null); 213 | 214 | // Set the route data : 215 | $this->routeData = [ 216 | 'routes' => 'myRoutes', 217 | 'vars' => null 218 | ]; 219 | 220 | // Set the routes resolver 221 | $this->setRoutesResolver(); 222 | 223 | // Call the dispatcher (what the application will do) 224 | $result = $this->dispatcher->dispatch('httpMethod', 'uri'); 225 | 226 | // Assert calls on the base dispatcher 227 | $this->assertBaseDispatcher('dispatch', 'httpMethod', 'uri'); 228 | $this->assertBaseDispatcher('dispatchVariableRoute', $this->routeData, 'uri'); 229 | 230 | // Assert that our dispatcher replaced the variables with the resolved ones 231 | $this->assertSame($result, [1 => 'data'], '-> Expected the dispatcher to don\'t call the binding resolver!'); 232 | } 233 | 234 | public function testDispatcherDoesNotCallTheResolverIfNoResolverSet() 235 | { 236 | // Assert no calls on the base dispatcher 237 | $this->assertBaseDispatcher(null); 238 | 239 | // Set the route data : 240 | $this->routeData = [ 241 | 'routes' => 'myRoutes', 242 | 'vars' => ['myVars'] 243 | ]; 244 | 245 | // Set the routes resolver 246 | $this->setRoutesResolver(); 247 | 248 | $this->dispatcher->setBindingResolver(null); 249 | 250 | // Call the dispatcher (what the application will do) 251 | $result = $this->dispatcher->dispatch('httpMethod', 'uri'); 252 | 253 | // Assert calls on the base dispatcher 254 | $this->assertBaseDispatcher('dispatch', 'httpMethod', 'uri'); 255 | $this->assertBaseDispatcher('dispatchVariableRoute', $this->routeData, 'uri'); 256 | 257 | // Assert that our dispatcher replaced the variables with the resolved ones 258 | $this->assertSame($result, [1 => 'data', 2 => ['myVars']], '-> Expected the dispatcher to don\'t call the binding resolver!'); 259 | } 260 | } 261 | 262 | class GroupCountBasedStub 263 | { 264 | protected $staticRouteMap; 265 | protected $variableRouteData; 266 | 267 | public function __construct($data) 268 | { 269 | // Record the call to the method 270 | $GLOBALS['baseDispacherAssertions'][] = ['construct', $data]; 271 | 272 | list($this->staticRouteMap, $this->variableRouteData) = $data; 273 | } 274 | 275 | public function dispatch($httpMethod, $uri) 276 | { 277 | // Record the call to the method 278 | $GLOBALS['baseDispacherAssertions'][] = ['dispatch', $httpMethod, $uri]; 279 | 280 | // Call the method dispatchVariableRoute 281 | return $this->dispatchVariableRoute($this->variableRouteData, $uri); 282 | } 283 | 284 | protected function dispatchVariableRoute($routeData, $uri) 285 | { 286 | // Record the call to the method 287 | $GLOBALS['baseDispacherAssertions'][] = ['dispatchVariableRoute', $routeData, $uri]; 288 | 289 | $data = [1 => 'data']; 290 | 291 | if ($routeData['vars']) { 292 | $data[2] = $routeData['vars']; 293 | } 294 | 295 | return $data; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lumen Route Binding 2 | 3 | [![Build Status](https://travis-ci.org/mmghv/lumen-route-binding.svg?branch=master)](https://travis-ci.org/mmghv/lumen-route-binding) 4 | [![Lumen Version](https://img.shields.io/badge/Lumen-5%20--%2011-orange.svg)](https://github.com/laravel/lumen) 5 | [![Latest Stable Version](https://poser.pugx.org/mmghv/lumen-route-binding/v/stable)](https://packagist.org/packages/mmghv/lumen-route-binding) 6 | [![Total Downloads](https://poser.pugx.org/mmghv/lumen-route-binding/downloads)](https://packagist.org/packages/mmghv/lumen-route-binding) 7 | [![Latest Unstable Version](https://poser.pugx.org/mmghv/lumen-route-binding/v/unstable)](https://packagist.org/packages/mmghv/lumen-route-binding) 8 | [![License](https://poser.pugx.org/mmghv/lumen-route-binding/license)](LICENSE) 9 | 10 | This package Adds support for `Route Model Binding` in Lumen (5 - 11). 11 | 12 | > As known, Lumen doesn't support `Route Model Binding` out of the box due to the fact that Lumen doesn't use the Illuminate router that Laravel uses, Instead, It uses [FastRoute](https://github.com/nikic/FastRoute) which is much faster. With this package, We add support for the powerful `Route Model Binding` while still benefit the speed of FastRoute in Lumen. 13 | 14 | # Table of Contents 15 | 16 | * [Installation](#installation) 17 | * [Defining the bindings](#where-to-define-our-bindings) 18 | * [Explicit Binding](#1-explicit-binding) 19 | * [Implicit Binding](#2-implicit-binding) 20 | * [Composite Binding](#3-composite-binding) 21 | * [DingoAPI Integration](#dingoapi-integration) 22 | 23 | ## Installation 24 | 25 | #### Using composer 26 | ``` 27 | composer require mmghv/lumen-route-binding "^1.0" 28 | ``` 29 | 30 | > It requires 31 | > ``` 32 | > php >= 7.1 33 | > Lumen 5 - 11 34 | > ``` 35 | 36 | #### Register the service provider 37 | 38 | In the coming section .. 39 | 40 | 41 | ## Usage 42 | 43 | > Route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user's ID, you can inject the entire `User` model instance that matches the given ID. 44 | 45 | ### Where to Define our Bindings 46 | 47 | Create a service provider that extends the package's one and place it in `app/Providers` : 48 | 49 | ```PHP 50 | // app/Providers/RouteBindingServiceProvider.php 51 | 52 | namespace App\Providers; 53 | 54 | use mmghv\LumenRouteBinding\RouteBindingServiceProvider as BaseServiceProvider; 55 | 56 | class RouteBindingServiceProvider extends BaseServiceProvider 57 | { 58 | /** 59 | * Boot the service provider 60 | */ 61 | public function boot() 62 | { 63 | // The binder instance 64 | $binder = $this->binder; 65 | 66 | // Here we define our bindings 67 | } 68 | } 69 | ``` 70 | 71 | Then register it in `bootstrap/app.php` : 72 | 73 | ```PHP 74 | $app->register(App\Providers\RouteBindingServiceProvider::class); 75 | ``` 76 | 77 | Now we can define our `bindings` in the `boot` method. 78 | 79 | ### Defining the Bindings 80 | 81 | We have **Three** types of bindings: 82 | 83 | #### 1) Explicit Binding 84 | 85 | We can explicitly bind a route wildcard name to a specific model using the `bind` method : 86 | 87 | ```PHP 88 | $binder->bind('user', 'App\User'); 89 | ``` 90 | 91 | This way, Anywhere in our routes if the wildcard `{user}` is found, It will be resolved to the `User` model instance that corresponds to the wildcard value, So we can define our route like this : 92 | 93 | ```PHP 94 | $router->get('profile/{user}', function(App\User $user) { 95 | // 96 | }); 97 | ``` 98 | 99 | Behind the scenes, The binder will resolve the model instance like this : 100 | 101 | ```PHP 102 | $instance = new App\User; 103 | return $instance->where($instance->getRouteKeyName(), $value)->firstOrFail(); 104 | ``` 105 | 106 | ##### Customizing The Key Name 107 | 108 | By default, It will use the model's ID column. Similar to Laravel, If you would like it to use another column when retrieving a given model class, you may override the `getRouteKeyName` method on the Eloquent model : 109 | 110 | ```PHP 111 | /** 112 | * Get the route key for the model. 113 | * 114 | * @return string 115 | */ 116 | public function getRouteKeyName() 117 | { 118 | return 'slug'; 119 | } 120 | ``` 121 | 122 | ##### Using a Custom Resolver Callback : 123 | 124 | If you wish to use your own resolution logic, you may pass a `Class@method` callable style or a `Closure` instead of the class name to the `bind` method, The callable will receive the value of the URI segment and should return the instance of the class that should be injected into the route : 125 | 126 | ```PHP 127 | // Using a 'Class@method' callable style 128 | $binder->bind('article', 'App\Article@findForRoute'); 129 | 130 | // Using a closure 131 | $binder->bind('article', function($value) { 132 | return \App\Article::where('slug', $value)->firstOrFail(); 133 | }); 134 | ``` 135 | 136 | ##### Handling the `NotFound` Exception : 137 | 138 | If no model found with the given key, The Eloquent `firstOrFail` will throw a `ModelNotFoundException`, To handle this exception, We can pass a closure as the third parameter to the `bind` method : 139 | 140 | ```PHP 141 | $binder->bind('article', 'App\Article', function($e) { 142 | // We can return a default value if the model not found : 143 | return new \App\Article(); 144 | 145 | // Or we can throw another exception for example : 146 | throw new \NotFoundHttpException; 147 | }); 148 | ``` 149 | 150 | #### 2) Implicit Binding 151 | 152 | Using the `implicitBind` method, We can tell the binder to automatically bind all models in a given namespace : 153 | 154 | ```PHP 155 | $binder->implicitBind('App'); 156 | ``` 157 | 158 | So in this example : 159 | 160 | ```PHP 161 | $router->get('articles/{article}', function($myArticle) { 162 | // 163 | }); 164 | ``` 165 | 166 | The binder will first check for any **explicit binding** that matches the `article` key. If no match found, It then (and according to our previous implicit binding) will check if the following class exists `App\Article` (The namespace + ucFirst(the key)), If it finds it, Then it will call `firstOrFail` on the class like the explicit binding and inject the returned instance into the route, **However**, If no classes found with this name, It will continue to the next binding (if any) and return the route parameters unchanged if no bindings matches. 167 | 168 | ##### Customizing The Key Name 169 | 170 | Similar to explicit binding, we could specify another column to be used to retrieve the model instance by overriding the `getRouteKeyName` method on the Eloquent model : 171 | 172 | ```PHP 173 | /** 174 | * Get the route key for the model. 175 | * 176 | * @return string 177 | */ 178 | public function getRouteKeyName() 179 | { 180 | return 'slug'; 181 | } 182 | ``` 183 | 184 | ##### Implicit Binding with Repositories 185 | 186 | We can use implicit binding with classes other than the `Eloquent` models, For example if we use something like `Repository Pattern` and would like our bindings to use the repository classes instead of the Eloquent models, We can do that. 187 | 188 | Repository classes names usually use a `Prefix` and\or `Suffix` beside the Eloquent model name, For example, The `Article` Eloquent model, May have a corresponding repository class with the name `EloquentArticleRepository`, We can set our implicit binding to use this prefix and\or suffix like this : 189 | 190 | ```PHP 191 | $binder->implicitBind('App\Repositories', 'Eloquent', 'Repository'); 192 | ``` 193 | 194 | (Of course we can leave out the `prefix` and\or the `suffix` if we don't use it) 195 | 196 | So in this example : 197 | 198 | ```PHP 199 | $router->get('articles/{article}', function($myArticle) { 200 | // 201 | }); 202 | ``` 203 | 204 | The binder will check if the following class exists `App\Repositories\EloquentArticleRepository` (The namespace + prefix + ucFirst(the key) + suffix), If it finds it, Then it will call `firstOrFail()` using the column from `getRouteKeyName()` (so you should have these methods on your repository). 205 | 206 | ##### Using Custom Method 207 | 208 | If you want to use a custom method on your class to retrieve the model instance, You can pass the method name as the fourth parameter : 209 | 210 | ```PHP 211 | $binder->implicitBind('App\Repositories', 'Eloquent', 'Repository', 'findForRoute'); 212 | ``` 213 | 214 | This way, The binder will call the custom method `findForRoute` on our repository passing the route wildcard value and expecting it to return the resolved instance. 215 | 216 | ###### Example of using a custom method with Implicit Binding while using the Repository Pattern : 217 | 218 | 1- defining our binding in the service provider : 219 | 220 | ```PHP 221 | $binder->implicitBind('App\Repositories', '', 'Repository', 'findForRoute'); 222 | ``` 223 | 224 | 2- defining our route in `routes.php` : 225 | 226 | ```PHP 227 | $router->get('articles/{article}', function(App\Article $article) { 228 | return view('articles.view', compact('article')); 229 | }); 230 | ``` 231 | 232 | 3- Adding our custom method in our repository in `apps/Repositories/ArticleRepository.php` : 233 | 234 | ```PHP 235 | /** 236 | * Find the Article for route-model-binding 237 | * 238 | * @param string $val wildcard value 239 | * 240 | * @return \App\Article 241 | */ 242 | public function findForRoute($val) 243 | { 244 | return $this->model->where('slug', $val)->firstOrFail(); 245 | } 246 | ``` 247 | 248 | ##### Handling the `NotFound` Exception : 249 | 250 | Similar to explicit binding, We can handle the exception thrown in the resolver method (the model `firstOrFail` or in our repository) by passing a closure as the fifth parameter to the method `implicitBind`. 251 | 252 | #### 3) Composite Binding 253 | 254 | Sometimes, you will have a route of two or more levels that contains wildcards of related models, Something like : 255 | 256 | ```PHP 257 | $router->get('posts/{post}/comments/{comment}', function(App\Post $post, App\Comment $comment) { 258 | // 259 | }); 260 | ``` 261 | 262 | In this example, If we use explicit or implicit binding, Each model will be resolved individually with no relation to each other, Sometimes that's OK, But what if we want to resolve these models in one binding to handle the relationship between them and maybe do a proper eager loading without repeating the process for each model individually, That's where `Composite Binding` comes into play. 263 | 264 | In `Composite Binding` we tell the binder to register a binding for multiple wildcards in a specific order. 265 | 266 | We use the method `compositeBind` passing an array of wildcards names as the first parameter, and a resolver callback (either a closure or a `Class@method` callable style) as the second parameter. 267 | 268 | ```PHP 269 | // Using a 'Class@method' callable style 270 | $binder->compositeBind(['post', 'comment'], 'App\Repositories\PostRepository@findPostCommentForRoute'); 271 | 272 | // Using a closure 273 | $binder->compositeBind(['post', 'comment'], function($postKey, $commentKey) { 274 | $post = \App\Post::findOrFail($postKey); 275 | $comment = $post->comments()->findOrFail($commentKey); 276 | 277 | return [$post, $comment]; 278 | }); 279 | ``` 280 | 281 | **Note:** 282 | This binding will match the route that has **only** and **exactly** the given wildcards (in this case `{post}` and `{comment}`) and they appear in the same exact **order**. The resolver callback will be handled the wildcards values and **MUST** return the resolved models in an array of the same count and order of the wildcards. 283 | 284 | **Note:** 285 | This type of binding takes a priority over any other type of binding, Meaning that in the previous example if we have an explicit or implicit binding for `post` and\or `comment`, None of them will take place as long as the route **as whole** matches a composite binding. 286 | 287 | ##### Handling the `NotFound` Exception : 288 | 289 | Similar to explicit and implicit binding, We can handle the exception thrown in the resolver callback by passing a closure as the third parameter to the method `compositeBind`. 290 | 291 | ## DingoAPI Integration 292 | 293 | **NOTE** 294 | This documentation is for [dingo/api](https://github.com/dingo/api) `version 2.*`, for earlier versions of `dingo`, follow this [link](https://github.com/mmghv/lumen-route-binding/issues/6). 295 | 296 | To integrate `dingo/api` with `LumenRouteBinding`, all you need to do is to replace the registration of the default `dingo` service provider with the custom one shipped with `LumenRouteBinding`: 297 | 298 | So remove this line in `bootstrap/app.php` : 299 | ```PHP 300 | $app->register(Dingo\Api\Provider\LumenServiceProvider::class); 301 | ``` 302 | 303 | And add this line instead : 304 | ```PHP 305 | $app->register(mmghv\LumenRouteBinding\DingoServiceProvider::class); 306 | ``` 307 | 308 | _(don't forget to also register the `LumenRouteBinding` service provider itself)_ 309 | 310 | That's it, Now you should be able to use `LumenRoutebinding` with `DingoAPI`. 311 | 312 | ## Contributing 313 | If you found an issue, Please report it [here](https://github.com/mmghv/lumen-route-binding/issues). 314 | 315 | Pull Requests are welcome, just make sure to follow the PSR-2 standards and don't forget to add tests. 316 | 317 | ## License & Copyright 318 | 319 | Copyright © 2016-2023, [Mohamed Gharib](https://github.com/mmghv). 320 | Released under the [MIT license](LICENSE). 321 | -------------------------------------------------------------------------------- /src/BindingResolver.php: -------------------------------------------------------------------------------- 1 | [binder, errorHandler]] 20 | * 21 | * @var array 22 | */ 23 | protected $bindings = []; 24 | 25 | /** 26 | * Namespaces for implicit bindings 27 | * [namespace, prefix, suffix, method, errorHandler] 28 | * 29 | * @var array 30 | */ 31 | protected $implicitBindings = []; 32 | 33 | /** 34 | * Composite wildcards bindings 35 | * [[wildcards], binder, errorHandler] 36 | * 37 | * @var array 38 | */ 39 | protected $compositeBindings = []; 40 | 41 | /** 42 | * Create new instance 43 | * 44 | * @param callable $classResolver 45 | */ 46 | public function __construct(callable $classResolver) 47 | { 48 | $this->classResolver = $classResolver; 49 | } 50 | 51 | /** 52 | * Resolve bindings for route parameters 53 | * 54 | * @param array $vars route parameters 55 | * 56 | * @return array route parameters with bindings resolved 57 | */ 58 | public function resolveBindings(array $vars) 59 | { 60 | // First check if the route $vars as a whole matches any registered composite binding 61 | if (count($vars) > 1 && !empty($this->compositeBindings)) { 62 | if ($r = $this->resolveCompositeBinding($vars)) { 63 | return $r; 64 | } 65 | } 66 | 67 | // If no composite binding found, check for explicit and implicit bindings 68 | if (!empty($this->implicitBindings) || !empty($this->bindings)) { 69 | foreach ($vars as $var => $value) { 70 | $vars[$var] = $this->resolveBinding($var, $value); 71 | } 72 | } 73 | 74 | return $vars; 75 | } 76 | 77 | /** 78 | * Check for and resolve the composite bindings if a match found 79 | * 80 | * @param array $vars the wildcards array 81 | * 82 | * @return array|null the wildcards array after been resolved, or NULL if no match found 83 | */ 84 | protected function resolveCompositeBinding($vars) 85 | { 86 | $keys = array_keys($vars); 87 | 88 | foreach ($this->compositeBindings as $binding) { 89 | if ($keys === $binding[0]) { 90 | $binder = $binding[1]; 91 | $errorHandler = $binding[2]; 92 | 93 | $callable = $this->getBindingCallable($binder, null); 94 | $r = $this->callBindingCallable($callable, $vars, $errorHandler, true); 95 | 96 | if (!is_array($r) || count($r) !== count($vars)) { 97 | throw new Exception("Route-Model-Binding (composite-bind) : Return value should be an array and should be of the same count as the wildcards!"); 98 | } 99 | 100 | // Combine the binding results with the keys 101 | return array_combine($keys, array_values($r)); 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Resolve binding for the given wildcard 108 | * 109 | * @param string $key wildcard key 110 | * @param string $value wildcard value 111 | * 112 | * @return mixed resolved binding 113 | */ 114 | protected function resolveBinding($key, $value) 115 | { 116 | // Explicit binding 117 | if (isset($this->bindings[$key])) { 118 | list($binder, $errorHandler) = $this->bindings[$key]; 119 | 120 | $callable = $this->getBindingCallable($binder, $value); 121 | 122 | return $this->callBindingCallable($callable, $value, $errorHandler); 123 | } 124 | 125 | // Implicit binding 126 | foreach ($this->implicitBindings as $binding) { 127 | $class = $binding['namespace'] . '\\' . $binding['prefix'] . ucfirst($key) . $binding['suffix']; 128 | 129 | if (class_exists($class)) { 130 | $instance = $this->resolveClass($class); 131 | 132 | // If special method name is defined, use it, otherwise, use the default 133 | if ($method = $binding['method']) { 134 | $callable = [$instance, $method]; 135 | } else { 136 | $callable = $this->getDefaultBindingResolver($instance, $value); 137 | } 138 | 139 | return $this->callBindingCallable( 140 | $callable, 141 | $value, 142 | $binding['errorHandler'] 143 | ); 144 | } 145 | } 146 | 147 | // Return the value unchanged if no binding found 148 | return $value; 149 | } 150 | 151 | /** 152 | * Get the callable for the binding 153 | * 154 | * @param mixed $binder 155 | * @param string $value 156 | * 157 | * @return callable 158 | * 159 | * @throws Exception 160 | * @throws InvalidArgumentException 161 | */ 162 | protected function getBindingCallable($binder, $value) 163 | { 164 | // If $binder is a callable then use it, otherwise resolve the callable : 165 | if (is_callable($binder)) { 166 | return $binder; 167 | 168 | // If $binder is string (class name or Class@method) 169 | } elseif (is_string($binder)) { 170 | // Check if binder is CLass@method callable style 171 | if (strpos($binder, '@') === false) { 172 | $class = $binder; 173 | $method = null; 174 | } else { 175 | list($class, $method) = explode('@', $binder); 176 | } 177 | 178 | if (!class_exists($class)) { 179 | throw new Exception("Route-Model-Binding : Class not found : [$class]"); 180 | } 181 | 182 | $instance = $this->resolveClass($class); 183 | 184 | // If a custom method defined, use it, Otherwise, use the default binding callable 185 | if ($method) { 186 | return [$instance, $method]; 187 | } else { 188 | return $this->getDefaultBindingResolver($instance, $value); 189 | } 190 | } 191 | 192 | throw new InvalidArgumentException('Route-Model-Binding : Invalid binder value, Expected callable or string'); 193 | } 194 | 195 | /** 196 | * Get the default binding resolver callable 197 | * 198 | * @param string $instance 199 | * @param string $value 200 | * 201 | * @return \Closure 202 | */ 203 | protected function getDefaultBindingResolver($instance, $value) 204 | { 205 | $instance = $instance->where($instance->getRouteKeyName(), $value); 206 | 207 | // Only the 'firstOrFail' is included in the binding callable to exclude the 208 | // exceptions of undefined methods (where, getRouteKeyName) from the errorHandler 209 | return function () use ($instance) { 210 | return $instance->firstOrFail(); 211 | }; 212 | } 213 | 214 | /** 215 | * Call the $classResolver callable to get the class instance 216 | * 217 | * @param string $class 218 | * 219 | * @return mixed 220 | */ 221 | protected function resolveClass($class) 222 | { 223 | return call_user_func($this->classResolver, $class); 224 | } 225 | 226 | /** 227 | * Call the resolved binding callable to get the resolved model(s) 228 | * 229 | * @param callable $callable binder callable 230 | * @param string|array $args wildcard(s) value(s) to be passed to the callable 231 | * @param null|callable $errorHandler handler to be called on exceptions (mostly ModelNotFoundException) 232 | * 233 | * @return mixed resolved model(s) 234 | * 235 | * @throws Exception 236 | */ 237 | protected function callBindingCallable($callable, $args, $errorHandler) 238 | { 239 | try { 240 | // Try to call the resolver method and retrieve the model 241 | if (is_array($args)) { 242 | return call_user_func_array($callable, array_values($args)); 243 | } else { 244 | return call_user_func($callable, $args); 245 | } 246 | } catch (Exception $e) { 247 | // If there's an error handler defined, call it, otherwise, re-throw the exception 248 | if (! is_null($errorHandler)) { 249 | return call_user_func($errorHandler, $e); 250 | } else { 251 | throw $e; 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * Explicit bind a model (name or closure) to a wildcard key. 258 | * 259 | * @param string $key wildcard 260 | * @param string|callable $binder model name, Class@method or resolver callable 261 | * @param null|callable $errorHandler handler to be called on exceptions (mostly ModelNotFoundException) 262 | * 263 | * @example (simple model binding) : 264 | * ->bind('user', 'App\User'); 265 | * 266 | * @example (use custom method (Class@method style)) 267 | * ->bind('user', 'App\User@findUser'); 268 | * 269 | * @example (custom binding closure) 270 | * ->bind('article', function($value) { 271 | * return \App\Article::where('slug', $value)->firstOrFail(); 272 | * }); 273 | * 274 | * @example (catch ModelNotFoundException error) 275 | * ->bind('article', 'App\Article', function($e) { 276 | * throw new NotFoundHttpException; // throw another exception 277 | * return new \App\Article(); // or return default value 278 | * }); 279 | */ 280 | public function bind($key, $binder, callable $errorHandler = null) 281 | { 282 | $this->bindings[$key] = [$binder, $errorHandler]; 283 | } 284 | 285 | /** 286 | * Implicit bind all models in the given namespace 287 | * 288 | * @param string $namespace the namespace where classes are resolved 289 | * @param string $prefix prefix to be added before class name 290 | * @param string $suffix suffix to be added after class name 291 | * @param null|string $method method name to be called on resolved object, omit it to default to : 292 | * object->where(object->getRouteKeyname(), $value)->firstOrFail() 293 | * @param null|callable $errorHandler handler to be called on exceptions (mostly ModelNotFoundException) 294 | * 295 | * @example (bind all models in 'App' namespace) : 296 | * ->implicitBind('App'); 297 | * 298 | * @example (bind all models to their repositories) : 299 | * ->implicitBind('App\Repositories', '', 'Repository'); 300 | * 301 | * @example (bind all models to their repositories using custom method) : 302 | * ->implicitBind('App\Repositories', '', 'Repository', 'findForRoute'); 303 | */ 304 | public function implicitBind($namespace, $prefix = '', $suffix = '', $method = null, callable $errorHandler = null) 305 | { 306 | $this->implicitBindings[] = compact('namespace', 'prefix', 'suffix', 'method', 'errorHandler'); 307 | } 308 | 309 | /** 310 | * Register a composite binding (more than one model) with a specific order 311 | * 312 | * @param array $keys wildcards composite 313 | * @param string|callable binder resolver callable or Class@method callable, will be passed the wildcards values 314 | * and should return an array of resolved values of the same count and order 315 | * @param null|callable $errorHandler handler to be called on exceptions (which is thrown in the resolver callable) 316 | * 317 | * @throws InvalidArgumentException 318 | * 319 | * @example (bind 2 wildcards composite {oneToMany relation}) 320 | * ->compositeBind(['post', 'comment'], function($post, $comment) { 321 | * $post = \App\Post::findOrFail($post); 322 | * $comment = $post->comments()->findOrFail($comment); 323 | * return [$post, $comment]; 324 | * }); 325 | * 326 | * @example (using Class@method callable style) 327 | * ->compositeBind(['post', 'comment'], 'App\Managers\PostManager@findPostComment'); 328 | */ 329 | public function compositeBind($keys, $binder, callable $errorHandler = null) 330 | { 331 | if (!is_array($keys)) { 332 | throw new InvalidArgumentException('Route-Model-Binding : Invalid $keys value, Expected array of wildcards names'); 333 | } 334 | 335 | if (count($keys) < 2) { 336 | throw new InvalidArgumentException('Route-Model-Binding : Invalid $keys value, Expected array of more than one wildcard'); 337 | } 338 | 339 | if (is_callable($binder)) { 340 | // normal callable is acceptable 341 | } elseif (is_string($binder) && strpos($binder, '@') !== false) { 342 | // Class@method callable is acceptable 343 | } else { 344 | throw new InvalidArgumentException("Route-Model-Binding : Binder must be a callable or a 'Class@method' string"); 345 | } 346 | 347 | $this->compositeBindings[] = [$keys, $binder, $errorHandler]; 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /tests/BindingResolverTest.php: -------------------------------------------------------------------------------- 1 | resetBinder(); 19 | 20 | $this->model = m::mock('overload:App\Models\Model'); 21 | $this->myTestRepo = m::mock('overload:App\Repositories\MyTestRepo'); 22 | 23 | $this->wildcards = [ 24 | 'zero' => 'val zero', 25 | 'one' => 'val one', 26 | 'two' => 'val two', 27 | 'three' => 'val three', 28 | ]; 29 | 30 | $this->expected = $this->wildcards; 31 | } 32 | 33 | public function tearDown(): void 34 | { 35 | m::close(); 36 | } 37 | 38 | protected function resetBinder() 39 | { 40 | $this->binder = new BindingResolver(function ($class) { 41 | return new $class; 42 | }); 43 | } 44 | 45 | /** 46 | * setup expectations on mock model to receive : 47 | * $model->where([$model->getRouteKeyName(), 'wildcard_value'])->firstOrFail(); 48 | * 49 | * @param Mock $model mock model to setup 50 | */ 51 | public function expectWhereRouteKeyNameFirstOrFail($model, $throwException = false) 52 | { 53 | $model->shouldReceive('getRouteKeyName')->once() 54 | ->andReturn('route_key'); 55 | 56 | $query = m::mock()->shouldReceive('firstOrFail')->once() 57 | ->withNoArgs() 58 | ->andReturn('bind_result'); 59 | 60 | $model->shouldReceive('where')->once() 61 | ->with('route_key', 'wildcard_value') 62 | ->andReturn($query->getMock()); 63 | 64 | if ($throwException) { 65 | $query->andThrow(new \Exception('NotFound')); 66 | } 67 | } 68 | 69 | public function testNoChangesIfNoVars() 70 | { 71 | $r = $this->binder->resolveBindings([]); 72 | 73 | $this->assertSame([], $r); 74 | } 75 | 76 | public function testNoChangesIfNoBindings() 77 | { 78 | $r = $this->binder->resolveBindings($this->wildcards); 79 | 80 | $this->assertSame($this->wildcards, $r); 81 | } 82 | 83 | public function testNoChangesIfBindingsDontMatch() 84 | { 85 | // set bindings 86 | $this->binder->implicitBind('App\Models'); 87 | 88 | $this->binder->bind('model', 'App\Models\Model'); 89 | 90 | $this->binder->bind('model2', function () { 91 | return 1; 92 | }); 93 | 94 | $this->binder->compositeBind(['model1', 'model2'], function () { 95 | return ['model1', 'model2']; 96 | }); 97 | 98 | // resolve bindings (done when route is dispatched) 99 | $r = $this->binder->resolveBindings($this->wildcards); 100 | 101 | // assert resolved bindings 102 | $this->assertSame($this->wildcards, $r); 103 | } 104 | 105 | public function testExplicitBindCallsWhereRouteKeyNameFirstOrFail() 106 | { 107 | $this->expectWhereRouteKeyNameFirstOrFail($this->model); 108 | 109 | $this->wildcards['model'] = 'wildcard_value'; 110 | $this->expected['model'] = 'bind_result'; 111 | 112 | // set bindings 113 | $this->binder->bind('model', 'App\Models\Model'); 114 | 115 | // resolve bindings (done when route is dispatched) 116 | $r = $this->binder->resolveBindings($this->wildcards); 117 | 118 | // assert resolved bindings 119 | $this->assertSame($this->expected, $r); 120 | } 121 | 122 | public function testExplicitBindAcceptsClassAtMethodCallableStyle() 123 | { 124 | $this->model->shouldReceive('myMethod')->once() 125 | ->with('wildcard_value')->andReturn('bind_result'); 126 | 127 | $this->wildcards['model'] = 'wildcard_value'; 128 | $this->expected['model'] = 'bind_result'; 129 | 130 | // set bindings 131 | $this->binder->bind('model', 'App\Models\Model@myMethod'); 132 | 133 | // resolve bindings (done when route is dispatched) 134 | $r = $this->binder->resolveBindings($this->wildcards); 135 | 136 | // assert resolved bindings 137 | $this->assertSame($this->expected, $r, '-> Class@method binding should be called with "wildcard_value" and it\'s return value ("bind_result") should be used as the binding result!'); 138 | } 139 | 140 | public function testExplicitBindAcceptsClosureBinder() 141 | { 142 | $this->wildcards['model'] = 'wildcard_value'; 143 | $this->expected['model'] = 'bind_result'; 144 | 145 | // set bindings 146 | $this->binder->bind('model', function ($wildcard) { 147 | return ($wildcard === 'wildcard_value') ? 'bind_result' : 'wrong wildcard value!'; 148 | }); 149 | 150 | // resolve bindings (done when route is dispatched) 151 | $r = $this->binder->resolveBindings($this->wildcards); 152 | 153 | // assert resolved bindings 154 | $this->assertSame($this->expected, $r, '-> Custom closure binding should be called with "wildcard_value" and it\'s return value ("bind_result") should be used as the binding result!'); 155 | } 156 | 157 | public function testExplicitBindThrowsExceptionIfClassNotFound() 158 | { 159 | $this->expectException(\Exception::class); 160 | $this->expectExceptionMessage('[App\Models\NotFoundModel]'); 161 | 162 | $this->wildcards['model'] = 'wildcard_value'; 163 | 164 | // set bindings 165 | $this->binder->bind('model', 'App\Models\NotFoundModel'); 166 | 167 | // resolve bindings (done when route is dispatched) 168 | $r = $this->binder->resolveBindings($this->wildcards); 169 | } 170 | 171 | public function testExplicitBindThrowsExceptionIfClassNotFoundUsingClassAtMethodStyle() 172 | { 173 | $this->expectException(\Exception::class); 174 | $this->expectExceptionMessage('[App\Models\NotFoundModel]'); 175 | 176 | $this->wildcards['model'] = 'wildcard_value'; 177 | 178 | // set bindings 179 | $this->binder->bind('model', 'App\Models\NotFoundModel@myMethod'); 180 | 181 | // resolve bindings (done when route is dispatched) 182 | $r = $this->binder->resolveBindings($this->wildcards); 183 | } 184 | 185 | public function testExplicitBindThrowsExceptionIfMethodNotFoundUsingClassAtMethodStyle() 186 | { 187 | $this->expectException(\Exception::class); 188 | $this->expectExceptionMessage('NotFoundMethod'); 189 | 190 | $this->wildcards['model'] = 'wildcard_value'; 191 | 192 | // set bindings 193 | $this->binder->bind('model', 'App\Models\Model@NotFoundMethod'); 194 | 195 | // resolve bindings (done when route is dispatched) 196 | $r = $this->binder->resolveBindings($this->wildcards); 197 | } 198 | 199 | public function testExplicitBindThrowsExceptionIfBinderIsInvalid() 200 | { 201 | $this->expectException(\InvalidArgumentException::class); 202 | $this->expectExceptionMessage('Invalid binder value'); 203 | 204 | $this->wildcards['model'] = 'wildcard_value'; 205 | 206 | // set bindings 207 | $this->binder->bind('model', ['SomeinvalidBinder']); 208 | 209 | // resolve bindings (done when route is dispatched) 210 | $r = $this->binder->resolveBindings($this->wildcards); 211 | } 212 | 213 | public function testExplicitBindRethrowsException() 214 | { 215 | $this->expectException(\Exception::class); 216 | $this->expectExceptionMessage('NotFound'); 217 | 218 | $this->expectWhereRouteKeyNameFirstOrFail($this->model, true); 219 | 220 | $this->wildcards['model'] = 'wildcard_value'; 221 | 222 | // set bindings 223 | $this->binder->bind('model', 'App\Models\Model'); 224 | 225 | // resolve bindings (done when route is dispatched) 226 | $r = $this->binder->resolveBindings($this->wildcards); 227 | } 228 | 229 | public function testExplicitBindWithClosureRethrowsException() 230 | { 231 | $this->expectException(\Exception::class); 232 | $this->expectExceptionMessage('NotFound'); 233 | 234 | $this->wildcards['model'] = 'wildcard_value'; 235 | 236 | // set bindings 237 | $this->binder->bind('model', function () { 238 | throw new \Exception('NotFound'); 239 | }); 240 | 241 | // resolve bindings (done when route is dispatched) 242 | $r = $this->binder->resolveBindings($this->wildcards); 243 | } 244 | 245 | public function testExplicitBindErrorHandler() 246 | { 247 | $this->expectWhereRouteKeyNameFirstOrFail($this->model, true); 248 | 249 | $this->wildcards['model'] = 'wildcard_value'; 250 | $this->expected['model'] = 'errorHandler_result'; 251 | 252 | // set bindings 253 | $this->binder->bind('model', 'App\Models\Model', function ($e) { 254 | if ($e instanceof \Exception) { 255 | return 'errorHandler_result'; 256 | } 257 | }); 258 | 259 | // resolve bindings (done when route is dispatched) 260 | $r = $this->binder->resolveBindings($this->wildcards); 261 | 262 | // assert resolved bindings 263 | $this->assertSame($this->expected, $r, '-> Exception should be handled to the errorHandler and "errorHandler_result" should be returned and used as the binding result!'); 264 | } 265 | 266 | public function testExplicitBindWithClosureErrorHandler() 267 | { 268 | $this->wildcards['model'] = 'wildcard_value'; 269 | $this->expected['model'] = 'errorHandler_result'; 270 | 271 | // set bindings 272 | $this->binder->bind('model', function () { 273 | throw new \Exception(); 274 | }, function ($e) { 275 | if ($e instanceof \Exception) { 276 | return 'errorHandler_result'; 277 | } 278 | }); 279 | 280 | // resolve bindings (done when route is dispatched) 281 | $r = $this->binder->resolveBindings($this->wildcards); 282 | 283 | // assert resolved bindings 284 | $this->assertSame($this->expected, $r, '-> Exception thrown in closure should be handled to the errorHandler and "errorHandler_result" should be returned and used as the binding result!'); 285 | } 286 | 287 | public function testImplicitBindCallsWhereRouteKeyNameFirstOrFailByDefault() 288 | { 289 | $this->expectWhereRouteKeyNameFirstOrFail($this->model); 290 | 291 | $this->wildcards['model'] = 'wildcard_value'; 292 | $this->expected['model'] = 'bind_result'; 293 | 294 | // set bindings 295 | $this->binder->implicitBind('App\Models'); 296 | 297 | // resolve bindings (done when route is dispatched) 298 | $r = $this->binder->resolveBindings($this->wildcards); 299 | 300 | // assert resolved bindings 301 | $this->assertSame($this->expected, $r); 302 | } 303 | 304 | public function testImplicitBindAcceptsPrefix() 305 | { 306 | $this->expectWhereRouteKeyNameFirstOrFail($this->myTestRepo); 307 | 308 | $this->wildcards['testRepo'] = 'wildcard_value'; 309 | $this->expected['testRepo'] = 'bind_result'; 310 | 311 | // set bindings 312 | $this->binder->implicitBind('App\Repositories', 'My'); 313 | 314 | // resolve bindings (done when route is dispatched) 315 | $r = $this->binder->resolveBindings($this->wildcards); 316 | 317 | // assert resolved bindings 318 | $this->assertSame($this->expected, $r); 319 | } 320 | 321 | public function testImplicitBindAcceptsSuffix() 322 | { 323 | $this->expectWhereRouteKeyNameFirstOrFail($this->myTestRepo); 324 | 325 | $this->wildcards['myTest'] = 'wildcard_value'; 326 | $this->expected['myTest'] = 'bind_result'; 327 | 328 | // set bindings 329 | $this->binder->implicitBind('App\Repositories', '', 'Repo'); 330 | 331 | // resolve bindings (done when route is dispatched) 332 | $r = $this->binder->resolveBindings($this->wildcards); 333 | 334 | // assert resolved bindings 335 | $this->assertSame($this->expected, $r); 336 | } 337 | 338 | public function testImplicitBindAcceptsPrefixAndSuffix() 339 | { 340 | $this->expectWhereRouteKeyNameFirstOrFail($this->myTestRepo); 341 | 342 | $this->wildcards['test'] = 'wildcard_value'; 343 | $this->expected['test'] = 'bind_result'; 344 | 345 | // set bindings 346 | $this->binder->implicitBind('App\Repositories', 'My', 'Repo'); 347 | 348 | // resolve bindings (done when route is dispatched) 349 | $r = $this->binder->resolveBindings($this->wildcards); 350 | 351 | // assert resolved bindings 352 | $this->assertSame($this->expected, $r); 353 | } 354 | 355 | public function testImplicitBindAcceptsDefinedMethodName() 356 | { 357 | $this->model->shouldReceive('findForRoute')->once() 358 | ->with('wildcard_value')->andReturn('bind_result'); 359 | 360 | $this->wildcards['model'] = 'wildcard_value'; 361 | $this->expected['model'] = 'bind_result'; 362 | 363 | // set bindings 364 | $this->binder->implicitBind('App\Models', '', '', 'findForRoute'); 365 | 366 | // resolve bindings (done when route is dispatched) 367 | $r = $this->binder->resolveBindings($this->wildcards); 368 | 369 | // assert resolved bindings 370 | $this->assertSame($this->expected, $r); 371 | } 372 | 373 | public function testImplicitBindIgnoresNotFoundModels() 374 | { 375 | $this->wildcards['NotFountModel'] = 'wildcard_value'; 376 | 377 | // set bindings 378 | $this->binder->implicitBind('App\Models'); 379 | 380 | // resolve bindings (done when route is dispatched) 381 | $r = $this->binder->resolveBindings($this->wildcards); 382 | 383 | // assert resolved bindings 384 | $this->assertSame($this->wildcards, $r, '-> Bindings for classes not found in the given namespace should be ignored and their values should not be touched!'); 385 | } 386 | 387 | public function testImplicitBindRethrowsException() 388 | { 389 | $this->expectException(\Exception::class); 390 | $this->expectExceptionMessage('NotFound'); 391 | 392 | $this->expectWhereRouteKeyNameFirstOrFail($this->model, true); 393 | 394 | $this->wildcards['model'] = 'wildcard_value'; 395 | 396 | // set bindings 397 | $this->binder->implicitBind('App\Models'); 398 | 399 | // resolve bindings (done when route is dispatched) 400 | $r = $this->binder->resolveBindings($this->wildcards); 401 | } 402 | 403 | public function testImplicitBindRethrowsExceptionWithDefinedMethod() 404 | { 405 | $this->expectException(\Exception::class); 406 | $this->expectExceptionMessage('NotFound'); 407 | 408 | $this->model->shouldReceive('findForRoute')->once() 409 | ->with('wildcard_value')->andThrow(new \Exception('NotFound')); 410 | 411 | $this->wildcards['model'] = 'wildcard_value'; 412 | 413 | // set bindings 414 | $this->binder->implicitBind('App\Models', '', '', 'findForRoute'); 415 | 416 | // resolve bindings (done when route is dispatched) 417 | $r = $this->binder->resolveBindings($this->wildcards); 418 | } 419 | 420 | public function testImplicitBindErrorHandler() 421 | { 422 | $this->expectWhereRouteKeyNameFirstOrFail($this->model, true); 423 | 424 | $this->wildcards['model'] = 'wildcard_value'; 425 | $this->expected['model'] = 'errorHandler_result'; 426 | 427 | // set bindings 428 | $this->binder->implicitBind('App\Models', '', '', '', function ($e) { 429 | if ($e instanceof \Exception) { 430 | return 'errorHandler_result'; 431 | } 432 | }); 433 | 434 | // resolve bindings (done when route is dispatched) 435 | $r = $this->binder->resolveBindings($this->wildcards); 436 | 437 | // assert resolved bindings 438 | $this->assertSame($this->expected, $r, '-> Exception should be handled to the errorHandler and "errorHandler_result" should be returned and used as the binding result!'); 439 | } 440 | 441 | public function testImplicitBindErrorHandlerWithDefinedMethod() 442 | { 443 | $this->model->shouldReceive('findForRoute')->once() 444 | ->with('wildcard_value')->andThrow(new \Exception); 445 | 446 | $this->wildcards['model'] = 'wildcard_value'; 447 | $this->expected['model'] = 'errorHandler_result'; 448 | 449 | // set bindings 450 | $this->binder->implicitBind('App\Models', '', '', 'findForRoute', function ($e) { 451 | if ($e instanceof \Exception) { 452 | return 'errorHandler_result'; 453 | } 454 | }); 455 | 456 | // resolve bindings (done when route is dispatched) 457 | $r = $this->binder->resolveBindings($this->wildcards); 458 | 459 | // assert resolved bindings 460 | $this->assertSame($this->expected, $r, '-> Exception thrown in the defined method should be handled to the errorHandler and "errorHandler_result" should be returned and used as the binding result!'); 461 | } 462 | 463 | public function testImplicitBindReturnsWithTheFirstMatch() 464 | { 465 | $ArticleRepo = m::mock('overload:App\Repositories\Article'); 466 | $ArticleManager = m::mock('overload:App\Managers\Article'); 467 | 468 | $this->expectWhereRouteKeyNameFirstOrFail($ArticleRepo); 469 | 470 | $this->wildcards['article'] = 'wildcard_value'; 471 | $this->expected['article'] = 'bind_result'; 472 | 473 | // set bindings 474 | $this->binder->implicitBind('App\NotFound'); 475 | $this->binder->implicitBind('App\Repositories'); 476 | $this->binder->implicitBind('App\Managers'); 477 | 478 | // resolve bindings (done when route is dispatched) 479 | $r = $this->binder->resolveBindings($this->wildcards); 480 | 481 | // assert resolved bindings 482 | $this->assertSame($this->expected, $r); 483 | } 484 | 485 | public function testExplicitBindWorksWithImplicitBind() 486 | { 487 | $this->expectWhereRouteKeyNameFirstOrFail($this->model); 488 | $this->expectWhereRouteKeyNameFirstOrFail($this->myTestRepo); 489 | 490 | $this->wildcards['model'] = 'wildcard_value'; 491 | $this->wildcards['myTestRepo'] = 'wildcard_value'; 492 | 493 | $this->expected['model'] = 'bind_result'; 494 | $this->expected['myTestRepo'] = 'bind_result'; 495 | 496 | // set bindings 497 | $this->binder->implicitBind('App\Repositories'); 498 | $this->binder->bind('model', 'App\Models\Model'); 499 | 500 | // resolve bindings (done when route is dispatched) 501 | $r = $this->binder->resolveBindings($this->wildcards); 502 | 503 | // assert resolved bindings 504 | $this->assertSame($this->expected, $r); 505 | } 506 | 507 | public function testExplicitBindTakesPriorityOverImplicitBind() 508 | { 509 | $this->expectWhereRouteKeyNameFirstOrFail($this->myTestRepo); 510 | 511 | $this->wildcards['model'] = 'wildcard_value'; 512 | $this->expected['model'] = 'bind_result'; 513 | 514 | // set bindings 515 | $this->binder->implicitBind('App\Models'); 516 | $this->binder->bind('model', 'App\Repositories\MyTestRepo'); 517 | 518 | // resolve bindings (done when route is dispatched) 519 | $r = $this->binder->resolveBindings($this->wildcards); 520 | 521 | // assert resolved bindings 522 | $this->assertSame($this->expected, $r, '-> Explicit binding should take Priority over implicit binding'); 523 | } 524 | 525 | public function testMultipleBindingsImplicitAndExplicit() 526 | { 527 | $this->wildcards['user'] = 'wildcard_for_user'; 528 | $this->wildcards['article'] = 'wildcard_for_article'; 529 | $this->wildcards['comment'] = 'wildcard_for_comment'; 530 | $this->wildcards['tag'] = 'wildcard_for_tag'; 531 | $this->wildcards['book'] = 'wildcard_value'; 532 | $this->wildcards['car'] = 'wildcard_value'; 533 | $this->wildcards['cat'] = 'wildcard_for_cat'; 534 | 535 | $this->expected['user'] = 'bind_result_for_user'; 536 | $this->expected['article'] = 'bind_result_for_article'; 537 | $this->expected['comment'] = 'bind_result_for_comment'; 538 | $this->expected['tag'] = 'wildcard_for_tag_result'; 539 | $this->expected['book'] = 'bind_result'; 540 | $this->expected['car'] = 'bind_result'; 541 | $this->expected['cat'] = 'bind_result_for_cat'; 542 | 543 | // ============================================================= 544 | $user = m::mock('overload:App\Repos\EloquentUserRepo'); 545 | $article = m::mock('overload:App\Repos\EloquentArticleRepo'); 546 | $comment = m::mock('overload:App\Repos\CommentRepo'); 547 | $tag = m::mock('overload:App\Repos\TagRepo'); 548 | $book = m::mock('overload:App\Models\Book'); 549 | $car = m::mock('overload:App\Models\Car'); 550 | $cat = m::mock('overload:App\Models\Cat'); 551 | 552 | $user->shouldReceive('findEloquentForRoute')->once() 553 | ->with($this->wildcards['user'])->andReturn($this->expected['user']); 554 | 555 | $article->shouldReceive('findEloquentForRoute')->once() 556 | ->with($this->wildcards['article'])->andReturn($this->expected['article']); 557 | 558 | $comment->shouldReceive('findForRoute')->once() 559 | ->with($this->wildcards['comment'])->andReturn($this->expected['comment']); 560 | 561 | $cat->shouldReceive('findCat')->once() 562 | ->with($this->wildcards['cat'])->andReturn($this->expected['cat']); 563 | 564 | $this->expectWhereRouteKeyNameFirstOrFail($book); 565 | $this->expectWhereRouteKeyNameFirstOrFail($car); 566 | // ============================================================= 567 | 568 | // set bindings 569 | $this->binder->implicitBind('App\Repos', 'Eloquent', 'Repo', 'findEloquentForRoute'); 570 | $this->binder->implicitBind('App\Repos', '', 'Repo', 'findForRoute'); 571 | $this->binder->implicitBind('App\Models'); 572 | 573 | $this->binder->bind('tag', function ($val) { 574 | return $val.'_result'; 575 | }); 576 | 577 | $this->binder->bind('book', 'App\Models\Book'); 578 | $this->binder->bind('cat', 'App\Models\Cat@findCat'); 579 | 580 | // resolve bindings (done when route is dispatched) 581 | $r = $this->binder->resolveBindings($this->wildcards); 582 | 583 | // assert resolved bindings 584 | $this->assertSame($this->expected, $r); 585 | } 586 | 587 | public function testCompositeBindAcceptsOnlyArrayOfParts() 588 | { 589 | $this->expectException(\InvalidArgumentException::class); 590 | $this->expectExceptionMessage('Invalid $keys value'); 591 | 592 | // set bindings 593 | $this->binder->compositeBind('string', function () { 594 | // 595 | }); 596 | } 597 | 598 | public function testCompositeBindAcceptsOnlyArrayOfMoreThanOnePart() 599 | { 600 | $this->expectException(\InvalidArgumentException::class); 601 | $this->expectExceptionMessage('Invalid $keys value'); 602 | 603 | // set bindings 604 | $this->binder->compositeBind(['model'], function () { 605 | // 606 | }); 607 | } 608 | 609 | public function testCompositeBindAcceptsArrayOfMoreThanOnePart() 610 | { 611 | // set bindings 612 | $this->binder->compositeBind(['part1', 'part2'], function () { 613 | // 614 | }); 615 | 616 | $this->binder->compositeBind(['part1', 'part2', 'part3'], function () { 617 | // 618 | }); 619 | 620 | $this->binder->compositeBind(['part1', 'part2', 'part3', 'part4'], function () { 621 | // 622 | }); 623 | 624 | $this->assertTrue(true); 625 | } 626 | 627 | public function testCompositeBindWorks() 628 | { 629 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 630 | $this->expected = ['parent' => 'parent_result', 'child' => 'child_result']; 631 | 632 | // set bindings 633 | $this->binder->compositeBind(['parent', 'child'], function ($parent, $child) { 634 | $parent = ($parent === $this->wildcards['parent']) ? $this->expected['parent'] : 'wrong wildcard value for [parent]!'; 635 | $child = ($child === $this->wildcards['child']) ? $this->expected['child'] : 'wrong wildcard value for [child]!'; 636 | return [$parent, $child]; 637 | }); 638 | 639 | // resolve bindings (done when route is dispatched) 640 | $r = $this->binder->resolveBindings($this->wildcards); 641 | 642 | // assert resolved bindings 643 | $this->assertSame($this->expected, $r, '-> Custom closure binding should be called with expected wildcards values and it\'s return should be used as the binding results'); 644 | } 645 | 646 | public function testCompositeBindWorksWithMoreThanTwoWildcards() 647 | { 648 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value', 'grand-child' => 'grand-child_value']; 649 | $this->expected = ['parent' => 'parent_result', 'child' => 'child_result', 'grand-child' => 'grand-child_result']; 650 | 651 | // set bindings 652 | $this->binder->compositeBind(['parent', 'child', 'grand-child'], function ($parent, $child, $grandChild) { 653 | $parent = ($parent === $this->wildcards['parent']) ? $this->expected['parent'] : 'wrong wildcard value for [parent]!'; 654 | $child = ($child === $this->wildcards['child']) ? $this->expected['child'] : 'wrong wildcard value for [child]!'; 655 | $grandChild = ($grandChild === $this->wildcards['grand-child']) ? $this->expected['grand-child'] : 'wrong wildcard value for [grand-child]!'; 656 | return [$parent, $child, $grandChild]; 657 | }); 658 | 659 | // resolve bindings (done when route is dispatched) 660 | $r = $this->binder->resolveBindings($this->wildcards); 661 | 662 | // assert resolved bindings 663 | $this->assertSame($this->expected, $r, '-> Custom closure binding should be called with expected wildcards values and it\'s return should be used as the binding results'); 664 | } 665 | 666 | public function testCompositeBindAcceptsClassAtMethodCallableStyle() 667 | { 668 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 669 | $this->expected = ['parent' => 'parent_result', 'child' => 'child_result']; 670 | 671 | $this->model->shouldReceive('myMethod')->once() 672 | ->with($this->wildcards['parent'], $this->wildcards['child']) 673 | ->andReturn([$this->expected['parent'], $this->expected['child']]); 674 | 675 | // set bindings 676 | $this->binder->compositeBind(['parent', 'child'], 'App\Models\Model@myMethod'); 677 | 678 | // resolve bindings (done when route is dispatched) 679 | $r = $this->binder->resolveBindings($this->wildcards); 680 | 681 | // assert resolved bindings 682 | $this->assertSame($this->expected, $r, '-> Class@method binding should be called with expected wildcards values and it\'s return should be used as the binding results'); 683 | } 684 | 685 | public function testCompositeBindAcceptsOnlyCallableOrClassAtMethodString() 686 | { 687 | $this->expectException(\Exception::class); 688 | $this->expectExceptionMessage('Binder must be'); 689 | 690 | // set bindings 691 | $this->binder->compositeBind(['parent', 'child'], 'App\Models\Model'); 692 | } 693 | 694 | public function testCompositeBindSquawksIfClassNotFound() 695 | { 696 | $this->expectException(\Exception::class); 697 | $this->expectExceptionMessage('[App\Models\NotFoundModel]'); 698 | 699 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 700 | 701 | // set bindings 702 | $this->binder->compositeBind(['parent', 'child'], 'App\Models\NotFoundModel@myMethod'); 703 | 704 | // resolve bindings (done when route is dispatched) 705 | $r = $this->binder->resolveBindings($this->wildcards); 706 | } 707 | 708 | public function testCompositeBindSquawksIfMethodNotFound() 709 | { 710 | $this->expectException(\Exception::class); 711 | $this->expectExceptionMessage('NotFoundMethod'); 712 | 713 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 714 | 715 | // set bindings 716 | $this->binder->compositeBind(['parent', 'child'], 'App\Models\Model@NotFoundMethod'); 717 | 718 | // resolve bindings (done when route is dispatched) 719 | $r = $this->binder->resolveBindings($this->wildcards); 720 | } 721 | 722 | public function testCompositeBindOnlyMatchesTheWholeWildcardsPartsWithTheSameOrder() 723 | { 724 | // ========================================================================= 725 | // 726 | $scenario = 'count does not match "<"'; 727 | $this->resetBinder(); 728 | $this->wildcards = ['wildcard1' => 1, 'wildcard2' => 2]; 729 | 730 | // set bindings 731 | $this->binder->compositeBind(['wildcard1', 'wildcard2', 'wildcard3'], function () { 732 | return [0, 0, 0]; 733 | }); 734 | 735 | // resolve bindings (done when route is dispatched) 736 | $r = $this->binder->resolveBindings($this->wildcards); 737 | 738 | // assert resolved bindings 739 | $this->assertSame($this->wildcards, $r, "-> composite binding should not match ($scenario) so the wildcards should be returned as is without being touched!"); 740 | // ========================================================================= 741 | // 742 | $scenario = 'count does not match ">"'; 743 | $this->resetBinder(); 744 | $this->wildcards = ['wildcard1' => 1, 'wildcard2' => 2, 'wildcard3' => 3]; 745 | 746 | // set bindings 747 | $this->binder->compositeBind(['wildcard1', 'wildcard2'], function () { 748 | return [0, 0]; 749 | }); 750 | 751 | // resolve bindings (done when route is dispatched) 752 | $r = $this->binder->resolveBindings($this->wildcards); 753 | 754 | // assert resolved bindings 755 | $this->assertSame($this->wildcards, $r, "-> composite binding should not match ($scenario) so the wildcards should be returned as is without being touched!"); 756 | // ========================================================================= 757 | // 758 | $scenario = 'first wildcard does not match'; 759 | $this->resetBinder(); 760 | $this->wildcards = ['wildcard1' => 1, 'wildcard2' => 2, 'wildcard3' => 3]; 761 | 762 | // set bindings 763 | $this->binder->compositeBind(['no-match', 'wildcard2', 'wildcard3'], function () { 764 | return [0, 0, 0]; 765 | }); 766 | 767 | // resolve bindings (done when route is dispatched) 768 | $r = $this->binder->resolveBindings($this->wildcards); 769 | 770 | // assert resolved bindings 771 | $this->assertSame($this->wildcards, $r, "-> composite binding should not match ($scenario) so the wildcards should be returned as is without being touched!"); 772 | // ========================================================================= 773 | // 774 | $scenario = 'last wildcard does not match'; 775 | $this->resetBinder(); 776 | $this->wildcards = ['wildcard1' => 1, 'wildcard2' => 2, 'wildcard3' => 3]; 777 | 778 | // set bindings 779 | $this->binder->compositeBind(['wildcard1', 'wildcard2', 'no-match'], function () { 780 | return [0, 0, 0]; 781 | }); 782 | 783 | // resolve bindings (done when route is dispatched) 784 | $r = $this->binder->resolveBindings($this->wildcards); 785 | 786 | // assert resolved bindings 787 | $this->assertSame($this->wildcards, $r, "-> composite binding should not match ($scenario) so the wildcards should be returned as is without being touched!"); 788 | // ========================================================================= 789 | // 790 | $scenario = 'composite order does not match'; 791 | $this->resetBinder(); 792 | $this->wildcards = ['wildcard1' => 1, 'wildcard2' => 2]; 793 | 794 | // set bindings 795 | $this->binder->compositeBind(['wildcard2', 'wildcard1'], function () { 796 | return [0, 0]; 797 | }); 798 | 799 | // resolve bindings (done when route is dispatched) 800 | $r = $this->binder->resolveBindings($this->wildcards); 801 | 802 | // assert resolved bindings 803 | $this->assertSame($this->wildcards, $r, "-> composite binding should not match ($scenario) so the wildcards should be returned as is without being touched!"); 804 | // ========================================================================= 805 | } 806 | 807 | public function testCompositeBindSquawksIfReturnValueIsNotAnArray() 808 | { 809 | $this->expectException(\Exception::class); 810 | $this->expectExceptionMessage('Return value should be'); 811 | 812 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 813 | $this->expected = ['parent' => 'parent_result', 'child' => 'child_result']; 814 | 815 | // set bindings 816 | $this->binder->compositeBind(['parent', 'child'], function () { 817 | return 'NoneArrayValue'; 818 | }); 819 | 820 | // resolve bindings (done when route is dispatched) 821 | $r = $this->binder->resolveBindings($this->wildcards); 822 | } 823 | 824 | public function testCompositeBindSquawksIfReturnValueIsNotAnArrayOfTheSameCountAsTheWildcards() 825 | { 826 | $this->expectException(\Exception::class); 827 | $this->expectExceptionMessage('Return value should be'); 828 | 829 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 830 | $this->expected = ['parent' => 'parent_result', 'child' => 'child_result']; 831 | 832 | // set bindings 833 | $this->binder->compositeBind(['parent', 'child'], function () { 834 | return ['ArrayOfOneItem']; 835 | }); 836 | 837 | // resolve bindings (done when route is dispatched) 838 | $r = $this->binder->resolveBindings($this->wildcards); 839 | } 840 | 841 | public function testCompositeBindRethrowsException() 842 | { 843 | $this->expectException(\Exception::class); 844 | $this->expectExceptionMessage('NotFound'); 845 | 846 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 847 | $this->expected = ['parent' => 'parent_result', 'child' => 'child_result']; 848 | 849 | // set bindings 850 | $this->binder->compositeBind(['parent', 'child'], function () { 851 | throw new \Exception('NotFound'); 852 | }); 853 | 854 | // resolve bindings (done when route is dispatched) 855 | $r = $this->binder->resolveBindings($this->wildcards); 856 | } 857 | 858 | public function testCompositeBindErrorHandler() 859 | { 860 | $this->wildcards = ['parent' => 'parent_value', 'child' => 'child_value']; 861 | $this->expected = ['parent' => 'errorHandler_result1', 'child' => 'errorHandler_result2']; 862 | 863 | // set bindings 864 | $this->binder->compositeBind(['parent', 'child'], function () { 865 | throw new \Exception(); 866 | }, function ($e) { 867 | if ($e instanceof \Exception) { 868 | return ['errorHandler_result1', 'errorHandler_result2']; 869 | } 870 | }); 871 | 872 | // resolve bindings (done when route is dispatched) 873 | $r = $this->binder->resolveBindings($this->wildcards); 874 | 875 | // assert resolved bindings 876 | $this->assertSame($this->expected, $r, '-> Exception thrown in closure should be handled to the errorHandler and then it\'s return should be used as the binding result!'); 877 | } 878 | 879 | public function testCompositeBindTakesPriorityOverOtherBindings() 880 | { 881 | $this->wildcards = ['model' => 'model_value', 'child' => 'child_value']; 882 | $this->expected = ['model' => 'model_result', 'child' => 'child_result']; 883 | 884 | // set bindings 885 | $this->binder->implicitBind('App\Models'); 886 | $this->binder->bind('model', 'App\Repositories\MyTestRepo'); 887 | 888 | $this->binder->compositeBind(['model', 'child'], function () { 889 | return ['model_result', 'child_result']; 890 | }); 891 | 892 | // resolve bindings (done when route is dispatched) 893 | $r = $this->binder->resolveBindings($this->wildcards); 894 | 895 | // assert resolved bindings 896 | $this->assertSame($this->expected, $r, '-> Composite binding should take Priority over explicit binding and implicit binding'); 897 | } 898 | } 899 | --------------------------------------------------------------------------------