Skip to content

Commit 4fdc784

Browse files
add PHP 8 attributes support and make library PHP 8 compatible
1 parent 47a3924 commit 4fdc784

12 files changed

+488
-15
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/vendor/
22
/.idea/
3+
composer.lock
4+
/tests/cache

README.md

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
# PHP Router
2-
3-
PHP Router is a simple and efficient routing library designed for PHP applications. It provides a straightforward way to define routes, handle HTTP requests, and generate URLs. Built with PSR-7 message implementation in mind, it seamlessly integrates with PHP applications.
1+
# PHP Router
42

3+
PHP Router is a simple and efficient routing library designed for PHP applications. It provides a straightforward way to
4+
define routes, handle HTTP requests, and generate URLs. Built with PSR-7 message implementation in mind, it seamlessly
5+
integrates with PHP applications.
56

67
## Installation
78

89
You can install PHP Router via Composer. Just run:
910

1011
### Composer Require
12+
1113
```
1214
composer require phpdevcommunity/php-router
1315
```
@@ -30,20 +32,23 @@ composer require phpdevcommunity/php-router
3032

3133
5. **Generate URLs**: Generate URLs for named routes.
3234

33-
3435
## Example
36+
3537
```php
3638
<?php
3739
class IndexController {
3840

41+
// PHP > 8.0
42+
#[\PhpDevCommunity\Attribute\Route(path: '/', name: 'home_page')]
3943
public function __invoke()
4044
{
4145
return 'Hello world!!';
4246
}
4347
}
4448

4549
class ArticleController {
46-
50+
// PHP > 8.0
51+
#[\PhpDevCommunity\Attribute\Route(path: '/api/articles', name: 'api_articles_collection')]
4752
public function getAll()
4853
{
4954
// db get all post
@@ -53,7 +58,8 @@ class ArticleController {
5358
['id' => 3]
5459
]);
5560
}
56-
61+
// PHP > 8.0
62+
#[\PhpDevCommunity\Attribute\Route(path: '/api/articles/{id}', name: 'api_articles')]
5763
public function get(int $id)
5864
{
5965
// db get post by id
@@ -76,11 +82,20 @@ class ArticleController {
7682

7783
```php
7884
// Define your routes
79-
$routes = [
80-
new \PhpDevCommunity\Route('home_page', '/', [IndexController::class]),
81-
new \PhpDevCommunity\Route('api_articles_collection', '/api/articles', [ArticleController::class, 'getAll']),
82-
new \PhpDevCommunity\Route('api_articles', '/api/articles/{id}', [ArticleController::class, 'get']),
83-
];
85+
86+
if (PHP_VERSION_ID >= 80000) {
87+
$attributeRouteCollector = new AttributeRouteCollector([
88+
IndexController::class,
89+
ArticleController::class
90+
]);
91+
$routes = $attributeRouteCollector->collect();
92+
}else {
93+
$routes = [
94+
new \PhpDevCommunity\Route('home_page', '/', [IndexController::class]),
95+
new \PhpDevCommunity\Route('api_articles_collection', '/api/articles', [ArticleController::class, 'getAll']),
96+
new \PhpDevCommunity\Route('api_articles', '/api/articles/{id}', [ArticleController::class, 'get']),
97+
];
98+
}
8499

