• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zfegg / psr-mvc / 6174499731

13 Sep 2023 03:19PM UTC coverage: 94.545% (-2.1%) from 96.623%
6174499731

Pull #20

github

web-flow
Merge f52f20a15 into 63d3fcb42
Pull Request #20: Remove factories, use `laminas/lamians-di` instead.

14 of 14 new or added lines in 3 files covered. (100.0%)

832 of 880 relevant lines covered (94.55%)

6.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.03
/src/Routing/RouteMetadata.php
1
<?php
2

3
declare(strict_types = 1);
4

5
namespace Zfegg\PsrMvc\Routing;
6

7
use FilesystemIterator;
8
use RecursiveDirectoryIterator;
9
use RecursiveIteratorIterator;
10
use RecursiveRegexIterator;
11
use ReflectionClass;
12
use ReflectionMethod;
13
use RegexIterator;
14
use Zfegg\PsrMvc\Attribute\Route;
15
use Zfegg\PsrMvc\Attribute\RouteGroup;
16

17
class RouteMetadata
18
{
19
    /**
20
     * The paths where to look for mapping files.
21
     *
22
     * @var string[]
23
     */
24
    private array $paths = [];
25

26
    /**
27
     * The paths excluded from path where to look for mapping files.
28
     *
29
     * @var string[]
30
     */
31
    private array $excludePaths = [];
32

33
    /**
34
     * The file extension of mapping documents.
35
     */
36
    private string $fileExtension;
37

38
    /**
39
     * Cache for AnnotationDriver#getAllClassNames().
40
     *
41
     * @var string[]|null
42
     * @psalm-var list<class-string>|null
43
     */
44
    private ?array $classNames = null;
45

46
    /**
47
     * @var array[]
48
     */
49
    private array $groups = [];
50

51
    private ParameterConverterInterface $parameterConverter;
52

53
    private ?string $cacheFile;
54

55
    public const CACHE_TEMPLATE = <<<EOT
56
        <?php
57
        return %s;
58
        EOT;
59

60
    /**
61
     * @param string[] $paths
62
     * @param string[] $excludePaths
63
     * @param array[]  $groups
64
     */
65
    public function __construct(
66
        array $paths = [],
67
        array $excludePaths = [],
68
        string $fileExtension = 'Controller.php',
69
        array $groups = [],
70
        ?ParameterConverterInterface $parameterConverter = null,
71
        ?string $cacheFile = null,
72
    ) {
73
        $this->addPaths($paths);
7✔
74
        $this->addExcludePaths($excludePaths);
7✔
75
        $this->fileExtension = $fileExtension;
7✔
76
        $this->parameterConverter = $parameterConverter ?? new SlugifyParameterConverter();
7✔
77
        $this->groups = $groups;
7✔
78
        $this->cacheFile = $cacheFile;
7✔
79
    }
80

81
    public function addGroup(string $name, array $group): void
82
    {
83
        $this->groups[$name] = $group;
1✔
84
    }
85

86
    /**
87
     * Appends lookup paths to metadata driver.
88
     *
89
     * @param string[] $paths
90
     *
91
     */
92
    public function addPaths(array $paths): void
93
    {
94
        $this->paths = array_unique(array_merge($this->paths, $paths));
7✔
95
    }
96

97
    /**
98
     * Retrieves the defined metadata lookup paths.
99
     *
100
     * @return string[]
101
     */
102
    public function getPaths(): array
103
    {
104
        return $this->paths;
1✔
105
    }
106

107
    /**
108
     * Append exclude lookup paths to metadata driver.
109
     *
110
     * @param string[] $paths
111
     *
112
     */
113
    public function addExcludePaths(array $paths): void
114
    {
115
        $this->excludePaths = array_unique(array_merge($this->excludePaths, $paths));
7✔
116
    }
117

118
    /**
119
     * Retrieve the defined metadata lookup exclude paths.
120
     *
121
     * @return string[]
122
     */
123
    public function getExcludePaths(): array
124
    {
125
        return $this->excludePaths;
1✔
126
    }
127

128
    /**
129
     * Gets the file extension used to look for mapping files under.
130
     */
131
    public function getFileExtension(): string
132
    {
133
        return $this->fileExtension;
1✔
134
    }
135

136
    /**
137
     * Sets the file extension used to look for mapping files under.
138
     */
139
    public function setFileExtension(string $fileExtension): void
140
    {
141
        $this->fileExtension = $fileExtension;
1✔
142
    }
143

144
    public function getAllClassNames(): array
145
    {
146
        if ($this->classNames !== null) {
7✔
147
            return $this->classNames;
2✔
148
        }
149

150
        $classes       = [];
7✔
151
        $includedFiles = [];
7✔
152

153
        foreach ($this->paths as $path) {
7✔
154
            if (! is_dir($path)) {
7✔
155
                continue;
×
156
            }
157

158
            $iterator = new RegexIterator(
7✔
159
                new RecursiveIteratorIterator(
7✔
160
                    new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
7✔
161
                    RecursiveIteratorIterator::LEAVES_ONLY
7✔
162
                ),
7✔
163
                '/^.+' . preg_quote($this->fileExtension) . '$/i',
7✔
164
                RecursiveRegexIterator::GET_MATCH
7✔
165
            );
7✔
166

167
            foreach ($iterator as $file) {
7✔
168
                $sourceFile = $file[0];
7✔
169

170
                if (! preg_match('(^phar:)i', $sourceFile)) {
7✔
171
                    $sourceFile = realpath($sourceFile);
7✔
172
                }
173

174
                foreach ($this->excludePaths as $excludePath) {
7✔
175
                    $realExcludePath = realpath($excludePath);
1✔
176
                    assert($realExcludePath !== false);
177
                    $exclude = str_replace('\\', '/', $realExcludePath);
1✔
178
                    $current = str_replace('\\', '/', $sourceFile);
1✔
179

180
                    if (strpos($current, $exclude) !== false) {
1✔
181
                        continue 2;
×
182
                    }
183
                }
184

185
                require_once $sourceFile;
7✔
186

187
                $includedFiles[] = $sourceFile;
7✔
188
            }
189
        }
190

191
        $declared = get_declared_classes();
7✔
192

193
        foreach ($declared as $className) {
7✔
194
            $rc         = new ReflectionClass($className);
7✔
195
            $sourceFile = $rc->getFileName();
7✔
196
            if (! in_array($sourceFile, $includedFiles)) {
7✔
197
                continue;
7✔
198
            }
199

200
            $classes[] = $className;
7✔
201
        }
202

203
        $this->classNames = $classes;
7✔
204

205
        return $classes;
7✔
206
    }
207

208
    /**
209
     * @return array[Route, [string, string]][]
210
     * @throws \ReflectionException
211
     */
212
    public function getRoutes(): array
213
    {
214
        if ($cachedRoutes = $this->loadCachedRoutes()) {
7✔
215
            return $cachedRoutes;
×
216
        }
217

218
        $classes = $this->getAllClassNames();
7✔
219
        $routes = [];
7✔
220

221
        foreach ($classes as $className) {
7✔
222
            $ref = new ReflectionClass($className);
7✔
223
            $baseRoutes = [];
7✔
224
            $routeToken = [];
7✔
225

226
            /** @var RouteGroup $routeGroupAttr */
227
            $routeGroupAttr = null;
7✔
228
            foreach ($ref->getAttributes(RouteGroup::class) as $classAttrRef) {
7✔
229
                $routeGroupAttr = $classAttrRef->newInstance();
7✔
230
                break;
7✔
231
            }
232

233
            foreach ($ref->getAttributes(Route::class) as $classAttrRef) {
7✔
234
                $baseRoutes[] = $classAttrRef->newInstance();
7✔
235
            }
236

237
            $routeToken['[controller]'] = $this->parameterConverter->convertClassNameToPath($className);
7✔
238

239
            foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
7✔
240
                foreach ($method->getAttributes(Route::class, 2) as $methodAttrRef) {
7✔
241
                    /** @var Route $routeAttr */
242
                    $routeAttr = $methodAttrRef->newInstance();
7✔
243
                    $routeAttr->options['controller'] = $className;
7✔
244
                    $routeAttr->options['action'] = $method->getName();
7✔
245

246
                    $routeToken['[action]'] = $this->parameterConverter->convertMethodToPath($method->getName());
7✔
247

248
                    $group = $this->groups[$routeGroupAttr?->name] ?? null;
7✔
249
                    if ($baseRoutes) {
7✔
250
                        foreach ($baseRoutes as $baseRoute) {
7✔
251
                            $newRouteAttr = $this->mergeRoute(
7✔
252
                                $routeToken,
7✔
253
                                $routeAttr,
7✔
254
                                $baseRoute,
7✔
255
                                $group,
7✔
256
                            );
7✔
257
                            $routes[] = [$newRouteAttr, [$className, $method->getName()], $group];
7✔
258
                        }
259
                    } else {
260
                        $newRouteAttr = $this->mergeRoute(
×
261
                            $routeToken,
×
262
                            $routeAttr,
×
263
                            null,
×
264
                            $group,
×
265
                        );
×
266
                        $routes[] = [$newRouteAttr, [$className, $method->getName()], $group];
×
267
                    }
268
                }
269
            }
270
        }
271

272
        $this->cacheRoutes($routes);
7✔
273

274
        return $routes;
7✔
275
    }
