├── src └── AttributeRoutes │ ├── Exception │ └── LogicException.php │ ├── RouteResource.php │ ├── RoutePresenter.php │ ├── AttributeReader │ ├── MethodReader.php │ └── ClassReader.php │ ├── VarExportTrait.php │ ├── Commands │ └── AttributeRoutes.php │ ├── ControllerFinder.php │ ├── RouteGroup.php │ ├── RouteFileGenerator.php │ ├── AbstractRouteRest.php │ ├── Route.php │ └── AttributeReader.php ├── psalm.xml ├── psalm_autoload.php ├── .php-cs-fixer.dist.php ├── LICENSE ├── phpcs.xml ├── composer.json ├── README.md └── rector.php /src/AttributeRoutes/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/AttributeRoutes/RouteResource.php: -------------------------------------------------------------------------------- 1 | resource('%s', %s);"; 26 | } 27 | -------------------------------------------------------------------------------- /psalm_autoload.php: -------------------------------------------------------------------------------- 1 | presenter('%s', %s);"; 27 | } 28 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | files() 9 | ->in(__DIR__) 10 | ->exclude('build') 11 | ->append([__FILE__]); 12 | 13 | $overrides = [ 14 | 'global_namespace_import' => [ 15 | 'import_constants' => true, 16 | 'import_functions' => true, 17 | 'import_classes' => true, 18 | ], 19 | ]; 20 | 21 | $options = [ 22 | 'finder' => $finder, 23 | 'cacheFile' => 'build/.php-cs-fixer.cache', 24 | ]; 25 | 26 | return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects(); 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Kenji Suzuki 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/AttributeRoutes/AttributeReader/MethodReader.php: -------------------------------------------------------------------------------- 1 | getMethods() as $method) { 24 | $attributes = $method->getAttributes(Route::class); 25 | 26 | if ($attributes === []) { 27 | continue; 28 | } 29 | 30 | foreach ($attributes as $attribute) { 31 | /** @var Route $route */ 32 | $route = $attribute->newInstance(); 33 | $route->setControllerMethod( 34 | $method->getDeclaringClass()->getName() . '::' . $method->getName() 35 | ); 36 | 37 | $routes[] = $route; 38 | } 39 | } 40 | 41 | return $routes; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AttributeRoutes/VarExportTrait.php: -------------------------------------------------------------------------------- 1 | \n[`, it will get converted to `=> [` 20 | * 21 | * @param mixed $expression 22 | * 23 | * @see https://www.php.net/manual/en/function.var-export.php 24 | */ 25 | private function varExport($expression): string 26 | { 27 | $export = var_export($expression, true); 28 | 29 | $patterns = [ 30 | '/array \(/' => '[', 31 | '/^([ ]*)\)(,?)$/m' => '$1]$2', 32 | "/=>[ ]?\n[ ]+\\[/" => '=> [', 33 | "/([ ]*)(\\'[^\\']+\\') => ([\\[\\'])/" => '$1$2 => $3', 34 | ]; 35 | $export = preg_replace(array_keys($patterns), array_values($patterns), $export); 36 | 37 | if ($export === null) { 38 | throw new RuntimeException('Failed to convert to short array syntax'); 39 | } 40 | 41 | return $export; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AttributeRoutes/Commands/AttributeRoutes.php: -------------------------------------------------------------------------------- 1 | $params 44 | */ 45 | public function run(array $params): void 46 | { 47 | $generator = new RouteFileGenerator(); 48 | $message = $generator->generate(); 49 | 50 | CLI::write($message); 51 | CLI::newLine(); 52 | CLI::write( 53 | 'Check your routes with the `' 54 | . CLI::color('php spark routes', 'green') 55 | . '` command.' 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/AttributeRoutes/ControllerFinder.php: -------------------------------------------------------------------------------- 1 | namespaces = $namespaces; 28 | $this->locator = Services::locator(); 29 | } 30 | 31 | /** 32 | * @return class-string[] 33 | */ 34 | public function find(): array 35 | { 36 | $classes = []; 37 | 38 | foreach ($this->namespaces as $namespace) { 39 | $files = $this->locator->listNamespaceFiles($namespace, 'Controllers'); 40 | 41 | foreach ($files as $file) { 42 | if (is_file($file)) { 43 | $classnameOrEmpty = $this->locator->getClassname($file); 44 | 45 | if ($classnameOrEmpty !== '') { 46 | /** @var class-string $classname */ 47 | $classname = $classnameOrEmpty; 48 | 49 | $reflection = new ReflectionClass($classname); 50 | if ($reflection->isAbstract()) { 51 | continue; 52 | } 53 | 54 | $classes[] = $classname; 55 | } 56 | } 57 | } 58 | } 59 | 60 | return $classes; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AttributeRoutes/AttributeReader/ClassReader.php: -------------------------------------------------------------------------------- 1 | getClassRoutes($class, RouteGroup::class); 22 | } 23 | 24 | /** 25 | * @param class-string $class 26 | * 27 | * @return RouteResource[] 28 | */ 29 | public function getResourceRoutes(string $class): array 30 | { 31 | return $this->getClassRoutes($class, RouteResource::class); 32 | } 33 | 34 | /** 35 | * @param class-string $class 36 | * 37 | * @return RoutePresenter[] 38 | */ 39 | public function getPresenterRoutes(string $class): array 40 | { 41 | return $this->getClassRoutes($class, RoutePresenter::class); 42 | } 43 | 44 | /** 45 | * @param class-string $class Controller class 46 | * @param class-string $route Route class 47 | * 48 | * @return list 49 | * 50 | * @template T 51 | */ 52 | private function getClassRoutes(string $class, string $route): array 53 | { 54 | $reflection = new ReflectionClass($class); 55 | 56 | $routes = []; 57 | 58 | $attributes = $reflection->getAttributes($route); 59 | 60 | if ($attributes === []) { 61 | return []; 62 | } 63 | 64 | foreach ($attributes as $attribute) { 65 | $route = $attribute->newInstance(); 66 | 67 | $routes[] = $route; 68 | } 69 | 70 | return $routes; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/AttributeRoutes/RouteGroup.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | private array $options; 24 | 25 | /** 26 | * @var Route[] 27 | */ 28 | private array $routes; 29 | 30 | /** 31 | * @param array $options 32 | */ 33 | public function __construct(string $name, array $options = []) 34 | { 35 | $this->name = $name; 36 | $this->options = $options; 37 | } 38 | 39 | public function getName(): string 40 | { 41 | return $this->name; 42 | } 43 | 44 | /** 45 | * @return array 46 | */ 47 | public function getOptions(): array 48 | { 49 | return $this->options; 50 | } 51 | 52 | /** 53 | * @param Route[] $routes 54 | */ 55 | public function setRoutes(array $routes): void 56 | { 57 | $this->routes = $routes; 58 | } 59 | 60 | public function asCode(): string 61 | { 62 | $options = str_replace( 63 | ["\n", ' ', ',]'], 64 | ['', '', ']'], 65 | $this->varExport($this->options) 66 | ); 67 | 68 | $code = sprintf( 69 | "\$routes->group('%s', %s, static function (\$routes) {", 70 | $this->getName(), 71 | $options 72 | ) . "\n"; 73 | 74 | $routeCode = ''; 75 | 76 | foreach ($this->routes as $route) { 77 | $routeCode .= $route->asCode(); 78 | } 79 | 80 | $routeCode = preg_replace('/^/m', ' ', $routeCode); 81 | $code .= $routeCode; 82 | 83 | return $code . "});\n"; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/AttributeRoutes/RouteFileGenerator.php: -------------------------------------------------------------------------------- 1 | getNamespace()); 32 | } 33 | 34 | if ($routesFile === '') { 35 | $routesFile = APPPATH . 'Config/RoutesFromAttribute.php'; 36 | } 37 | 38 | $this->routesFile = $routesFile; 39 | $this->finder = new ControllerFinder($namespaces); 40 | $this->reader = new AttributeReader(); 41 | } 42 | 43 | /** 44 | * @return list 45 | */ 46 | public function getRoutes(): array 47 | { 48 | $controllers = $this->finder->find(); 49 | 50 | $routes = []; 51 | 52 | foreach ($controllers as $controller) { 53 | $routes = [...$routes, ...$this->reader->getRoutes($controller)]; 54 | } 55 | 56 | return $routes; 57 | } 58 | 59 | public function getRoutesCode(): string 60 | { 61 | $routes = $this->getRoutes(); 62 | 63 | $code = ''; 64 | 65 | foreach ($routes as $route) { 66 | $code .= $route->asCode(); 67 | } 68 | 69 | return $code; 70 | } 71 | 72 | /** 73 | * @return string successful message 74 | */ 75 | public function generate(): string 76 | { 77 | $code = <<<'PHP' 78 | getRoutesCode(); 84 | 85 | file_put_contents($this->routesFile, $code, LOCK_EX); 86 | 87 | return clean_path($this->routesFile) . ' generated.'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | src 19 | tests 20 | */tmp/* 21 | */Fake/* 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/AttributeRoutes/AbstractRouteRest.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected array $options; 25 | 26 | /** 27 | * @var class-string|null 28 | */ 29 | protected ?string $controller = null; 30 | 31 | /** 32 | * @var string[]|null 33 | */ 34 | protected ?array $only = null; 35 | 36 | /** 37 | * @var string[] 38 | */ 39 | protected array $validMethods; 40 | 41 | protected string $codeTemplate; 42 | 43 | /** 44 | * @param array $options 45 | */ 46 | public function __construct(string $name, array $options = []) 47 | { 48 | $this->name = $name; 49 | $this->options = $options; 50 | } 51 | 52 | public function getName(): string 53 | { 54 | return $this->name; 55 | } 56 | 57 | /** 58 | * @return array 59 | */ 60 | public function getOptions(): array 61 | { 62 | return $this->options; 63 | } 64 | 65 | /** 66 | * @return string[]|null 67 | */ 68 | public function getOnly(): ?array 69 | { 70 | return $this->only; 71 | } 72 | 73 | /** 74 | * @param class-string $controller 75 | */ 76 | public function setController(string $controller): void 77 | { 78 | $this->controller = $controller; 79 | } 80 | 81 | public function isValidMethod(string $method): bool 82 | { 83 | return in_array($method, $this->validMethods, true); 84 | } 85 | 86 | /** 87 | * @param string[] $only 88 | */ 89 | public function setOnly(array $only): void 90 | { 91 | foreach ($only as $method) { 92 | assert($this->isValidMethod($method)); 93 | } 94 | 95 | if (count($only) === count($this->validMethods)) { 96 | $only = []; 97 | } 98 | 99 | $this->only = $only; 100 | } 101 | 102 | public function asCode(): string 103 | { 104 | assert( 105 | $this->controller !== null, 106 | 'You must set $controller with setController().' 107 | ); 108 | assert( 109 | $this->only !== null, 110 | 'You must set $only with setOnly().' 111 | ); 112 | 113 | $options = ['controller' => $this->controller]; 114 | $options = array_merge($options, $this->options); 115 | $options = str_replace( 116 | ["\n", ' ', ',]', '\\\\', "[ '", ', ]'], 117 | [' ', '', ']', '\\', "['", ']'], 118 | $this->varExport($options) 119 | ); 120 | 121 | // add only 122 | if ($this->only !== []) { 123 | $options = str_replace( 124 | ']', 125 | ", 'only' => ['" . implode("', '", $this->only) . "']]", 126 | $options 127 | ); 128 | } 129 | 130 | return sprintf( 131 | $this->codeTemplate, 132 | $this->getName(), 133 | $options 134 | ) . "\n"; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kenjis/ci4-attribute-routes", 3 | "type": "library", 4 | "description": "CodeIgniter4 Attribute Routes module", 5 | "keywords": ["codeigniter4","routing"], 6 | "homepage": "https://github.com/kenjis/ci4-attribute-routes", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Kenji Suzuki", 11 | "homepage": "https://github.com/kenjis" 12 | } 13 | ], 14 | "config": { 15 | "allow-plugins": { 16 | "dealerdirect/phpcodesniffer-composer-installer": true, 17 | "phpstan/extension-installer": true 18 | }, 19 | "preferred-install": { 20 | "codeigniter4/codeigniter4": "source", 21 | "*": "dist" 22 | } 23 | }, 24 | "require": { 25 | "php": "^8.0" 26 | }, 27 | "require-dev": { 28 | "codeigniter4/codeigniter4": "dev-develop", 29 | "codeigniter4/devkit": "^1.0", 30 | "phpunit/phpunit": "^9.5", 31 | "kenjis/phpunit-helper": "^1.1.2", 32 | "doctrine/coding-standard": "^9.0", 33 | "squizlabs/php_codesniffer": "^3.6", 34 | "phpmd/phpmd": "^2.11", 35 | "phpmetrics/phpmetrics": "^2.7", 36 | "vimeo/psalm": "^4.18", 37 | "psalm/plugin-phpunit": "^0.13", 38 | "rector/rector": "0.15.13", 39 | "icanhazstring/composer-unused": "^0.8.1" 40 | }, 41 | "suggest": { 42 | "ext-fileinfo": "Improves mime type detection for files" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Kenjis\\CI4\\AttributeRoutes\\": "src/AttributeRoutes" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Kenjis\\CI4\\AttributeRoutes\\": "tests/AttributeRoutes" 52 | } 53 | }, 54 | "repositories": [ 55 | { 56 | "type": "vcs", 57 | "url": "https://github.com/codeigniter4/codeigniter4" 58 | } 59 | ], 60 | "minimum-stability": "dev", 61 | "prefer-stable": true, 62 | "scripts": { 63 | "test": "phpunit", 64 | "coverage": "php -dzend_extension=xdebug.so -dxdebug.mode=coverage ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage", 65 | "phpdbg": "phpdbg -qrr ./vendor/bin/phpunit --coverage-text --coverage-html ./build/coverage --coverage-clover=coverage.xml", 66 | "pcov": "php -dextension=pcov.so -d pcov.enabled=1 ./vendor/bin/phpunit --coverage-text --coverage-html=build/coverage --coverage-clover=coverage.xml", 67 | "cs": [ 68 | "phpcs", 69 | "php-cs-fixer fix --verbose --dry-run --diff" 70 | ], 71 | "cs-fix": [ 72 | "phpcbf src tests", 73 | "php-cs-fixer fix --verbose --diff" 74 | ], 75 | "metrics": "phpmetrics --report-html=build/metrics --exclude=Exception src", 76 | "clean": ["phpstan clear-result-cache", "psalm --clear-cache"], 77 | "sa": ["phpstan analyse", "psalm --show-info=true"], 78 | "tests": ["@cs", "@sa", "@test"], 79 | "build": ["@clean", "@cs", "@sa", "@pcov", "@metrics"] 80 | }, 81 | "scripts-descriptions": { 82 | "test": "Run unit tests", 83 | "coverage": "Generate test coverage report", 84 | "phpdbg": "Generate test coverage report (phpdbg)", 85 | "pcov": "Generate test coverage report (pcov)", 86 | "cs": "Check the coding style", 87 | "cs-fix": "Fix the coding style", 88 | "clean": "Delete tmp files", 89 | "sa": "Run static analysis", 90 | "metrics": "Build metrics report", 91 | "tests": "Run tests and quality checks", 92 | "build": "Build project" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/AttributeRoutes/Route.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | private array $options; 33 | 34 | private ?string $controllerMethod = null; 35 | 36 | /** 37 | * @param string[] $methods 38 | * @param array $options 39 | */ 40 | public function __construct(string $uri, array $methods = [], array $options = []) 41 | { 42 | $this->validateMethods($methods); 43 | 44 | $this->uri = $uri; 45 | $this->methods = $methods; 46 | $this->options = $options; 47 | } 48 | 49 | /** 50 | * @param string[] $methods 51 | */ 52 | private function validateMethods(array $methods): void 53 | { 54 | $validMethods = [ 55 | 'get', 56 | 'post', 57 | 'put', 58 | 'patch', 59 | 'delete', 60 | 'options', 61 | 'head', 62 | 'cli', 63 | ]; 64 | 65 | foreach ($methods as $method) { 66 | if (! in_array($method, $validMethods, true)) { 67 | if ($method === 'add') { 68 | throw new LogicException('$routes->add() is not secure. Do not use.'); 69 | } 70 | 71 | throw new LogicException(sprintf('Invalid method: %s', $method)); 72 | } 73 | } 74 | } 75 | 76 | public function setControllerMethod(string $controllerMethod): void 77 | { 78 | $this->controllerMethod = '\\' . $controllerMethod . $this->getArgs(); 79 | } 80 | 81 | /** 82 | * Returns the path like `/$1/$2` for placeholders 83 | */ 84 | private function getArgs(): string 85 | { 86 | preg_match_all('/\(.+?\)/', $this->uri, $matches); 87 | $count = is_countable($matches[0]) ? count($matches[0]) : 0; 88 | 89 | $args = ''; 90 | 91 | if ($count > 0) { 92 | for ($i = 1; $i <= $count; $i++) { 93 | $args .= '/$' . $i; 94 | } 95 | } 96 | 97 | return $args; 98 | } 99 | 100 | public function asCode(): string 101 | { 102 | assert( 103 | $this->controllerMethod !== null, 104 | 'You must set $controllerMethod with setControllerMethod().' 105 | ); 106 | 107 | $code = ''; 108 | 109 | foreach ($this->methods as $method) { 110 | if ($this->options === []) { 111 | $code .= sprintf( 112 | "\$routes->%s('%s', '%s');", 113 | $method, 114 | $this->uri, 115 | $this->controllerMethod, 116 | ) . "\n"; 117 | 118 | continue; 119 | } 120 | 121 | $code .= sprintf( 122 | "\$routes->%s('%s', '%s', %s);", 123 | $method, 124 | $this->uri, 125 | $this->controllerMethod, 126 | $this->varExport($this->options) 127 | ) . "\n"; 128 | } 129 | 130 | return $code; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/AttributeRoutes/AttributeReader.php: -------------------------------------------------------------------------------- 1 | classReader = new ClassReader(); 22 | $this->methodReader = new MethodReader(); 23 | } 24 | 25 | /** 26 | * @param class-string $class 27 | * 28 | * @return list 29 | */ 30 | public function getRoutes(string $class) 31 | { 32 | $groupRoutes = $this->getGroup($class); 33 | $resourceRoutes = $this->getResource($class); 34 | $presenterRoutes = $this->getPresenter($class); 35 | 36 | $routes = array_merge($groupRoutes, $resourceRoutes, $presenterRoutes); 37 | if ($routes !== []) { 38 | return $routes; 39 | } 40 | 41 | return $this->methodReader->getRoutes($class); 42 | } 43 | 44 | /** 45 | * @param class-string $class 46 | * 47 | * @return RouteGroup[] 48 | */ 49 | private function getGroup(string $class): array 50 | { 51 | $groups = $this->classReader->getGroupRoutes($class); 52 | 53 | foreach ($groups as $group) { 54 | $methodRoutes = $this->methodReader->getRoutes($class); 55 | $group->setRoutes($methodRoutes); 56 | } 57 | 58 | return $groups; 59 | } 60 | 61 | /** 62 | * @param class-string $class 63 | * 64 | * @return RouteResource[] 65 | */ 66 | private function getResource(string $class): array 67 | { 68 | $resources = $this->classReader->getResourceRoutes($class); 69 | 70 | if ($resources === []) { 71 | return []; 72 | } 73 | 74 | $reflection = new ReflectionClass($class); 75 | 76 | foreach ($resources as $resource) { 77 | $resource->setController($reflection->getName()); 78 | 79 | $only = []; 80 | 81 | foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 82 | $methodName = $method->getName(); 83 | 84 | if ($resource->isValidMethod($methodName)) { 85 | $only[] = $methodName; 86 | } 87 | } 88 | 89 | $resource->setOnly($only); 90 | } 91 | 92 | return $resources; 93 | } 94 | 95 | /** 96 | * @param class-string $class 97 | * 98 | * @return RoutePresenter[] 99 | */ 100 | private function getPresenter(string $class): array 101 | { 102 | $presenters = $this->classReader->getPresenterRoutes($class); 103 | 104 | if ($presenters === []) { 105 | return []; 106 | } 107 | 108 | $reflection = new ReflectionClass($class); 109 | 110 | foreach ($presenters as $presenter) { 111 | $presenter->setController($reflection->getName()); 112 | 113 | $only = []; 114 | 115 | foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 116 | $methodName = $method->getName(); 117 | 118 | if ($presenter->isValidMethod($methodName)) { 119 | $only[] = $methodName; 120 | } 121 | } 122 | 123 | $presenter->setOnly($only); 124 | } 125 | 126 | return $presenters; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter4 Attribute Routes 2 | 3 | This package generates a **Routes File** from the **Attribute Routes** in your **Controllers**. 4 | 5 | - You can set routes in your Controllers, and disable **Auto Routing**. 6 | - It generates a Routes File, so, there is no extra overhead at runtime. 7 | - The generated Routes File can be used on PHP 7.3 production servers. 8 | 9 | ```php 10 | use Kenjis\CI4\AttributeRoutes\Route; 11 | 12 | class SomeController extends BaseController 13 | { 14 | #[Route('path', methods: ['get'])] 15 | public function index() 16 | { 17 | ... 18 | } 19 | } 20 | ``` 21 | 22 | ## Requirements 23 | 24 | - CodeIgniter 4.3.1 or later 25 | - Composer 26 | - PHP 8.0 or later 27 | 28 | ## Installation 29 | 30 | ```sh-session 31 | $ composer require kenjis/ci4-attribute-routes 32 | ``` 33 | 34 | ## Configuration 35 | 36 | 1. Add the following code to the bottom of your `app/Config/Routes.php` file: 37 | ```php 38 | /* 39 | * Attribute Routes 40 | * 41 | * To update the route file, run the following command: 42 | * $ php spark route:update 43 | * 44 | * @see https://github.com/kenjis/ci4-attribute-routes 45 | */ 46 | if (file_exists(APPPATH . 'Config/RoutesFromAttribute.php')) { 47 | require APPPATH . 'Config/RoutesFromAttribute.php'; 48 | } 49 | ``` 50 | 51 | 2. Disable auto routing and enable route priority: 52 | ```diff 53 | --- a/app/Config/Routes.php 54 | +++ b/app/Config/Routes.php 55 | @@ -22,7 +22,8 @@ $routes->setDefaultController('Home'); 56 | $routes->setDefaultMethod('index'); 57 | $routes->setTranslateURIDashes(false); 58 | $routes->set404Override(); 59 | -$routes->setAutoRoute(true); 60 | +$routes->setAutoRoute(false); 61 | +$routes->setPrioritize(); 62 | ``` 63 | 64 | This is optional, but strongly recommended. 65 | 66 | ## Quick Start 67 | 68 | ### 1. Add Attribute Routes to your Controllers 69 | 70 | Add `#[Route()]` attributes to your Controller methods. 71 | 72 | ```php 73 | 1])] 110 | ``` 111 | 112 | ### RouteGroup 113 | 114 | ```php 115 | use Kenjis\CI4\AttributeRoutes\RouteGroup; 116 | 117 | #[RouteGroup('', options: ['filter' => 'auth'])] 118 | class GroupController extends BaseController 119 | { 120 | #[Route('group/a', methods: ['get'])] 121 | public function getA(): void 122 | { 123 | ... 124 | } 125 | ... 126 | } 127 | ``` 128 | 129 | ### RouteResource 130 | 131 | ```php 132 | use Kenjis\CI4\AttributeRoutes\RouteResource; 133 | 134 | #[RouteResource('photos', options: ['websafe' => 1])] 135 | class ResourceController extends ResourceController 136 | { 137 | ... 138 | } 139 | ``` 140 | 141 | ### RoutePresenter 142 | 143 | ```php 144 | use Kenjis\CI4\AttributeRoutes\RoutePresenter; 145 | 146 | #[RoutePresenter('presenter')] 147 | class PresenterController extends ResourcePresenter 148 | { 149 | ... 150 | } 151 | ``` 152 | 153 | ## Trouble Shooting 154 | 155 | ### No routes in the generated routes file 156 | 157 | You must import the attribute classes in your controllers. 158 | 159 | E.g.: 160 | ```php 161 | use Kenjis\CI4\AttributeRoutes\Route; 162 | ... 163 | #[Route('news', methods: ['get'])] 164 | public function index() 165 | ``` 166 | 167 | ### Can't be routed correctly, or 404 error occurs 168 | 169 | Show your routes with the `php spark routes` command, and check the order of the routes. 170 | The first matched route is the one that is executed. 171 | The placeholders like `(.*)` or `([^/]+)` takes any characters or segment. So you have to move the routes like that to the bottom. 172 | 173 | In one controller, you can move the methods having such routes to the bottom. 174 | 175 | Or set the priority of the routes with `options`: 176 | ```php 177 | #[Route('news/(:segment)', methods: ['get'], options: ['priority' => 1])] 178 | ``` 179 | Zero is the default priority, and the higher the number specified in the `priority` option, the lower route priority in the processing queue. 180 | 181 | ## For Development 182 | 183 | ### Installation 184 | 185 | composer install 186 | 187 | ### Available Commands 188 | 189 | composer test // Run unit test 190 | composer tests // Test and quality checks 191 | composer cs-fix // Fix the coding style 192 | composer sa // Run static analysys tools 193 | composer run-script --list // List all commands 194 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | sets([ 41 | SetList::DEAD_CODE, 42 | LevelSetList::UP_TO_PHP_74, 43 | PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD, 44 | PHPUnitSetList::PHPUNIT_100, 45 | ]); 46 | 47 | $rectorConfig->parallel(); 48 | 49 | // The paths to refactor (can also be supplied with CLI arguments) 50 | $rectorConfig->paths([ 51 | __DIR__ . '/src/', 52 | __DIR__ . '/tests/', 53 | ]); 54 | 55 | // Include Composer's autoload - required for global execution, remove if running locally 56 | $rectorConfig->autoloadPaths([ 57 | __DIR__ . '/vendor/autoload.php', 58 | ]); 59 | 60 | // Do you need to include constants, class aliases, or a custom autoloader? 61 | $rectorConfig->bootstrapFiles([ 62 | realpath(getcwd()) . '/vendor/codeigniter4/codeigniter4/system/Test/bootstrap.php', 63 | ]); 64 | 65 | if (is_file(__DIR__ . '/phpstan.neon.dist')) { 66 | $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist'); 67 | } 68 | 69 | // Set the target version for refactoring 70 | $rectorConfig->phpVersion(PhpVersion::PHP_74); 71 | 72 | // Auto-import fully qualified class names 73 | $rectorConfig->importNames(); 74 | 75 | // Are there files or rules you need to skip? 76 | $rectorConfig->skip([ 77 | __DIR__ . '/app/Views', 78 | 79 | JsonThrowOnErrorRector::class, 80 | StringifyStrNeedlesRector::class, 81 | 82 | // Note: requires php 8 83 | RemoveUnusedPromotedPropertyRector::class, 84 | 85 | // Ignore tests that might make calls without a result 86 | RemoveEmptyMethodCallRector::class => [ 87 | __DIR__ . '/tests', 88 | ], 89 | 90 | // May load view files directly when detecting classes 91 | StringClassNameToClassConstantRector::class, 92 | 93 | // May be uninitialized on purpose 94 | AddDefaultValueForUndefinedVariableRector::class, 95 | ]); 96 | 97 | // auto import fully qualified class names 98 | $rectorConfig->importNames(); 99 | 100 | $rectorConfig->rule(SimplifyUselessVariableRector::class); 101 | $rectorConfig->rule(RemoveAlwaysElseRector::class); 102 | $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); 103 | $rectorConfig->rule(ForToForeachRector::class); 104 | $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); 105 | $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); 106 | $rectorConfig->rule(SimplifyStrposLowerRector::class); 107 | $rectorConfig->rule(CombineIfRector::class); 108 | $rectorConfig->rule(SimplifyIfReturnBoolRector::class); 109 | $rectorConfig->rule(InlineIfToExplicitIfRector::class); 110 | $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); 111 | $rectorConfig->rule(ShortenElseIfRector::class); 112 | $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); 113 | $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); 114 | $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); 115 | $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); 116 | $rectorConfig->rule(AddPregQuoteDelimiterRector::class); 117 | $rectorConfig->rule(SimplifyRegexPatternRector::class); 118 | $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); 119 | $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); 120 | $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); 121 | $rectorConfig->rule(NormalizeNamespaceByPSR4ComposerAutoloadRector::class); 122 | $rectorConfig->rule(StringClassNameToClassConstantRector::class); 123 | $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class); 124 | $rectorConfig->rule(CompleteDynamicPropertiesRector::class); 125 | }; 126 | --------------------------------------------------------------------------------