├── LICENSE ├── README.md ├── composer.json ├── pint.json ├── src ├── Breadcrumbs.php ├── BreadcrumbsComponent.php ├── BreadcrumbsMiddleware.php ├── BreadcrumbsServiceProvider.php ├── Crumb.php ├── Manager.php ├── Registrar.php └── Trail.php └── views └── breadcrumbs.blade.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexandr Chernyaev 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 |

2 | Laravel Breadcrumbs 3 |

4 | 5 | 6 |

7 | Tests 8 | codecov 9 | Total Downloads 10 | Latest Version on Packagist 11 |

12 | 13 | 14 | ## Introduction 15 | 16 | Breadcrumbs display a list of links indicating the position of the current page in the whole site hierarchy. For example, breadcrumbs like `Home / Sample Post / Edit` means the user is viewing an edit page for the "Sample Post." He can click on "Sample Post" to view that page or click on "Home" to return to the homepage. 17 | 18 | 19 | > [Home](#) / [Sample Post](#) / Edit 20 | 21 | This package for the [Laravel framework](https://laravel.com/) will make it easy to build breadcrumbs in your application. 22 | 23 | ## Installation 24 | 25 | Run this at the command line: 26 | ```php 27 | composer require tabuna/breadcrumbs 28 | ``` 29 | 30 | This will update `composer.json` and install the package into the `vendor/` directory. 31 | 32 | ## Define your breadcrumbs 33 | 34 | Now you can define breadcrumbs directly in the route files: 35 | 36 | ```php 37 | use Tabuna\Breadcrumbs\Trail; 38 | 39 | // Home 40 | Route::get('/', fn () => view('home')) 41 | ->name('home') 42 | ->breadcrumbs(fn (Trail $trail) => 43 | $trail->push('Home', route('home')) 44 | ); 45 | 46 | // Home > About 47 | Route::get('/about', fn () => view('home')) 48 | ->name('about') 49 | ->breadcrumbs(fn (Trail $trail) => 50 | $trail->parent('home')->push('About', route('about')) 51 | ); 52 | ``` 53 | 54 | You can also get arguments from the request: 55 | 56 | ```php 57 | Route::get('/category/{category}', function (Category $category){ 58 | //In this example, the category object is your Eloquent model. 59 | //code... 60 | }) 61 | ->name('category') 62 | ->breadcrumbs(fn (Trail $trail, Category $category) => 63 | $trail->push($category->title, route('category', $category->id)) 64 | ); 65 | ``` 66 | 67 | 68 | ## Route detection 69 | 70 | The package tries to reduce the number of lines needed. For this, you can skip passing the results of the `route()` methods. 71 | The following two declarations will be equivalent: 72 | 73 | ```php 74 | Route::get('/', fn () => view('home')) 75 | ->name('home') 76 | ->breadcrumbs(fn (Trail $trail) => 77 | $trail->push('Home', route('home')) 78 | ); 79 | 80 | Route::get('/', fn () => view('home')) 81 | ->name('home') 82 | ->breadcrumbs(fn (Trail $trail) => 83 | $trail->push('Home', 'home') 84 | ); 85 | ``` 86 | 87 | 88 | ## Like to use a separate route file? 89 | 90 | You can do this simply by adding the desired file to the service provider 91 | 92 | ```php 93 | namespace App\Providers; 94 | 95 | use Illuminate\Support\ServiceProvider; 96 | 97 | class BreadcrumbsServiceProvider extends ServiceProvider 98 | { 99 | /** 100 | * Bootstrap the application events. 101 | */ 102 | public function boot(): void 103 | { 104 | require base_path('routes/breadcrumbs.php'); 105 | } 106 | } 107 | ``` 108 | 109 | Then it will be your special file in the route directory: 110 | 111 | ```php 112 | // routes/breadcrumbs.php 113 | 114 | 115 | // Photos 116 | Breadcrumbs::for('photo.index', fn (Trail $trail) => 117 | $trail->parent('home')->push('Photos', route('photo.index')) 118 | ); 119 | ``` 120 | 121 | 122 | 123 | ## Route resource 124 | 125 | When using resources, a whole group of routes is declared for which you must specify values manually 126 | 127 | ```php 128 | // routes/web.php 129 | 130 | Route::resource('photos', 'PhotoController'); 131 | ```` 132 | 133 | It’s better to specify this in service providers, since route files can be cached 134 | 135 | ```php 136 | namespace App\Providers; 137 | 138 | use Illuminate\Support\ServiceProvider; 139 | use Tabuna\Breadcrumbs\Breadcrumbs; 140 | use Tabuna\Breadcrumbs\Trail; 141 | 142 | class BreadcrumbsServiceProvider extends ServiceProvider 143 | { 144 | /** 145 | * Bootstrap any application services. 146 | */ 147 | public function boot(): void 148 | { 149 | Breadcrumbs::for('photos.index', fn (Trail $trail) => 150 | $trail->push('Photos', route('home')) 151 | ); 152 | 153 | Breadcrumbs::for('photos.create', fn (Trail $trail) => 154 | $trail 155 | ->parent('photos.index', route('photos.index')) 156 | ->push('Add new photo', route('home')) 157 | ); 158 | } 159 | } 160 | ``` 161 | 162 | ## Output the breadcrumbs use Blade Component 163 | 164 | You can use the output component: 165 | 166 | ```blade 167 | 170 | ``` 171 | 172 | To define classes of list items, you can specify: 173 | 174 | ```blade 175 | 179 | ``` 180 | 181 | You can also pass parameters: 182 | 183 | ```blade 184 | 187 | ``` 188 | 189 | And call named routes explicitly: 190 | 191 | ```blade 192 | 195 | ``` 196 | 197 | ## Output the breadcrumbs use Blade view 198 | 199 | In order to display breadcrumbs on the desired page, simply call: 200 | 201 | ```blade 202 | @if(Breadcrumbs::has()) 203 | @foreach (Breadcrumbs::current() as $crumbs) 204 | @if ($crumbs->url() && !$loop->last) 205 | 210 | @else 211 | 214 | @endif 215 | @endforeach 216 | @endif 217 | ``` 218 | 219 | And results in this output: 220 | 221 | > [Home](#) / About 222 | 223 | ## Credits 224 | 225 | For several years, I successfully used the [Dave James Miller](https://github.com/davejamesmiller/laravel-breadcrumbs) package to solve my problems, but he stopped developing and supporting it. After a long search for alternatives, I liked the [Dwight Watson](https://github.com/dwightwatson) package, but the isolation of breadcrumbs from the announcement of the routes did not give me rest. That's why I created this package. It uses the code of both previous packages. 226 | 227 | ## License 228 | 229 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 230 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabuna/breadcrumbs", 3 | "description": "An easy way to add breadcrumbs to your Laravel app.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Alexandr Chernyaev", 8 | "email": "bliz48rus@gmail.com" 9 | }, 10 | { 11 | "name": "Dwight Watson", 12 | "email": "dwight@studentservices.com.au" 13 | }, 14 | { 15 | "name": "Dave James Miller", 16 | "email": "dave@davejamesmiller.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1", 21 | "ext-json": "*", 22 | "laravel/framework": "^10.0|^11.0|^12.0", 23 | "laravel/serializable-closure": "^1.0|^2.0" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^8.0|^9.0|^10.0", 27 | "phpunit/phpunit": "^10.5|^11.0|^12.0", 28 | "phpunit/php-code-coverage": "^10.|^11.0|^12.0", 29 | "laravel/pint": "^1.0", 30 | "vimeo/psalm": "^5.0|^6.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Tabuna\\Breadcrumbs\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Tabuna\\Breadcrumbs\\Tests\\": "tests/" 40 | } 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "Tabuna\\Breadcrumbs\\BreadcrumbsServiceProvider" 46 | ], 47 | "aliases": { 48 | "Breadcrumbs": "Tabuna\\Breadcrumbs\\Breadcrumbs" 49 | } 50 | } 51 | }, 52 | "config": { 53 | "sort-packages": true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "array_syntax": { 5 | "syntax": "short" 6 | }, 7 | "ordered_imports": { 8 | "sort_algorithm": "alpha" 9 | }, 10 | "no_unused_imports": true, 11 | "no_whitespace_before_comma_in_array": true, 12 | "not_operator_with_successor_space": true, 13 | "trailing_comma_in_multiline": { 14 | "elements": [ 15 | "arrays" 16 | ] 17 | }, 18 | "phpdoc_scalar": true, 19 | "unary_operator_spaces": true, 20 | "binary_operator_spaces": { 21 | "operators": { 22 | "=>": "align_single_space" 23 | } 24 | }, 25 | "blank_line_before_statement": { 26 | "statements": [ 27 | "continue", 28 | "declare", 29 | "return", 30 | "throw", 31 | "try" 32 | ] 33 | }, 34 | "phpdoc_separation": true, 35 | "phpdoc_align": true, 36 | "phpdoc_order": true, 37 | "phpdoc_single_line_var_spacing": true, 38 | "phpdoc_var_without_name": true, 39 | "no_superfluous_phpdoc_tags": false, 40 | "class_attributes_separation": { 41 | "elements": { 42 | "method": "one" 43 | } 44 | }, 45 | "method_argument_space": { 46 | "on_multiline": "ignore" 47 | }, 48 | "trim_array_spaces": true, 49 | "single_trait_insert_per_statement": false, 50 | "new_with_parentheses": false, 51 | "php_unit_method_casing": { 52 | "case": "camel_case" 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Breadcrumbs.php: -------------------------------------------------------------------------------- 1 | route !== null) { 37 | return $this->manager->generate($this->route, $this->parameters); 38 | } 39 | 40 | return $this->manager->current($this->parameters); 41 | } 42 | 43 | /** 44 | * Determine if the component should be rendered. 45 | * 46 | * @return bool 47 | */ 48 | public function shouldRender(): bool 49 | { 50 | return $this->manager->has($this->route); 51 | } 52 | 53 | /** 54 | * Get the view / contents that represent the component. 55 | * 56 | * @return \Illuminate\View\View|string 57 | */ 58 | public function render() 59 | { 60 | return view('breadcrumbs::breadcrumbs'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/BreadcrumbsMiddleware.php: -------------------------------------------------------------------------------- 1 | router = $router; 34 | $this->breadcrumbs = $breadcrumbs; 35 | } 36 | 37 | /** 38 | * @param Request $request 39 | * @param Closure $next 40 | * 41 | * @throws \Throwable 42 | * 43 | * @return mixed 44 | */ 45 | public function handle(Request $request, Closure $next) 46 | { 47 | collect($this->router->getRoutes()->getIterator()) 48 | ->filter(function (Route $route) { 49 | return array_key_exists(self::class, $route->defaults); 50 | }) 51 | ->filter(function (Route $route) { 52 | return $route->getName() !== null; 53 | }) 54 | ->filter(function (Route $route) { 55 | return ! $this->breadcrumbs->has($route->getName()); 56 | }) 57 | ->each(function (Route $route) { 58 | $serialize = $route->defaults[self::class]; 59 | 60 | /** @var SerializableClosure $callback */ 61 | $callback = unserialize($serialize); 62 | 63 | $this->breadcrumbs->for($route->getName(), $callback->getClosure()); 64 | }); 65 | 66 | optional($request->route())->forgetParameter(self::class); 67 | 68 | return $next($request); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/BreadcrumbsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(Manager::class); 31 | $this->loadViewsFrom(__DIR__.'/../views', 'breadcrumbs'); 32 | 33 | \Illuminate\Support\Facades\Route::middlewareGroup('breadcrumbs', [ 34 | BreadcrumbsMiddleware::class, 35 | ]); 36 | 37 | if (Route::hasMacro('breadcrumbs')) { 38 | return; 39 | } 40 | 41 | Route::macro('breadcrumbs', function (Closure $closure) { 42 | /** @var Route $this */ 43 | $this->middleware('breadcrumbs') 44 | ->defaults(BreadcrumbsMiddleware::class, serialize(new SerializableClosure($closure))); 45 | 46 | return $this; 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Crumb.php: -------------------------------------------------------------------------------- 1 | title = $title; 35 | $this->url = $url; 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function jsonSerialize(): array 42 | { 43 | return [ 44 | 'title' => $this->title(), 45 | 'url' => $this->url(), 46 | ]; 47 | } 48 | 49 | /** 50 | * Get the crumb title. 51 | * 52 | * @return string 53 | */ 54 | public function title(): string 55 | { 56 | return $this->title; 57 | } 58 | 59 | /** 60 | * Get the crumb URL. 61 | * 62 | * @return string|null 63 | */ 64 | public function url(): ?string 65 | { 66 | return Route::has($this->url) ? route($this->url) : $this->url; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Manager.php: -------------------------------------------------------------------------------- 1 | generator = $generator; 34 | } 35 | 36 | /** 37 | * Register a breadcrumb definition by passing it off to the registrar. 38 | * 39 | * @param string $route 40 | * @param \Closure $definition 41 | * 42 | * @throws \Throwable 43 | * 44 | * @return void 45 | */ 46 | public function for(string $route, Closure $definition) 47 | { 48 | $this->generator->register($route, $definition); 49 | } 50 | 51 | /** 52 | * @param null $parameters 53 | * 54 | * @throws \Throwable 55 | * 56 | * @return Collection 57 | */ 58 | public function current($parameters = null): Collection 59 | { 60 | $name = optional(Route::current())->getName(); 61 | 62 | if ($name === null) { 63 | return collect(); 64 | } 65 | 66 | return $this->generate($name, $parameters); 67 | } 68 | 69 | /** 70 | * @param string $route 71 | * @param mixed|null $parameters 72 | * 73 | * @throws \Throwable 74 | * 75 | * @return Collection 76 | */ 77 | public function generate(string $route, $parameters = null): Collection 78 | { 79 | $parameters = Arr::wrap($parameters); 80 | 81 | return $this->generator->generate($route, $parameters); 82 | } 83 | 84 | /** 85 | * @param string|null $name 86 | * 87 | * @return bool 88 | */ 89 | public function has(?string $name = null): bool 90 | { 91 | $name = $name ?? Route::currentRouteName(); 92 | 93 | if ($name === null) { 94 | return false; 95 | } 96 | 97 | return $this->generator->has($name); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Registrar.php: -------------------------------------------------------------------------------- 1 | has($name), 31 | Exception::class, 32 | "No breadcrumbs defined for route [{$name}]."); 33 | 34 | return $this->definitions[$name]; 35 | } 36 | 37 | /** 38 | * Return whether a definition exists for a route name. 39 | * 40 | * @param string $name 41 | * 42 | * @return bool 43 | */ 44 | public function has(string $name): bool 45 | { 46 | return array_key_exists($name, $this->definitions); 47 | } 48 | 49 | /** 50 | * Set the registration for a route name. 51 | * 52 | * @param string $name 53 | * @param \Closure $definition 54 | * 55 | * @throws \Throwable 56 | * 57 | * @return void 58 | */ 59 | public function set(string $name, Closure $definition): void 60 | { 61 | $this->definitions[$name] = $definition; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Trail.php: -------------------------------------------------------------------------------- 1 | breadcrumbs = new Collection; 33 | } 34 | 35 | /** 36 | * Register a definition with the registrar. 37 | * 38 | * @param string $name 39 | * @param Closure $definition 40 | * 41 | * @throws \Throwable 42 | * 43 | * @return void 44 | */ 45 | public function register(string $name, Closure $definition): void 46 | { 47 | $this->registrar->set($name, $definition); 48 | } 49 | 50 | /** 51 | * Generate the collection of breadcrumbs from the given route. 52 | * 53 | * @param string $route 54 | * @param array $parameters 55 | * 56 | * @throws \Throwable 57 | * 58 | * @return Collection 59 | */ 60 | public function generate(string $route, array $parameters = []): Collection 61 | { 62 | $this->breadcrumbs = $this->breadcrumbs->whenNotEmpty(function () { 63 | return new Collection(); 64 | }); 65 | 66 | $parameters = $this->getRouteByNameParameters($route, $parameters); 67 | 68 | if ($route && $this->registrar->has($route)) { 69 | $this->call($route, $parameters); 70 | } 71 | 72 | return $this->breadcrumbs; 73 | } 74 | 75 | /** 76 | * @param string $name 77 | * @param array $parameters 78 | * 79 | * @return array 80 | */ 81 | private function getRouteByNameParameters(string $name, array $parameters): array 82 | { 83 | if (! empty($parameters)) { 84 | return $parameters; 85 | } 86 | 87 | $route = Route::currentRouteName() === $name 88 | ? Route::current() 89 | : Route::getRoutes()->getByName($name); 90 | 91 | return optional($route)->parameters ?? $parameters; 92 | } 93 | 94 | /** 95 | * Call the breadcrumb definition with the given parameters. 96 | * 97 | * @param string $name 98 | * @param array $parameters 99 | * 100 | * @throws \Throwable 101 | * 102 | * @return void 103 | */ 104 | protected function call(string $name, array $parameters): void 105 | { 106 | $definition = $this->registrar->get($name); 107 | 108 | $parameters = Arr::prepend(array_values($parameters), $this); 109 | 110 | call_user_func_array($definition, $parameters); 111 | } 112 | 113 | /** 114 | * Call a parent route with the given parameters. 115 | * 116 | * @param string $name 117 | * @param mixed $parameters 118 | * 119 | * @throws \Throwable 120 | * 121 | * @return Trail 122 | */ 123 | public function parent(string $name, ...$parameters): self 124 | { 125 | $this->call($name, $parameters); 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * Add a breadcrumb to the collection. 132 | * 133 | * @param string $title 134 | * @param string|null $url 135 | * 136 | * @return Trail 137 | */ 138 | public function push(string $title, ?string $url = null): self 139 | { 140 | $this->breadcrumbs->push(new Crumb($title, $url)); 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * @param string $name 147 | * 148 | * @return bool 149 | */ 150 | public function has(string $name): bool 151 | { 152 | return $this->registrar->has($name); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /views/breadcrumbs.blade.php: -------------------------------------------------------------------------------- 1 | @foreach ($generate() as $crumbs) 2 | @if ($crumbs->url() && !$loop->last) 3 |
  • merge(['class' => $class]) }}> 4 | 5 | {{ $crumbs->title() }} 6 | 7 |
  • 8 | @else 9 |
  • merge(['class' => $class. ' '. $active]) }}> 10 | {{ $crumbs->title() }} 11 |
  • 12 | @endif 13 | @endforeach 14 | --------------------------------------------------------------------------------