├── .editorconfig ├── .styleci.yml ├── CONTRIBUTING.md ├── LICENSE ├── composer.json ├── readme.md └── src ├── Exceptions └── NameExistsException.php ├── Facades └── Menu.php ├── IsAssociatedWithMenu.php ├── Menu.php ├── MenuItem.php ├── MenuItemCollection.php └── MenuServiceProvider.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/RSpeekenbrink/laravel-inertia-menu). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Remco Speekenbrink 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. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rspeekenbrink/laravel-menu", 3 | "description": "Simple menu generation in Laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Remco Speekenbrink", 9 | "email": "contact@rspeekenbrink.nl" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.2|^8.0", 14 | "illuminate/contracts": "^6|^7|^8|^9|^10|^11|^12", 15 | "illuminate/database": "^6|^7|^8|^9|^10|^11|^12", 16 | "illuminate/support": "^6|^7|^8|^9|^10|^11|^12", 17 | "ext-json": "*" 18 | }, 19 | "require-dev": { 20 | "fakerphp/faker":"^1.9.1", 21 | "illuminate/auth": "^6|^7|^8|^9|^10|^11|^12", 22 | "orchestra/testbench": "^4.8|^5|^6|^7|^8|^9|^10", 23 | "phpunit/phpunit": "^7|^8|^9|^10|^11" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "RSpeekenbrink\\LaravelMenu\\": "src" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "RSpeekenbrink\\LaravelMenu\\Tests\\": "tests" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit" 37 | }, 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "RSpeekenbrink\\LaravelMenu\\MenuServiceProvider" 42 | ], 43 | "aliases": { 44 | "Menu": "RSpeekenbrink\\LaravelMenu\\Facades\\Menu" 45 | } 46 | } 47 | }, 48 | "minimum-stability": "dev" 49 | } 50 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Menu 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/rspeekenbrink/laravel-menu.svg?style=flat-square)](https://packagist.org/packages/rspeekenbrink/laravel-menu) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/rspeekenbrink/laravel-menu.svg?style=flat-square)](https://packagist.org/packages/rspeekenbrink/laravel-menu) 5 | 6 | Create menu objects server-sided without sweat for Front-End adoption. 7 | 8 | ## Installation 9 | 10 | You can install the package via composer: 11 | 12 | ```bash 13 | composer require rspeekenbrink/laravel-menu 14 | ``` 15 | 16 | ## Usage 17 | 18 | A default menu will already be registered and bound to the `Menu` facade. You can add items to the menu like this: 19 | 20 | ```php 21 | Menu::add('itemName', '/'); 22 | 23 | 24 | // Menu::toArray() Output: 25 | [ 26 | [ 27 | 'name' => 'itemName', 28 | 'route' => '/', 29 | 'active' => true, 30 | ] 31 | ] 32 | ``` 33 | 34 | **The itemName should be unique within the menu since this is the identifier of the item in the Menu.** 35 | 36 | ### Route attribute and Active state 37 | 38 | The route can be an absolute route like ```'/dashboard/profile'``` or a name of a route like ```'dashboard.index'``` for the automatic active state checking to work properly. If you want to use route names we recommend you to use [Ziggy](https://github.com/tightenco/ziggy) to convert the names to URLs in your front-end. 39 | 40 | The active attribute is a boolean that will be true if the route matches the route of the current request (path or name wise). 41 | 42 | ### Nested Routes 43 | 44 | To create nested items you could use the following: 45 | 46 | ```php 47 | Menu::add('dashboard', '/')->addChildren(function () { 48 | Menu::add('stats', '/stats'); 49 | Menu::add('profile', '/profile'); 50 | }); 51 | 52 | // Menu::toArray() Output: 53 | [ 54 | [ 55 | 'name' => 'dashboard', 56 | 'route' => '/', 57 | 'active' => true, 58 | 'children' => [ 59 | [ 60 | 'name' => 'dashboard.stats', 61 | 'route' => '/stats', 62 | 'active' => false, 63 | ], 64 | [ 65 | 'name' => 'dashboard.profile', 66 | 'route' => '/profile', 67 | 'active' => false, 68 | ] 69 | ] 70 | ] 71 | ] 72 | ``` 73 | 74 | ### Attributes 75 | 76 | You can pass attributes to the MenuItem to define values like Title or anything else you desire; 77 | 78 | ```php 79 | Menu::add('itemName', '/', ['title' => 'Dashboard', 'someAttribute' => 231, 'another' => 'value2']); 80 | 81 | 82 | // Menu::toArray() Output: 83 | [ 84 | [ 85 | 'name' => 'itemName', 86 | 'route' => '/', 87 | 'active' => true, 88 | 'title' => 'Dashboard', 89 | 'someAttribute' => 231, 90 | 'another' => 'value2, 91 | ] 92 | ] 93 | ``` 94 | 95 | ### Adding items condition wise or via Auth Guards 96 | 97 | If you would like to add menu items conditionwise, for example only add a menu item if a user is logged in, you can do it like this: 98 | 99 | ```php 100 | Menu::addIf($conditionOrClosure, 'itemName', $route, $attributes); 101 | ``` 102 | 103 | Or pass a Auth Guard: 104 | 105 | ```php 106 | Menu::addIfCan('MyAuthGuard', 'itemName', $route, $attributes); 107 | ``` 108 | 109 | ### Usage with InertiaJS 110 | 111 | The main purpose of this package is to create Menu objects that can be adopted easily by the Front-End. 112 | One of the easiest ways to transfer the objects from the back to the front is by using [InertiaJS](https://inertiajs.com/). 113 | 114 | ```php 115 | Inertia::share([ 116 | 'menu' => function () { 117 | return Menu::toArray(); 118 | } 119 | ]); 120 | ``` 121 | 122 | Then for example in your inertia-vue layout template; 123 | 124 | ```vue 125 | 143 | ``` 144 | 145 | ## Testing 146 | 147 | ``` bash 148 | composer test 149 | ``` 150 | 151 | ## Contributing 152 | 153 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 154 | 155 | ## Security 156 | 157 | If you discover any security related issues, please email contact@rspeekenbrink.nl instead of using the issue tracker. 158 | 159 | ## License 160 | 161 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 162 | -------------------------------------------------------------------------------- /src/Exceptions/NameExistsException.php: -------------------------------------------------------------------------------- 1 | menu; 18 | } 19 | 20 | /** 21 | * Set the menu instance associated with the class. 22 | * 23 | * @param Menu $menu 24 | * @return $this 25 | */ 26 | protected function setMenu(Menu $menu) 27 | { 28 | $this->menu = $menu; 29 | 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Menu.php: -------------------------------------------------------------------------------- 1 | menuItems = new MenuItemCollection(); 26 | } 27 | 28 | /** 29 | * Add a new MenuItem to the menu. 30 | * 31 | * @param string $name 32 | * @param string $route 33 | * @param array $attributes 34 | * @return MenuItem 35 | * 36 | * @throws NameExistsException 37 | */ 38 | public function add(string $name, string $route, array $attributes = []) 39 | { 40 | if ($this->hasParent()) { 41 | $name = end($this->parentStack)->getName().'.'.$name; 42 | } 43 | 44 | if ($this->menuItems->hasName($name)) { 45 | throw new NameExistsException($name); 46 | } 47 | 48 | $item = $this->createItem($name, $route, $attributes); 49 | 50 | $this->pushItem($item); 51 | 52 | return $item; 53 | } 54 | 55 | /** 56 | * Push the given item to the correct stacks. 57 | * 58 | * @param MenuItem $item 59 | * @return MenuItemCollection 60 | */ 61 | protected function pushItem(MenuItem $item) 62 | { 63 | if ($this->hasParent()) { 64 | return end($this->parentStack)->addChild($item); 65 | } 66 | 67 | return $this->menuItems->add($item); 68 | } 69 | 70 | /** 71 | * Check if there is a parent in the parentstack. 72 | * 73 | * @return bool 74 | */ 75 | protected function hasParent() 76 | { 77 | return ! empty($this->parentStack); 78 | } 79 | 80 | /** 81 | * Add a new menuItem to the menu if the condition is true. 82 | * 83 | * @param mixed $condition 84 | * @param string $name 85 | * @param string $route 86 | * @param array $attributes 87 | * @return MenuItem 88 | * 89 | * @throws NameExistsException 90 | */ 91 | public function addIf($condition, string $name, string $route, array $attributes = []) 92 | { 93 | return $this->resolveCondition($condition) ? $this->add($name, $route, $attributes) : null; 94 | } 95 | 96 | /** 97 | * Add a new menuItem to the menu when authorized. 98 | * 99 | * @param string|array $authorization 100 | * @param string $name 101 | * @param string $route 102 | * @param array $attributes 103 | * @return MenuItem 104 | * 105 | * @throws NameExistsException 106 | */ 107 | public function addIfCan($authorization, string $name, string $route, array $attributes = []) 108 | { 109 | $arguments = is_array($authorization) ? $authorization : [$authorization]; 110 | $ability = array_shift($arguments); 111 | 112 | return $this->addIf(app(Gate::class)->allows($ability, $arguments), $name, $route, $attributes); 113 | } 114 | 115 | /** 116 | * Resolve the condition. 117 | * 118 | * @param $condition 119 | * @return bool 120 | */ 121 | protected function resolveCondition($condition) 122 | { 123 | return is_callable($condition) ? $condition() : $condition; 124 | } 125 | 126 | /** 127 | * Create new MenuItem instance. 128 | * 129 | * @param string $name 130 | * @param string $route 131 | * @param array $attributes 132 | * @return MenuItem 133 | */ 134 | protected function createItem(string $name, string $route, array $attributes = []) 135 | { 136 | $item = $this->newItem($name, $route, $attributes); 137 | 138 | return $item; 139 | } 140 | 141 | /** 142 | * Create new MenuItem object. 143 | * 144 | * @param string $name 145 | * @param string $route 146 | * @param array $attributes 147 | * @return MenuItem 148 | */ 149 | protected function newItem(string $name, string $route, array $attributes = []) 150 | { 151 | return new MenuItem($name, $route, $this, $attributes); 152 | } 153 | 154 | /** 155 | * Find and return a MenuItem by name. 156 | * 157 | * @param $name 158 | * @return MenuItem 159 | */ 160 | public function getItemByName($name) 161 | { 162 | return $this->menuItems->getItemByName($name); 163 | } 164 | 165 | /** 166 | * Get an index of an item by name. 167 | * 168 | * @param string $name 169 | * @return int 170 | */ 171 | public function getIndexByName($name) 172 | { 173 | return $this->menuItems->getIndexByName($name); 174 | } 175 | 176 | /** 177 | * Get the menu's MenuItemCollection. 178 | * 179 | * @return MenuItemCollection 180 | */ 181 | public function getMenuItems() 182 | { 183 | return $this->menuItems; 184 | } 185 | 186 | /** 187 | * Get the instance as an array. 188 | * 189 | * @return array 190 | */ 191 | public function toArray() 192 | { 193 | return $this->getMenuItems()->toArray(); 194 | } 195 | 196 | /** 197 | * @param MenuItem $parent 198 | * @param Closure $items 199 | */ 200 | public function loadChildren(MenuItem $parent, Closure $items) 201 | { 202 | $this->parentStack[] = $parent; 203 | 204 | $items(); 205 | 206 | array_pop($this->parentStack); 207 | } 208 | 209 | /** 210 | * Convert the object to its JSON representation. 211 | * 212 | * @param int $options 213 | * @return string 214 | */ 215 | public function toJson($options = 0) 216 | { 217 | return json_encode($this->toArray(), $options); 218 | } 219 | 220 | /** 221 | * Convert the object into something JSON serializable. 222 | * 223 | * @return array 224 | */ 225 | public function jsonSerialize(): mixed 226 | { 227 | return $this->toArray(); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/MenuItem.php: -------------------------------------------------------------------------------- 1 | initializeItem($name, $route, $menu); 46 | 47 | $this->fill($attributes); 48 | } 49 | 50 | /** 51 | * Initialize the values of the MenuItem. 52 | * 53 | * @param string $name 54 | * @param string $route 55 | * @param Menu $menu 56 | */ 57 | protected function initializeItem(string $name, string $route, Menu $menu) 58 | { 59 | $this->setMenu($menu); 60 | $this->setName($name); 61 | $this->setRoute($route); 62 | 63 | $this->children = new MenuItemCollection(); 64 | } 65 | 66 | /** 67 | * Fill the menuItem with the given attributes. 68 | * 69 | * @param array $attributes 70 | * @return $this 71 | */ 72 | public function fill(array $attributes) 73 | { 74 | foreach ($attributes as $key => $value) { 75 | if ($this->isFillable($key)) { 76 | $this->setAttribute($key, $value); 77 | } 78 | } 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Convert some of the variables of the MenuItem to array. 85 | * 86 | * @return array 87 | */ 88 | protected function variablesToArray() 89 | { 90 | $array = [ 91 | 'name' => $this->getName(), 92 | 'route' => $this->getRoute(), 93 | 'active' => $this->isActive(), 94 | ]; 95 | 96 | if (count($this->getChildren()) > 0) { 97 | $array['children'] = $this->getChildren()->toArray(); 98 | } 99 | 100 | return $array; 101 | } 102 | 103 | /** 104 | * Get an attribute array of all arrayable attributes. 105 | * 106 | * @return array 107 | */ 108 | protected function getArrayableAttributes() 109 | { 110 | return $this->attributes; 111 | } 112 | 113 | /** 114 | * Get the casts array. 115 | * 116 | * @return array 117 | */ 118 | public function getCasts() 119 | { 120 | return $this->casts; 121 | } 122 | 123 | /** 124 | * Get the children of the MenuItem. 125 | * 126 | * @return MenuItemCollection 127 | */ 128 | public function getChildren() 129 | { 130 | return $this->children; 131 | } 132 | 133 | /** 134 | * Add children to the MenuItem. 135 | * 136 | * @param self $item 137 | * @return $this 138 | */ 139 | public function addChild(self $item) 140 | { 141 | $this->children->add($item); 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Add multiple children to the MenuItem. 148 | * 149 | * @param Closure $items 150 | * @return $this 151 | */ 152 | public function addChildren(Closure $items) 153 | { 154 | $this->menu->loadChildren($this, $items); 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * Get the attributes that should be converted to dates. 161 | * 162 | * @return array 163 | */ 164 | public function getDates() 165 | { 166 | return [ 167 | 'created_at', 168 | 'updated_at', 169 | ]; 170 | } 171 | 172 | /** 173 | * Get the name of the menuItem. 174 | * 175 | * @return string 176 | */ 177 | public function getName() 178 | { 179 | return $this->name; 180 | } 181 | 182 | /** 183 | * Set the name of the menuItem. 184 | * 185 | * @param $name 186 | * @return $this 187 | */ 188 | protected function setName($name) 189 | { 190 | $this->name = $name; 191 | 192 | return $this; 193 | } 194 | 195 | /** 196 | * Set the route of the menuItem. 197 | * 198 | * @param string $route 199 | * @return $this 200 | */ 201 | public function setRoute(string $route) 202 | { 203 | $this->route = $route; 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Returns the route of the menuItem. 210 | * 211 | * @return string 212 | */ 213 | public function getRoute() 214 | { 215 | return $this->route; 216 | } 217 | 218 | /** 219 | * Return the attributes of the MenuItem as array. 220 | * 221 | * @return array 222 | */ 223 | public function toArray() 224 | { 225 | return array_merge($this->attributesToArray(), $this->variablesToArray()); 226 | } 227 | 228 | /** 229 | * Convert the model's attributes to an array. 230 | * 231 | * @return array 232 | */ 233 | public function attributesToArray() 234 | { 235 | // If an attribute is a date, we will cast it to a string after converting it 236 | // to a DateTime / Carbon instance. This is so we will get some consistent 237 | // formatting while accessing attributes vs. arraying / JSONing a model. 238 | $attributes = $this->addDateAttributesToArray( 239 | $attributes = $this->getArrayableAttributes() 240 | ); 241 | 242 | // Here we will grab all of the appended, calculated attributes to this model 243 | // as these attributes are not really in the attributes array, but are run 244 | // when we need to array or JSON the model for convenience to the coder. 245 | foreach ($this->getArrayableAppends() as $key) { 246 | $attributes[$key] = $this->mutateAttributeForArray($key, null); 247 | } 248 | 249 | return $attributes; 250 | } 251 | 252 | /** 253 | * Convert the object to its JSON representation. 254 | * 255 | * @param int $options 256 | * @return string 257 | */ 258 | public function toJson($options = 0) 259 | { 260 | return json_encode($this->toArray(), $options); 261 | } 262 | 263 | /** 264 | * Convert the object into something JSON serializable. 265 | * 266 | * @return array 267 | */ 268 | public function jsonSerialize(): mixed 269 | { 270 | return $this->toArray(); 271 | } 272 | 273 | /** 274 | * Determine if the given attribute may be mass assigned. 275 | * 276 | * @param string $key 277 | * @return bool 278 | */ 279 | public function isFillable($key) 280 | { 281 | return ! in_array($key, $this->guardedAttributes); 282 | } 283 | 284 | /** 285 | * Returns if menuItem is active. 286 | * 287 | * @return bool 288 | */ 289 | public function isActive() 290 | { 291 | return Request::is($this->getRoute()) || (Route::currentRouteName() == $this->getRoute()); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/MenuItemCollection.php: -------------------------------------------------------------------------------- 1 | filter(function (MenuItem $item) use ($name) { 18 | return $item->getName() == $name ?: $item->getChildren()->hasName($name); 19 | })->count() > 0; 20 | } 21 | 22 | /** 23 | * Get a MenuItem from the collection by name. 24 | * 25 | * @param string $name 26 | * @return MenuItem 27 | */ 28 | public function getItemByName(string $name) 29 | { 30 | if ($item = $this->filter(function (MenuItem $item) use ($name) { 31 | return $item->getName() == $name; 32 | })->first()) { 33 | return $item; 34 | } 35 | 36 | // Nested Search 37 | foreach ($this as $item) { 38 | if ($item instanceof MenuItem) { 39 | if ($foundItem = $item->getChildren()->getItemByName($name)) { 40 | return $foundItem; 41 | } 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Get an index of an item by name. 48 | * 49 | * @param string $name 50 | * @return mixed 51 | */ 52 | public function getIndexByName(string $name) 53 | { 54 | return $this->search(function (MenuItem $item) use ($name) { 55 | return $item->getName() == $name; 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/MenuServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerMenu(); 17 | } 18 | 19 | protected function registerMenu() 20 | { 21 | $this->app->singleton('menu', function ($app) { 22 | return new Menu(); 23 | }); 24 | } 25 | } 26 | --------------------------------------------------------------------------------