diff --git a/src/UrlHelper.php b/src/UrlHelper.php index 4945270..6bd393e 100644 --- a/src/UrlHelper.php +++ b/src/UrlHelper.php @@ -13,6 +13,7 @@ use InvalidArgumentException; use Mezzio\Router\RouteResult; use Mezzio\Router\RouterInterface; +use Psr\Http\Message\ServerRequestInterface; use function array_merge; use function count; @@ -40,6 +41,11 @@ class UrlHelper */ private $result; + /** + * @var ServerRequestInterface + */ + private $request; + /** * @var RouterInterface */ @@ -102,6 +108,13 @@ public function __invoke( $routeParams = $this->mergeParams($routeName, $result, $routeParams); } + $reuseQueryParams = ! isset($options['reuse_query_params']) || (bool) $options['reuse_query_params']; + + if ($result && $reuseQueryParams) { + // Merge current request params with passed query params + $queryParams = $this->mergeQueryParams($routeName, $result, $queryParams); + } + // Generate the route $path = $basePath . $this->router->generateUri($routeName, $routeParams, $routerOptions); @@ -153,6 +166,19 @@ public function getRouteResult() : ?RouteResult return $this->result; } + /** + * Set request instance + */ + public function setRequest(ServerRequestInterface $request) : void + { + $this->request = $request; + } + + public function getRequest() : ?ServerRequestInterface + { + return $this->request; + } + /** * Internal accessor for retrieving the base path. */ @@ -190,7 +216,9 @@ private function generateUriFromResult(array $params, RouteResult $result, array * invocation, with the latter having precedence. * * @param string $route Route name. - * @param array $params Parameters provided at invocation. + * @param RouteResult $result RouteResult instance + * @param array $params Route parameters + * @return array Merged parameters */ private function mergeParams(string $route, RouteResult $result, array $params) : array { @@ -205,6 +233,35 @@ private function mergeParams(string $route, RouteResult $result, array $params) return array_merge($result->getMatchedParams(), $params); } + /** + * Merge requested route query params with existing request query parameters. + * + * If route result represents routing failure, returns the params verbatim + * + * If the route result does not represent the same route name requested, + * returns the params verbatim. + * + * Otherwise, merges the current request query parameters with the specified query + * parameters with the latter having precedence. + * + * @param string $route Route name + * @param RouteResult $result RouteResult instance + * @param array $params Params to be merged with request params + * @return array + */ + private function mergeQueryParams(string $route, RouteResult $result, array $params) : array + { + if ($result->isFailure()) { + return $params; + } + + if ($result->getMatchedRouteName() !== $route) { + return $params; + } + + return array_merge($this->getRequest()->getQueryParams(), $params); + } + /** * Append query string arguments to a URI string, if any are present. */ diff --git a/src/UrlHelperMiddleware.php b/src/UrlHelperMiddleware.php index 2bb4f89..dba7d5f 100644 --- a/src/UrlHelperMiddleware.php +++ b/src/UrlHelperMiddleware.php @@ -32,11 +32,15 @@ public function __construct(UrlHelper $helper) } /** + * Inject the helper with the request instance. + * * Inject the UrlHelper instance with a RouteResult, if present as a request attribute. * Injects the helper, and then dispatches the next middleware. */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { + $this->helper->setRequest($request); + $result = $request->getAttribute(RouteResult::class, false); if ($result instanceof RouteResult) { diff --git a/test/UrlHelperMiddlewareTest.php b/test/UrlHelperMiddlewareTest.php index 3d2f5d1..c78dd42 100644 --- a/test/UrlHelperMiddlewareTest.php +++ b/test/UrlHelperMiddlewareTest.php @@ -48,6 +48,7 @@ public function testInvocationInjectsHelperWithRouteResultWhenPresentInRequest() $request = $this->prophesize(ServerRequestInterface::class); $request->getAttribute(RouteResult::class, false)->willReturn($routeResult); $this->helper->setRouteResult($routeResult)->shouldBeCalled(); + $this->helper->setRequest($request)->shouldBeCalled(); $handler = $this->prophesize(RequestHandlerInterface::class); $handler->handle(Argument::type(ServerRequestInterface::class))->will([$response, 'reveal']); @@ -65,6 +66,7 @@ public function testInvocationDoesNotInjectHelperWithRouteResultWhenAbsentInRequ $request = $this->prophesize(ServerRequestInterface::class); $request->getAttribute(RouteResult::class, false)->willReturn(false); + $this->helper->setRequest($request)->shouldBeCalled(); $this->helper->setRouteResult(Argument::any())->shouldNotBeCalled(); $handler = $this->prophesize(RequestHandlerInterface::class); diff --git a/test/UrlHelperTest.php b/test/UrlHelperTest.php index 7b82107..58d65be 100644 --- a/test/UrlHelperTest.php +++ b/test/UrlHelperTest.php @@ -21,6 +21,8 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; use ReflectionProperty; use stdClass; use TypeError; @@ -43,7 +45,12 @@ public function setUp(): void public function createHelper() { - return new UrlHelper($this->router->reveal()); + $request = $this->prophesize(ServerRequestInterface::class); + $request->getQueryParams()->willReturn([]); + + $helper = new UrlHelper($this->router->reveal()); + $helper->setRequest($request->reveal()); + return $helper; } public function testRaisesExceptionOnInvocationIfNoRouteProvidedAndNoResultPresent() @@ -231,7 +238,7 @@ public function testGenerateProxiesToInvokeMethod() $fragmentIdentifier = 'foobar'; $options = ['router' => ['foobar' => 'baz'], 'reuse_result_params' => false]; - $helper = Mockery::mock(UrlHelper::class)->shouldDeferMissing(); + $helper = Mockery::mock(UrlHelper::class)->makePartial(); $helper->shouldReceive('__invoke') ->once() ->with($routeName, $routeParams, $queryParams, $fragmentIdentifier, $options) @@ -398,6 +405,82 @@ public function testGetRouteResultWithRouteResultSet() $this->assertInstanceOf(RouteResult::class, $helper->getRouteResult()); } + public function testWillNotReuseQueryParamsIfReuseQueryParamsFlagIsFalseWhenGeneratingUri() + { + $result = $this->prophesize(RouteResult::class); + $result->isFailure()->willReturn(false); + $result->getMatchedRouteName()->willReturn('resource'); + $result->getMatchedParams()->willReturn([]); + + $this->router->generateUri('resource', [], [])->willReturn('URL'); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getQueryParams()->wilLReturn(['foo' => 'bar']); + + $helper = $this->createHelper(); + $helper->setRouteResult($result->reveal()); + $helper->setRequest($request->reveal()); + + $this->assertEquals('URL', $helper('resource', [], [], null, ['reuse_query_params' => false])); + } + + public function testWillReuseQueryParamsIfReuseQueryParamsFlagIsTrueWhenGeneratingUri() + { + $result = $this->prophesize(RouteResult::class); + $result->isFailure()->willReturn(false); + $result->getMatchedRouteName()->willReturn('resource'); + $result->getMatchedParams()->willReturn([]); + + $this->router->generateUri('resource', [], [])->willReturn('URL'); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getQueryParams()->wilLReturn(['foo' => 'bar']); + + $helper = $this->createHelper(); + $helper->setRouteResult($result->reveal()); + $helper->setRequest($request->reveal()); + + $this->assertEquals('URL?foo=bar', $helper('resource', [], [], null, ['reuse_query_params' => true])); + } + + public function testWillReuseQueryParamsIfReuseQueryParamsFlagIsMissingGeneratingUri() + { + $result = $this->prophesize(RouteResult::class); + $result->isFailure()->willReturn(false); + $result->getMatchedRouteName()->willReturn('resource'); + $result->getMatchedParams()->willReturn([]); + + $this->router->generateUri('resource', [], [])->willReturn('URL'); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getQueryParams()->wilLReturn(['foo' => 'bar']); + + $helper = $this->createHelper(); + $helper->setRouteResult($result->reveal()); + $helper->setRequest($request->reveal()); + + $this->assertEquals('URL?foo=bar', $helper('resource')); + } + + public function testCanOverrideRequestQueryParams() + { + $result = $this->prophesize(RouteResult::class); + $result->isFailure()->willReturn(false); + $result->getMatchedRouteName()->willReturn('resource'); + $result->getMatchedParams()->willReturn([]); + + $this->router->generateUri('resource', [], [])->willReturn('URL'); + + $request = $this->prophesize(ServerRequestInterface::class); + $request->getQueryParams()->wilLReturn(['foo' => 'bar']); + + $helper = $this->createHelper(); + $helper->setRouteResult($result->reveal()); + $helper->setRequest($request->reveal()); + + $this->assertEquals('URL?foo=foo', $helper('resource', [], ['foo' => 'foo'])); + } + private function assertAttributeSame($expected, $attribute, $object) { $r = new ReflectionProperty($object, $attribute);