├── .gitignore ├── resources └── views │ └── components │ ├── divider.blade.php │ ├── icon.blade.php │ ├── menu.blade.php │ ├── children.blade.php │ ├── header.blade.php │ └── item.blade.php ├── phpunit.xml ├── src ├── Facades │ ├── Menus.php │ └── MenusManager.php ├── Components │ ├── Header.php │ ├── Divider.php │ ├── Children.php │ ├── Icon.php │ ├── Menu.php │ └── Item.php ├── MenusManager.php ├── Menu.php ├── Providers │ └── PackageServiceProvider.php ├── Traits │ └── HasItems.php └── Item.php ├── tests ├── TestCase.php └── MenusManagerTest.php ├── .php-cs-fixer.dist.php ├── .github └── workflows │ └── php-cs-fixer.yml ├── LICENSE ├── composer.json ├── README.md └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | build/ 4 | .DS_Store 5 | *.cache 6 | -------------------------------------------------------------------------------- /resources/views/components/divider.blade.php: -------------------------------------------------------------------------------- 1 |
merge($item->attributes) }}> 2 | -------------------------------------------------------------------------------- /resources/views/components/icon.blade.php: -------------------------------------------------------------------------------- 1 |
merge($item->attributes) }}> 2 | @isset($slot) 3 | {{ $slot }} 4 | @else 5 | {{ $icon }} 6 | @endif 7 |
8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Facades/Menus.php: -------------------------------------------------------------------------------- 1 | notPath('vendor/*') 5 | ->notPath('resources/*') 6 | ->notPath('database/*') 7 | ->notPath('storage/*') 8 | ->notPath('node_modules/*') 9 | ->in([ 10 | __DIR__ . '/src', 11 | ]) 12 | ->name('*.php') 13 | ->notName('*.blade.php') 14 | ->ignoreDotFiles(true) 15 | ->ignoreVCS(true); 16 | 17 | $config = new PhpCsFixer\Config(); 18 | 19 | return $config 20 | ->setRules([ 21 | '@PSR2' => true, 22 | '@PhpCsFixer' => true, 23 | 'concat_space' => [ 24 | 'spacing' => 'one' 25 | ] 26 | ]) 27 | ->setFinder($finder); 28 | -------------------------------------------------------------------------------- /.github/workflows/php-cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: Check and fix code styling with php-cs-fixer 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | style: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | with: 15 | ref: ${{ github.head_ref }} 16 | 17 | - name: Run php-cs-fixer 18 | uses: docker://oskarstark/php-cs-fixer-ga 19 | with: 20 | args: --config=.php-cs-fixer.dist.php --allow-risky=yes 21 | 22 | - name: Commit changes 23 | uses: stefanzweifel/git-auto-commit-action@v4.10.0 24 | with: 25 | commit_message: Automatically applied php-cs-fixer changes 26 | -------------------------------------------------------------------------------- /resources/views/components/menu.blade.php: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /resources/views/components/children.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @foreach($items as $item) 3 | @if ($item->isDivider()) 4 | 5 | @else 6 | @if($item->isActive()) 7 | 8 | @else 9 | 10 | @endif 11 | @endif 12 | @endforeach 13 |
14 | -------------------------------------------------------------------------------- /src/Components/Header.php: -------------------------------------------------------------------------------- 1 | item = $item; 25 | } 26 | 27 | /** 28 | * Get the view / contents that represents the component. 29 | * 30 | * @return \Closure|Htmlable|string|View 31 | */ 32 | public function render() 33 | { 34 | if ($this->item && $this->item->isVisible()) { 35 | return view('menus-manager::components.header'); 36 | } 37 | 38 | return ''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Components/Divider.php: -------------------------------------------------------------------------------- 1 | item = $item; 25 | } 26 | 27 | /** 28 | * Get the view / contents that represents the component. 29 | * 30 | * @return \Closure|Htmlable|string|View 31 | */ 32 | public function render() 33 | { 34 | if ($this->item && $this->item->isVisible()) { 35 | return view('menus-manager::components.divider'); 36 | } 37 | 38 | return ''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Components/Children.php: -------------------------------------------------------------------------------- 1 | items = $items ?? collect(); 25 | } 26 | 27 | /** 28 | * Get the view / contents that represents the component. 29 | * 30 | * @return \Closure|Htmlable|string|View 31 | */ 32 | public function render() 33 | { 34 | if ($this->items->count()) { 35 | return view('menus-manager::components.children'); 36 | } 37 | 38 | return ''; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Components/Icon.php: -------------------------------------------------------------------------------- 1 | item = $item; 32 | $this->icon = $item && $item->hasIcon() ? $item->icon : null; 33 | } 34 | 35 | /** 36 | * Get the view / contents that represents the component. 37 | * 38 | * @return \Closure|Htmlable|string|View 39 | */ 40 | public function render() 41 | { 42 | if ($this->item && $this->icon) { 43 | return view('menus-manager::components.icon'); 44 | } 45 | 46 | return ''; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hexadog 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 | -------------------------------------------------------------------------------- /src/Components/Menu.php: -------------------------------------------------------------------------------- 1 | menu = Menus::get($name); 35 | $this->items = $this->menu ? $this->menu->items() : collect(); 36 | } 37 | 38 | /** 39 | * Get the view / contents that represents the component. 40 | * 41 | * @return \Closure|Htmlable|string|View 42 | */ 43 | public function render() 44 | { 45 | if ($this->menu) { 46 | return view('menus-manager::components.menu'); 47 | } 48 | 49 | return ''; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /resources/views/components/header.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($item->haschildren()) 3 | 16 | 17 | 18 | @else 19 |
merge($item->attributes) }}> 20 | 21 | {{ $item->title }} 22 |
23 | @endif 24 |
25 | -------------------------------------------------------------------------------- /src/Components/Item.php: -------------------------------------------------------------------------------- 1 | item = $item; 25 | } 26 | 27 | /** 28 | * Get the view / contents that represents the component. 29 | * 30 | * @return \Closure|Htmlable|string|View 31 | */ 32 | public function render() 33 | { 34 | if ($this->item && $this->item->isVisible()) { 35 | if ($this->item->isHeader()) { 36 | return view('menus-manager::components.header'); 37 | } 38 | 39 | if ($this->item->isDivider()) { 40 | return view('menus-manager::components.divider'); 41 | } 42 | 43 | return view('menus-manager::components.item'); 44 | } 45 | 46 | return ''; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /resources/views/components/item.blade.php: -------------------------------------------------------------------------------- 1 | @if($item->haschildren()) 2 |
merge($item->attributes) }}> 3 | 16 | 17 | 18 |
19 | @else 20 | merge($item->attributes)->merge(['href' => $item->getUrl()]) }}> 21 | 22 | {{ $item->title }} 23 | 24 | @endif 25 | -------------------------------------------------------------------------------- /src/MenusManager.php: -------------------------------------------------------------------------------- 1 | menus = collect(); 22 | } 23 | 24 | /** 25 | * Magic method to manipulate menus Collection with ease. 26 | * 27 | * @param string $method_name 28 | * @param array $args 29 | */ 30 | public function __call($method_name, $args) 31 | { 32 | if (!method_exists($this, $method_name)) { 33 | return call_user_func_array([$this->menus, $method_name], $args); 34 | } 35 | } 36 | 37 | /** 38 | * Get all menus as array. 39 | */ 40 | public function all(): array 41 | { 42 | return $this->menus->toArray(); 43 | } 44 | 45 | /** 46 | * Register a menu or get the existing one. 47 | * 48 | * @param string $name 49 | */ 50 | public function register($name): Menu 51 | { 52 | if (!$menu = $this->get($name)) { 53 | $menu = new Menu($name); 54 | 55 | $this->menus->put($name, $menu); 56 | } 57 | 58 | return $menu; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Menu.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | $this->items = collect(); 26 | } 27 | 28 | /** 29 | * Search item by key and value recursively. 30 | * 31 | * @param string $key 32 | * @param string $value 33 | * 34 | * @return mixed 35 | */ 36 | public function searchBy($key, $value, ?callable $callback = null): ?Item 37 | { 38 | $matchItem = null; 39 | 40 | $this->items->first(function ($item) use (&$matchItem, $key, $value) { 41 | if ($foundItem = $item->findBy($key, $value)) { 42 | $matchItem = $foundItem; 43 | } 44 | }); 45 | 46 | if (is_callable($callback) && $matchItem) { 47 | call_user_func($callback, $matchItem); 48 | } 49 | 50 | return $matchItem; 51 | } 52 | 53 | public function toArray() 54 | { 55 | return [ 56 | 'name' => $this->name, 57 | 'items' => $this->items->toArray(), 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexadog/laravel-menus-manager", 3 | "description": "Dynamic Menus Management package for your Laravel application", 4 | "license": "MIT", 5 | "support": { 6 | "issues": "https://github.com/hexadog/laravel-menus-manager/issues", 7 | "source": "https://github.com/hexadog/laravel-menus-manager" 8 | }, 9 | "authors": [ 10 | { 11 | "name": "Gaetan", 12 | "email": "gaetan@hexadog.com" 13 | } 14 | ], 15 | "minimum-stability": "stable", 16 | "require": { 17 | "illuminate/routing": "^7.0|^8.0|^9.0|^10.0|^11.0", 18 | "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0", 19 | "illuminate/view": "^7.0|^8.0|^9.0|^10.0|^11.0" 20 | }, 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^3.14", 23 | "phpunit/phpunit": "^7.0|^8.0|^9.0|^10.0", 24 | "orchestra/testbench": "~5.2|^7.1|^8.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Hexadog\\MenusManager\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Hexadog\\MenusManager\\Tests\\": "tests" 34 | } 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "Hexadog\\MenusManager\\Providers\\PackageServiceProvider" 40 | ], 41 | "dont-discover": [] 42 | } 43 | }, 44 | "scripts": { 45 | "test": "vendor/bin/phpunit", 46 | "test:windows": "vendor\\bin\\phpunit", 47 | "check-style": "vendor/bin/phpcs --extensions=php ./src", 48 | "fix-style": "vendor/bin/php-cs-fixer fix" 49 | } 50 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | Latest Stable Version 6 | 7 | 8 | CircleCI Build 9 | 10 | 11 | Total Downloads 12 | 13 | 14 | License 15 | 16 |

17 | 18 | 19 | ## Introduction 20 | hexadog/laravel-menus-manager is a Laravel package to ease dynamic menus management. 21 | 22 | 23 | ## Installation 24 | This package requires PHP 7.3 and Laravel 7.0 or higher. 25 | 26 | To get started, install Menus Manager using Composer: 27 | ```shell 28 | composer require hexadog/laravel-menus-manager 29 | ``` 30 | 31 | The package will automatically register its service provider. 32 | 33 | ## Documentation 34 | You can find the full documentation [here](https://laravel-menus-manager.netlify.app) 35 | 36 | 37 | ## Credits 38 | - This package is inspired by the work of [rinvex/laravel-menus](https://github.com/rinvex/laravel-menus) 39 | - Logo made by [DesignEvo free logo creator](https://www.designevo.com/logo-maker/) 40 | 41 | 42 | ## License 43 | Laravel Menus Manager is open-sourced software licensed under the [MIT license](LICENSE). 44 | -------------------------------------------------------------------------------- /src/Providers/PackageServiceProvider.php: -------------------------------------------------------------------------------- 1 | strapPublishers(); 37 | 38 | $this->loadViewsFrom($this->getPath('resources/views'), 'menus-manager'); 39 | $this->loadViewComponentsAs('menus', [ 40 | Components\Children::class, 41 | Components\Divider::class, 42 | Components\Header::class, 43 | Components\Icon::class, 44 | Components\Item::class, 45 | Components\Menu::class, 46 | ]); 47 | } 48 | 49 | /** 50 | * Register the application services. 51 | */ 52 | public function register(): void 53 | { 54 | $this->app->singleton(MenusManager::class, function () { 55 | return new MenusManager(); 56 | }); 57 | 58 | AliasLoader::getInstance()->alias('MenusManager', MenusManagerFacade::class); 59 | AliasLoader::getInstance()->alias('Menus', MenusFacade::class); 60 | } 61 | 62 | /** 63 | * Get the services provided by the provider. 64 | * 65 | * @return array 66 | */ 67 | public function provides() 68 | { 69 | return [MenusManager::class]; 70 | } 71 | 72 | /** 73 | * Get Package absolute path. 74 | * 75 | * @param string $path 76 | */ 77 | protected function getPath($path = '') 78 | { 79 | // We get the child class 80 | $rc = new \ReflectionClass(get_class($this)); 81 | 82 | return dirname($rc->getFileName()) . '/../../' . $path; 83 | } 84 | 85 | /** 86 | * Get Module normalized namespace. 87 | * 88 | * @param mixed $prefix 89 | */ 90 | protected function getNormalizedNamespace($prefix = '') 91 | { 92 | return Str::start(Str::lower(self::PACKAGE_NAME), $prefix); 93 | } 94 | 95 | /** 96 | * Bootstrap our Publishers. 97 | */ 98 | protected function strapPublishers() 99 | { 100 | $this->publishes([ 101 | $this->getPath('resources/views') => resource_path('views/vendor/menus-manager'), 102 | ], 'views'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Traits/HasItems.php: -------------------------------------------------------------------------------- 1 | items, $method_name], $args); 27 | } 28 | } 29 | 30 | /** 31 | * Add new item. 32 | */ 33 | public function add(array $attributes = []): Item 34 | { 35 | $item = new Item($attributes, $this); 36 | 37 | if (!array_key_exists('order', $attributes)) { 38 | $item->order = count($this->items); 39 | } 40 | 41 | $this->items->push($item); 42 | 43 | return $item; 44 | } 45 | 46 | /** 47 | * Add new divider menu item. 48 | */ 49 | public function divider(array $attributes = []): Item 50 | { 51 | return $this->add(compact('attributes'))->asDivider(); 52 | } 53 | 54 | /** 55 | * Find item by key and value. 56 | * 57 | * @return mixed 58 | */ 59 | public function findBy(string $key, string $value): ?Item 60 | { 61 | return $this->items->filter(function ($item) use ($key, $value) { 62 | return $item->{$key} === $value; 63 | })->first(); 64 | } 65 | 66 | /** 67 | * Find item by given title or add it. 68 | * 69 | * @return mixed 70 | */ 71 | public function findByTitleOrAdd(\Closure|string $title, array $attributes = []): ?Item 72 | { 73 | if (!($item = $this->findBy('title', $title instanceof \Closure ? $title() : $title))) { 74 | $item = $this->add(compact('title', 'attributes')); 75 | } 76 | 77 | return $item; 78 | } 79 | 80 | /** 81 | * Add new header menu item. 82 | */ 83 | public function header(\Closure|string $title, array $attributes = []): Item 84 | { 85 | return $this->add(compact('title', 'attributes'))->asHeader(); 86 | } 87 | 88 | /** 89 | * Get items. 90 | * 91 | * @return Collection 92 | */ 93 | public function items() 94 | { 95 | return $this->items->sortBy(function ($item) { 96 | return $item->order; 97 | }); 98 | } 99 | 100 | /** 101 | * Register new menu item using registered route. 102 | * 103 | * @param mixed $route 104 | */ 105 | public function route($route, \Closure|string $title, array $attributes = []): Item 106 | { 107 | return $this->add(compact('route', 'title', 'attributes')); 108 | } 109 | 110 | /** 111 | * Register new menu item using url. 112 | */ 113 | public function url(string $url, \Closure|string $title, array $attributes = []): Item 114 | { 115 | return $this->add(compact('url', 'title', 'attributes')); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome**. We accept contributions via Pull Requests on [Github](https://github.com/hexadog/laravel-menus-manager). 4 | 5 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 6 | 7 | Please note we have a code of conduct, please follow it in all your interactions with the project. 8 | 9 | ## Pull Request Process 10 | 11 | 1. **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 12 | 2. **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. **Consider our release cycle** - We try to follow the [SemVer v2.0.0](http://semver.org/) versioning scheme. 15 | 4. **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 16 | 17 | **Happy coding**! 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 24 | 25 | ### Our Standards 26 | 27 | Examples of behavior that contributes to creating a positive environment 28 | include: 29 | 30 | * Using welcoming and inclusive language 31 | * Being respectful of differing viewpoints and experiences 32 | * Gracefully accepting constructive criticism 33 | * Focusing on what is best for the community 34 | * Showing empathy towards other community members 35 | 36 | Examples of unacceptable behavior by participants include: 37 | 38 | * The use of sexualized language or imagery and unwelcome sexual attention or 39 | advances 40 | * Trolling, insulting/derogatory comments, and personal or political attacks 41 | * Public or private harassment 42 | * Publishing others' private information, such as a physical or electronic 43 | address, without explicit permission 44 | * Other conduct which could reasonably be considered inappropriate in a 45 | professional setting 46 | 47 | ### Our Responsibilities 48 | 49 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 50 | 51 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 52 | 53 | ### Scope 54 | 55 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 56 | 57 | ### Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at `gaetan@hexadog.com`. All 60 | complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. 61 | Further details of specific enforcement policies may be posted separately. 62 | 63 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 64 | 65 | ### Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 68 | 69 | [homepage]: http://contributor-covenant.org 70 | [version]: http://contributor-covenant.org/version/1/4/ 71 | -------------------------------------------------------------------------------- /tests/MenusManagerTest.php: -------------------------------------------------------------------------------- 1 | menu = Menus::register('main'); 24 | } 25 | 26 | /** @test */ 27 | public function it_makes_a_menu() 28 | { 29 | self::assertInstanceOf(Menu::class, $this->menu); 30 | } 31 | 32 | /** @test */ 33 | public function it_makes_multiple_menu() 34 | { 35 | Menus::register('secondary'); 36 | 37 | self::assertEquals(2, Menus::count()); 38 | } 39 | 40 | /** @test */ 41 | public function it_makes_a_menu_singleton() 42 | { 43 | $menu2 = Menus::register('main'); 44 | 45 | self::assertEquals($this->menu, $menu2); 46 | } 47 | 48 | /** @test */ 49 | public function it_makes_an_empty_menu_item() 50 | { 51 | $menuItem = new Item(); 52 | 53 | self::assertInstanceOf(Item::class, $menuItem); 54 | } 55 | 56 | /** @test */ 57 | public function it_makes_a_menu_item() 58 | { 59 | $menuItem = $this->menu->url('https://hexadog.com', 'hexadog'); 60 | 61 | self::assertInstanceOf(Item::class, $menuItem); 62 | } 63 | 64 | /** @test */ 65 | public function it_makes_a_menu_item_as_header() 66 | { 67 | $menuItem = $this->menu->header('hexadog'); 68 | 69 | self::assertEquals('header', $menuItem->type); 70 | self::assertEquals('hexadog', $menuItem->title); 71 | } 72 | 73 | /** @test */ 74 | public function it_makes_a_menu_item_as_divider() 75 | { 76 | $menuItem = $this->menu->divider(); 77 | 78 | self::assertEquals('divider', $menuItem->type); 79 | } 80 | 81 | /** @test */ 82 | public function it_makes_a_menu_item_with_properties() 83 | { 84 | $properties = [ 85 | 'attributes' => [], 86 | 'icon' => '', 87 | 'order' => 1, 88 | 'route' => 'index', 89 | 'title' => 'hexadog', 90 | 'type' => 'header', 91 | 'url' => 'https://hexadog.com', 92 | ]; 93 | 94 | $menuItem = new Item($properties); 95 | 96 | self::assertEquals($properties, Arr::except($menuItem->properties, 'attributes.id')); 97 | } 98 | 99 | /** @test */ 100 | public function it_can_get_item_attributes() 101 | { 102 | $menuItem = new Item(); 103 | 104 | self::assertNotNull($menuItem->attributes); 105 | } 106 | 107 | /** @test */ 108 | public function it_set_default_attribute_id() 109 | { 110 | $menuItem = new Item(); 111 | 112 | self::assertNotNull($menuItem->attributes['id']); 113 | } 114 | 115 | /** @test */ 116 | public function it_can_get_item_attributes_as_html_string() 117 | { 118 | $menuItem = new Item([ 119 | 'attributes' => [ 120 | 'class' => 'link' 121 | ] 122 | ]); 123 | 124 | self::assertNotNull($menuItem->getAttributes()); 125 | self::assertEquals('class="link"', $menuItem->getAttributes('id')); 126 | } 127 | 128 | /** @test */ 129 | public function it_can_set_icon() 130 | { 131 | $menuItem = (new Item())->icon(''); 132 | 133 | self::assertEquals('', $menuItem->icon); 134 | } 135 | 136 | /** @test */ 137 | public function it_can_set_route() 138 | { 139 | $menuItem = new Item(); 140 | $menuItem->route = 'index'; 141 | 142 | self::assertEquals('index', $menuItem->route); 143 | } 144 | 145 | /** @test */ 146 | public function it_can_set_url() 147 | { 148 | $menuItem = new Item(); 149 | $menuItem->url = 'https://hexadog.com'; 150 | 151 | self::assertEquals('https://hexadog.com', $menuItem->url); 152 | } 153 | 154 | /** @test */ 155 | public function it_can_set_order() 156 | { 157 | $menuItem = (new Item())->order(2); 158 | 159 | self::assertEquals(2, $menuItem->order); 160 | } 161 | 162 | /** @test */ 163 | public function it_can_add_multiple_items() 164 | { 165 | $this->menu->route('index', 'Home'); 166 | $this->menu->url('https://hexadog.com', 'hexadog'); 167 | 168 | self::assertEquals(2, $this->menu->items()->count()); 169 | } 170 | 171 | /** @test */ 172 | public function it_can_add_item_children() 173 | { 174 | $menuItem = $this->menu->route('index', 'Home'); 175 | $menuItem->url('https://hexadog.com', 'hexadog'); 176 | 177 | self::assertEquals(1, $menuItem->children()->count()); 178 | } 179 | 180 | /** @test */ 181 | public function it_can_add_order_items() 182 | { 183 | $this->menu->route('index', 'Home')->order(3); 184 | $this->menu->url('https://hexadog.com', 'hexadog')->order(1); 185 | $this->menu->url('https://laravel.com', 'laravel')->order(2); 186 | 187 | $items = $this->menu->items(); 188 | 189 | self::assertEquals('hexadog', $items->first()->title); 190 | self::assertEquals('laravel', $items->get(2)->title); 191 | self::assertEquals('Home', $items->last()->title); 192 | } 193 | 194 | /** @test */ 195 | public function it_can_get_the_correct_url_for_url_type() 196 | { 197 | $menuItem = $this->menu->url('https://hexadog.com', 'hexadog'); 198 | 199 | self::assertEquals('https://hexadog.com', $menuItem->getUrl()); 200 | } 201 | 202 | /** @test */ 203 | public function it_can_get_the_correct_url_for_route_type() 204 | { 205 | $this->app['router']->get('/', ['as' => 'index']); 206 | $this->app['router']->get('/contact', ['as' => 'contact']); 207 | 208 | $menuItem = $this->menu->route('index', 'Home'); 209 | $menuItem2 = $this->menu->route('contact', 'Contact'); 210 | $menuItem3 = $this->menu->route(['contact', ['type' => 'support']], 'Contact'); 211 | 212 | self::assertEquals('http://localhost', $menuItem->getUrl()); 213 | self::assertEquals('http://localhost/contact', $menuItem2->getUrl()); 214 | self::assertEquals('http://localhost/contact?type=support', $menuItem3->getUrl()); 215 | } 216 | 217 | /** @test */ 218 | public function it_can_get_item_as_array() 219 | { 220 | $menuItem = new Item(); 221 | $itemAsArray = [ 222 | 'attributes' => $menuItem->attributes, 223 | 'active' => false, 224 | 'children' => [], 225 | 'icon' => null, 226 | 'order' => 9000, 227 | 'title' => '', 228 | 'type' => 'link', 229 | 'url' => '' 230 | ]; 231 | 232 | self::assertEquals($itemAsArray, $menuItem->toArray()); 233 | } 234 | 235 | /** @test */ 236 | public function it_can_get_menu_as_array() 237 | { 238 | $menuItem = $this->menu->add(); 239 | $itemAsArray = [ 240 | 'name' => 'main', 241 | 'items' => [ 242 | 0 => [ 243 | 'attributes' => $menuItem->attributes, 244 | 'active' => false, 245 | 'children' => [], 246 | 'icon' => null, 247 | 'order' => 9000, 248 | 'title' => '', 249 | 'type' => 'link', 250 | 'url' => '' 251 | ] 252 | ] 253 | ]; 254 | 255 | self::assertEquals($itemAsArray, $this->menu->toArray()); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/Item.php: -------------------------------------------------------------------------------- 1 | [], 31 | 'icon' => null, 32 | 'order' => 0, 33 | 'route' => null, 34 | 'title' => '', 35 | 'type' => 'link', // link | divider | header 36 | 'url' => '#', 37 | ]; 38 | 39 | /** 40 | * The hide callbacks collection. 41 | * 42 | * @var Collection 43 | */ 44 | protected $visibleCallbacks; 45 | 46 | /** 47 | * Constructor. 48 | * 49 | * @param mixed $parent 50 | */ 51 | public function __construct(array $properties = [], $parent = null) 52 | { 53 | $this->visibleCallbacks = collect(); 54 | $this->items = collect(); 55 | 56 | $this->parent = $parent; 57 | 58 | // Generate id attribute if not provided 59 | if (is_null(Arr::get($properties, 'attributes.id'))) { 60 | Arr::set($properties, 'attributes.id', str_replace('.', '', uniqid('id-', true))); 61 | } 62 | 63 | $this->fill($properties); 64 | } 65 | 66 | /** 67 | * Get item attribute. 68 | * 69 | * @param string $key 70 | * 71 | * @return mixed 72 | */ 73 | public function __get($key) 74 | { 75 | if ('properties' === $key) { 76 | return $this->properties; 77 | } 78 | 79 | $value = Arr::get($this->properties, $key); 80 | 81 | if ($value instanceof \Closure) { 82 | $value = $value(); 83 | } 84 | 85 | return $value; 86 | } 87 | 88 | /** 89 | * Set item attribute. 90 | * 91 | * @param string $key 92 | * @param mixed $value 93 | * 94 | * @return mixed 95 | */ 96 | public function __set($key, $value) 97 | { 98 | return Arr::set($this->properties, $key, $value); 99 | } 100 | 101 | /** 102 | * Set the current item as header. 103 | */ 104 | public function asHeader(): Item 105 | { 106 | return $this->fill([ 107 | 'type' => 'header', 108 | ]); 109 | } 110 | 111 | /** 112 | * Set the current item as divider. 113 | */ 114 | public function asDivider(): Item 115 | { 116 | return $this->fill([ 117 | 'type' => 'divider', 118 | ]); 119 | } 120 | 121 | /** 122 | * Get the curent item children. 123 | */ 124 | public function children(): Collection 125 | { 126 | return $this->items->sortBy(function ($item) { 127 | return $item->order; 128 | }); 129 | } 130 | 131 | /** 132 | * Fill the item properties. 133 | * 134 | * @param array $properties 135 | */ 136 | public function fill($properties): Item 137 | { 138 | $this->properties = array_merge($this->properties, $properties); 139 | 140 | return $this; 141 | } 142 | 143 | /** 144 | * Get the item attributes as HTML String. 145 | * 146 | * @param mixed $except 147 | * 148 | * @return string 149 | */ 150 | public function getAttributes($except = null) 151 | { 152 | return $this->htmlAttributes(Arr::except($this->properties['attributes'], $except)); 153 | } 154 | 155 | /** 156 | * Get item url. 157 | */ 158 | public function getUrl(): string 159 | { 160 | if ($this->route) { 161 | if (is_array($this->route)) { 162 | return URL::route(Arr::get($this->route, 0), Arr::get($this->route, 1, [])); 163 | } 164 | 165 | if (is_string($this->route)) { 166 | return URL::route($this->route); 167 | } 168 | } 169 | 170 | if ($this->url) { 171 | if (is_array($this->route)) { 172 | return URL::to(Arr::get($this->url, 0), Arr::get($this->url, 1, []), true); 173 | } 174 | 175 | return URL::to($this->url, [], true); 176 | } 177 | 178 | return '#'; 179 | } 180 | 181 | /** 182 | * Check if the current item has children. 183 | */ 184 | public function hasChildren(): bool 185 | { 186 | return $this->items->isNotEmpty(); 187 | } 188 | 189 | /** 190 | * Check if icon is set for the current item. 191 | */ 192 | public function hasIcon(): bool 193 | { 194 | return !is_null($this->icon); 195 | } 196 | 197 | /** 198 | * Check if item is active 199 | * If a child is active then item is active too. 200 | */ 201 | public function isActive() 202 | { 203 | // Check if one of the children is active 204 | foreach ($this->children() as $child) { 205 | if ($child->isActive()) { 206 | return true; 207 | } 208 | } 209 | 210 | // Custom set active path 211 | if ($path = $this->getActiveWhen()) { 212 | return Request::is($path); 213 | } 214 | 215 | $path = ltrim(str_replace(url('/'), '', $this->getUrl()), '/'); 216 | 217 | return Request::is( 218 | $path, 219 | $path . '/*' 220 | ); 221 | } 222 | 223 | /** 224 | * @param string $path 225 | * 226 | * @return $this 227 | */ 228 | public function isActiveWhen($path) 229 | { 230 | // Remove unwanted chars 231 | $path = ltrim($path, '/'); 232 | $path = rtrim($path, '/'); 233 | $path = rtrim($path, '?'); 234 | 235 | $this->activeWhen = $path; 236 | 237 | return $this; 238 | } 239 | 240 | /** 241 | * @return string 242 | */ 243 | public function getActiveWhen() 244 | { 245 | return $this->activeWhen; 246 | } 247 | 248 | /** 249 | * Check if the current item is divider. 250 | */ 251 | public function isDivider(): bool 252 | { 253 | return 'divider' === $this->type; 254 | } 255 | 256 | /** 257 | * Check if the current item is header. 258 | */ 259 | public function isHeader(): bool 260 | { 261 | return 'header' === $this->type; 262 | } 263 | 264 | /** 265 | * Check if the current item is hidden. 266 | */ 267 | public function isHidden(): bool 268 | { 269 | return !$this->isVisible(); 270 | } 271 | 272 | /** 273 | * Check if the current item is visible. 274 | */ 275 | public function isVisible(): bool 276 | { 277 | return (bool) $this->visibleCallbacks->every(function ($callback) { 278 | return call_user_func($callback); 279 | }); 280 | } 281 | 282 | /** 283 | * Set the current item icon. 284 | */ 285 | public function icon(string $icon): Item 286 | { 287 | $this->icon = $icon; 288 | 289 | return $this; 290 | } 291 | 292 | /** 293 | * Set visible callback for current menu item. 294 | * 295 | * @param mixed $callback 296 | */ 297 | public function if($callback): Item 298 | { 299 | if (!is_callable($callback)) { 300 | $callback = function () use ($callback) { 301 | return $callback; 302 | }; 303 | } 304 | 305 | $this->visibleCallbacks->push($callback); 306 | 307 | return $this; 308 | } 309 | 310 | /** 311 | * Set the current item order. 312 | */ 313 | public function order(int $order = 0): Item 314 | { 315 | $this->order = $order; 316 | 317 | return $this; 318 | } 319 | 320 | /** 321 | * Get Item parent. 322 | * 323 | * @return mixed 324 | */ 325 | public function parent() 326 | { 327 | return $this->parent; 328 | } 329 | 330 | /** 331 | * Search item by key and value recursively. 332 | * 333 | * @param string $key 334 | * @param string $value 335 | * 336 | * @return mixed 337 | */ 338 | public function searchBy($key, $value, ?callable $callback = null): ?Item 339 | { 340 | $matchItem = null; 341 | 342 | if ($this->{$key} === $value) { 343 | $matchItem = $this; 344 | } else { 345 | $this->items->each(function ($item) use (&$matchItem, $key, $value) { 346 | if ($foundItem = $item->findBy($key, $value)) { 347 | $matchItem = $foundItem; 348 | } 349 | }); 350 | } 351 | 352 | if (is_callable($callback) && $matchItem) { 353 | call_user_func($callback, $matchItem); 354 | } 355 | 356 | return $matchItem; 357 | } 358 | 359 | /** 360 | * Get the instance as an array. 361 | * 362 | * @return array 363 | */ 364 | public function toArray() 365 | { 366 | return [ 367 | 'attributes' => $this->attributes, 368 | 'active' => $this->isActive(), 369 | 'children' => $this->hasChildren() ? $this->children()->toArray() : [], 370 | 'icon' => $this->icon, 371 | 'order' => $this->order, 372 | 'title' => $this->title, 373 | 'type' => $this->type, 374 | 'url' => $this->getUrl(), 375 | ]; 376 | } 377 | 378 | /** 379 | * Return attributes in html format. 380 | * 381 | * @param array $attributes 382 | * 383 | * @return string 384 | */ 385 | private function htmlAttributes($attributes) 386 | { 387 | return new HtmlString(join(' ', array_map(function ($key) use ($attributes) { 388 | if (is_bool($attributes[$key])) { 389 | return $attributes[$key] ? $key : ''; 390 | } 391 | 392 | return $key . '="' . $attributes[$key] . '"'; 393 | }, array_keys($attributes)))); 394 | } 395 | } 396 | --------------------------------------------------------------------------------