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