├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── .styleci.yml ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── resources └── views │ ├── bootstrap3.blade.php │ ├── bootstrap4.blade.php │ ├── bulma.blade.php │ └── foundation6.blade.php ├── src ├── Breadcrumb.php ├── BreadcrumbLink.php ├── BreadcrumbLinkFactory.php ├── BreadcrumbServiceProvider.php ├── Facades │ └── Breadcrumb.php └── RouteParameterBinder.php └── tests ├── BreadcrumbTest.php ├── TemplateTest.php ├── TestCase.php └── database └── migrations ├── 2018_12_05_000000_create_foos_table.php └── 2018_12_05_000001_create_bars_table.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 12 * * *' 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | php: [7.4, 8.0] 17 | laravel: [^8.0] 18 | dependency-version: [prefer-lowest, prefer-stable] 19 | 20 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | - name: Composer cache 27 | uses: actions/cache@v2 28 | with: 29 | path: ~/.composer/cache/files 30 | key: composer-php-${{ matrix.php }}-laravel-${{ matrix.laravel }}-composer-${{ hashFiles('composer.json') }} 31 | 32 | - name: Setup PHP 33 | uses: shivammathur/setup-php@v2 34 | with: 35 | php-version: ${{ matrix.php }} 36 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, exif, iconv 37 | coverage: none 38 | 39 | - name: Install dependencies 40 | run: | 41 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 42 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 43 | 44 | - name: Execute tests 45 | run: vendor/bin/phpunit 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.vscode 3 | /vendor 4 | composer.lock 5 | .DS_Store 6 | Thumbs.db 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | enabled: 3 | - length_ordered_imports 4 | 5 | disabled: 6 | - alpha_ordered_imports 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kevin Pohl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Add breadcrumbs to your routes 2 | 3 | [![Latest Version](https://img.shields.io/packagist/v/fragkp/laravel-route-breadcrumb.svg?style=flat-square)](https://github.com/fragkp/laravel-route-breadcrumb/releases) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/fragkp/laravel-route-breadcrumb.svg?style=flat-square)](https://packagist.org/packages/fragkp/laravel-route-breadcrumb) 5 | 6 | This package tries to give a simple solution for breadcrumbs. Add breadcrumbs direct to your routes and display them in your views. 7 | 8 | ## Installation 9 | 10 | You can install the package via composer: 11 | 12 | ```bash 13 | composer require fragkp/laravel-route-breadcrumb 14 | ``` 15 | 16 | If you want also use the facade to access the main breadcrumb class, add this line to your facades array in config/app.php: 17 | 18 | ```php 19 | 'Breadcrumb' => Fragkp\LaravelRouteBreadcrumb\Facades\Breadcrumb::class, 20 | ``` 21 | 22 | This package contains some pre-built views for the most active css-frameworks: 23 | - [Bootstrap 3](https://github.com/fragkp/laravel-route-breadcrumb/tree/master/resources/views/bootstrap3.blade.php) 24 | - [Bootstrap 4](https://github.com/fragkp/laravel-route-breadcrumb/tree/master/resources/views/bootstrap4.blade.php) 25 | - [Bulma](https://github.com/fragkp/laravel-route-breadcrumb/tree/master/resources/views/bulma.blade.php) 26 | - [Foundation 6](https://github.com/fragkp/laravel-route-breadcrumb/tree/master/resources/views/foundation6.blade.php) 27 | 28 | If you want to use one of these views, include it in this way: 29 | 30 | ```php 31 | @include('laravel-breadcrumb::bootstrap3') 32 | ``` 33 | 34 | To customize the pre-built views, run this command: 35 | 36 | ```bash 37 | php artisan vendor:publish Fragkp\LaravelRouteBreadcrumb\BreadcrumbServiceProvider --tag=views 38 | ``` 39 | > Note: You could also create your own [custom view](#view-example) to display breadcrumb links. 40 | 41 | ## Usage 42 | 43 | ### Defining breadcrumbs 44 | 45 | #### Basic 46 | 47 | To add a breadcrumb title to your route, call the `breadcrumb` method and pass your title. 48 | ```php 49 | Route::get('/')->breadcrumb('Your custom title'); 50 | ``` 51 | 52 | #### Index 53 | 54 | On some websites, you wish to have always an index inside your breadcrumbs. Use the `breadcrumbIndex` method. 55 | **This method should only be used once.** 56 | > Note: `breadcrumbIndex` sets also the breadcrumb title for this route. 57 | ```php 58 | Route::get('/')->breadcrumbIndex('Start'); 59 | 60 | Route::get('/foo')->breadcrumb('Your custom title'); 61 | ``` 62 | 63 | #### Inside groups 64 | 65 | The `breadcrumb` method will also work inside route groups. 66 | ```php 67 | Route::get('/')->breadcrumbIndex('Start'); 68 | 69 | Route::prefix('/foo')->group(function () { 70 | Route::get('/bar')->breadcrumb('Your custom title'); 71 | }); 72 | ``` 73 | 74 | #### Group index 75 | 76 | Also, it is possible to specify a group index title by calling `breadcrumbGroup`. 77 | **This method should only be used once inside a group.** 78 | > Note: `breadcrumbGroup` sets also the breadcrumb title for this route. 79 | ```php 80 | Route::get('/')->breadcrumbIndex('Start'); 81 | 82 | Route::prefix('/foo')->group(function () { 83 | Route::get('/')->breadcrumbGroup('Foo group index'); 84 | 85 | Route::get('/bar')->breadcrumb('Your custom title'); 86 | }); 87 | ``` 88 | 89 | #### Custom title resolver 90 | 91 | If you want to customize your breadcrumb title, you could pass a closure to all breadcrumb methods. 92 | ```php 93 | Route::get('/')->breadcrumb(function () { 94 | return 'Your custom title'; 95 | }); 96 | ``` 97 | 98 | You could also pass a fully qualified class name. This will invoke your class. 99 | ```php 100 | Route::get('/')->breadcrumb(YourCustomTitleResolver::class); 101 | 102 | class YourCustomTitleResolver 103 | { 104 | public function __invoke() 105 | { 106 | return 'Your custom title'; 107 | } 108 | } 109 | ``` 110 | 111 | You may also pass a callable. 112 | ```php 113 | Route::get('/foo/{id}')->breadcrumb([app('my_breadcrumb_resolver'), 'resolve']); 114 | 115 | // my_breadcrumb_resolver 116 | class MyBreadcrumbResolver 117 | { 118 | public function resolve($id) 119 | { 120 | $title = $this->repo->findById($id); 121 | 122 | return $title->getName(); 123 | } 124 | } 125 | ``` 126 | 127 | ##### Route parameters 128 | 129 | All route parameters will be passed to your resolver. Route model binding is also supported. 130 | ```php 131 | Route::get('/{foo}')->breadcrumb(YourCustomTitleResolver::class); 132 | 133 | class YourCustomTitleResolver 134 | { 135 | public function __invoke(Foo $foo) 136 | { 137 | return "Title: {$foo->title}"; 138 | } 139 | } 140 | ``` 141 | 142 | ### Accessing breadcrumb 143 | 144 | #### Links 145 | 146 | The `links` method will return a `Collection` of `BreadcrumbLink`. 147 | > Note: The array is indexed by the uri. 148 | ```php 149 | app(Breadcrumb::class)->links(); // or use here the facade 150 | ``` 151 | Example result: 152 | ```php 153 | Illuminate\Support\Collection {#266 154 | #items: array:2 [ 155 | "/" => Fragkp\LaravelRouteBreadcrumb\BreadcrumbLink {#41 156 | +uri: "/" 157 | +title: "Start" 158 | } 159 | "foo" => Fragkp\LaravelRouteBreadcrumb\BreadcrumbLink {#262 160 | +uri: "foo" 161 | +title: "Your custom title" 162 | } 163 | ] 164 | } 165 | ``` 166 | 167 | #### Index 168 | 169 | The `index` method will return a single instance of `BreadcrumbLink`. If you haven't defined any index, null is returned. 170 | ```php 171 | app(Breadcrumb::class)->index(); // or use here the facade 172 | ``` 173 | Example result: 174 | ```php 175 | Fragkp\LaravelRouteBreadcrumb\BreadcrumbLink {#36 176 | +uri: "/" 177 | +title: "Start" 178 | } 179 | ``` 180 | 181 | #### Current 182 | 183 | The `current` method will return a single instance of `BreadcrumbLink`. If no route is provided (e.g. on errors), null is returned. 184 | ```php 185 | app(Breadcrumb::class)->current(); // or use here the facade 186 | ``` 187 | Example result: 188 | ```php 189 | Fragkp\LaravelRouteBreadcrumb\BreadcrumbLink {#36 190 | +uri: "/" 191 | +title: "Your custom title" 192 | } 193 | ``` 194 | 195 | #### View example 196 | 197 | A good way to access the breadcrumb inside your views is to bound it via a View Composer. 198 | > For more information about View Composers, have a look at the [Laravel docs](https://laravel.com/docs/5.6/views#view-composers). 199 | ```php 200 | // app/Providers/AppServiceProvider.php 201 | 202 | class AppServiceProvider extends ServiceProvider 203 | { 204 | public function boot() 205 | { 206 | View::composer('your-view', function ($view) { 207 | $view->with('breadcrumb', app(Breadcrumb::class)->links()); 208 | }); 209 | } 210 | } 211 | ``` 212 | ```php 213 | // resources/views/breadcrumb.blade.php 214 | 215 | 222 | ``` 223 | 224 | ## Testing 225 | 226 | ``` bash 227 | ./vendor/bin/phpunit 228 | ``` 229 | 230 | ## License 231 | 232 | MIT License (MIT). Please see [License File](LICENSE.md) for more information. 233 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fragkp/laravel-route-breadcrumb", 3 | "description": "", 4 | "keywords": [ 5 | "laravel", 6 | "route", 7 | "breadcrumb" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Kevin Pohl" 13 | } 14 | ], 15 | "require": { 16 | "php": "^7.4|^8.0", 17 | "laravel/framework": "^8.0" 18 | }, 19 | "require-dev": { 20 | "orchestra/testbench": "^6.0", 21 | "orchestra/database": "^6.0", 22 | "phpunit/phpunit": "^9.4", 23 | "mockery/mockery": "^1.4" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Fragkp\\LaravelRouteBreadcrumb\\": "src" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Fragkp\\LaravelRouteBreadcrumb\\Tests\\": "tests" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit", 37 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 38 | }, 39 | "config": { 40 | "sort-packages": true 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "Fragkp\\LaravelRouteBreadcrumb\\BreadcrumbServiceProvider" 48 | ], 49 | "aliases": { 50 | "Breadcrumb": "Fragkp\\LaravelRouteBreadcrumb\\Facades\\Breadcrumb" 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /resources/views/bootstrap3.blade.php: -------------------------------------------------------------------------------- 1 | @if ($breadcrumb->isNotEmpty()) 2 | 13 | @endif 14 | -------------------------------------------------------------------------------- /resources/views/bootstrap4.blade.php: -------------------------------------------------------------------------------- 1 | @if ($breadcrumb->isNotEmpty()) 2 | 15 | @endif 16 | -------------------------------------------------------------------------------- /resources/views/bulma.blade.php: -------------------------------------------------------------------------------- 1 | @if ($breadcrumb->isNotEmpty()) 2 | 17 | @endif 18 | -------------------------------------------------------------------------------- /resources/views/foundation6.blade.php: -------------------------------------------------------------------------------- 1 | @if ($breadcrumb->isNotEmpty()) 2 | 17 | @endif 18 | -------------------------------------------------------------------------------- /src/Breadcrumb.php: -------------------------------------------------------------------------------- 1 | router = $router; 35 | $this->request = $request; 36 | } 37 | 38 | /** 39 | * @return \Fragkp\LaravelRouteBreadcrumb\BreadcrumbLink|null 40 | */ 41 | public function index() 42 | { 43 | $indexRoute = $this->routes()->first(function (Route $route) { 44 | return $route->getAction('breadcrumbIndex'); 45 | }); 46 | 47 | if (! $indexRoute) { 48 | return; 49 | } 50 | 51 | return app(BreadcrumbLinkFactory::class)->create($indexRoute->uri(), $indexRoute); 52 | } 53 | 54 | /** 55 | * @return \Illuminate\Support\Collection 56 | */ 57 | public function links() 58 | { 59 | $links = $this->groupLinks(); 60 | 61 | if ($indexLink = $this->index()) { 62 | $links->prepend($indexLink, $indexLink->uri); 63 | } 64 | 65 | if ($currentLink = $this->current()) { 66 | $links->put($currentLink->uri, $currentLink); 67 | } 68 | 69 | if ($indexLink && is_null($currentLink)) { 70 | return Collection::make([$indexLink->uri => $indexLink]); 71 | } 72 | 73 | return $links; 74 | } 75 | 76 | /** 77 | * @return \Illuminate\Support\Collection 78 | */ 79 | protected function groupLinks() 80 | { 81 | $pathPrefixes = $this->groupPrefixes( 82 | $this->request->path() 83 | ); 84 | 85 | $routeUriPrefixes = $this->groupPrefixes( 86 | optional($this->request->route())->uri() ?? '' 87 | ); 88 | 89 | return $this->routes() 90 | ->filter(function (Route $route) use ($routeUriPrefixes) { 91 | return in_array($route->uri(), $routeUriPrefixes, true); 92 | }) 93 | ->filter(function (Route $route) { 94 | return $route->getAction('breadcrumb') && $route->getAction('breadcrumbGroup'); 95 | }) 96 | ->mapWithKeys(function (Route $route) use ($pathPrefixes) { 97 | $routeUri = $pathPrefixes[substr_count($route->uri(), '/') + 1]; 98 | 99 | return [$routeUri => app(BreadcrumbLinkFactory::class)->create($routeUri, $route)]; 100 | }); 101 | } 102 | 103 | /** 104 | * @return \Fragkp\LaravelRouteBreadcrumb\BreadcrumbLink|null 105 | */ 106 | public function current() 107 | { 108 | $route = $this->request->route(); 109 | 110 | if (! $route || ! $route->getAction('breadcrumb')) { 111 | return; 112 | } 113 | 114 | return app(BreadcrumbLinkFactory::class)->create($this->request->path(), $route); 115 | } 116 | 117 | /** 118 | * @return \Illuminate\Support\Collection|\Illuminate\Routing\Route[] 119 | */ 120 | protected function routes() 121 | { 122 | $breadcrumbCollection = ($route = $this->request->route()) 123 | ? $route->getAction('breadcrumbCollection') 124 | : null; 125 | 126 | if ($this->routes) { 127 | return $this->routes; 128 | } 129 | 130 | $routes = Collection::make($this->router->getRoutes()->getRoutes()); 131 | 132 | if (! is_null($breadcrumbCollection)) { 133 | $routes = $routes->filter(function (Route $route) use ($breadcrumbCollection) { 134 | return $route->getAction('breadcrumbCollection') === $breadcrumbCollection; 135 | }); 136 | } 137 | 138 | return $this->routes = $routes; 139 | } 140 | 141 | /** 142 | * @param string $currentPath 143 | * @return array 144 | */ 145 | protected function groupPrefixes(string $currentPath) 146 | { 147 | $prefixes = explode('/', $currentPath); 148 | 149 | $prefixes = array_map(function ($prefix) use ($prefixes) { 150 | $startPrefixes = implode('/', array_slice($prefixes, 0, array_search($prefix, $prefixes))); 151 | 152 | return ltrim("{$startPrefixes}/{$prefix}", '/'); 153 | }, $prefixes); 154 | 155 | $prefixes = array_filter($prefixes); 156 | 157 | return Arr::prepend($prefixes, '/'); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/BreadcrumbLink.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 24 | $this->title = $title; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/BreadcrumbLinkFactory.php: -------------------------------------------------------------------------------- 1 | request = $request; 23 | } 24 | 25 | /** 26 | * @param string $uri 27 | * @param \Illuminate\Routing\Route $route 28 | * @return \Fragkp\LaravelRouteBreadcrumb\BreadcrumbLink|null 29 | */ 30 | public function create(string $uri, Route $route) 31 | { 32 | try { 33 | $route = RouteParameterBinder::bind($this->request, $route); 34 | 35 | $resolvedTitle = static::resolveTitle( 36 | $route->getAction('breadcrumb'), 37 | static::routeParameters($route) 38 | ); 39 | } catch (Throwable $error) { 40 | return; 41 | } 42 | 43 | return new BreadcrumbLink($uri, $resolvedTitle); 44 | } 45 | 46 | /** 47 | * @param \Illuminate\Routing\Route $route 48 | * @return array 49 | */ 50 | protected static function routeParameters(Route $route) 51 | { 52 | return $route->hasParameters() 53 | ? array_values($route->parameters()) 54 | : []; 55 | } 56 | 57 | /** 58 | * @param string|callable|\Closure $title 59 | * @param array $parameters 60 | * @return string 61 | */ 62 | protected static function resolveTitle($title, array $parameters) 63 | { 64 | if ($title instanceof Closure) { 65 | return $title(...$parameters); 66 | } 67 | 68 | if (is_string($title) && class_exists($title)) { 69 | return app($title)(...$parameters); 70 | } 71 | 72 | if (is_array($title) && is_callable($title)) { 73 | return $title(...$parameters); 74 | } 75 | 76 | return $title; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/BreadcrumbServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/../resources/views/', 'laravel-breadcrumb'); 19 | 20 | $this->publishes([ 21 | __DIR__.'/../resources/views' => base_path('resources/views/vendor/laravel-breadcrumb'), 22 | ], 'views'); 23 | 24 | $this->app->singleton(Breadcrumb::class); 25 | 26 | View::composer('laravel-breadcrumb::*', function (\Illuminate\View\View $view) { 27 | $view->with('breadcrumb', $this->app->make(Breadcrumb::class)->links()); 28 | }); 29 | } 30 | 31 | /** 32 | * Register the application services. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | if (! Route::hasMacro('breadcrumb')) { 39 | Route::macro('breadcrumb', function ($title) { 40 | $this->action['breadcrumb'] = $title; 41 | 42 | return $this; 43 | }); 44 | } 45 | 46 | if (! Route::hasMacro('breadcrumbIndex')) { 47 | Route::macro('breadcrumbIndex', function ($title) { 48 | $this->action['breadcrumbIndex'] = true; 49 | $this->action['breadcrumb'] = $title; 50 | 51 | return $this; 52 | }); 53 | } 54 | 55 | if (! Route::hasMacro('breadcrumbGroup')) { 56 | Route::macro('breadcrumbGroup', function ($title) { 57 | $this->action['breadcrumbGroup'] = true; 58 | $this->action['breadcrumb'] = $title; 59 | 60 | return $this; 61 | }); 62 | } 63 | 64 | if (! Route::hasMacro('breadcrumbCollection')) { 65 | Route::macro('breadcrumbCollection', function (string $collection) { 66 | $this->action['breadcrumbCollection'] = $collection; 67 | 68 | return $this; 69 | }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Facades/Breadcrumb.php: -------------------------------------------------------------------------------- 1 | getCompiled())) { 19 | return $route; 20 | } 21 | 22 | $compiledRouteParameters = $compiledRoute->getVariables(); 23 | 24 | if (! empty($compiledRouteParameters)) { 25 | $currentParameters = $request->route()->parameters(); 26 | 27 | $route->bind(new Request); 28 | 29 | foreach (Arr::only($currentParameters, $compiledRouteParameters) as $name => $parameter) { 30 | $route->setParameter($name, $parameter); 31 | } 32 | } 33 | 34 | return $route; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/BreadcrumbTest.php: -------------------------------------------------------------------------------- 1 | get('/foo')->assertSuccessful(); 20 | 21 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 22 | 23 | $this->assertCount(0, $breadcrumbLinks); 24 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 25 | $this->assertEmpty($breadcrumbLinks); 26 | 27 | $this->assertNull(app(Breadcrumb::class)->current()); 28 | $this->assertNull(app(Breadcrumb::class)->index()); 29 | } 30 | 31 | /** @test */ 32 | public function it_is_empty_when_no_route_is_found() 33 | { 34 | $this->get('/foo')->assertStatus(404); 35 | 36 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 37 | 38 | $this->assertCount(0, $breadcrumbLinks); 39 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 40 | $this->assertEmpty($breadcrumbLinks); 41 | 42 | $this->assertNull(app(Breadcrumb::class)->current()); 43 | $this->assertNull(app(Breadcrumb::class)->index()); 44 | } 45 | 46 | /** @test */ 47 | public function it_is_empty_when_an_error_occurs() 48 | { 49 | Route::get('/foo', function () { 50 | throw new \Exception; 51 | }); 52 | 53 | $this->get('/foo')->assertStatus(500); 54 | 55 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 56 | 57 | $this->assertCount(0, $breadcrumbLinks); 58 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 59 | $this->assertEmpty($breadcrumbLinks); 60 | 61 | $this->assertNull(app(Breadcrumb::class)->current()); 62 | $this->assertNull(app(Breadcrumb::class)->index()); 63 | } 64 | 65 | /** @test */ 66 | public function it_returns_always_the_breadcrumb_index() 67 | { 68 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 69 | 70 | Route::get('/foo', function () { 71 | throw new \Exception; 72 | }); 73 | 74 | $this->get('/foo')->assertStatus(500); 75 | 76 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 77 | 78 | $this->assertCount(1, $breadcrumbLinks); 79 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 80 | $this->assertEquals(new Collection([ 81 | '/' => new BreadcrumbLink('/', 'Start'), 82 | ]), $breadcrumbLinks); 83 | 84 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 85 | 86 | $this->assertNull(app(Breadcrumb::class)->current()); 87 | } 88 | 89 | /** @test */ 90 | public function it_returns_only_the_matched_breadcrumb() 91 | { 92 | Route::get('/foo', TestBreadcrumbController::class)->breadcrumb('Foo'); 93 | Route::get('/bar/camp', TestBreadcrumbController::class)->breadcrumb('Bar'); 94 | Route::get('/zoo/deep/crew', TestBreadcrumbController::class)->breadcrumb('Zoo'); 95 | Route::get('/baz', TestBreadcrumbController::class)->breadcrumb('Baz'); 96 | 97 | $this->get('/zoo/deep/crew')->assertSuccessful(); 98 | 99 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 100 | 101 | $this->assertCount(1, $breadcrumbLinks); 102 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 103 | $this->assertEquals(new Collection([ 104 | 'zoo/deep/crew' => new BreadcrumbLink('zoo/deep/crew', 'Zoo'), 105 | ]), $breadcrumbLinks); 106 | 107 | $this->assertNull(app(Breadcrumb::class)->index()); 108 | 109 | $this->assertEquals(new BreadcrumbLink('zoo/deep/crew', 'Zoo'), app(Breadcrumb::class)->current()); 110 | } 111 | 112 | /** @test */ 113 | public function it_returns_only_the_matched_and_defined_index_breadcrumbs() 114 | { 115 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 116 | Route::get('/foo', TestBreadcrumbController::class)->breadcrumb('First'); 117 | 118 | $this->get('/foo')->assertSuccessful(); 119 | 120 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 121 | 122 | $this->assertCount(2, $breadcrumbLinks); 123 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 124 | $this->assertEquals(new Collection([ 125 | '/' => new BreadcrumbLink('/', 'Start'), 126 | 'foo' => new BreadcrumbLink('foo', 'First'), 127 | ]), $breadcrumbLinks); 128 | 129 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 130 | 131 | $this->assertEquals(new BreadcrumbLink('foo', 'First'), app(Breadcrumb::class)->current()); 132 | } 133 | 134 | /** @test */ 135 | public function it_returns_always_the_first_index() 136 | { 137 | Route::get('/bar', TestBreadcrumbController::class)->breadcrumbIndex('Start first'); 138 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start second'); 139 | Route::get('/zoo', TestBreadcrumbController::class)->breadcrumbIndex('Start third'); 140 | Route::get('/foo', TestBreadcrumbController::class)->breadcrumb('First'); 141 | 142 | $this->get('/foo')->assertSuccessful(); 143 | 144 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 145 | 146 | $this->assertCount(2, $breadcrumbLinks); 147 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 148 | $this->assertEquals(new Collection([ 149 | 'bar' => new BreadcrumbLink('bar', 'Start first'), 150 | 'foo' => new BreadcrumbLink('foo', 'First'), 151 | ]), $breadcrumbLinks); 152 | 153 | $this->assertEquals(new BreadcrumbLink('bar', 'Start first'), app(Breadcrumb::class)->index()); 154 | 155 | $this->assertEquals(new BreadcrumbLink('foo', 'First'), app(Breadcrumb::class)->current()); 156 | } 157 | 158 | /** @test */ 159 | public function it_handles_same_uri_groups_as_different_breadcrumbs() 160 | { 161 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 162 | Route::get('/bar', TestBreadcrumbController::class)->breadcrumbGroup('bar'); 163 | Route::get('/bar/bar', TestBreadcrumbController::class)->breadcrumb('bar bar'); 164 | 165 | $this->get('/bar/bar')->assertSuccessful(); 166 | 167 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 168 | 169 | $this->assertCount(3, $breadcrumbLinks); 170 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 171 | $this->assertEquals(new Collection([ 172 | '/' => new BreadcrumbLink('/', 'Start'), 173 | 'bar' => new BreadcrumbLink('bar', 'bar'), 174 | 'bar/bar' => new BreadcrumbLink('bar/bar', 'bar bar'), 175 | ]), $breadcrumbLinks); 176 | 177 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 178 | 179 | $this->assertEquals(new BreadcrumbLink('bar/bar', 'bar bar'), app(Breadcrumb::class)->current()); 180 | } 181 | 182 | /** @test */ 183 | public function it_handles_route_collection() 184 | { 185 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 186 | Route::get('/bar', TestBreadcrumbController::class)->breadcrumbGroup('bar'); 187 | Route::get('/bar/bar', TestBreadcrumbController::class)->breadcrumb('bar bar'); 188 | 189 | $this->get('/bar/bar')->assertSuccessful(); 190 | 191 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 192 | 193 | $this->assertCount(3, $breadcrumbLinks); 194 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 195 | $this->assertEquals(new Collection([ 196 | '/' => new BreadcrumbLink('/', 'Start'), 197 | 'bar' => new BreadcrumbLink('bar', 'bar'), 198 | 'bar/bar' => new BreadcrumbLink('bar/bar', 'bar bar'), 199 | ]), $breadcrumbLinks); 200 | 201 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 202 | 203 | $this->assertEquals(new BreadcrumbLink('bar/bar', 'bar bar'), app(Breadcrumb::class)->current()); 204 | } 205 | 206 | /** @test */ 207 | public function it_can_handle_the_breadcrumb_title_by_closure() 208 | { 209 | Route::get('/foo', TestBreadcrumbController::class)->breadcrumb(function () { 210 | return 'Closure title'; 211 | }); 212 | 213 | $this->get('/foo')->assertSuccessful(); 214 | 215 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 216 | 217 | $this->assertCount(1, $breadcrumbLinks); 218 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 219 | $this->assertEquals(new Collection([ 220 | 'foo' => new BreadcrumbLink('foo', 'Closure title'), 221 | ]), $breadcrumbLinks); 222 | 223 | $this->assertEquals(new BreadcrumbLink('foo', 'Closure title'), app(Breadcrumb::class)->current()); 224 | } 225 | 226 | /** @test */ 227 | public function it_can_handle_the_breadcrumb_title_by_callable() 228 | { 229 | $resolver = new CustomTitleResolver(); 230 | 231 | Route::get('/foo', TestBreadcrumbController::class)->breadcrumb([$resolver, 'getTitle']); 232 | 233 | $this->get('/foo')->assertSuccessful(); 234 | 235 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 236 | 237 | $this->assertCount(1, $breadcrumbLinks); 238 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 239 | $this->assertEquals(new Collection([ 240 | 'foo' => new BreadcrumbLink('foo', 'Custom title'), 241 | ]), $breadcrumbLinks); 242 | 243 | $this->assertEquals(new BreadcrumbLink('foo', 'Custom title'), app(Breadcrumb::class)->current()); 244 | } 245 | 246 | /** @test */ 247 | public function it_can_handle_the_breadcrumb_title_by_callable_with_parameters() 248 | { 249 | $resolver = new CustomTitleResolver(); 250 | 251 | Route::get('/products/{category}/{name}', TestBreadcrumbController::class)->breadcrumb([$resolver, 'getTitleWithParameters']); 252 | 253 | $this->get('/products/shirts/foo')->assertSuccessful(); 254 | 255 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 256 | 257 | $this->assertCount(1, $breadcrumbLinks); 258 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 259 | $this->assertEquals(new Collection([ 260 | 'products/shirts/foo' => new BreadcrumbLink('products/shirts/foo', 'Custom title: shirts > foo'), 261 | ]), $breadcrumbLinks); 262 | 263 | $this->assertEquals(new BreadcrumbLink('products/shirts/foo', 'Custom title: shirts > foo'), app(Breadcrumb::class)->current()); 264 | } 265 | 266 | /** @test */ 267 | public function it_can_handle_the_breadcrumb_title_by_custom_class() 268 | { 269 | Route::get('/foo', TestBreadcrumbController::class)->breadcrumb(CustomTitleResolver::class); 270 | 271 | $this->get('/foo')->assertSuccessful(); 272 | 273 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 274 | 275 | $this->assertCount(1, $breadcrumbLinks); 276 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 277 | $this->assertEquals(new Collection([ 278 | 'foo' => new BreadcrumbLink('foo', 'Class title'), 279 | ]), $breadcrumbLinks); 280 | 281 | $this->assertEquals(new BreadcrumbLink('foo', 'Class title'), app(Breadcrumb::class)->current()); 282 | } 283 | 284 | /** @test */ 285 | public function it_returns_the_breadcrumb_inside_a_group() 286 | { 287 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 288 | 289 | Route::prefix('foo')->group(function () { 290 | Route::get('/bar', TestBreadcrumbController::class)->breadcrumb('Inside group'); 291 | }); 292 | 293 | $this->get('/foo/bar')->assertSuccessful(); 294 | 295 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 296 | 297 | $this->assertCount(2, $breadcrumbLinks); 298 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 299 | $this->assertEquals(new Collection([ 300 | '/' => new BreadcrumbLink('/', 'Start'), 301 | 'foo/bar' => new BreadcrumbLink('foo/bar', 'Inside group'), 302 | ]), $breadcrumbLinks); 303 | 304 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 305 | 306 | $this->assertEquals(new BreadcrumbLink('foo/bar', 'Inside group'), app(Breadcrumb::class)->current()); 307 | } 308 | 309 | /** @test */ 310 | public function it_returns_the_breadcrumb_inside_a_group_with_the_group_index() 311 | { 312 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 313 | 314 | Route::prefix('foo')->group(function () { 315 | Route::get('/', TestBreadcrumbController::class)->breadcrumbGroup('Inside group - index'); 316 | Route::get('/bar', TestBreadcrumbController::class)->breadcrumb('Inside group - bar'); 317 | }); 318 | 319 | $this->get('/foo/bar')->assertSuccessful(); 320 | 321 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 322 | 323 | $this->assertCount(3, $breadcrumbLinks); 324 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 325 | $this->assertEquals(new Collection([ 326 | '/' => new BreadcrumbLink('/', 'Start'), 327 | 'foo' => new BreadcrumbLink('foo', 'Inside group - index'), 328 | 'foo/bar' => new BreadcrumbLink('foo/bar', 'Inside group - bar'), 329 | ]), $breadcrumbLinks); 330 | 331 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 332 | 333 | $this->assertEquals(new BreadcrumbLink('foo/bar', 'Inside group - bar'), app(Breadcrumb::class)->current()); 334 | } 335 | 336 | /** @test */ 337 | public function it_can_handle_multiple_nested_groups() 338 | { 339 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 340 | 341 | Route::prefix('foo')->group(function () { 342 | Route::get('/', TestBreadcrumbController::class)->breadcrumbGroup('Inside group - index'); 343 | Route::get('/bar', TestBreadcrumbController::class)->breadcrumb('Inside group - bar'); 344 | 345 | Route::prefix('baz')->group(function () { 346 | Route::get('/zoo', TestBreadcrumbController::class)->breadcrumb('Inside nested group - zoo'); 347 | 348 | Route::prefix('too')->group(function () { 349 | Route::get('/', TestBreadcrumbController::class)->breadcrumbGroup('Inside nested group - group - index'); 350 | Route::get('/crew', TestBreadcrumbController::class)->breadcrumb('Inside nested group - group - crew'); 351 | }); 352 | }); 353 | }); 354 | 355 | $this->get('/foo/baz/too/crew')->assertSuccessful(); 356 | 357 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 358 | 359 | $this->assertCount(4, $breadcrumbLinks); 360 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 361 | $this->assertEquals(new Collection([ 362 | '/' => new BreadcrumbLink('/', 'Start'), 363 | 'foo' => new BreadcrumbLink('foo', 'Inside group - index'), 364 | 'foo/baz/too' => new BreadcrumbLink('foo/baz/too', 'Inside nested group - group - index'), 365 | 'foo/baz/too/crew' => new BreadcrumbLink('foo/baz/too/crew', 'Inside nested group - group - crew'), 366 | ]), $breadcrumbLinks); 367 | 368 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 369 | 370 | $this->assertEquals(new BreadcrumbLink('foo/baz/too/crew', 'Inside nested group - group - crew'), app(Breadcrumb::class)->current()); 371 | } 372 | 373 | /** @test */ 374 | public function it_can_handle_route_model_binding() 375 | { 376 | $this->migrate(); 377 | 378 | Foo::create(); 379 | 380 | Route::middleware(SubstituteBindings::class)->get('/binding/{foo}', function (Foo $foo) { 381 | return $foo->id; 382 | })->breadcrumb('First'); 383 | 384 | $this->get('/binding/1')->assertSuccessful()->assertSee('1'); 385 | 386 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 387 | 388 | $this->assertCount(1, $breadcrumbLinks); 389 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 390 | $this->assertEquals(new Collection([ 391 | 'binding/1' => new BreadcrumbLink('binding/1', 'First'), 392 | ]), $breadcrumbLinks); 393 | 394 | $this->assertEquals(new BreadcrumbLink('binding/1', 'First'), app(Breadcrumb::class)->current()); 395 | } 396 | 397 | /** @test */ 398 | public function it_can_handle_route_model_binding_and_resolves_title_by_closure() 399 | { 400 | $this->migrate(); 401 | 402 | Foo::create(); 403 | 404 | Route::middleware(SubstituteBindings::class)->get('/binding/{foo}', function (Foo $foo) { 405 | return $foo->id; 406 | })->breadcrumb(function (Foo $foo) { 407 | return "Id: {$foo->id}"; 408 | }); 409 | 410 | $this->get('/binding/1')->assertSuccessful()->assertSee('1'); 411 | 412 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 413 | 414 | $this->assertCount(1, $breadcrumbLinks); 415 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 416 | $this->assertEquals(new Collection([ 417 | 'binding/1' => new BreadcrumbLink('binding/1', 'Id: 1'), 418 | ]), $breadcrumbLinks); 419 | 420 | $this->assertEquals(new BreadcrumbLink('binding/1', 'Id: 1'), app(Breadcrumb::class)->current()); 421 | } 422 | 423 | /** @test */ 424 | public function it_can_handle_route_model_binding_and_resolves_title_by_callable() 425 | { 426 | $this->migrate(); 427 | 428 | Foo::create(); 429 | 430 | $resolver = new CustomTitleResolver(); 431 | 432 | Route::middleware(SubstituteBindings::class)->get('/binding/{foo}', function (Foo $foo) { 433 | return $foo->id; 434 | })->breadcrumb([$resolver, 'getTitleFromModel']); 435 | 436 | $this->get('/binding/1')->assertSuccessful()->assertSee('1'); 437 | 438 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 439 | 440 | $this->assertCount(1, $breadcrumbLinks); 441 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 442 | $this->assertEquals(new Collection([ 443 | 'binding/1' => new BreadcrumbLink('binding/1', 'Custom title: 1'), 444 | ]), $breadcrumbLinks); 445 | 446 | $this->assertEquals(new BreadcrumbLink('binding/1', 'Custom title: 1'), app(Breadcrumb::class)->current()); 447 | } 448 | 449 | /** @test */ 450 | public function it_can_handle_route_model_binding_and_resolves_title_by_custom_class() 451 | { 452 | $this->migrate(); 453 | 454 | Foo::create(); 455 | Foo::create(); 456 | 457 | Route::middleware(SubstituteBindings::class)->get('/binding/{foo}', function (Foo $foo) { 458 | return $foo->id; 459 | })->breadcrumb(CustomRouteModelBindingTitleResolver::class); 460 | 461 | $this->get('/binding/2')->assertSuccessful()->assertSee('2'); 462 | 463 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 464 | 465 | $this->assertCount(1, $breadcrumbLinks); 466 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 467 | $this->assertEquals(new Collection([ 468 | 'binding/2' => new BreadcrumbLink('binding/2', 'Id: 2'), 469 | ]), $breadcrumbLinks); 470 | 471 | $this->assertEquals(new BreadcrumbLink('binding/2', 'Id: 2'), app(Breadcrumb::class)->current()); 472 | } 473 | 474 | /** @test */ 475 | public function it_can_handle_multiple_route_model_bindings_inside_groups() 476 | { 477 | $this->migrate(); 478 | 479 | Foo::create(); 480 | Bar::create(); 481 | Bar::create(); 482 | Bar::create(); 483 | Bar::create(); 484 | Bar::create(); 485 | 486 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 487 | 488 | Route::prefix('first-group')->group(function () { 489 | Route::get('/')->breadcrumbGroup('Inside first group'); 490 | 491 | Route::prefix('second-group')->group(function () { 492 | Route::get('/')->breadcrumbGroup('Inside second group'); 493 | 494 | Route::middleware(SubstituteBindings::class)->get('/{foo}/{bar}', function (Foo $foo, Bar $bar) { 495 | return $foo->id.'-'.$bar->id; 496 | })->breadcrumb('Binding'); 497 | }); 498 | }); 499 | 500 | $this->get('/first-group/second-group/1/5')->assertSuccessful()->assertSee('1-5'); 501 | 502 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 503 | 504 | $this->assertCount(4, $breadcrumbLinks); 505 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 506 | $this->assertEquals(new Collection([ 507 | '/' => new BreadcrumbLink('/', 'Start'), 508 | 'first-group' => new BreadcrumbLink('first-group', 'Inside first group'), 509 | 'first-group/second-group' => new BreadcrumbLink('first-group/second-group', 'Inside second group'), 510 | 'first-group/second-group/1/5' => new BreadcrumbLink('first-group/second-group/1/5', 'Binding'), 511 | ]), $breadcrumbLinks); 512 | 513 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 514 | 515 | $this->assertEquals(new BreadcrumbLink('first-group/second-group/1/5', 'Binding'), app(Breadcrumb::class)->current()); 516 | } 517 | 518 | /** @test */ 519 | public function it_can_handle_custom_route_model_binding() 520 | { 521 | Route::bind('customBinding', function ($value) { 522 | return new CustomBinding($value); 523 | }); 524 | 525 | Route::middleware(SubstituteBindings::class)->get('/binding/{customBinding}', function (CustomBinding $customBinding) { 526 | return $customBinding->value; 527 | })->breadcrumb('First'); 528 | 529 | $this->get('/binding/foo')->assertSuccessful()->assertSee('foo'); 530 | 531 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 532 | 533 | $this->assertCount(1, $breadcrumbLinks); 534 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 535 | $this->assertEquals(new Collection([ 536 | 'binding/foo' => new BreadcrumbLink('binding/foo', 'First'), 537 | ]), $breadcrumbLinks); 538 | 539 | $this->assertEquals(new BreadcrumbLink('binding/foo', 'First'), app(Breadcrumb::class)->current()); 540 | } 541 | 542 | /** @test */ 543 | public function it_can_handle_nested_route_parameters_with_route_model_binding_for_group_index() 544 | { 545 | Route::bind('customBinding', function ($value) { 546 | return new CustomBinding($value); 547 | }); 548 | 549 | Route::bind('secondBinding', function ($value) { 550 | return new SecondBinding($value); 551 | }); 552 | 553 | Route::middleware(SubstituteBindings::class)->group(function () { 554 | Route::get('/binding/{customBinding}', function (CustomBinding $customBinding) { 555 | return $customBinding->value; 556 | })->breadcrumbGroup(function (CustomBinding $customBinding) { 557 | return $customBinding->value; 558 | }); 559 | 560 | Route::get('/binding/{customBinding}/{secondBinding}', function (CustomBinding $customBinding, SecondBinding $secondBinding) { 561 | return $customBinding->value.'-'.$secondBinding->value; 562 | })->breadcrumb(function (CustomBinding $customBinding, SecondBinding $secondBinding) { 563 | return $customBinding->value.'-'.$secondBinding->value; 564 | }); 565 | }); 566 | 567 | $this->get('/binding/foo/bar')->assertSuccessful()->assertSee('foo-bar'); 568 | 569 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 570 | 571 | $this->assertCount(2, $breadcrumbLinks); 572 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 573 | $this->assertEquals(new Collection([ 574 | 'binding/foo' => new BreadcrumbLink('binding/foo', 'foo'), 575 | 'binding/foo/bar' => new BreadcrumbLink('binding/foo/bar', 'foo-bar'), 576 | ]), $breadcrumbLinks); 577 | 578 | $this->assertEquals(new BreadcrumbLink('binding/foo/bar', 'foo-bar'), app(Breadcrumb::class)->current()); 579 | } 580 | 581 | /** @test */ 582 | public function it_returns_the_index_when_nested_routes_with_parameters_doesnt_match_route_binding_current() 583 | { 584 | Route::bind('customBinding', function ($value) { 585 | return new CustomBinding($value); 586 | }); 587 | 588 | Route::bind('secondBinding', function () { 589 | return abort(404); 590 | }); 591 | 592 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 593 | 594 | Route::middleware(SubstituteBindings::class)->group(function () { 595 | Route::get('/binding/{customBinding}', function (CustomBinding $customBinding) { 596 | return $customBinding->value; 597 | })->breadcrumbGroup(function (CustomBinding $customBinding) { 598 | return $customBinding->value; 599 | }); 600 | 601 | Route::get('/binding/{customBinding}/{parameter}', function (CustomBinding $customBinding, $parameters) { 602 | return $customBinding->value.'-'.$parameters; 603 | })->breadcrumb(function (CustomBinding $customBinding, $parameters) { 604 | return $customBinding->value.'-'.$parameters; 605 | }); 606 | 607 | Route::get('/binding/{customBinding}/{parameter}/{secondBinding}', function (CustomBinding $customBinding, int $parameter, SecondBinding $secondBinding) { 608 | return $customBinding->value.'-'.$parameter.'-'.$secondBinding->value; 609 | })->breadcrumb(function (CustomBinding $customBinding, int $parameter, SecondBinding $secondBinding) { 610 | return $customBinding->value.'-'.$parameter.'-'.$secondBinding->value; 611 | }); 612 | }); 613 | 614 | $this->get('/binding/foo/bar/baz')->assertStatus(404); 615 | 616 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 617 | 618 | $this->assertCount(1, $breadcrumbLinks); 619 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 620 | $this->assertEquals(new Collection([ 621 | '/' => new BreadcrumbLink('/', 'Start'), 622 | ]), $breadcrumbLinks); 623 | 624 | $this->assertNull(app(Breadcrumb::class)->current()); 625 | 626 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 627 | } 628 | 629 | /** @test */ 630 | public function it_returns_the_index_when_nested_routes_with_parameters_doesnt_match_route_binding_links() 631 | { 632 | Route::bind('customBinding', function ($value) { 633 | return new CustomBinding($value); 634 | }); 635 | 636 | Route::bind('secondBinding', function ($value) { 637 | return abort(404); 638 | }); 639 | 640 | Route::get('/', TestBreadcrumbController::class)->breadcrumbIndex('Start'); 641 | 642 | Route::middleware(SubstituteBindings::class)->group(function () { 643 | Route::get('/binding/{customBinding}', function (CustomBinding $customBinding) { 644 | return $customBinding->value; 645 | })->breadcrumbGroup(function (CustomBinding $customBinding) { 646 | return $customBinding->value; 647 | }); 648 | 649 | Route::get('/binding/{customBinding}/{secondBinding}', function (CustomBinding $customBinding, SecondBinding $secondBinding) { 650 | return $customBinding->value.'-'.$secondBinding->value; 651 | })->breadcrumb(function (CustomBinding $customBinding, SecondBinding $secondBinding) { 652 | return $customBinding->value.'-'.$secondBinding->value; 653 | }); 654 | 655 | Route::get('/binding/{customBinding}/{secondBinding}/{parameter}', function (CustomBinding $customBinding, SecondBinding $secondBinding, int $parameter) { 656 | return $customBinding->value.'-'.$secondBinding->value.'-'.$parameter; 657 | })->breadcrumb(function (CustomBinding $customBinding, SecondBinding $secondBinding, int $parameter) { 658 | return $customBinding->value.'-'.$secondBinding->value.'-'.$parameter; 659 | }); 660 | }); 661 | 662 | $this->get('/binding/foo/bar/baz')->assertStatus(404); 663 | 664 | $breadcrumbLinks = app(Breadcrumb::class)->links(); 665 | 666 | $this->assertCount(1, $breadcrumbLinks); 667 | $this->assertInstanceOf(Collection::class, $breadcrumbLinks); 668 | $this->assertEquals(new Collection([ 669 | '/' => new BreadcrumbLink('/', 'Start'), 670 | ]), $breadcrumbLinks); 671 | 672 | $this->assertNull(app(Breadcrumb::class)->current()); 673 | 674 | $this->assertEquals(new BreadcrumbLink('/', 'Start'), app(Breadcrumb::class)->index()); 675 | } 676 | } 677 | 678 | class TestBreadcrumbController 679 | { 680 | public function __invoke() 681 | { 682 | // 683 | } 684 | } 685 | 686 | class Foo extends Model 687 | { 688 | // 689 | } 690 | 691 | class Bar extends Model 692 | { 693 | // 694 | } 695 | 696 | class CustomBinding 697 | { 698 | public function __construct($value) 699 | { 700 | $this->value = $value; 701 | } 702 | } 703 | 704 | class SecondBinding 705 | { 706 | public function __construct($value) 707 | { 708 | $this->value = $value; 709 | } 710 | } 711 | 712 | class CustomTitleResolver 713 | { 714 | public function __invoke() 715 | { 716 | return 'Class title'; 717 | } 718 | 719 | public function getTitle() 720 | { 721 | return 'Custom title'; 722 | } 723 | 724 | public function getTitleWithParameters(...$params) 725 | { 726 | return 'Custom title: '.\implode(' > ', $params); 727 | } 728 | 729 | public function getTitleFromModel(Foo $foo) 730 | { 731 | return "Custom title: {$foo->id}"; 732 | } 733 | } 734 | 735 | class CustomRouteModelBindingTitleResolver 736 | { 737 | public function __invoke(Foo $foo) 738 | { 739 | return "Id: {$foo->id}"; 740 | } 741 | } 742 | -------------------------------------------------------------------------------- /tests/TemplateTest.php: -------------------------------------------------------------------------------- 1 | get('/foo')->assertStatus(404); 13 | 14 | $html = (string) view('laravel-breadcrumb::bootstrap3'); 15 | 16 | $this->assertEmpty($html); 17 | } 18 | 19 | /** @test */ 20 | public function it_returns_the_correct_bootstrap_3_template() 21 | { 22 | Route::get('/', TestTemplateController::class)->breadcrumbIndex('Start'); 23 | Route::get('/foo', TestTemplateController::class)->breadcrumb('First'); 24 | 25 | $this->get('/foo')->assertStatus(200); 26 | 27 | $html = (string) view('laravel-breadcrumb::bootstrap3'); 28 | 29 | $this->assertXmlStringEqualsXmlString(' 30 | 37 | ', $html); 38 | } 39 | 40 | /** @test */ 41 | public function it_returns_an_empty_bootstrap_4_template() 42 | { 43 | $this->get('/foo')->assertStatus(404); 44 | 45 | $html = (string) view('laravel-breadcrumb::bootstrap4'); 46 | 47 | $this->assertEmpty($html); 48 | } 49 | 50 | /** @test */ 51 | public function it_returns_the_correct_bootstrap_4_template() 52 | { 53 | Route::get('/', TestTemplateController::class)->breadcrumbIndex('Start'); 54 | Route::get('/foo', TestTemplateController::class)->breadcrumb('First'); 55 | 56 | $this->get('/foo')->assertStatus(200); 57 | 58 | $html = (string) view('laravel-breadcrumb::bootstrap4'); 59 | 60 | $this->assertXmlStringEqualsXmlString(' 61 | 70 | ', $html); 71 | } 72 | 73 | /** @test */ 74 | public function it_returns_an_empty_bulma_template() 75 | { 76 | $this->get('/foo')->assertStatus(404); 77 | 78 | $html = (string) view('laravel-breadcrumb::bulma'); 79 | 80 | $this->assertEmpty($html); 81 | } 82 | 83 | /** @test */ 84 | public function it_returns_the_correct_bulma_template() 85 | { 86 | Route::get('/', TestTemplateController::class)->breadcrumbIndex('Start'); 87 | Route::get('/foo', TestTemplateController::class)->breadcrumb('First'); 88 | 89 | $this->get('/foo')->assertStatus(200); 90 | 91 | $html = (string) view('laravel-breadcrumb::bulma'); 92 | 93 | $this->assertXmlStringEqualsXmlString(' 94 | 105 | ', $html); 106 | } 107 | 108 | /** @test */ 109 | public function it_returns_an_empty_foundation_6_template() 110 | { 111 | $this->get('/foo')->assertStatus(404); 112 | 113 | $html = (string) view('laravel-breadcrumb::foundation6'); 114 | 115 | $this->assertEmpty($html); 116 | } 117 | 118 | /** @test */ 119 | public function it_returns_the_correct_foundation_6_template() 120 | { 121 | Route::get('/', TestTemplateController::class)->breadcrumbIndex('Start'); 122 | Route::get('/foo', TestTemplateController::class)->breadcrumb('First'); 123 | 124 | $this->get('/foo')->assertStatus(200); 125 | 126 | $html = (string) view('laravel-breadcrumb::foundation6'); 127 | 128 | $this->assertXmlStringEqualsXmlString(' 129 | 140 | ', $html); 141 | } 142 | } 143 | 144 | class TestTemplateController 145 | { 146 | public function __invoke() 147 | { 148 | // 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 20 | $app['config']->set('database.connections.testbench', [ 21 | 'driver' => 'sqlite', 22 | 'database' => ':memory:', 23 | 'prefix' => '', 24 | ]); 25 | } 26 | 27 | protected function migrate() 28 | { 29 | $this->loadMigrationsFrom(realpath(__DIR__.'/database/migrations')); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/database/migrations/2018_12_05_000000_create_foos_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('foos'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/database/migrations/2018_12_05_000001_create_bars_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('bars'); 30 | } 31 | } 32 | --------------------------------------------------------------------------------