├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE.txt
├── README.md
├── composer.json
├── phpunit.xml
├── src
├── Breadcrumbs
│ ├── Crumb.php
│ ├── Exceptions
│ │ ├── DefinitionAlreadyExistsException.php
│ │ └── DefinitionNotFoundException.php
│ ├── Facade.php
│ ├── Generator.php
│ ├── Manager.php
│ ├── Registrar.php
│ └── ServiceProvider.php
├── config
│ └── breadcrumbs.php
└── views
│ ├── bootstrap3.blade.php
│ └── bootstrap4.blade.php
└── tests
├── CrumbTest.php
├── Exceptions
├── DefinitionAlreadyExistsExceptionTest.php
└── DefinitionNotFoundExceptionTest.php
├── ManagerTest.php
├── RegistrarTest.php
└── TestCase.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 | charset = utf-8
3 |
4 | [*.php]
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | spaces_around_operators = true
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.phar
3 | composer.lock
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.2
5 | - 7.3
6 | - 7.4
7 | - 8.0
8 |
9 | before_script:
10 | - travis_retry composer self-update
11 | - travis_retry composer update --prefer-source --no-interaction
12 |
13 | script: vendor/bin/phpunit --verbose
14 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Dwight Watson
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 | Breadcrumbs for Laravel
2 | =======================
3 |
4 | ## Warning: this package is currently incompatible with Laravel 7. I have started a rewrite that would support Laravel 7 but I have no ETA. You'll likely want to consider another breadcrumb package in the meantime.
5 |
6 | [](https://travis-ci.org/dwightwatson/breadcrumbs)
7 | [](https://packagist.org/packages/watson/breadcrumbs)
8 | [](https://packagist.org/packages/watson/breadcrumbs)
9 |
10 | Breadcrumbs is a simple breadcrumb generator for Laravel that tries to hook into the magic to make it easy to get up and running.
11 |
12 | ## Installation
13 |
14 | Require the package through Composer as per usual.
15 |
16 | ```sh
17 | $ composer require watson/breadcrumbs
18 | ```
19 |
20 | ## Usage
21 |
22 | Create a new file at `routes/breadcrumbs.php` to define your breadcrumbs. By default the package will work with named routes which works with resourceful routing. However, you're also free to define routes by the controller action/pair.
23 |
24 | ```php
25 | Breadcrumbs::for('admin.pages.index', function ($trail) {
26 | $trail->add('Admin', route('admin.pages.index'));
27 | });
28 |
29 | Breadcrumbs::for('admin.users.index', function ($trail) {
30 | $trail->parent('admin.pages.index');
31 | $trail->add('Users', route('admin.users.index'));
32 | });
33 |
34 | Breadcrumbs::for('admin.users.show', function ($trail, User $user) {
35 | $trail->parent('admin.users.index');
36 | $trail->add($user->full_name, route('admin.users.show', $user));
37 | });
38 |
39 | Breadcrumbs::for('admin.users.edit', function ($trail, User $user) {
40 | $trail->parent('admin.users.show', $user);
41 | $trail->add('Edit', route('admin.users.edit', $user));
42 | });
43 |
44 | Breadcrumbs::for('admin.users.roles.index', function ($trail, User $user) {
45 | $trail->parent('admin.users.show', $user);
46 | $trail->add('Roles', route('admin.users.roles.index', $user));
47 | });
48 |
49 | Breadcrumbs::for('admin.users.roles.show', function ($trail, User $user, Role $role) {
50 | $trail->parent('admin.users.roles.index', $user, $role);
51 | $trail->add('Edit', route('admin.users.roles.show', [$user, $role]));
52 | });
53 | ```
54 |
55 | Note that you can call `parent()` from within a breadcrumb definition which lets you build up the breadcrumb tree. Pass any parameters you need further up through the second parameter.
56 |
57 | If you want to use controller/action pairs instead of named routes that's fine too. Use the usual Laravel syntax and the package will correctly map it up for you. Note that if the route is named the package will always looked for a named breadcrumb first.
58 |
59 | ```php
60 | Breadcrumbs::for('PagesController@getIndex', function ($trail) {
61 | $trail->add('Home', action('PagesController@getIndex'));
62 | });
63 |
64 | Breadcrumbs::for('secret.page', function ($trail) {
65 | $trail->add('Secret page', url('secret'))
66 | });
67 | ```
68 |
69 | ### Rendering the breadcrumbs
70 |
71 | In your view file, you simply need to call the `render()` method wherever you want your breadcrumbs to appear. It's that easy. If there are no breadcrumbs for the current route, then nothing will be returned.
72 |
73 | ```php
74 | {{ Breadcrumbs::render() }}
75 | ```
76 |
77 | You don't need to escape the content of the breadcrumbs, it's already wrapped in an instance of `Illuminate\Support\HtmlString` so Laravel knows just how to use it.
78 |
79 | ### Multiple breadcrumb files
80 |
81 | If you find that your breadcrumbs files is starting to get a little bigger you may like to break it out into multiple, smaller files. If that's the case you can simply `require` other breadcrumb files at the top of your default definition file.
82 |
83 | ```php
84 | require 'breadcrumbs.admin.php';
85 | ```
86 |
87 | ### Customising the breadcrumb view
88 |
89 | The package ships with a Bootstrap 3 compatible view which you can publish and customise as you need, or override completely with your own view. Simply run the following command to publish the view.
90 |
91 | ```sh
92 | $ php artisan vendor:publish --provider="Watson\Breadcrumbs\ServiceProvider" --tag=views
93 | ```
94 |
95 | This will publish the default `bootstrap3` view to your `resources/views/vendor/breadcrumbs` directory from which you can edit the file to your heart's content. If you want to use your own view instead, run the following command to publish the config file.
96 |
97 | ```sh
98 | $ php artisan vendor:publish --provider="Watson\Breadcrumbs\ServiceProvider" --tag=config
99 | ```
100 |
101 | This will publish `config/breadcrumbs.php` which provides you the option to set your own view file for your breadcrumbs.
102 |
103 | ## Credits
104 |
105 | This package is inspired by the work of [Dave James Miller](https://github.com/davejamesmiller/laravel-breadcrumbs), which I've used for some time. It has been re-written by scratch for my use case with a little more magic and less customisation, plus taking advantage of some newer features in PHP. Many thanks to Dave for his work.
106 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "watson/breadcrumbs",
3 | "description": "Breadcrumbs made easy for Laravel.",
4 | "keywords": ["laravel", "breadcrumbs"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Dwight Watson",
9 | "email": "dwight@studentservices.com.au"
10 | }
11 | ],
12 | "require": {
13 | "php": "^7.2.5||^8.0",
14 | "illuminate/routing": "^6.0||^7.0||^8.0",
15 | "illuminate/support": "^6.0||^7.0||^8.0",
16 | "illuminate/view": "^6.0||^7.0||^8.0"
17 | },
18 | "require-dev": {
19 | "mockery/mockery": "^1.3.1||^1.4.2",
20 | "phpunit/phpunit": "^8.2.3||^9.0",
21 | "orchestra/testbench": "^4.0||^5.0||^6.1"
22 | },
23 | "autoload": {
24 | "psr-4": {
25 | "Watson\\": "src"
26 | }
27 | },
28 | "autoload-dev": {
29 | "psr-4": {
30 | "Watson\\Breadcrumbs\\Tests\\": "tests"
31 | }
32 | },
33 | "extra": {
34 | "laravel": {
35 | "providers": [
36 | "Watson\\Breadcrumbs\\ServiceProvider"
37 | ],
38 | "aliases": {
39 | "Breadcrumbs": "Watson\\Breadcrumbs\\Facade"
40 | }
41 | }
42 | },
43 | "config": {
44 | "preferred-install": "dist"
45 | },
46 | "prefer-stable": true,
47 | "minimum-stability": "dev"
48 | }
49 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Breadcrumbs/Crumb.php:
--------------------------------------------------------------------------------
1 | title = $title;
31 | $this->url = $url;
32 | }
33 |
34 | /**
35 | * Get the crumb title.
36 | *
37 | * @return string
38 | */
39 | public function title(): string
40 | {
41 | return $this->title;
42 | }
43 |
44 | /**
45 | * Get the crumb URL.
46 | *
47 | * @return string
48 | */
49 | public function url()
50 | {
51 | return $this->url;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Breadcrumbs/Exceptions/DefinitionAlreadyExistsException.php:
--------------------------------------------------------------------------------
1 | router = $router;
43 | $this->registrar = $registrar;
44 | $this->breadcrumbs = new Collection;
45 | }
46 |
47 | /**
48 | * Register a definition with the registrar.
49 | *
50 | * @param string $name
51 | * @param \Closure $definition
52 | * @return void
53 | * @throws \Watson\Breadcrumbs\Exceptions\DefinitionAlreadyExists
54 | */
55 | public function register(string $name, Closure $definition)
56 | {
57 | $this->registrar->set($name, $definition);
58 | }
59 |
60 | /**
61 | * Generate the collection of breadcrumbs from the given route.
62 | *
63 | * @return \Illuminate\Support\Collection
64 | */
65 | public function generate(array $parameters = null): Collection
66 | {
67 | $route = $this->router->current();
68 |
69 | $parameters = isset($parameters) ? Arr::wrap($parameters) : $route->parameters;
70 |
71 | if ($route && $this->registrar->has($route->getName())) {
72 | $this->call($route->getName(), $parameters);
73 | }
74 |
75 | return $this->breadcrumbs;
76 | }
77 |
78 | /**
79 | * Call a parent route with the given parameters.
80 | *
81 | * @param string $name
82 | * @param mixed $parameters
83 | * @return void
84 | */
85 | public function parent(string $name, ...$parameters)
86 | {
87 | $this->call($name, $parameters);
88 | }
89 |
90 | /**
91 | * Add a breadcrumb to the collection.
92 | *
93 | * @param string $title
94 | * @param string $url
95 | * @return void
96 | */
97 | public function add(string $title, string $url)
98 | {
99 | $this->breadcrumbs->push(new Crumb($title, $url));
100 | }
101 |
102 | /**
103 | * Call the breadcrumb definition with the given parameters.
104 | *
105 | * @param string $name
106 | * @param array $parameters
107 | * @return void
108 | * @throws \Watson\Breadcrumbs\DefinitionNotFoundException
109 | */
110 | protected function call(string $name, array $parameters)
111 | {
112 | $definition = $this->registrar->get($name);
113 |
114 | $parameters = Arr::prepend(array_values($parameters), $this);
115 |
116 | call_user_func_array($definition, $parameters);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Breadcrumbs/Manager.php:
--------------------------------------------------------------------------------
1 | view = $view;
45 | $this->config = $config;
46 | $this->generator = $generator;
47 | }
48 |
49 | /**
50 | * Register a breadcrumb definition by passing it off to the registrar.
51 | *
52 | * @param string $route
53 | * @param \Closure $definition
54 | * @return void
55 | */
56 | public function for(string $route, Closure $definition)
57 | {
58 | $this->generator->register($route, $definition);
59 | }
60 |
61 | /**
62 | * Render the breadcrumbs as an HTML string
63 | *
64 | * @param array $parameters
65 | * @return \Illuminate\Contracts\Support\Htmlable
66 | */
67 | public function render($parameters = null): ?Htmlable
68 | {
69 | if ($breadcrumbs = $this->generator->generate($parameters)) {
70 | return $this->view->make($this->config->get('breadcrumbs.view'), compact('breadcrumbs'));
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Breadcrumbs/Registrar.php:
--------------------------------------------------------------------------------
1 | has($name)) {
28 | throw new DefinitionNotFoundException("No breadcrumbs defined for route [{$name}].");
29 | }
30 |
31 | return $this->definitions[$name];
32 | }
33 |
34 | /**
35 | * Return whether a definition exists for a route name
36 | *
37 | * @param string $name
38 | * @return bool
39 | */
40 | public function has(string $name): bool
41 | {
42 | return array_key_exists($name, $this->definitions);
43 | }
44 |
45 | /**
46 | * Set the registration for a route name.
47 | *
48 | * @param string $name
49 | * @param \Closure $definition
50 | * @return void
51 | * @throws \Watson\Breadcrumbs\DefinitionAlreadyExists
52 | */
53 | public function set(string $name, Closure $definition)
54 | {
55 | if ($this->has($name)) {
56 | throw new DefinitionAlreadyExistsException(
57 | "Breadcrumbs have already been defined for route [{$name}]."
58 | );
59 | }
60 |
61 | $this->definitions[$name] = $definition;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Breadcrumbs/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(__DIR__.'/../config/breadcrumbs.php', 'breadcrumbs');
18 |
19 | $this->app->singleton('breadcrumbs', Manager::class);
20 | }
21 |
22 | /**
23 | * Perform post-registration booting of services.
24 | *
25 | * @return void
26 | */
27 | public function boot()
28 | {
29 | $this->publishes([
30 | __DIR__.'/../config/breadcrumbs.php' => config_path('breadcrumbs.php'),
31 | ], 'config');
32 |
33 | $this->loadViewsFrom(__DIR__.'/../views', 'breadcrumbs');
34 |
35 | if (file_exists($file = $this->app['path.base'].'/routes/breadcrumbs.php')) {
36 | require $file;
37 | }
38 | }
39 |
40 | /**
41 | * Get the services provided by the provider.
42 | *
43 | * @return array
44 | */
45 | public function provides()
46 | {
47 | return ['breadcrumbs'];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/config/breadcrumbs.php:
--------------------------------------------------------------------------------
1 | 'breadcrumbs::bootstrap4',
16 |
17 | ];
18 |
--------------------------------------------------------------------------------
/src/views/bootstrap3.blade.php:
--------------------------------------------------------------------------------
1 | @if ($breadcrumbs->count())
2 |
3 | @foreach ($breadcrumbs as $breadcrumb)
4 | @if ($breadcrumb->url() && $loop->remaining)
5 | - {{ $breadcrumb->title() }}
6 | @else
7 | - {{ $breadcrumb->title() }}
8 | @endif
9 | @endforeach
10 |
11 | @endif
12 |
--------------------------------------------------------------------------------
/src/views/bootstrap4.blade.php:
--------------------------------------------------------------------------------
1 | @if ($breadcrumbs->count())
2 |
17 | @endif
18 |
--------------------------------------------------------------------------------
/tests/CrumbTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('foo', $crumb->title());
15 | }
16 |
17 | /** @test */
18 | function it_returns_url()
19 | {
20 | $crumb = new Crumb('foo', 'bar');
21 |
22 | $this->assertEquals('bar', $crumb->url());
23 | }
24 |
25 | /** @test */
26 | function it_returns_null_url()
27 | {
28 | $crumb = new Crumb('foo');
29 |
30 | $this->assertNull($crumb->url());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Exceptions/DefinitionAlreadyExistsExceptionTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(DefinitionAlreadyExistsException::class, $result);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/Exceptions/DefinitionNotFoundExceptionTest.php:
--------------------------------------------------------------------------------
1 | assertInstanceOf(DefinitionNotFoundException::class, $result);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/ManagerTest.php:
--------------------------------------------------------------------------------
1 | view = Mockery::mock(Factory::class);
22 | $this->config = Mockery::mock(Repository::class);
23 | $this->generator = Mockery::mock(Generator::class);
24 |
25 | $this->breadcrumbs = new Manager($this->view, $this->config, $this->generator);
26 | }
27 |
28 | /** @test */
29 | function it_passes_registrations_to_registrar()
30 | {
31 | $closure = function () {
32 | return 'hello';
33 | };
34 |
35 | $this->generator->shouldReceive('register')
36 | ->with('foo', $closure)
37 | ->once();
38 |
39 | $this->breadcrumbs->for('foo', $closure);
40 | }
41 |
42 | /** @test */
43 | function it_renders_the_correct_view_with_breadcrumbs()
44 | {
45 | $this->generator->shouldReceive('generate')
46 | ->once()
47 | ->with(Mockery::on(function ($argument) {
48 | return $argument === null;
49 | }))
50 | ->andReturn(collect([1, 2, 3]));
51 |
52 | $this->config->shouldReceive('get')
53 | ->with('breadcrumbs.view')
54 | ->once()
55 | ->andReturn('index.html');
56 |
57 | $this->view->shouldReceive('make')
58 | ->with('index.html', ['breadcrumbs' => collect([1, 2, 3])])
59 | ->once()
60 | ->andReturn(new HtmlString('foo'));
61 |
62 | $result = $this->breadcrumbs->render();
63 |
64 | $this->assertEquals('foo', $result->toHtml());
65 | $this->assertInstanceOf(HtmlString::class, $result);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/RegistrarTest.php:
--------------------------------------------------------------------------------
1 | registrar = new Registrar;
18 | }
19 |
20 | /** @test */
21 | function it_returns_an_existing_definition()
22 | {
23 | $closure = function () {
24 | return 'hello';
25 | };
26 |
27 | $this->registrar->set('foo', $closure);
28 |
29 | $result = $this->registrar->get('foo');
30 |
31 | $this->assertEquals($closure, $result);
32 | }
33 |
34 | /** @test */
35 | function it_throws_when_definition_does_not_exist()
36 | {
37 | $this->expectException(DefinitionNotFoundException::class);
38 |
39 | $this->registrar->get('foo');
40 | }
41 |
42 | /** @test */
43 | function it_returns_true_if_definition_exists()
44 | {
45 | $this->registrar->set('foo', function () {});
46 |
47 | $result = $this->registrar->has('foo');
48 |
49 | $this->assertTrue($result);
50 | }
51 |
52 | /** @test */
53 | function it_returns_false_if_definition_does_not_exist()
54 | {
55 | $result = $this->registrar->has('foo');
56 |
57 | $this->assertFalse($result);
58 | }
59 |
60 | /** @test */
61 | function it_throws_if_setting_existing_definition()
62 | {
63 | $this->expectException(DefinitionAlreadyExistsException::class);
64 |
65 | $closure = function () {
66 | return 'hello';
67 | };
68 |
69 | $this->registrar->set('foo', $closure);
70 |
71 | $this->registrar->set('foo', $closure);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |