Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.58% covered (success)
96.58%
113 / 117
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
MakeRoutes
96.58% covered (success)
96.58%
113 / 117
90.00% covered (success)
90.00%
9 / 10
47
0.00% covered (danger)
0.00%
0 / 1
 run
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
8.14
 getFilepath
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 getFileContents
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 makeCollections
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
8
 getClasses
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
8
 getRoutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getOrigins
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
 sortOrigins
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 sortRoutes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getRoutesNotFound
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Dev Commands Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\CLI\Commands;
11
12use Framework\CLI\CLI;
13use Framework\CLI\Command;
14use Framework\MVC\App;
15use Framework\Routing\Reflector;
16use Framework\Routing\RouteActions;
17use ReflectionClass;
18
19/**
20 * Class MakeRoutes.
21 *
22 * @package dev-commands
23 */
24class MakeRoutes extends Command
25{
26    protected string $name = 'makeroutes';
27    protected string $description = 'Make routes file.';
28    protected string $usage = 'makeroutes [options] [filepath]';
29    protected array $options = [
30        '-o' => 'Overwrite the file if it exists.',
31        '-s' => 'Show file contents.',
32    ];
33
34    public function run() : void
35    {
36        $filepath = $this->getFilepath();
37        $contents = $this->getFileContents();
38        if ($filepath !== null) {
39            if ( ! $this->console->getOption('o') && \is_file($filepath)) {
40                $prompt = CLI::prompt('File already exists. Overwrite?', ['y', 'n']);
41                if ($prompt !== 'y') {
42                    CLI::write('Aborted.');
43                    return;
44                }
45            }
46            CLI::liveLine('Putting contents in ' . CLI::style($filepath, 'yellow') . '...');
47            \file_put_contents($filepath, $contents);
48            CLI::liveLine('Contents written in ' . CLI::style($filepath, 'yellow') . '.', true);
49        }
50        if ($filepath === null || $this->console->getOption('s')) {
51            CLI::write('File contents:', 'green');
52            CLI::write($contents);
53        }
54    }
55
56    /**
57     * Check if the filepath is absolute and return it or make it relative to
58     * the current working directory and return.
59     *
60     * @return string|null the filepath or null if it is not set
61     */
62    protected function getFilepath() : ?string
63    {
64        $filepath = $this->console->getArgument(0);
65        if ($filepath === null) {
66            return null;
67        }
68        if (\str_starts_with($filepath, '/')
69            || (isset($filepath[1]) && $filepath[1] === ':')
70        ) {
71            return $filepath;
72        }
73        return \getcwd() . \DIRECTORY_SEPARATOR . $filepath;
74    }
75
76    protected function getFileContents() : string
77    {
78        $contents = "<?php\n";
79        $contents .= "\n";
80        $contents .= "use Framework\\MVC\\App;\n";
81        $contents .= "use Framework\\Routing\\RouteCollection;\n";
82        $contents .= "\n";
83        $collections = $this->makeCollections();
84        $contents .= "App::router(){$collections}";
85        return $contents;
86    }
87
88    protected function makeCollections() : string
89    {
90        $contents = '';
91        foreach ($this->getOrigins() as $origin => $routes) {
92            if ($origin !== 'null') {
93                $origin = "'{$origin}'";
94            }
95            $contents .= "->serve({$origin}, static function (RouteCollection \$routes) : void {\n";
96            foreach ($routes['routes'] as $route) {
97                foreach ($route['methods'] as $method) {
98                    $method = \strtolower($method);
99                    $arguments = '';
100                    if ($route['arguments'] !== '') {
101                        $arguments = "/{$route['arguments']}";
102                    }
103                    $name = '';
104                    if ($route['name'] !== null) {
105                        $name = ", '{$route['name']}'";
106                    }
107                    $contents .= "    \$routes->{$method}('{$route['path']}', '{$route['action']}{$arguments}'{$name});\n";
108                }
109            }
110            foreach ($routes['routesNotFound'] as $routeNotFound) {
111                $contents .= "    \$routes->notFound('{$routeNotFound['action']}');\n";
112            }
113            $contents .= '})';
114        }
115        $contents .= ";\n";
116        return $contents;
117    }
118
119    /**
120     * @return array<int,string>
121     */
122    protected function getClasses() : array
123    {
124        $autoloader = App::autoloader();
125        $locator = App::locator();
126        $files = [];
127        foreach ($autoloader->getNamespaces() as $namespaces) {
128            foreach ($namespaces as $directory) {
129                $files = [...$files, ...$locator->listFiles($directory)];
130            }
131        }
132        foreach ($autoloader->getClasses() as $file) {
133            $files[] = $file;
134        }
135        $files = \array_unique($files);
136        \sort($files);
137        $actions = [];
138        foreach ($files as $file) {
139            $className = $locator->getClassName($file);
140            if ($className === null) {
141                continue;
142            }
143            $class = new ReflectionClass($className); // @phpstan-ignore-line
144            if ($class->isInstantiable() && $class->isSubclassOf(RouteActions::class)) {
145                $actions[] = $className;
146            }
147        }
148        return $actions;
149    }
150
151    /**
152     * @return array<int,array<mixed>>
153     */
154    protected function getRoutes() : array
155    {
156        $classes = $this->getClasses();
157        $routes = [];
158        foreach ($classes as $class) {
159            $reflector = new Reflector($class); // @phpstan-ignore-line
160            $routes = [...$routes, ...$reflector->getRoutes()];
161        }
162        return $routes;
163    }
164
165    /**
166     * @return array<string,array<mixed>>
167     */
168    protected function getOrigins() : array
169    {
170        $origins = [];
171        foreach ($this->getRoutes() as $route) {
172            if (empty($route['origins'])) {
173                $origins['null']['routes'][] = $route;
174                $origins['null']['routesNotFound'] = [];
175                continue;
176            }
177            foreach ($route['origins'] as $origin) {
178                $origins[$origin]['routes'][] = $route;
179                $origins[$origin]['routesNotFound'] = [];
180            }
181        }
182        foreach ($this->getRoutesNotFound() as $route) {
183            if (empty($route['origins'])) {
184                if ( ! isset($origins['null']['routes'])) {
185                    $origins['null']['routes'] = [];
186                }
187                $origins['null']['routesNotFound'][] = $route;
188                continue;
189            }
190            foreach ($route['origins'] as $origin) {
191                if ( ! isset($origins[$origin]['routes'])) {
192                    $origins[$origin]['routes'] = [];
193                }
194                $origins[$origin]['routesNotFound'][] = $route;
195            }
196        }
197        $origins = $this->sortOrigins($origins);
198        foreach ($origins as &$routes) {
199            $routes['routes'] = $this->sortRoutes($routes['routes']);
200        }
201        unset($routes);
202        return $origins;
203    }
204
205    /**
206     * @param array<mixed> $origins
207     *
208     * @return array<mixed>
209     */
210    protected function sortOrigins(array $origins) : array
211    {
212        \ksort($origins);
213        if (isset($origins['null'])) {
214            $last = $origins['null'];
215            unset($origins['null']);
216            $origins['null'] = $last;
217        }
218        return $origins;
219    }
220
221    /**
222     * @param array<mixed> $routes
223     *
224     * @return array<mixed>
225     */
226    protected function sortRoutes(array $routes) : array
227    {
228        \usort($routes, static function ($route1, $route2) {
229            $cmp = \strcmp($route1['path'], $route2['path']);
230            if ($cmp === 0) {
231                $cmp = \strcmp($route1['methods'][0], $route2['methods'][0]);
232            }
233            return $cmp;
234        });
235        return $routes;
236    }
237
238    /**
239     * @return array<int,array<mixed>>
240     */
241    protected function getRoutesNotFound() : array
242    {
243        $classes = $this->getClasses();
244        $routes = [];
245        foreach ($classes as $class) {
246            $reflector = new Reflector($class); // @phpstan-ignore-line
247            $routes = [...$routes, ...$reflector->getRoutesNotFound()];
248        }
249        return $routes;
250    }
251}