├── .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 | [![Build Status](https://travis-ci.org/dwightwatson/breadcrumbs.svg?branch=master)](https://travis-ci.org/dwightwatson/breadcrumbs) 7 | [![Total Downloads](https://poser.pugx.org/watson/breadcrumbs/downloads.svg)](https://packagist.org/packages/watson/breadcrumbs) 8 | [![License](https://poser.pugx.org/watson/breadcrumbs/license.svg)](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 | 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 |