├── 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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
206 |
207 | {{ $crumbs->title() }}
208 |
209 |
210 | @else
211 |
212 | {{ $crumbs->title() }}
213 |
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 |
--------------------------------------------------------------------------------