Skip to content

Commit e95fbc8

Browse files
IBX-6222: Fixed Request Parser to include non-core routes as REST (#67)
For more details see https://issues.ibexa.co/browse/IBX-6222 and #67 Key changes: * Introduced REST URI Parser * Fixed Router-based Request Parser to rely on URI Parser due to BC * Injected URI Parser into REST RequestListener to avoid redundancy * [Tests] Added integration coverage for UriParserInterface Service --------- Co-authored-by: Konrad Oboza <[email protected]>
1 parent d61cf88 commit e95fbc8

File tree

9 files changed

+501
-189
lines changed

9 files changed

+501
-189
lines changed

src/bundle/EventListener/RequestListener.php

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,39 @@
66
*/
77
namespace Ibexa\Bundle\Rest\EventListener;
88

9+
use Ibexa\Bundle\Rest\UriParser\UriParser;
10+
use Ibexa\Contracts\Rest\UriParser\UriParserInterface;
911
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1012
use Symfony\Component\HttpFoundation\Request;
1113
use Symfony\Component\HttpKernel\Event\RequestEvent;
1214
use Symfony\Component\HttpKernel\KernelEvents;
1315

1416
/**
17+
* @internal
18+
*
1519
* REST request listener.
1620
*
1721
* Flags a REST request as such using the is_rest_request attribute.
1822
*/
1923
class RequestListener implements EventSubscriberInterface
2024
{
21-
public const REST_PREFIX_PATTERN = '/^\/api\/[a-zA-Z0-9-_]+\/v\d+(\.\d+)?\//';
25+
/**
26+
* @deprecated rely on \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead.
27+
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
28+
*/
29+
public const REST_PREFIX_PATTERN = UriParser::DEFAULT_REST_PREFIX_PATTERN;
30+
31+
private UriParserInterface $uriParser;
32+
33+
public function __construct(UriParserInterface $uriParser)
34+
{
35+
$this->uriParser = $uriParser;
36+
}
2237

2338
/**
2439
* @return array
2540
*/
26-
public static function getSubscribedEvents()
41+
public static function getSubscribedEvents(): array
2742
{
2843
return [
2944
// 10001 is to ensure that REST requests are tagged before CorsListener is called
@@ -33,24 +48,22 @@ public static function getSubscribedEvents()
3348

3449
/**
3550
* If the request is a REST one, sets the is_rest_request request attribute.
36-
*
37-
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
3851
*/
39-
public function onKernelRequest(RequestEvent $event)
52+
public function onKernelRequest(RequestEvent $event): void
4053
{
41-
$isRestRequest = true;
42-
43-
if (!$this->hasRestPrefix($event->getRequest())) {
44-
$isRestRequest = false;
45-
}
46-
47-
$event->getRequest()->attributes->set('is_rest_request', $isRestRequest);
54+
$event->getRequest()->attributes->set(
55+
'is_rest_request',
56+
$this->uriParser->isRestRequest($event->getRequest())
57+
);
4858
}
4959

5060
/**
5161
* @param \Symfony\Component\HttpFoundation\Request $request
5262
*
5363
* @return bool
64+
*
65+
* @deprecated use \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead
66+
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
5467
*/
5568
protected function hasRestPrefix(Request $request)
5669
{

src/bundle/RequestParser/Router.php

Lines changed: 16 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,55 +6,37 @@
66
*/
77
namespace Ibexa\Bundle\Rest\RequestParser;
88

9-
use Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException;
9+
use Ibexa\Contracts\Rest\UriParser\UriParserInterface;
1010
use Ibexa\Rest\RequestParser;
1111
use Symfony\Component\HttpFoundation\Request;
12-
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
1312
use Symfony\Component\Routing\RouterInterface;
1413

1514
/**
15+
* @deprecated use \Ibexa\Contracts\Rest\UriParser\UriParserInterface instead
16+
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface
17+
*
1618
* Router based request parser.
1719
*/
1820
class Router implements RequestParser
1921
{
20-
/**
21-
* @var \Symfony\Cmf\Component\Routing\ChainRouter
22-
*/
23-
private $router;
22+
private RouterInterface $router;
23+
24+
private UriParserInterface $uriParser;
2425

25-
public function __construct(RouterInterface $router)
26+
public function __construct(RouterInterface $router, UriParserInterface $uriParser)
2627
{
2728
$this->router = $router;
29+
$this->uriParser = $uriParser;
2830
}
2931

3032
/**
31-
* @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException If no match was found
33+
* @return array<mixed> matched route configuration and parameters
34+
*
35+
* @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException If no match was found
3236
*/
33-
public function parse($url)
37+
public function parse($url): array
3438
{
35-
// we create a request with a new context in order to match $url to a route and get its properties
36-
$request = Request::create($url, 'GET');
37-
$originalContext = $this->router->getContext();
38-
$context = clone $originalContext;
39-
$context->fromRequest($request);
40-
$this->router->setContext($context);
41-
42-
try {
43-
$matchResult = $this->router->matchRequest($request);
44-
} catch (ResourceNotFoundException $e) {
45-
// Note: this probably won't occur in real life because of the legacy matcher
46-
$this->router->setContext($originalContext);
47-
throw new InvalidArgumentException("No route matched '$url'");
48-
}
49-
50-
if (!$this->matchesRestRequest($matchResult)) {
51-
$this->router->setContext($originalContext);
52-
throw new InvalidArgumentException("No route matched '$url'");
53-
}
54-
55-
$this->router->setContext($originalContext);
56-
57-
return $matchResult;
39+
return $this->uriParser->matchUri($url);
5840
}
5941

6042
public function generate($type, array $values = [])
@@ -63,35 +45,11 @@ public function generate($type, array $values = [])
6345
}
6446

6547
/**
66-
* @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException If $attribute wasn't found in the match
48+
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException If $attribute wasn't found in the match
6749
*/
6850
public function parseHref($href, $attribute)
6951
{
70-
$parsingResult = $this->parse($href);
71-
72-
if (!isset($parsingResult[$attribute])) {
73-
throw new InvalidArgumentException("No attribute '$attribute' in route matched from $href");
74-
}
75-
76-
return $parsingResult[$attribute];
77-
}
78-
79-
/**
80-
* Checks if a router match response matches a REST resource.
81-
*
82-
* @param array $match Match array returned by Router::match() / Router::matchRequest()
83-
*
84-
* @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException if the \$match isn't valid
85-
*
86-
* @return bool
87-
*/
88-
private function matchesRestRequest(array $match)
89-
{
90-
if (!isset($match['_route'])) {
91-
throw new InvalidArgumentException('Invalid $match parameter, no _route key');
92-
}
93-
94-
return strpos($match['_route'], 'ibexa.rest.') === 0;
52+
return $this->uriParser->getAttributeFromUri($href, $attribute);
9553
}
9654
}
9755

src/bundle/Resources/config/services.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ parameters:
66
- '(^application/vnd\.ibexa\.api\.[A-Za-z]+\+xml$)'
77
- '(^application/xml$)'
88
- '(^.*/.*$)'
9+
ibexa.rest.path_prefix.pattern: !php/const \Ibexa\Bundle\Rest\UriParser\UriParser::DEFAULT_REST_PREFIX_PATTERN
910

1011
services:
1112
Ibexa\Bundle\Rest\Serializer\SerializerFactory:
@@ -43,7 +44,15 @@ services:
4344

4445
Ibexa\Bundle\Rest\RequestParser\Router:
4546
arguments:
46-
- "@router"
47+
$router: '@router'
48+
$uriParser: '@Ibexa\Contracts\Rest\UriParser\UriParserInterface'
49+
50+
Ibexa\Contracts\Rest\UriParser\UriParserInterface: '@Ibexa\Bundle\Rest\UriParser\UriParser'
51+
52+
Ibexa\Bundle\Rest\UriParser\UriParser:
53+
arguments:
54+
$urlMatcher: '@Symfony\Component\Routing\Matcher\UrlMatcherInterface'
55+
$restPrefixPattern: '%ibexa.rest.path_prefix.pattern%'
4756

4857
Ibexa\Rest\Input\ParserTools: ~
4958

@@ -193,6 +202,8 @@ services:
193202
tags: [controller.service_arguments]
194203

195204
Ibexa\Bundle\Rest\EventListener\RequestListener:
205+
arguments:
206+
$uriParser: '@Ibexa\Contracts\Rest\UriParser\UriParserInterface'
196207
tags:
197208
- { name: kernel.event_subscriber }
198209

src/bundle/UriParser/UriParser.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Bundle\Rest\UriParser;
10+
11+
use Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException;
12+
use Ibexa\Contracts\Rest\UriParser\UriParserInterface;
13+
use Symfony\Component\HttpFoundation\Request;
14+
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
15+
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
16+
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
17+
18+
/**
19+
* @internal
20+
*/
21+
final class UriParser implements UriParserInterface
22+
{
23+
/**
24+
* @internal rely on \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest
25+
* or \Ibexa\Contracts\Rest\UriParser\UriParserInterface::hasRestPrefix instead.
26+
*
27+
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
28+
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::hasRestPrefix()
29+
*/
30+
public const DEFAULT_REST_PREFIX_PATTERN = '/^\/api\/[a-zA-Z0-9-_]+\/v\d+(\.\d+)?\//';
31+
32+
private UrlMatcherInterface $urlMatcher;
33+
34+
private string $restPrefixPattern;
35+
36+
public function __construct(
37+
UrlMatcherInterface $urlMatcher,
38+
string $restPrefixPattern = self::DEFAULT_REST_PREFIX_PATTERN
39+
) {
40+
$this->urlMatcher = $urlMatcher;
41+
$this->restPrefixPattern = $restPrefixPattern;
42+
}
43+
44+
public function matchUri(string $uri, string $method = 'GET'): array
45+
{
46+
if (!$this->hasRestPrefix($uri)) {
47+
// keeping the original exception message for BC, otherwise could be more verbose
48+
throw new InvalidArgumentException("No route matched '$uri'");
49+
}
50+
51+
$request = Request::create($uri, $method);
52+
53+
$originalContext = $this->urlMatcher->getContext();
54+
$context = clone $originalContext;
55+
$context->fromRequest($request);
56+
$this->urlMatcher->setContext($context);
57+
58+
try {
59+
return $this->urlMatcher->match($request->getPathInfo());
60+
} catch (MethodNotAllowedException $e) {
61+
// seems MethodNotAllowedException has no message set
62+
$allowedMethods = implode(', ', $e->getAllowedMethods());
63+
throw new InvalidArgumentException(
64+
"Method '$method' is not allowed for '$uri'. Allowed: [$allowedMethods]",
65+
$e->getCode(),
66+
$e
67+
);
68+
} catch (ResourceNotFoundException $e) {
69+
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
70+
} finally {
71+
$this->urlMatcher->setContext($originalContext);
72+
}
73+
}
74+
75+
public function getAttributeFromUri(string $uri, string $attribute, string $method = 'GET'): string
76+
{
77+
$parsingResult = $this->matchUri($uri, $method);
78+
79+
if (!isset($parsingResult[$attribute])) {
80+
throw new InvalidArgumentException("No attribute '$attribute' in route matched from $uri");
81+
}
82+
83+
return (string)$parsingResult[$attribute];
84+
}
85+
86+
public function isRestRequest(Request $request): bool
87+
{
88+
return $this->hasRestPrefix($request->getPathInfo());
89+
}
90+
91+
public function hasRestPrefix(string $uri): bool
92+
{
93+
return (bool)preg_match($this->restPrefixPattern, $uri);
94+
}
95+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\Contracts\Rest\UriParser;
10+
11+
use Symfony\Component\HttpFoundation\Request;
12+
13+
interface UriParserInterface
14+
{
15+
/**
16+
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException If $attribute wasn't found in the matched URI attributes
17+
*/
18+
public function getAttributeFromUri(string $uri, string $attribute, string $method = 'GET'): string;
19+
20+
public function isRestRequest(Request $request): bool;
21+
22+
public function hasRestPrefix(string $uri): bool;
23+
24+
/**
25+
* @internal use getAttributeFromUri
26+
*
27+
* @return array<mixed> matched route configuration and parameters
28+
*
29+
* @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException
30+
*/
31+
public function matchUri(string $uri, string $method = 'GET'): array;
32+
}

0 commit comments

Comments
 (0)