Skip to content

Commit 706b34d

Browse files
2.0.0
- PHP 8 version - используются именованные поля в методах - опции задаются через константы - экспериментальная фича: алиасы для регулярок - оптимизация `compileHandler()` - getRouter('*') раскрывает группы - обновленный и подробный README
1 parent e644911 commit 706b34d

File tree

4 files changed

+529
-205
lines changed

4 files changed

+529
-205
lines changed

README.md

Lines changed: 261 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,92 +2,213 @@
22

33
Попытка написать свой роутер на базе https://github.com/nikic/FastRoute
44

5-
Реализует возможность статического класса AppRouter с методами:
5+
Реализует возможность статического класса AppRouter.
66

77
- `init()`
88
- `get()`
99
- `post()`
1010
- etc
1111

12-
Выделено в отдельный пакет для возможности обновления отдельно от основного класса фреймворка и отдельного подключения.
12+
# Пример использования:
1313

14+
```php
15+
use Arris\AppRouter;
16+
use Arris\Exceptions\{
17+
AppRouterHandlerError,
18+
AppRouterMethodNotAllowedException,
19+
AppRouterNotFoundException
20+
};
21+
22+
try {
23+
AppRouter::init(
24+
logger: null,
25+
allowEmptyHandlers: true,
26+
);
27+
28+
AppRouter::get('/', [ DynamicClass::class, 'present_dynamic_method'], 'root');
29+
30+
AppRouter::get('/function/', 'example_function', 'root.function_call');
31+
32+
AppRouter::group(
33+
prefix: '/admin',
34+
before: 'MiddleAdmin@before',
35+
after: [ MiddleAdmin::class, 'after' ],
36+
callback: function () {
37+
AppRouter::get('/', function () { d('this is simple closure'); }, 'admin.root');
38+
39+
AppRouter::get('/foo[/]', 'StaticClass@present_static_method', 'admin.foo');
40+
41+
AppRouter::get('/list/', [StaticClass::class, 'present_static_method'], 'admin.list');
42+
43+
AppRouter::group(
44+
prefix: '/users',
45+
before: [MiddleAdminUsers::class, 'before'],
46+
after: [MiddleAdminUsers::class, 'after'],
47+
callback: static function() {
48+
AppRouter::get('/', [ DynamicClass::class, 'users'], 'admin.users.root');
49+
AppRouter::get('/all/', 'DynamicClass@all', 'admin.users.all');
50+
AppRouter::get('/invoke/', 'DynamicClass@' , 'admin.users.invoke');
51+
AppRouter::get('/list/', [StaticClass::class, 'method_not_exist'], 'admin.users.list');
52+
AppRouter::get('/empty/[{id:\d+}[/]]', /*[ DynamicClass::class, 'create']*/ [] , 'admin.users.empty');
53+
}
54+
);
55+
}
56+
);
57+
58+
AppRouter::dispatch();
59+
60+
} catch (AppRouterHandlerError|AppRouterNotFoundException|AppRouterMethodNotAllowedException $e) {
61+
var_dump($e->getMessage());
62+
} catch (RuntimeException|Exception $e) {
63+
var_dump($e);
64+
echo "<br>" . PHP_EOL;
65+
}
66+
```
67+
68+
# Детали
1469

15-
# Инициализация
70+
## init - Инициализация роутера
1671

1772
```php
18-
AppRouter::init(AppLogger::scope('routing'), [
19-
// опции
20-
]);
73+
AppRouter::init(
74+
logger: AppLogger::scope('routing'),
75+
/* other options */
76+
);
2177
```
2278

2379
Опции:
2480

25-
- `defaultNamespace` - неймспейс по-умолчанию
26-
- `namespace` - алиас `defaultNamespace`
27-
- `prefix` - текущий префикс URL (аналогично поведению для групп)
28-
- `routeReplacePattern` - ?
29-
- `allowEmptyHandlers` (false) - разрешить пустые (заданные как `[]`) хэндлеры? Если false - кидается исключение `AppRouterHandlerError: Handler not found or empty`.
30-
- `allowEmptyGroups` (false) - разрешить ли пустые группы? Пустой считается группа без роутов. Если разрешено - для такой группы будут парситься миддлвары и опции.
81+
- `namespace` - неймспейс по-умолчанию, может быть задан вызовом `AppRouter::setDefaultNamespace()`
82+
- `prefix` - префикс URL (аналогично поведению для групп)
83+
- `allowEmptyHandlers` (false) - разрешить ли пустые хэндлеры?
84+
- `allowEmptyGroups` (false) - разрешить ли пустые группы?
3185

32-
Важно отметить, что "пустой" handler может быть описан двумя способами:
86+
## setOption - переопределение опций
3387

34-
- `null` - такой handler просто пропускается, роут в таком случае вернет `AppRouter::NotFoundException: URL not found`
35-
- `[]` - поведение зависит от опции `allowEmptyHandlers`:
36-
- `= true` - хэндлер не делает ничего, хотя проходится вся цепочка посредников до него и после него
37-
- `= false` - кидается исключение `AppRouterHandlerError - Handler not found or empty`
88+
Некоторые опции могут быть переопределены только вызовом:
3889

90+
```php
91+
AppRouter::setOption(name, value);
92+
```
93+
94+
Допустимые имена опций:
95+
- `AppRouter::OPTION_ALLOW_EMPTY_HANDLERS` - разрешить пустые (заданные как `[]`) хэндлеры? Если false - кидается исключение `AppRouterHandlerError: Handler not found or empty`.
96+
- `AppRouter::OPTION_ALLOW_EMPTY_GROUPS` - разрешить ли пустые группы? Пустой считается группа без роутов. Если разрешено - для такой группы будут парситься миддлвары и опции.
97+
- `AppRouter::OPTION_DEFAULT_ROUTE` - дефолтное значение для реверс-роутинга
98+
- `AppRouter::OPTION_USE_ALIASES` - разрешить ли алиасы?
99+
100+
101+
## Декларация роутов
102+
103+
Методы: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`
39104

40105
```php
106+
AppRouter::method(
107+
route: '/my/awesome/uri/',
108+
handler: хэндлер,
109+
name: 'имя'
110+
);
111+
```
41112

42-
AppRouter::get('/', function () {
43-
CLIConsole::say('Call from /');
44-
}, 'root');
113+
- `route` - строка (с регулярками/алиасами регулярок)
114+
- `handler` - хэндлер
115+
- `name` - имя роута для обратного роутинга (reverse routing)
45116

46-
AppRouter::group([
47-
'prefix' => '/auth',
48-
'namespace' => 'Auth',
49-
'before' => static function () { CLIConsole::say('Called BEFORE middleware for /auth/*'); },
50-
'after' => null
51-
], static function() {
117+
### Как можно задать handler?
52118

53-
AppRouter::get('/login', function () {
54-
CLIConsole::say('Call /auth/login');
55-
});
119+
- `function() { }`, то есть Closure;
120+
- `[Class::class, 'method']` - массив из двух элементов, подразумевается, что метод динамический, то есть класс будет инстанциирован перед вызовом метода.
121+
- `Class@method` - строка, содержащая `@`. Будет применена рефлексия для вычисления типа метода. Если метод динамический - класс будет инстанциирован.
122+
- `Class@` - будет вызван метод `__invoke()` у класса.
123+
- `function` - функция
124+
- `null` - строго пустой роут, вызов **всегда** выбросит исключение `AppRouterNotFoundException -> URL not found`
125+
- `[]`. По умолчанию будет выброшено исключение `AppRouterHandlerError`, но... есть нюанс:
56126

57-
AppRouter::group(['prefix' => '/ajax'], static function() {
127+
### Пустой хэндлер?
58128

59-
AppRouter::get('/getKey', function (){
60-
CLIConsole::say('Call from /test/ajax/getKey');
61-
}, 'auth:ajax:getKey');
129+
Если задать опцию `allowEmptyHandlers: true` или вызвать `AppRouter::setOption('allowEmptyHandlers', true)`, то можно
130+
будет использовать пустые хэндлеры, например:
62131

63-
});
132+
```php
133+
AppRouter::get('/admin/users/', [], 'admin.users.root');
134+
```
135+
136+
В этом случае пройдет стандартная цепочка роутинга - будут инстанциированы и вызваны миддлвары, сначала before, потом в обратном порядке after, например:
137+
138+
```
139+
string(30) "Class MiddleAdmin instantiated"
140+
string(19) "MiddleAdmin::before"
141+
string(35) "Class MiddleAdminUsers instantiated"
142+
string(24) "MiddleAdminUsers::before"
64143
65-
AppRouter::get('/get', function (){
66-
CLIConsole::say('Call from /test/get (declared after /ajax prefix group');
67-
});
144+
<тут должен был обрабатываться хэндлер, но он пуст>
68145
69-
AppRouter::group(['prefix' => '/2'], static function() {
70-
AppRouter::get('/3', function () {
71-
CLIConsole::say('Call from /test/2/3');
72-
});
73-
});
146+
string(23) "MiddleAdminUsers::after"
147+
string(18) "MiddleAdmin::after"
148+
```
149+
150+
## Группировка роутов
74151

75-
});
152+
```php
153+
\Arris\AppRouter::group(
154+
prefix: '/admin',
155+
before: 'MiddleAdmin@before',
156+
after: [ MiddleAdmin::class, 'after' ],
157+
callback: function () {
158+
/* роуты группы */
159+
}
160+
);
76161