85100
// Initialize the router
86101
$router = new \PhpDevCommunity\Router($routes, 'http://localhost');
@@ -120,15 +135,18 @@ try {
120135

121136
## Route Definition
122137

123-
Routes can be defined using the `Route` class provided by PHP Router. You can specify HTTP methods, attribute constraints, and handler methods for each route.
138+
Routes can be defined using the `Route` class provided by PHP Router. You can specify HTTP methods, attribute
139+
constraints, and handler methods for each route.
124140

125141
```php
126142
$route = new \PhpDevCommunity\Route('api_articles_post', '/api/articles', [ArticleController::class, 'post'], ['POST']);
127143
$route = new \PhpDevCommunity\Route('api_articles_put', '/api/articles/{id}', [ArticleController::class, 'put'], ['PUT']);
128144
```
145+
129146
### Easier Route Definition with Static Methods
130147

131-
To make route definition even simpler and more intuitive, the `RouteTrait` provides static methods for creating different types of HTTP routes. Here's how to use them:
148+
To make route definition even simpler and more intuitive, the `RouteTrait` provides static methods for creating
149+
different types of HTTP routes. Here's how to use them:
132150

133151
#### Method `get()`
134152

@@ -224,7 +242,8 @@ $route = Route::delete('delete_item', '/item/{id}', [ItemController::class, 'del
224242

225243
### Using `where` Constraints in the Route Object
226244

227-
The `Route` object allows you to define constraints on route parameters using the `where` methods. These constraints validate and filter parameter values based on regular expressions. Here's how to use them:
245+
The `Route` object allows you to define constraints on route parameters using the `where` methods. These constraints
246+
validate and filter parameter values based on regular expressions. Here's how to use them:
228247

229248
#### Method `whereNumber()`
230249

@@ -535,7 +554,8 @@ Example Usage:
535554
$route = (new Route('product', '/product/{code}'))->where('code', '\d{4}');
536555
```
537556

538-
By using these `where` methods, you can apply precise constraints on your route parameters, ensuring proper validation of input values.
557+
By using these `where` methods, you can apply precise constraints on your route parameters, ensuring proper validation
558+
of input values.
539559

540560
## Generating URLs
541561

@@ -546,6 +566,7 @@ echo $router->generateUri('home_page'); // /
546566
echo $router->generateUri('api_articles', ['id' => 1]); // /api/articles/1
547567
echo $router->generateUri('api_articles', ['id' => 1], true); // http://localhost/api/articles/1
548568
```
569+
549570
## Contributing
550571

551572
Contributions are welcome! Feel free to open issues or submit pull requests to help improve the library.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
namespace PhpDevCommunity\Attribute;
4+
5+
use PhpDevCommunity\Helper;
6+
7+
final class AttributeRouteCollector
8+
{
9+
private array $classes;
10+
private ?string $cacheDir;
11+
12+
public function __construct(array $classes, ?string $cacheDir = null)
13+
{
14+
if (PHP_VERSION_ID < 80000) {
15+
throw new \LogicException('Attribute routes are only supported in PHP 8.0+');
16+
}
17+
$this->classes = array_unique($classes);
18+
$this->cacheDir = $cacheDir;
19+
if ($this->cacheDir && !is_dir($this->cacheDir)) {
20+
throw new \InvalidArgumentException(sprintf(
21+
'Cache directory "%s" does not exist',
22+
$this->cacheDir
23+
));
24+
}
25+
}
26+
27+
public function generateCache(): void
28+
{
29+
if (!$this->cacheIsEnabled()) {
30+
throw new \LogicException('Cache is not enabled, if you want to enable it, please set the cacheDir on the constructor');
31+
}
32+
$this->collect();
33+
}
34+
35+
public function clearCache(): void
36+
{
37+
if (!$this->cacheIsEnabled()) {
38+
throw new \LogicException('Cache is not enabled, if you want to enable it, please set the cacheDir on the constructor');
39+
}
40+
41+
foreach ($this->classes as $class) {
42+
$cacheFile = $this->getCacheFile($class);
43+
if (file_exists($cacheFile)) {
44+
unlink($cacheFile);
45+
}
46+
}
47+
}
48+
49+
/**
50+
* @return array<\PhpDevCommunity\Route
51+
* @throws \ReflectionException
52+
*/
53+
public function collect(): array
54+
{
55+
$routes = [];
56+
foreach ($this->classes as $class) {
57+
$routes = array_merge($routes, $this->getRoutes($class));
58+
}
59+
return $routes;
60+
}
61+
62+
63+
private function getRoutes(string $class): array
64+
{
65+
if ($this->cacheIsEnabled() && ( $cached = $this->get($class))) {
66+
return $cached;
67+
}
68+
$refClass = new \ReflectionClass($class);
69+
$routes = [];
70+
71+
$controllerAttr = $refClass->getAttributes(
72+
ControllerRoute::class,
73+
\ReflectionAttribute::IS_INSTANCEOF
74+
)[0] ?? null;
75+
$controllerRoute = $controllerAttr ? $controllerAttr->newInstance() : new ControllerRoute('');
76+
foreach ($refClass->getMethods() as $method) {
77+
foreach ($method->getAttributes(
78+
Route::class,
79+
\ReflectionAttribute::IS_INSTANCEOF
80+
) as $attr) {
81+
/**
82+
* @var Route $instance
83+
*/
84+
$instance = $attr->newInstance();
85+
$route = new \PhpDevCommunity\Route(
86+
$instance->getName(),
87+
$controllerRoute->getPath().$instance->getPath(),
88+
[$class, $method->getName()],
89+
$instance->getMethods()
90+
);
91+
92+
$route->format($instance->getFormat() ?: $controllerRoute->getFormat());
93+
foreach ($instance->getOptions() as $key => $value) {
94+
if (!str_starts_with($key, 'where') || $key === 'where') {
95+
throw new \InvalidArgumentException(
96+
'Invalid option "' . $key . '". Options must start with "where".'
97+
);
98+
}
99+
if (is_array($value)) {
100+
$route->$key(...$value);
101+
continue;
102+
}
103+
$route->$key($value);
104+
}
105+
$routes[$instance->getName()] = $route;
106+
}
107+
}
108+
$routes = array_values($routes);
109+
if ($this->cacheIsEnabled()) {
110+
$this->set($class, $routes);
111+
}
112+
113+
return $routes;
114+
115+
}
116+
117+
private function cacheIsEnabled(): bool
118+
{
119+
return $this->cacheDir !== null;
120+
}
121+
122+
private function get(string $class): ?array
123+
{
124+
$cacheFile = $this->getCacheFile($class);
125+
if (!is_file($cacheFile)) {
126+
return null;
127+
}
128+
129+
return require $cacheFile;
130+
}
131+
132+
private function set(string $class, array $routes): void
133+
{
134+
$cacheFile = $this->getCacheFile($class);
135+
$content = "<?php\n\nreturn " . var_export($routes, true) . ";\n";
136+
file_put_contents($cacheFile, $content);
137+
}
138+
139+
private function getCacheFile(string $class): string
140+
{
141+
return $this->cacheDir . '/' .md5($class) . '.php';
142+
}
143+
}

src/Attribute/ControllerRoute.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace PhpDevCommunity\Attribute;
4+
5+
use PhpDevCommunity\Helper;
6+
7+
#[\Attribute(\Attribute::TARGET_CLASS)]
8+
final class ControllerRoute
9+
{
10+
private string $path;
11+
private ?string $format;
12+
13+
public function __construct(string $path, string $format = null)
14+
{
15+
$this->path = Helper::trimPath($path);
16+
$this->format = $format;
17+
}
18+
19+
public function getPath(): string
20+
{
21+
return $this->path;
22+
}
23+
24+
public function getFormat(): ?string
25+
{
26+
return $this->format;
27+
}
28+
}

src/Attribute/Route.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace PhpDevCommunity\Attribute;
4+
5+
use PhpDevCommunity\Helper;
6+
7+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
8+
final class Route
9+
{
10+
private string $path;
11+
private string $name;
12+
/**
13+
* @var array|string[]
14+
*/
15+
private array $methods;
16+
private array $options;
17+
private ?string $format;
18+
19+
public function __construct(string $path, string $name, array $methods = ['GET', 'POST'], array $options = [], string $format = null)
20+
{
21+
$this->path = Helper::trimPath($path);
22+
$this->name = $name;
23+
$this->methods = $methods;
24+
$this->options = $options;
25+
$this->format = $format;
26+
}
27+
28+
public function getPath(): string
29+
{
30+
return $this->path;
31+
}
32+
33+
public function getName(): string
34+
{
35+
return $this->name;
36+
}
37+
38+
public function getMethods(): array
39+
{
40+
return $this->methods;
41+
}
42+
43+
public function getOptions(): array
44+
{
45+
return $this->options;
46+
}
47+
48+
public function getFormat(): ?string
49+
{
50+
return $this->format;
51+
}
52+
}

0 commit comments

Comments
 (0)