core/lib/Drupal/Core/Url.php | 52 ++++++++++++++- core/tests/Drupal/Tests/Core/UrlTest.php | 110 +++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 999f447..2c970a7 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -15,6 +15,7 @@ use Drupal\Core\Utility\UnroutedUrlAssemblerInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\RouteNotFoundException; /** * Defines an object that holds information about a URL. @@ -196,9 +197,15 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) { * base://robots.txt. For URLs that have Drupal routes (that is, most pages * generated by Drupal), use Url::fromRoute(). * + * For resolving URLs to an entity, you may use the + * entity://{entity_type}/{entity_id} scheme. For example entity://node/1 + * would resolve to the entity.node.canonical route with a node parameter of + * one. + * * @param string $uri * The URI of the external resource including the scheme. For Drupal paths * that are not handled by the routing system, use base:// for the scheme. + * For entity URLs you may use entity://{entity_type}/{entity_id}. * @param array $options * (optional) An associative array of additional URL options, with the * following elements: @@ -217,7 +224,7 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) { * respectively. TRUE enforces HTTPS and FALSE enforces HTTP. * * @return \Drupal\Core\Url - * A new Url object for an unrouted (non-Drupal) URL. + * A new Url object for an unrouted (non-Drupal) URL or a routed entity URI. * * @throws \InvalidArgumentException * Thrown when the passed in path has no scheme. @@ -225,10 +232,15 @@ public static function fromRouteMatch(RouteMatchInterface $route_match) { * @see static::fromRoute() */ public static function fromUri($uri, $options = array()) { - if (!parse_url($uri, PHP_URL_SCHEME)) { + if (!($scheme = parse_url($uri, PHP_URL_SCHEME))) { throw new \InvalidArgumentException(String::format('The URI "@uri" is invalid. You must use a valid URI scheme. Use base:// for a path, e.g., to a Drupal file that needs the base path. Do not use this for internal paths controlled by Drupal.', ['@uri' => $uri])); } + // Special case entity:// URIs. Map these to the canonical entity route. + if ($scheme === 'entity') { + return static::fromEntityUri($uri); + } + $url = new static($uri, array(), $options); $url->setUnrouted(); @@ -236,6 +248,42 @@ public static function fromUri($uri, $options = array()) { } /** + * Create a new Url object for entity URIs. + * + * @param string $uri + * An URI of the form entity://{entity_type}/{entity_id}. + * + * @return \Drupal\Core\Url + * A new Url object for an entity's canonical route. + * + * @throws \InvalidArgumentException + * Thrown when the entity URI is invalid or the entity does not have a valid + * canonical link template. + * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException + * Thrown when the entity's canonical route does not exist. + */ + protected static function fromEntityUri($uri) { + $uri_parts = parse_url($uri); + $entity_type_id = $uri_parts['host']; + $entity_id = trim($uri_parts['path'], '/'); + if ($uri_parts['scheme'] != 'entity' || $entity_id === '') { + throw new \InvalidArgumentException(String::format('The entity URI "@uri" is invalid. You must specify the entity id in the URL. e.g., entity://node/1 for loading the canonical path to node entity with id 1.', + ['@uri' => $uri])); + } + + $entity_type = \Drupal::entityManager()->getDefinition($entity_type_id); + if (!$entity_type->hasLinkTemplate('canonical')) { + throw new \InvalidArgumentException(String::format('The entity type "@entity_type_id" does not have a "canonical" link template.', ['@entity_type_id' => $entity_type_id])); + } + + $url = new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id]); + // Validate the route exists, ::toString() will throw a + // RouteNotFoundException if the route does not exist. + $url->toString(); + return $url; + } + + /** * Returns the Url object matching a request. * * SECURITY NOTE: The request path is not checked to be valid and accessible diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index a93226a..8528084 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -9,13 +9,16 @@ use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Entity\EntityType; use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Url; use Drupal\Tests\UnitTestCase; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Route; /** @@ -44,6 +47,13 @@ class UrlTest extends UnitTestCase { protected $router; /** + * The route provider. + * + * @var \Drupal\Tests\Core\Routing\TestRouterInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $routeProvider; + + /** * An array of values to use for the test. * * @var array @@ -51,6 +61,13 @@ class UrlTest extends UnitTestCase { protected $map; /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $entityManager; + + /** * {@inheritdoc} */ protected function setUp() { @@ -68,9 +85,14 @@ protected function setUp() { ->will($this->returnValueMap($this->map)); $this->router = $this->getMock('Drupal\Tests\Core\Routing\TestRouterInterface'); + $this->routeProvider = $this->getMock('Symfony\Cmf\Component\Routing\RouteProviderInterface'); + $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $this->container = new ContainerBuilder(); $this->container->set('router.no_access_checks', $this->router); + $this->container->set('router.route_provider', $this->routeProvider); $this->container->set('url_generator', $this->urlGenerator); + $this->container->set('entity.manager', $this->entityManager); \Drupal::setContainer($this->container); } @@ -418,6 +440,94 @@ public function testFromRouteMatch() { } /** + * Tests the fromUri() method with an entity:// URI. + * + * @covers ::fromUri + */ + public function testEntityUris() { + $entity_type = new EntityType(['id' => 'test_entity', 'links' => ['canonical' => '/test_entity/{test_entity}']]); + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->willReturn($entity_type); + + $map = []; + $map[] = [['entity.test_entity.canonical'], ['test_entity/1']]; + + $this->routeProvider->expects($this->any()) + ->method('getRoutesByNames') + ->will($this->returnValueMap($map)); + + $uri = 'entity://test_entity/1'; + $url = Url::fromUri($uri); + $this->assertSame('entity.test_entity.canonical', $url->getRouteName()); + $this->assertEquals(['test_entity' => '1'], $url->getRouteParameters()); + } + + /** + * Tests the fromUri() method with an invalid entity:// URI. + * + * @covers ::fromUri + * @expectedException \Symfony\Component\Routing\Exception\RouteNotFoundException + */ + public function testInvalidEntityUriRoute() { + $entity_type = new EntityType(['id' => 'invalid_entity', 'links' => ['canonical' => '/example/{example}']]); + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->willReturn($entity_type); + + // Make the mocked URL generator behave like the actual one. + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('entity.invalid_entity.canonical', ['invalid_entity' => 1]) + ->willThrowException(new RouteNotFoundException('Route "entity.invalid_entity.canonical" does not exist.')); + + $uri = 'entity://invalid_entity/1'; + Url::fromUri($uri); + } + + /** + * Tests the fromUri() method with a missing canonical link template. + * + * @covers ::fromUri + * @expectedException \InvalidArgumentException + */ + public function testInvalidEntityUriWithMissingCanonicalLinkTemplate() { + $entity_type = new EntityType(['id' => 'test_entity']); + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->willReturn($entity_type); + + // Make the mocked URL generator behave like the actual one. + $this->urlGenerator->expects($this->never()) + ->method('generateFromRoute'); + + $uri = 'entity://test_entity/1'; + Url::fromUri($uri); + } + + /** + * Tests the fromUri() method with an invalid entity:// URI. + * + * @covers ::fromUri + * @expectedException \Symfony\Component\Routing\Exception\InvalidParameterException + */ + public function testInvalidEntityUriParameter() { + $entity_type = new EntityType(['id' => 'example', 'links' => ['canonical' => '/example/{example}']]); + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->willReturn($entity_type); + + // Make the mocked URL generator behave like the actual one. + $this->urlGenerator->expects($this->once()) + ->method('generateFromRoute') + ->with('entity.test_entity.canonical', ['test_entity' => '1/blah']) + ->willThrowException(new InvalidParameterException('Parameter "test_entity" for route "/test_entity/{test_entity}" must match "[^/]++" ("1/blah" given) to generate a corresponding URL..')); + + $uri = 'entity://test_entity/1/blah'; + Url::fromUri($uri); + } + + /** * Creates a mock access manager for the access tests. * * @param bool $access