77-
AppRouter::get('/root', function (){
78-
CLIConsole::say('Call from /root (declared after /ajax prefix group ; after /test prefix group)');
79-
});
80162

81-
AppRouter::group([], function (){
82-
AppRouter::get('/not_group', function () {
163+
```
164+
165+
## Реверс-роутинг
83166

84-
});
85-
});
167+
`AppRouter::getRouter(name)` возвращает URL, соответствующий имени роута.
168+
169+
При этом, имя `*` вернет все маршруты. Если имя не найдено - будет возвращен роут по-умолчанию.
170+
171+
При этом:
172+
173+
- именованные группы-плейсхолдеры будут заменены на переданные переменные
174+
- необязательные оконечные слэши будут заменены на обязательные
175+
- будут удалены необязательные группы
176+
177+
Таким образом, если роут определен:
178+
```php
179+
AppRouter::get('/entry/delete/{id}/', 'handler', 'callback_entry_delete');
180+
```
181+
182+
То вызов
183+
```php
184+
Arris\AppRouter::getRouter('callback_entry_delete', [ 'id' => 15 ])
185+
```
186+
187+
Сгенерирует строчку: `/entry/delete/15/`
188+
189+
### Роут по-умолчанию
190+
191+
Если роут **не найден** или передан пустой роут - будет возвращен URL `/`. Это поведение может быть переопределено вызовом:
192+
193+
```php
194+
AppRouter::setOption('getRouterDefaultValue', '/foo/bar');
195+
```
196+
197+
### Вызов реверс-роутинга в шаблонах
198+
199+
Что полезно, реверс-роутинг может вызываться в Smarty-шаблонах:
200+
```
201+
<button data-url="{Arris\AppRouter::getRouter('callback_entry_delete', [ 'id' => $item.user_id ])}">Delete Entry</button>
202+
```
203+
Что требует определения в Smarty или Arris.Presenter:
204+
```php
205+
->registerClass("Arris\AppRouter", "Arris\AppRouter")
86206
```
87207

88-
# Исключения (Exceptions)
89208

90-
Класс кидает три исключения:
209+
## Исключения (Exceptions)
210+
211+
Класс может выкинуть три исключения:
91212

92213
- `AppRouterHandlerError` - ошибка в хэндлере (пустой, неправильный, итп)
93214
- `AppRouterNotFoundException` - роут не определен (URL ... not found)
@@ -96,7 +217,95 @@ AppRouter::group([], function (){
96217
При этом передается расширенная информация по роуту, получить которую можно через метод `$e->getError()`, потому что
97218
переопределить финальный метод `getMessage()` НЕВОЗМОЖНО.
98219

220+
## ЭКСПЕРИМЕНТАЛЬНАЯ ФИЧА: АЛИАСЫ
221+
222+
Включается с помощью
223+
```php
224+
AppRouter::setOption('useAliases', true);
225+
```
226+
227+
После этого можно задать алиасы:
228+
229+
```php
230+
AppRouter::addAlias([
231+
[ 'userid' => '\d+' ],
232+
[ 'username' => '[a-zA-Z]+' ]
233+
]);
234+
```
235+
236+
И определить роуты:
237+
```php
238+
AppRouter::get(
239+
route: '/user/{userid}[/]',
240+
handler: function ($userid = 0) { var_dump('Closure => userid: ' . $userid ); },
241+
name: 'root.userid'
242+
);
243+
244+
AppRouter::get(
245+
route: '/user/{username}[/]',
246+
handler: function ($username = 'anon') { var_dump('Closure => username: ' . $username ); },
247+
name: 'root.username'
248+
);
249+
```
250+
251+
Теперь при вызове `/user/<value>/` в зависимости от совпадения с регуляркой будет вызван один из хэндлеров:
252+
253+
- `\d+`, то есть число - хэндлер userid
254+
- `[a-zA-Z]+`, то есть латинская строка - хэндлер username
255+
256+
### Реверс-роутинг и алиасы
257+
258+
Прекрасно работает:
259+
```php
260+
echo AppRouter::getRouter('root.userid', [ 'userid' => 42 ]); // => /user/42/
261+
echo AppRouter::getRouter('root.username', [ 'username' => 'wombat' ]); // => /user/wombat/
262+
```
263+
264+
265+
### Опциональные группы и алиасы
266+
267+
В данном случае объявить опциональной можно только одну группу, хотя так делать не стоит:
268+
269+
```php
270+
AppRouter::get('/user/{userid}[/]', function ($userid = 0) { var_dump('Closure => userid: ' . $userid ); });
271+
AppRouter::get('/user/[{username}[/]]', function ($username = 'anon') { var_dump('Closure => username: ' . $username ); });
272+
```
273+
274+
При переходе на `/user/` произойдет вызов хэндлера **username**.
275+
276+
Объявление двух групп опциональными вызовет исключение:
277+
```
278+
BadRouteException: Cannot register two routes matching "/user/" for method "GET"
279+
```
280+
281+
### Как на самом деле сделать "опциональную" группу:
282+
283+
```php
284+
AppRouter::get('/user/', function () { var_dump('Closure => user root ' ); });
285+
AppRouter::get('/user/{userid}[/]', function ($userid = 0) { var_dump('Closure => userid: ' . $userid ); });
286+
AppRouter::get('/user/{username}[/]', function ($username = 'anon') { var_dump('Closure => username: ' . $username ); });
287+
```
288+
289+
- `/user` = 'Closure => user root'
290+
- `/user/123/` = 'Closure => userid: 123'
291+
- `/username/wombat/` = 'Closure => username: wombat'
292+
293+
294+
### Использование алиасов с выключенной опцией useAliases
295+
296+
Вызовет исключение:
297+
```
298+
BadRouteException: Cannot register two routes matching "/user/([^/]+)" for method "GET""
299+
```
300+
Происходит это, очевидно, потому что без алиасов подгруппы `{userid}` и `{username}` раскрываются в `([^/]+)`, а роуты с
301+
одинаковыми URL определить нельзя.
302+
303+
304+
**NB:**
305+
306+
В версии 2.0.* реализованы **только** глобальные алиасы. Возможности задать алиасы для роутов группы (и только для них) нет.
99307

308+
------
100309
# ToDo
101310

102311
- Опция `middlewareNamespace` для `init()` - неймспейс посредников по умолчанию.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
}
2222
],
2323
"require": {
24-
"php": ">=7.4 || 8.*",
24+
"php": "8.*",
2525
"psr/log": "*"
2626
},
2727
"require-dev": {

0 commit comments

Comments
 (0)