├── src ├── Language │ └── en │ │ └── Menus.php ├── Config │ ├── Filters.php │ └── Menus.php ├── Styles │ ├── BootstrapStyle.php │ ├── BreadcrumbsStyle.php │ └── AdminLTEStyle.php ├── Breadcrumb.php ├── Menu.php ├── Filters │ └── MenusFilter.php └── Menus │ └── BreadcrumbsMenu.php ├── roave-bc-check.yaml ├── infection.json.dist ├── .php-cs-fixer.dist.php ├── composer-unused.php ├── LICENSE ├── SECURITY.md ├── composer.json ├── CHANGELOG.md ├── deptrac.yaml ├── rector.php └── README.md /src/Language/en/Menus.php: -------------------------------------------------------------------------------- 1 | 'Home', 7 | ]; 8 | -------------------------------------------------------------------------------- /roave-bc-check.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#\[BC\] SKIPPED: .+ could not be found in the located source#' 4 | -------------------------------------------------------------------------------- /src/Config/Filters.php: -------------------------------------------------------------------------------- 1 | aliases['menus'] = MenusFilter::class; 12 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src/" 5 | ], 6 | "excludes": [ 7 | "Config", 8 | "Database/Migrations", 9 | "Views" 10 | ] 11 | }, 12 | "logs": { 13 | "text": "build/infection.log" 14 | }, 15 | "mutators": { 16 | "@default": true 17 | }, 18 | "bootstrap": "vendor/codeigniter4/framework/system/Test/bootstrap.php" 19 | } 20 | -------------------------------------------------------------------------------- /src/Config/Menus.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public $aliases = [ 16 | 'breadcrumbs' => BreadcrumbsMenu::class, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | files() 9 | ->in([ 10 | __DIR__ . '/src/', 11 | __DIR__ . '/tests/', 12 | ]) 13 | ->exclude('build') 14 | ->append([__FILE__]); 15 | 16 | $overrides = []; 17 | 18 | $options = [ 19 | 'finder' => $finder, 20 | 'cacheFile' => 'build/.php-cs-fixer.cache', 21 | ]; 22 | 23 | return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); 24 | -------------------------------------------------------------------------------- /src/Styles/BootstrapStyle.php: -------------------------------------------------------------------------------- 1 | builder 21 | ->addClass('navbar-nav mr-auto') 22 | ->registerFilter(static function (Link $link) { 23 | $link->addParentClass('nav-item'); 24 | $link->addClass('nav-link'); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer-unused.php: -------------------------------------------------------------------------------- 1 | addNamedFilter(NamedFilter::fromString('symfony/config')) 13 | // ->addPatternFilter(PatternFilter::fromString('/symfony-.*/')) 14 | ->setAdditionalFilesFor('codeigniter4/framework', [ 15 | ...Glob::glob(__DIR__ . '/vendor/codeigniter4/framework/system/Helpers/*.php'), 16 | ]); 17 | }; 18 | -------------------------------------------------------------------------------- /src/Styles/BreadcrumbsStyle.php: -------------------------------------------------------------------------------- 1 | builder 22 | ->addClass('breadcrumb') 23 | ->setWrapperTag('ol') 24 | ->wrap('nav', ['aria-label' => 'breadcrumb']) 25 | ->registerFilter(static function (Link $link) { 26 | $link->addParentClass('breadcrumb-item'); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Styles/AdminLTEStyle.php: -------------------------------------------------------------------------------- 1 | builder 21 | ->addClass('nav nav-pills nav-sidebar flex-column') 22 | ->setActiveClass('active menu-open') 23 | ->setAttribute('data-widget', 'treeview') 24 | ->setAttribute('role', 'menu') 25 | ->setAttribute('data-accordion', 'false') 26 | ->registerFilter(static function (Link $link) { 27 | $link->addParentClass('nav-item'); 28 | $link->addClass('nav-link'); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Breadcrumb.php: -------------------------------------------------------------------------------- 1 | getSegments(); 35 | $display = ucfirst(end($segments) ?: lang('Menus.home')); 36 | } 37 | 38 | $this->url = $url; 39 | $this->display = $display; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tatter Software 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The development team and community take all security issues seriously. **Please do not make public any uncovered flaws.** 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Thank you for improving the security of our code! Any assistance in removing security flaws will be acknowledged. 8 | 9 | **Please report security flaws by emailing the development team directly: support@tattersoftware.com**. 10 | 11 | The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating 12 | the next steps in handling your report. After the initial reply to your report, the security team will endeavor to keep you informed of the 13 | progress towards a fix and full announcement, and may ask for additional information or guidance. 14 | 15 | ## Disclosure Policy 16 | 17 | When the security team receives a security bug report, they will assign it to a primary handler. 18 | This person will coordinate the fix and release process, involving the following steps: 19 | 20 | - Confirm the problem and determine the affected versions. 21 | - Audit code to find any potential similar problems. 22 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. 23 | 24 | ## Comments on this Policy 25 | 26 | If you have suggestions on how this process could be improved please submit a Pull Request. 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tatter/menus", 3 | "description": "Dynamic menus for CodeIgniter 4", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "codeigniter", 8 | "codeigniter4", 9 | "menus", 10 | "navigation" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Matthew Gatner", 15 | "email": "mgatner@tattersoftware.com", 16 | "homepage": "https://tattersoftware.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "homepage": "https://github.com/tattersoftware/codeigniter4-menus", 21 | "require": { 22 | "php": "^7.4 || ^8.0", 23 | "spatie/menu": "^2.10 || ^3.0" 24 | }, 25 | "require-dev": { 26 | "codeigniter4/framework": "^4.1", 27 | "tatter/tools": "^2.0" 28 | }, 29 | "minimum-stability": "dev", 30 | "prefer-stable": true, 31 | "autoload": { 32 | "psr-4": { 33 | "Tatter\\Menus\\": "src" 34 | }, 35 | "exclude-from-classmap": [ 36 | "**/Database/Migrations/**" 37 | ] 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Tests\\Support\\": "tests/_support" 42 | } 43 | }, 44 | "config": { 45 | "allow-plugins": { 46 | "ergebnis/composer-normalize": true, 47 | "phpstan/extension-installer": true 48 | } 49 | }, 50 | "scripts": { 51 | "analyze": "phpstan analyze", 52 | "ci": [ 53 | "Composer\\Config::disableProcessTimeout", 54 | "@deduplicate", 55 | "@analyze", 56 | "@test", 57 | "@inspect", 58 | "rector process", 59 | "@style" 60 | ], 61 | "deduplicate": "phpcpd app/ src/", 62 | "inspect": "deptrac analyze --cache-file=build/deptrac.cache", 63 | "mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit", 64 | "retool": "retool", 65 | "style": "php-cs-fixer fix --verbose --ansi --using-cache=no", 66 | "test": "phpunit" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Menu.php: -------------------------------------------------------------------------------- 1 | detectCurrent(); 32 | $root = (new URI(site_url()))->getPath() ?: '/'; 33 | 34 | $this->builder = $builder ?? BaseMenu::new()->setActive($current, $root); 35 | 36 | foreach (class_uses_recursive($this) as $trait) { 37 | $method = 'apply' . class_basename($trait); 38 | 39 | if (method_exists($this, $method)) { 40 | $this->{$method}(); 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Returns the underlying Spatie Menu. 47 | */ 48 | public function builder(): BaseMenu 49 | { 50 | return $this->builder; 51 | } 52 | 53 | /** 54 | * Builds the Menu and returns the 55 | * rendered HTML string. 56 | */ 57 | abstract public function __toString(): string; 58 | 59 | /** 60 | * Returns the current URL to use for determining 61 | * which menu items should be active. 62 | * Due to this bug: 63 | * - https://github.com/codeigniter4/CodeIgniter4/issues/4116 64 | * ...we cannot use current_url(). This method can be 65 | * replaced if that bug is fixed or if we get this: 66 | * - https://github.com/codeigniter4/CodeIgniter4/pull/4647 67 | * 68 | * @internal 69 | */ 70 | protected function detectCurrent(): string 71 | { 72 | // Force path discovery in a new IncomingRequest 73 | $request = service('request', null, false); 74 | $path = ltrim($request->detectPath($request->config->uriProtocol), '/'); 75 | 76 | // Build the full URL based on the config and path 77 | $url = rtrim($request->config->baseURL, '/ ') . '/'; 78 | 79 | // Check for an index page 80 | if ($request->config->indexPage !== '') { 81 | $url .= $request->config->indexPage; 82 | 83 | // If there is a path then we need a separator 84 | if ($path !== '') { 85 | $url .= '/'; 86 | } 87 | } 88 | 89 | return (string) (new URI($url . $path)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Filters/MenusFilter.php: -------------------------------------------------------------------------------- 1 | getBody())) { 44 | return null; 45 | } 46 | 47 | // Check CLI separately for coverage 48 | if (is_cli() && ENVIRONMENT !== 'testing') { 49 | return null; // @codeCoverageIgnore 50 | } 51 | 52 | // Only run on HTMl content 53 | if (strpos($response->getHeaderLine('Content-Type'), 'html') === false) { 54 | return null; 55 | } 56 | 57 | $config = config('Menus'); 58 | $body = $response->getBody(); 59 | 60 | // Locate each placeholder 61 | foreach ($arguments as $alias) { 62 | if (! isset($config->aliases[$alias])) { 63 | throw new RuntimeException('Unknown menu alias requested: ' . $alias); 64 | } 65 | 66 | if (! class_exists($class = $config->aliases[$alias])) { 67 | throw new RuntimeException('Unable to locate menu class: ' . $class); 68 | } 69 | 70 | if (! is_a($class, Menu::class, true)) { 71 | throw new RuntimeException($class . ' must extend ' . Menu::class); 72 | } 73 | 74 | // Grab the menu content 75 | $content = (string) (new $class()); 76 | $count = 0; 77 | 78 | // Swap the content for the placeholder 79 | $body = str_replace('{{' . $alias . '}}', $content, $body, $count); 80 | } 81 | 82 | // Use the new body and return the updated Response 83 | return $response->setBody($body); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.0.0](https://github.com/tattersoftware/codeigniter4-menus/tree/v1.0.0) (2021-06-15) 4 | 5 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-menus/compare/v0.9.3...v1.0.0) 6 | 7 | **Merged pull requests:** 8 | 9 | - Loosen Filter [\#20](https://github.com/tattersoftware/codeigniter4-menus/pull/20) ([MGatner](https://github.com/MGatner)) 10 | 11 | ## [v0.9.3](https://github.com/tattersoftware/codeigniter4-menus/tree/v0.9.3) (2021-05-05) 12 | 13 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-menus/compare/v0.9.2...v0.9.3) 14 | 15 | **Merged pull requests:** 16 | 17 | - Active discovery [\#14](https://github.com/tattersoftware/codeigniter4-menus/pull/14) ([MGatner](https://github.com/MGatner)) 18 | 19 | ## [v0.9.2](https://github.com/tattersoftware/codeigniter4-menus/tree/v0.9.2) (2021-04-29) 20 | 21 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-menus/compare/v0.9.1...v0.9.2) 22 | 23 | **Closed issues:** 24 | 25 | - Redirects Fail [\#12](https://github.com/tattersoftware/codeigniter4-menus/issues/12) 26 | - Rename Traits [\#11](https://github.com/tattersoftware/codeigniter4-menus/issues/11) 27 | - Config: Packaged Menus [\#10](https://github.com/tattersoftware/codeigniter4-menus/issues/10) 28 | 29 | **Merged pull requests:** 30 | 31 | - Styles & Fixes [\#13](https://github.com/tattersoftware/codeigniter4-menus/pull/13) ([MGatner](https://github.com/MGatner)) 32 | 33 | ## [v0.9.1](https://github.com/tattersoftware/codeigniter4-menus/tree/v0.9.1) (2021-04-23) 34 | 35 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-menus/compare/v0.9.0...v0.9.1) 36 | 37 | **Closed issues:** 38 | 39 | - Stylers [\#5](https://github.com/tattersoftware/codeigniter4-menus/issues/5) 40 | - To String [\#4](https://github.com/tattersoftware/codeigniter4-menus/issues/4) 41 | 42 | **Merged pull requests:** 43 | 44 | - Breadcrumbs [\#9](https://github.com/tattersoftware/codeigniter4-menus/pull/9) ([MGatner](https://github.com/MGatner)) 45 | - Stringable [\#8](https://github.com/tattersoftware/codeigniter4-menus/pull/8) ([MGatner](https://github.com/MGatner)) 46 | - Implement Expeditable tests [\#7](https://github.com/tattersoftware/codeigniter4-menus/pull/7) ([MGatner](https://github.com/MGatner)) 47 | - Traits [\#6](https://github.com/tattersoftware/codeigniter4-menus/pull/6) ([MGatner](https://github.com/MGatner)) 48 | 49 | ## [v0.9.0](https://github.com/tattersoftware/codeigniter4-menus/tree/v0.9.0) (2021-04-15) 50 | 51 | [Full Changelog](https://github.com/tattersoftware/codeigniter4-menus/compare/f1ff4dde50a21223b9b4f6c6bd2ec1037bbc32b2...v0.9.0) 52 | 53 | **Merged pull requests:** 54 | 55 | - Finalize for release [\#3](https://github.com/tattersoftware/codeigniter4-menus/pull/3) ([MGatner](https://github.com/MGatner)) 56 | - Tests [\#2](https://github.com/tattersoftware/codeigniter4-menus/pull/2) ([MGatner](https://github.com/MGatner)) 57 | - Spatie, Menu class [\#1](https://github.com/tattersoftware/codeigniter4-menus/pull/1) ([MGatner](https://github.com/MGatner)) 58 | 59 | 60 | 61 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 62 | -------------------------------------------------------------------------------- /src/Menus/BreadcrumbsMenu.php: -------------------------------------------------------------------------------- 1 | uri; 42 | 43 | // Always start with the base URL 44 | $breadcrumbs = [ 45 | new Breadcrumb(base_url(), lang('Menus.home')), 46 | ]; 47 | 48 | // Add each segment 49 | $segments = []; 50 | 51 | foreach ($uri->getSegments() as $segment) { 52 | $segments[] = $segment; 53 | $breadcrumbs[] = new Breadcrumb(site_url($segments)); 54 | } 55 | self::set($breadcrumbs); 56 | 57 | return self::get(); 58 | } 59 | 60 | /** 61 | * Returns the currently-configured crumbs. 62 | * 63 | * @return Breadcrumb[]|null 64 | */ 65 | public static function get(): ?array 66 | { 67 | return self::$breadcrumbs; 68 | } 69 | 70 | /** 71 | * Sets the crumbs used to build the Menu. 72 | * 73 | * @param Breadcrumb[]|null $breadcrumbs 74 | */ 75 | public static function set(?array $breadcrumbs) 76 | { 77 | self::$breadcrumbs = $breadcrumbs; 78 | } 79 | 80 | /** 81 | * Removes and returns the last crumb. 82 | * 83 | * @return Breadcrumb 84 | */ 85 | public static function pop(): ?Breadcrumb 86 | { 87 | return null === self::$breadcrumbs ? null : array_pop(self::$breadcrumbs); 88 | } 89 | 90 | /** 91 | * Adds a new Breadcrumb to the Menu. 92 | * 93 | * @return int New number of items in the Menu 94 | */ 95 | public static function push(Breadcrumb $breadcrumb): int 96 | { 97 | self::$breadcrumbs ??= []; 98 | 99 | return array_push(self::$breadcrumbs, $breadcrumb); 100 | } 101 | 102 | //-------------------------------------------------------------------- 103 | /** 104 | * Builds the Menu and returns the 105 | * rendered HTML string. 106 | */ 107 | public function __toString(): string 108 | { 109 | // If no breadcrumbs are set then initiate discovery 110 | if (null === self::$breadcrumbs) { 111 | self::discover(); 112 | } 113 | 114 | // Use the last item without a link 115 | $last = self::pop(); 116 | 117 | foreach (self::$breadcrumbs as $breadcrumb) { 118 | $this->builder->link($breadcrumb->url, $breadcrumb->display); 119 | } 120 | 121 | return $this->builder 122 | ->html($last->display, [ 123 | 'class' => 'breadcrumb-item active', 124 | ])->render(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /deptrac.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | paths: 3 | - ./src/ 4 | - ./vendor/codeigniter4/framework/system/ 5 | exclude_files: 6 | - '#.*test.*#i' 7 | layers: 8 | - name: Model 9 | collectors: 10 | - type: bool 11 | must: 12 | - type: className 13 | regex: .*[A-Za-z]+Model$ 14 | must_not: 15 | - type: directory 16 | regex: vendor/.* 17 | - name: Vendor Model 18 | collectors: 19 | - type: bool 20 | must: 21 | - type: className 22 | regex: .*[A-Za-z]+Model$ 23 | - type: directory 24 | regex: vendor/.* 25 | - name: Controller 26 | collectors: 27 | - type: bool 28 | must: 29 | - type: className 30 | regex: .*\/Controllers\/.* 31 | must_not: 32 | - type: directory 33 | regex: vendor/.* 34 | - name: Vendor Controller 35 | collectors: 36 | - type: bool 37 | must: 38 | - type: className 39 | regex: .*\/Controllers\/.* 40 | - type: directory 41 | regex: vendor/.* 42 | - name: Config 43 | collectors: 44 | - type: bool 45 | must: 46 | - type: directory 47 | regex: src/Config/.* 48 | must_not: 49 | - type: className 50 | regex: .*Services 51 | - type: directory 52 | regex: vendor/.* 53 | - name: Vendor Config 54 | collectors: 55 | - type: bool 56 | must: 57 | - type: directory 58 | regex: vendor/.*/Config/.* 59 | must_not: 60 | - type: className 61 | regex: .*Services 62 | - name: Entity 63 | collectors: 64 | - type: bool 65 | must: 66 | - type: directory 67 | regex: src/Entities/.* 68 | must_not: 69 | - type: directory 70 | regex: vendor/.* 71 | - name: Vendor Entity 72 | collectors: 73 | - type: bool 74 | must: 75 | - type: directory 76 | regex: vendor/.*/Entities/.* 77 | - name: View 78 | collectors: 79 | - type: bool 80 | must: 81 | - type: directory 82 | regex: src/Views/.* 83 | must_not: 84 | - type: directory 85 | regex: vendor/.* 86 | - name: Vendor View 87 | collectors: 88 | - type: bool 89 | must: 90 | - type: directory 91 | regex: vendor/.*/Views/.* 92 | - name: Service 93 | collectors: 94 | - type: className 95 | regex: .*Services.* 96 | ruleset: 97 | Entity: 98 | - Config 99 | - Model 100 | - Service 101 | - Vendor Config 102 | - Vendor Entity 103 | - Vendor Model 104 | Config: 105 | - Service 106 | - Vendor Config 107 | Model: 108 | - Config 109 | - Entity 110 | - Service 111 | - Vendor Config 112 | - Vendor Entity 113 | - Vendor Model 114 | Service: 115 | - Config 116 | - Vendor Config 117 | 118 | # Ignore anything in the Vendor layers 119 | Vendor Model: 120 | - Config 121 | - Service 122 | - Vendor Config 123 | - Vendor Controller 124 | - Vendor Entity 125 | - Vendor Model 126 | - Vendor View 127 | Vendor Controller: 128 | - Service 129 | - Vendor Config 130 | - Vendor Controller 131 | - Vendor Entity 132 | - Vendor Model 133 | - Vendor View 134 | Vendor Config: 135 | - Config 136 | - Service 137 | - Vendor Config 138 | - Vendor Controller 139 | - Vendor Entity 140 | - Vendor Model 141 | - Vendor View 142 | Vendor Entity: 143 | - Service 144 | - Vendor Config 145 | - Vendor Controller 146 | - Vendor Entity 147 | - Vendor Model 148 | - Vendor View 149 | Vendor View: 150 | - Service 151 | - Vendor Config 152 | - Vendor Controller 153 | - Vendor Entity 154 | - Vendor Model 155 | - Vendor View 156 | skip_violations: 157 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | sets([SetList::DEAD_CODE, LevelSetList::UP_TO_PHP_74, PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, PHPUnitSetList::PHPUNIT_80]); 40 | $rectorConfig->parallel(); 41 | // The paths to refactor (can also be supplied with CLI arguments) 42 | $rectorConfig->paths([ 43 | __DIR__ . '/src/', 44 | __DIR__ . '/tests/', 45 | ]); 46 | 47 | // Include Composer's autoload - required for global execution, remove if running locally 48 | $rectorConfig->autoloadPaths([ 49 | __DIR__ . '/vendor/autoload.php', 50 | ]); 51 | 52 | // Do you need to include constants, class aliases, or a custom autoloader? 53 | $rectorConfig->bootstrapFiles([ 54 | realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php', 55 | ]); 56 | 57 | if (is_file(__DIR__ . '/phpstan.neon.dist')) { 58 | $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist'); 59 | } 60 | 61 | // Set the target version for refactoring 62 | $rectorConfig->phpVersion(PhpVersion::PHP_74); 63 | 64 | // Auto-import fully qualified class names 65 | $rectorConfig->importNames(); 66 | 67 | // Are there files or rules you need to skip? 68 | $rectorConfig->skip([ 69 | __DIR__ . '/src/Config/Filters.php', 70 | __DIR__ . '/src/Views', 71 | 72 | JsonThrowOnErrorRector::class, 73 | StringifyStrNeedlesRector::class, 74 | 75 | // Note: requires php 8 76 | RemoveUnusedPromotedPropertyRector::class, 77 | 78 | // Ignore tests that might make calls without a result 79 | RemoveEmptyMethodCallRector::class => [ 80 | __DIR__ . '/tests', 81 | ], 82 | 83 | // Ignore files that should not be namespaced 84 | NormalizeNamespaceByPSR4ComposerAutoloadRector::class => [ 85 | __DIR__ . '/src/Helpers', 86 | ], 87 | 88 | // May load view files directly when detecting classes 89 | StringClassNameToClassConstantRector::class, 90 | 91 | // May be uninitialized on purpose 92 | AddDefaultValueForUndefinedVariableRector::class, 93 | ]); 94 | $rectorConfig->rule(SimplifyUselessVariableRector::class); 95 | $rectorConfig->rule(RemoveAlwaysElseRector::class); 96 | $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); 97 | $rectorConfig->rule(ForToForeachRector::class); 98 | $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); 99 | $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); 100 | $rectorConfig->rule(SimplifyStrposLowerRector::class); 101 | $rectorConfig->rule(CombineIfRector::class); 102 | $rectorConfig->rule(SimplifyIfReturnBoolRector::class); 103 | $rectorConfig->rule(InlineIfToExplicitIfRector::class); 104 | $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); 105 | $rectorConfig->rule(ShortenElseIfRector::class); 106 | $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); 107 | $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); 108 | $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); 109 | $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); 110 | $rectorConfig->rule(AddPregQuoteDelimiterRector::class); 111 | $rectorConfig->rule(SimplifyRegexPatternRector::class); 112 | $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); 113 | $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); 114 | $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); 115 | $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); 116 | $rectorConfig 117 | ->ruleWithConfiguration(TypedPropertyRector::class, [ 118 | TypedPropertyRector::INLINE_PUBLIC => false, 119 | ]); 120 | }; 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tatter\Menus 2 | Dynamic menus for CodeIgniter 4 3 | 4 | [![](https://github.com/tattersoftware/codeigniter4-menus/workflows/PHPUnit/badge.svg)](https://github.com/tattersoftware/codeigniter4-menus/actions?query=workflow%3A%22PHPUnit%22) 5 | [![](https://github.com/tattersoftware/codeigniter4-menus/workflows/PHPStan/badge.svg)](https://github.com/tattersoftware/codeigniter4-menus/actions?query=workflow%3A%PHPStan%22) 6 | [![Coverage Status](https://coveralls.io/repos/github/tattersoftware/codeigniter4-menus/badge.svg?branch=develop)](https://coveralls.io/github/tattersoftware/codeigniter4-menus?branch=develop) 7 | 8 | ## Quick Start 9 | 10 | 1. Install with Composer: `> composer require tatter/menus` 11 | 2. Create your menus by extending `Tatter\Menus\Menu` 12 | 3. Add your menu aliases to `Config\Menus` 13 | 4. Apply `MenuFilter` to all routes that need menus 14 | 15 | ## Features 16 | 17 | **Menus** provides dynamic menus across your application. **Menus** organizes and injects the 18 | menu content, so you can focus on building. 19 | 20 | ## Installation 21 | 22 | Install easily via Composer to take advantage of CodeIgniter 4's autoloading capabilities 23 | and always be up-to-date: 24 | * `> composer require tatter/menus` 25 | 26 | Or, install manually by downloading the source files and adding the directory to 27 | `app/Config/Autoload.php`. 28 | 29 | ## Configuration (optional) 30 | 31 | The library's default behavior can be altered by extending its config file. Copy 32 | **examples/Menus.php** to **app/Config/** and follow the instructions 33 | in the comments. If no config file is found in app/Config the library will use its own. 34 | 35 | ## Usage 36 | 37 | ### Building 38 | 39 | **Menus** is built on `Spatie\Menu` with all of its wonderful, dynamic and fluent functionality. 40 | Use their documentation to craft your menus as simple or complex as you like: 41 | * [Version 2](https://spatie.be/docs/menu/v2) 42 | * [Version 3](https://spatie.be/docs/menu/v3) (PHP 8 only) 43 | 44 | Create your menus by extending `Tatter\Menus\Menu`. You will notice in the source code that 45 | `Menu` requires you to provide one method: `public function __toString(): string;`. You may use the 46 | supplied `$builder` property to access the underlying `Spatie\Menu` to build your menu, 47 | or provide your own HTML code or `view()` return. Some examples: 48 | ``` 49 | class MainMenu extends \Tatter\Menus\Menu 50 | { 51 | public function __toString(): string 52 | { 53 | return $this->builder 54 | ->link(site_url('/'), 'Home') 55 | ->link(site_url('/about'), 'About') 56 | ->html('
') 57 | ->link(site_url('/contact'), 'Contact') 58 | ->render(); 59 | } 60 | } 61 | 62 | class FruitMenu extends \Tatter\Menus\Menu 63 | { 64 | public function __toString(): string 65 | { 66 | return view('menus/fruit', ['active' => 'banana']); 67 | } 68 | } 69 | ``` 70 | 71 | Note: `$builder` is initialized with "set active" to the current URL. You may call `setActive()` 72 | again to remove or change the active menu item. Due to a limitation in `Spatie\Menu` with mixing 73 | relative and absolute URLs you must supply full URL values (e.g. with `site_url()`) to your 74 | `Menu` if you want to use this default "active" URL. 75 | 76 | ### Deploying 77 | 78 | Since `Menu` is `Stringable` it can be used in your view or layout files as is. 79 | However, **Menus** also comes with a [Controller Filter](https://codeigniter4.github.io/CodeIgniter4/incoming/filters.html) 80 | that you can use to inject menu content directly into your responses. First you need to create 81 | an alias for each `Menu` class you would like to use. Create **app/Config/Menus.php** (or 82 | start with a copy from the **examples** folder) and add your menu classes to the `$aliases` 83 | array. For example: 84 | ``` 85 | class Menus extends \Tatter\Menus\Config\Menus 86 | { 87 | /** 88 | * Menu class aliases. 89 | * 90 | * @var array 91 | */ 92 | public $aliases = [ 93 | 'main' => \App\Menus\MainMenu::class, 94 | 'fruit' => \ShopModule\FruitMenu::class, 95 | ]; 96 | } 97 | ``` 98 | 99 | Once aliases are set up you can pass them as an argument to the `MenuFilter` for any route: 100 | ``` 101 | $routes->add('shop/(:any)', 'ShopModule\ShopController::show/$1', ['filter' => 'menus:fruit']); 102 | ``` 103 | 104 | Then in your view or layout put the placeholder token with the name of the alias target in 105 | double curly braces: 106 | ``` 107 | 108 | 109 | {{main}} 110 |

Fruit Shop

111 | {{fruit}} 112 | ... 113 | ``` 114 | 115 | Note that sometimes it is preferable to apply the filter in bulk using **app/Config/Filters.php**. 116 | Unfortunately parameters are [not yet supported](https://github.com/codeigniter4/CodeIgniter4/issues/2078) 117 | in `Config\Filters`, but you can work around this by creating your own parameter-specific Filter: 118 | ``` 119 | find($userId); 159 | 160 | // Start with the default breadcrumbs 161 | BreadcrumbsMenu::discover(); 162 | 163 | // Pop off the numeric last segment 164 | BreadcrumbsMenu::pop(); 165 | 166 | // Replace it with the user's name 167 | BreadcrumbsMenu::push(new Breadcrumb(current_url(), $user->name)); 168 | 169 | return view('users/show', ['user' => $user]); 170 | } 171 | } 172 | ``` 173 | ... if you have the filter in place the rest is handled for you. 174 | --------------------------------------------------------------------------------