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

willy68 / pg-router / 16153203361

08 Jul 2025 08:00PM UTC coverage: 94.978% (+13.9%) from 81.095%
16153203361

push

github

willy68
First commit

30 of 32 new or added lines in 4 files covered. (93.75%)

5 existing lines in 1 file now uncovered.

870 of 916 relevant lines covered (94.98%)

8.34 hits per line

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

90.27
/src/Router.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Pg\Router;
6

7
use Pg\Router\DuplicateDetector\DuplicateDetectorInterface;
8
use Pg\Router\DuplicateDetector\DuplicateMethodMapDetector;
9
use Pg\Router\Generator\UrlGenerator;
10
use Pg\Router\Matcher\MarkDataMatcher;
11
use Pg\Router\Matcher\MatcherInterface;
12
use Pg\Router\Middlewares\MiddlewareAwareStackTrait;
13
use Pg\Router\RegexCollector\MarkRegexCollector;
14
use Pg\Router\RegexCollector\RegexCollectorInterface;
15
use Psr\Cache\CacheException;
16
use Psr\Cache\CacheItemPoolInterface;
17
use Psr\Cache\InvalidArgumentException;
18
use Psr\Http\Message\ServerRequestInterface as Request;
19
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
20

21
class Router implements RouterInterface
22
{
23
    use MiddlewareAwareStackTrait;
24
    use RouteCollectionTrait;
25

26
    public const CONFIG_CACHE_ENABLED = 'cache_enabled';
27
    public const CONFIG_CACHE_DIR = 'cache_dir';
28
    public const CONFIG_CACHE_POOL_FACTORY = 'cache_pool_factory';
29

30
    private string $cacheDir = 'tmp/cache';
31
    private string $cacheKey = 'router_parsed_data';
32
    /** @var callable: CachePoolInterface */
33
    private $cachePoolFactory = null;
34
    private ?CacheItemPoolInterface $cachePool = null;
35
    private ?array $parsedData = null;
36
    private bool $hasParsedData = false;
37
    protected ?DuplicateDetectorInterface $detector = null;
38
    /** @var Route[] */
39
    protected array $routes = [];
40
    /** @var callable(array|object): MatcherInterface|null */
41
    protected $matcherFactory = null;
42
    private ?RegexCollectorInterface $regexCollector;
43

44
    /**
45
     * <code>
46
      $router = new Router (
47
           null,
48
           null,
49
           [
50
               Router::CONFIG_CACHE_ENABLED => ($env === 'prod'),
51
               Router::CONFIG_CACHE_DIR => '/tmp/cache',
52
               Router::CONFIG_CACHE_POOL_FACTORY => function (): CacheItemPoolInterface {...},
53
           ]
54
      )
55
     * </code>
56
     *
57
     * @param RegexCollectorInterface|null $regexCollector
58
     * @param callable|null $matcherFactory
59
     * @param array|null $config
60
     * @throws CacheException
61
     */
62
    public function __construct(
11✔
63
        ?RegexCollectorInterface $regexCollector = null,
64
        ?callable $matcherFactory = null,
65
        ?array $config = null
66
    ) {
67
        $this->regexCollector = $regexCollector;
11✔
68
        $this->matcherFactory = $matcherFactory;
11✔
69
        $this->loadConfig($config);
11✔
70
    }
71

72
    /**
73
     * Load configuration parameters.
74
     *
75
     * @param array|null $config
76
     * @throws CacheException
77
     */
78
    private function loadConfig(?array $config = null): void
11✔
79
    {
80
        if ($config === null) {
11✔
81
            return;
5✔
82
        }
83

84
        $cacheEnabled = (bool)($config[self::CONFIG_CACHE_ENABLED] ?? false);
6✔
85
        $this->cacheDir = (string)($config[self::CONFIG_CACHE_DIR] ?? $this->cacheDir);
6✔
86
        $this->cachePoolFactory = $config[self::CONFIG_CACHE_POOL_FACTORY] ?? $this->getCachePoolFactory();
6✔
87

88
        if ($cacheEnabled) {
6✔
89
            $this->loadCachePool();
5✔
90
            $this->loadParsedData();
5✔
91
        }
92
    }
93

94
    /**
95
     * @throws CacheException
96
     */
97
    private function loadCachePool(): void
5✔
98
    {
99
        if ($this->cachePool === null) {
5✔
100
            $cachePoolFactory = $this->cachePoolFactory;
5✔
101
            if (!is_callable($cachePoolFactory)) {
5✔
102
                throw new \Symfony\Component\Cache\Exception\InvalidArgumentException(
×
103
                    'Cache pool factory must be a callable.'
×
104
                );
×
105
            }
106
            $this->cachePool = $cachePoolFactory();
5✔
107
            if (!$this->cachePool instanceof CacheItemPoolInterface) {
5✔
108
                throw new \Symfony\Component\Cache\Exception\InvalidArgumentException(
×
109
                    'Cache pool factory must return an instance of CacheItemPoolInterface.'
×
110
                );
×
111
            }
112
        }
113
    }
114

115
    /**
116
     * Get the default cache pool factory callable.
117
     *
118
     * @return callable (array|object): CacheItemPoolInterface
119
     */
120
    protected function getCachePoolFactory(): callable
5✔
121
    {
122
        return fn(): CacheItemPoolInterface => new PhpFilesAdapter(
5✔
123
            'Router',
5✔
124
            0,
5✔
125
            $this->cacheDir,
5✔
126
            true
5✔
127
        );
5✔
128
    }
129

130
    public function route(
7✔
131
        string $path,
132
        callable|array|string $callback,
133
        ?string $name = null,
134
        ?array $methods = null
135
    ): Route {
136
        $route = new Route($path, $callback, $name, $methods);
7✔
137
        $this->addRoute($route);
7✔
138
        return $route;
7✔
139
    }
140

141
    public function addRoute(Route $route): Route
9✔
142
    {
143
        $this->duplicateRoute($route);
9✔
144
        $this->routes[$route->getName()] = $route;
9✔
145
        return $route;
9✔
146
    }
147

148
    protected function duplicateRoute(Route $route): void
9✔
149
    {
150
        $this->getDuplicateDetector()->detectDuplicate($route);
9✔
151
    }
152

153
    protected function getDuplicateDetector(): DuplicateDetectorInterface
9✔
154
    {
155
        return $this->detector ??= new DuplicateMethodMapDetector();
9✔
156
    }
157

158
    /**
159
     * @throws InvalidArgumentException
160
     */
161
    public function match(Request $request): RouteResult
4✔
162
    {
163
        $uri = $request->getUri()->getPath();
4✔
164
        $method = $request->getMethod();
4✔
165
        $matcher = $this->getMatcher();
4✔
166

167
        $route = $matcher->match($uri, $method);
4✔
168

169
        if ($route) {
4✔
170
            return RouteResult::fromRouteSuccess(
3✔
171
                $this->routes[$matcher->getMatchedRouteName()],
3✔
172
                $matcher->getAttributes()
3✔
173
            );
3✔
174
        }
175

176
        $allowedMethods = $matcher->getAllowedMethods();
1✔
177

178
        return RouteResult::fromRouteFailure(!empty($allowedMethods) ? $allowedMethods : null);
1✔
179
    }
180

181
    /**
182
     * @throws InvalidArgumentException
183
     */
184
    protected function getMatcher(?array $routes = null): MatcherInterface
4✔
185
    {
186
        $this->matcherFactory ??= $this->getMatcherFactory();
4✔
187
        $routes ??= $this->getParsedData();
4✔
188
        return ($this->matcherFactory)($routes);
4✔
189
    }
190

191
    /**
192
     * @return callable(array|object): MatcherInterface
193
     */
194
    protected function getMatcherFactory(): callable
4✔
195
    {
196
        return fn($routes): MatcherInterface => new MarkDataMatcher($routes);
4✔
197
    }
198

199
    /**
200
     * @throws InvalidArgumentException
201
     */
202
    protected function getParsedData(): array
6✔
203
    {
204
        if ($this->hasParsedData) {
6✔
205
            return $this->parsedData;
2✔
206
        }
207

208
        $this->getRegexCollector()->addRoutes($this->routes);
6✔
209
        $data = $this->getRegexCollector()->getData();
6✔
210

211
        if ($this->cachePool) {
6✔
212
            $cacheItem = $this->cachePool->getItem($this->cacheKey);
3✔
213
            $cacheItem->set($data);
3✔
214
            $this->cachePool->save($cacheItem);
3✔
215
        }
216

217
        return $data;
6✔
218
    }
219

220
    /**
221
     * @throws InvalidArgumentException
222
     */
223
    protected function loadParsedData(): ?array
5✔
224
    {
225
        $parsedData = null;
5✔
226
        if ($this->cachePool) {
5✔
227
            $cacheItem = $this->cachePool->getItem($this->cacheKey);
5✔
228
            if ($cacheItem->isHit()) {
5✔
229
                $parsedData = $cacheItem->get();
3✔
230
            }
231
        }
232

233
        if ($parsedData === null) {
5✔
234
            return null;
5✔
235
        }
236

237
        if (!is_array($parsedData)) {
3✔
UNCOV
238
            throw new \InvalidArgumentException("Parse data must be an array.");
×
239
        }
240

241
        $this->hasParsedData = true;
3✔
242
        return ($this->parsedData = $parsedData);
3✔
243
    }
244

245
    protected function getRegexCollector(): RegexCollectorInterface
6✔
246
    {
247
        return $this->regexCollector ??= new MarkRegexCollector();
6✔
248
    }
249

250
    public function generateUri(string $name, array $substitutions = [], array $queryParams = []): string
1✔
251
    {
252
        $uri = (new UrlGenerator($this))->generate($name, $substitutions);
1✔
253
        if (!empty($queryParams)) {
1✔
254
            return $uri . '?' . http_build_query($queryParams);
1✔
255
        }
256
        return $uri;
1✔
257
    }
258

259
    /**
260
     * Create multiple routes with the same prefix.
261
     */
UNCOV
262
    public function group(string $prefix, callable $callable): RouteGroup
×
263
    {
UNCOV
264
        $group = new RouteGroup($prefix, $callable, $this);
×
UNCOV
265
        $group();
×
UNCOV
266
        return $group;
×
267
    }
268

269
    /**
270
     * Generate CRUD routes.
271
     *
272
     * @param string $prefixPath
273
     * @param string $callable
274
     * @param string $prefixName
275
     * @return RouteGroup
276
     */
277
    public function crud(string $prefixPath, string $callable, string $prefixName): RouteGroup
1✔
278
    {
279
        $group = new RouteGroup(
1✔
280
            $prefixPath,
1✔
281
            function (RouteGroup $route) use ($callable, $prefixName) {
1✔
282
                $route->crud($callable, $prefixName);
1✔
283
            },
1✔
284
            $this
1✔
285
        );
1✔
286
        $group();
1✔
287
        return $group;
1✔
288
    }
289

290
    /**
291
     * @return Route[]
292
     */
293
    public function getRoutes(): array
1✔
294
    {
295
        return $this->routes;
1✔
296
    }
297

298
    public function getRouteName(string $name): ?Route
2✔
299
    {
300
        return $this->routes[$name] ?? null;
2✔
301
    }
302

303
    public function clearCache(): void
1✔
304
    {
305
        $this->cachePool?->clear();
1✔
306
        $this->parsedData = null;
1✔
307
        $this->hasParsedData = false;
1✔
308
    }
309
}
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

© 2026 Coveralls, Inc