├── .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 |

4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
--------------------------------------------------------------------------------