276

277
    private function mergeRoute(
278
        array $replacePairs,
279
        Route $route,
280
        ?Route $baseRoute = null,
281
        ?array $group = null
282
    ): Route {
283
        $route = clone $route;
7✔
284

285
        if ($baseRoute && ! str_starts_with($route->path, '/')) {
7✔
286
            $route->path = $baseRoute->path . ($route->path ? '/' . $route->path : '');
7✔
287
            $route->options = array_merge($baseRoute->options, $route->options);
7✔
288
            $route->middlewares = array_merge($baseRoute->middlewares, $route->middlewares);
7✔
289

290
            if (! $route->name && $baseRoute->name) {
7✔
291
                $route->name = $baseRoute->name;
×
292
            }
293
        }
294

295
        if ($group) {
7✔
296
            $route->path = $group['prefix'] . $route->path;
7✔
297
            $route->middlewares = array_merge($group['middlewares'], $route->middlewares);
7✔
298

299
            if ($route->name && isset($group['name'])) {
7✔
300
                $route->name = $group['name'] . $route->name;
7✔
301
            }
302
        }
303

304
        if ($route->name !== null) {
7✔
305
            $route->name = strtr($route->name, $replacePairs);
7✔
306
        }
307
        $route->path = strtr($route->path, $replacePairs);
7✔
308

309
        return $route;
7✔
310
    }
311

312

313
    /**
314
     * Load routes from cache
315
     *
316
     */
317
    private function loadCachedRoutes(): ?array
318
    {
319
        if (! $this->cacheFile) {
7✔
320
            return null;
7✔
321
        }
322
        set_error_handler(static function (): void {
×
323
        }, E_WARNING); // suppress php warnings
×
324
        $routes = include $this->cacheFile;
×
325
        restore_error_handler();
×
326

327
        // Cache file does not exist
328
        if (! is_array($routes)) {
×
329
            return null;
×
330
        }
331

332
        foreach ($routes as &$route) {
×
333
            $route[0] = new Route(...$route[0]);
×
334
        }
335

336
        return $routes;
×
337
    }
338

339
    /**
340
     * Save routes to cache
341
     */
342
    private function cacheRoutes(array $routes): void
343
    {
344
        if (! $this->cacheFile) {
7✔
345
            return ;
7✔
346
        }
347

348
        foreach ($routes as &$route) {
×
349
            $route[0] = get_object_vars($route[0]);
×
350
        }
351

352
        file_put_contents(
×
353
            $this->cacheFile,
×
354
            sprintf(self::CACHE_TEMPLATE, var_export($routes, true))
×
355
        );
×
356
    }
357
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc