Skip to content

Commit

Permalink
Use custom RouteParser wrapper to add fallback routes
Browse files Browse the repository at this point in the history
Ref #1244
  • Loading branch information
lcharette committed Feb 17, 2024
1 parent 9a3edfb commit 9530a59
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 31 deletions.
9 changes: 6 additions & 3 deletions app/src/ServicesProvider/RoutingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
namespace UserFrosting\Sprinkle\Core\ServicesProvider;

use Slim\App;
use Slim\Interfaces\RouteParserInterface;
use UserFrosting\ServicesProvider\ServicesProviderInterface;
use UserFrosting\Sprinkle\Core\Util\RouteParser;
use UserFrosting\Sprinkle\Core\Util\RouteParserInterface;

/*
* Slim's routing related services with the UF router.
Expand All @@ -25,12 +26,14 @@ public function register(): array
{
return [
/**
* Alias for Router Parser in CI for easier access.
* Implement our own RouteParser to allow fallback routes.
*
* @see https://www.slimframework.com/docs/v4/objects/routing.html#route-names
*/
RouteParserInterface::class => function (App $app) {
return $app->getRouteCollector()->getRouteParser();
$slimRouteCollector = $app->getRouteCollector();

return new RouteParser($slimRouteCollector);
},
];
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/Twig/Extensions/RoutesExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

namespace UserFrosting\Sprinkle\Core\Twig\Extensions;

use Slim\Interfaces\RouteParserInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use UserFrosting\Sprinkle\Core\Util\RouteParserInterface;

class RoutesExtension extends AbstractExtension
{
Expand Down
103 changes: 103 additions & 0 deletions app/src/Util/RouteParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

/*
* UserFrosting Core Sprinkle (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/sprinkle-core
* @copyright Copyright (c) 2021 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/sprinkle-core/blob/master/LICENSE.md (MIT License)
*/

namespace UserFrosting\Sprinkle\Core\Util;

use Psr\Http\Message\UriInterface;
use RuntimeException;
use Slim\Interfaces\RouteCollectorInterface;

/**
* Wrapper around Slim's RouteParser. Allows to add 'fallback' routes when names
* routes are not found.
*
* @see https://github.com/userfrosting/UserFrosting/issues/1244
*/
class RouteParser implements RouteParserInterface
{
public function __construct(
protected RouteCollectorInterface $routeCollector,
) {
}

/**
* {@inheritDoc}
*/
public function relativeUrlFor(
string $routeName,
array $data = [],
array $queryParams = [],
?string $fallbackRoute = null,
): string {
try {
$result = $this->routeCollector->getRouteParser()->relativeUrlFor($routeName, $data, $queryParams);
} catch (RuntimeException $e) {
if ($fallbackRoute !== null) {
$result = $fallbackRoute;
} else {
throw $e;
}
}

return $result;
}

/**
* {@inheritDoc}
*/
public function urlFor(
string $routeName,
array $data = [],
array $queryParams = [],
?string $fallbackRoute = null,
): string {
try {
$result = $this->routeCollector->getRouteParser()->urlFor($routeName, $data, $queryParams);
} catch (RuntimeException $e) {
if ($fallbackRoute !== null) {
$basePath = $this->routeCollector->getBasePath();
$result = $basePath . $fallbackRoute;
} else {
throw $e;
}
}

return $result;
}

/**
* {@inheritDoc}
*/
public function fullUrlFor(
UriInterface $uri,
string $routeName,
array $data = [],
array $queryParams = [],
?string $fallbackRoute = null,
): string {
try {
$result = $this->routeCollector->getRouteParser()->fullUrlFor($uri, $routeName, $data, $queryParams);
} catch (RuntimeException $e) {
if ($fallbackRoute !== null) {
$path = $this->urlFor($routeName, $data, $queryParams, $fallbackRoute);
$scheme = $uri->getScheme();
$authority = $uri->getAuthority();
$protocol = ($scheme !== '' ? $scheme . ':' : '') . ($authority !== '' ? '//' . $authority : '');
$result = $protocol . $path;
} else {
throw $e;
}
}

return $result;
}
}
74 changes: 74 additions & 0 deletions app/src/Util/RouteParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

/*
* UserFrosting Core Sprinkle (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/sprinkle-core
* @copyright Copyright (c) 2021 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/sprinkle-core/blob/master/LICENSE.md (MIT License)
*/

namespace UserFrosting\Sprinkle\Core\Util;

use InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use RuntimeException;
use Slim\Interfaces\RouteParserInterface as SlimRouteParserInterface;

interface RouteParserInterface extends SlimRouteParserInterface
{
/**
* Build the path for a named route excluding the base path
*
* @param string $routeName Route name
* @param array<string, string> $data Named argument replacement data
* @param array<string, string> $queryParams Optional query string parameters
* @param string|null $fallbackRoute Optional fallback route (the actual route, not the route name)
*
* @throws RuntimeException If named route does not exist
* @throws InvalidArgumentException If required data not provided
*/
public function relativeUrlFor(
string $routeName,
array $data = [],
array $queryParams = [],
?string $fallbackRoute = null,
): string;

/**
* Build the path for a named route including the base path
*
* @param string $routeName Route name
* @param array<string, string> $data Named argument replacement data
* @param array<string, string> $queryParams Optional query string parameters
* @param string|null $fallbackRoute Optional fallback route (the actual route, not the route name)
*
* @throws RuntimeException If named route does not exist
* @throws InvalidArgumentException If required data not provided
*/
public function urlFor(
string $routeName,
array $data = [],
array $queryParams = [],
?string $fallbackRoute = null,
): string;

/**
* Get fully qualified URL for named route
*
* @param UriInterface $uri
* @param string $routeName Route name
* @param array<string, string> $data Named argument replacement data
* @param array<string, string> $queryParams Optional query string parameters
* @param string|null $fallbackRoute Optional fallback route (the actual route, not the route name)
*/
public function fullUrlFor(
UriInterface $uri,
string $routeName,
array $data = [],
array $queryParams = [],
?string $fallbackRoute = null,
): string;
}
27 changes: 0 additions & 27 deletions app/tests/Integration/ServicesProvider/RoutingServiceTest.php

This file was deleted.

4 changes: 4 additions & 0 deletions app/tests/Integration/Twig/RoutesExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ public function testUrlFor(): void

$result = $view->fetchFromString("{{ urlFor('alerts') }}");
$this->assertSame('/alerts', $result);

// Test with fallback
$result = $view->fetchFromString("{{ urlFor('index', [], [], '/foo') }}");
$this->assertSame('/foo', $result);
}
}
102 changes: 102 additions & 0 deletions app/tests/Integration/Util/RouteParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

/*
* UserFrosting Core Sprinkle (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/sprinkle-core
* @copyright Copyright (c) 2021 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/sprinkle-core/blob/master/LICENSE.md (MIT License)
*/

namespace UserFrosting\Sprinkle\Core\Tests\Integration\Twig;

use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Psr\Http\Message\UriInterface;
use Slim\App;
use UserFrosting\Sprinkle\Core\Tests\CoreTestCase;
use UserFrosting\Sprinkle\Core\Util\RouteParser;
use UserFrosting\Sprinkle\Core\Util\RouteParserInterface;

/**
* Tests RoutesExtension.
*/
class RouteParserTest extends CoreTestCase
{
use MockeryPHPUnitIntegration;

private RouteParser $parser;

/**
* Create parser manually to set basepath
*/
public function setUp(): void
{
parent::setUp();

/** @var App */
$app = $this->ci->get(App::class);
$collector = $app->getRouteCollector();
$collector->setBasePath('/Myfoo');

$this->parser = new RouteParser($collector);
}

public function testRelativeUrlFor(): void
{
// Valid
$this->assertSame('/alerts', $this->parser->relativeUrlFor('alerts'));

// Invalid, with fallback
$this->assertSame('/fallback', $this->parser->relativeUrlFor('invalid', fallbackRoute: '/fallback'));

// Invalid, no fallback
$this->expectExceptionMessage('Named route does not exist for name: invalid');
$this->parser->relativeUrlFor('invalid');
}

public function testUrlFor(): void
{
// Valid
$this->assertSame('/Myfoo/alerts', $this->parser->urlFor('alerts'));

// Invalid, with fallback
$this->assertSame('/Myfoo/fallback', $this->parser->urlFor('invalid', fallbackRoute: '/fallback'));

// Invalid, no fallback
$this->expectExceptionMessage('Named route does not exist for name: invalid');
$this->parser->urlFor('invalid');
}

public function testFullUrlFor(): void
{
/** @var UriInterface */
$uri = Mockery::mock(UriInterface::class)
->shouldReceive('getScheme')->times(2)->andReturn('http')
->shouldReceive('getAuthority')->times(2)->andReturn('localhost')
->getMock();

// Valid
$this->assertSame('http://localhost/Myfoo/alerts', $this->parser->fullUrlFor($uri, 'alerts'));

// Invalid, with fallback
$this->assertSame('http://localhost/Myfoo/fallback', $this->parser->fullUrlFor($uri, 'invalid', fallbackRoute: '/fallback'));

// Invalid, no fallback
$this->expectExceptionMessage('Named route does not exist for name: invalid');
$this->parser->fullUrlFor($uri, 'invalid');
}

public function testService(): void
{
/** @var RouteParserInterface */
$parser = $this->ci->get(RouteParserInterface::class);

$this->assertSame('/alerts', $parser->relativeUrlFor('alerts'));
$this->assertSame('/fallback', $parser->relativeUrlFor('invalid', fallbackRoute: '/fallback'));
$this->expectExceptionMessage('Named route does not exist for name: invalid');
$parser->relativeUrlFor('invalid');
}
}

0 comments on commit 9530a59

Please sign in to comment.