Issue #1970360 by Crell, linclark, dawehner, YesCT, matt2000: Entities should define URI templates and standard links.

8.0.x
Alex Pott 2013-06-13 09:19:53 +01:00
parent 4b23474b69
commit 342f132b35
16 changed files with 212 additions and 23 deletions

View File

@ -252,6 +252,41 @@ class EntityType extends Plugin {
*/
public $menu_path_wildcard;
/**
* Link templates using the URI template syntax.
*
* Links are an array of standard link relations to the URI template that
* should be used for them. Where possible, link relationships should use
* established IANA relationships rather than custom relationships.
*
* Every entity type should, at minimum, define "canonical", which is the
* pattern for URIs to that entity. Even if the entity will have no HTML page
* exposed to users it should still have a canonical URI in order to be
* compatible with web services. Entities that will be user-editable via an
* HTML page must also define an "edit-form" relationship.
*
* By default, the following placeholders are supported:
* - entityType: The machine name of the entity type.
* - bundle: The bundle machine name of the entity.
* - id: The unique ID of the entity.
* - uuid: The UUID of the entity.
* - [entityType]: The entity type itself will also be a valid token for the
* ID of the entity. For instance, a placeholder of {node} used on the Node
* class would have the same value as {id}. This is generally preferred
* over "id" for better self-documentation.
*
* Specific entity types may also expand upon this list by overriding the
* uriPlaceholderReplacements() method.
*
* @link http://www.iana.org/assignments/link-relations/link-relations.xml @endlink
* @link http://tools.ietf.org/html/rfc6570 @endlink
*
* @var array
*/
public $links = array(
'canonical' => '/entity/{entityType}/{id}',
);
/**
* Specifies whether a module exposing permissions for the current entity type
* should use entity-type level granularity, bundle level granularity or just

View File

@ -181,6 +181,19 @@ class Entity implements IteratorAggregate, EntityInterface {
return $uri;
}
/**
* {@inheritdoc}
*
* Returns a list of URI relationships supported by this entity.
*
* @return array
* An array of link relationships supported by this entity.
*/
public function uriRelationships() {
$entity_info = $this->entityInfo();
return isset($entity_info['links']) ? array_keys($entity_info['links']) : array();
}
/**
* Implements \Drupal\Core\Entity\EntityInterface::get().
*/

View File

@ -211,6 +211,13 @@ class EntityBCDecorator implements IteratorAggregate, EntityInterface {
$this->decorated = clone $this->decorated;
}
/**
* Forwards the call to the decorated entity.
*/
public function uriRelationships() {
return $this->decorated->uriRelationships();
}
/**
* Forwards the call to the decorated entity.
*/
@ -355,8 +362,8 @@ class EntityBCDecorator implements IteratorAggregate, EntityInterface {
/**
* Forwards the call to the decorated entity.
*/
public function uri() {
return $this->decorated->uri();
public function uri($rel = 'canonical') {
return $this->decorated->uri($rel);
}
/**

View File

@ -135,6 +135,14 @@ interface EntityInterface extends ComplexDataInterface, AccessibleInterface, Tra
*/
public function uri();
/**
* Returns a list of URI relationships supported by this entity.
*
* @return array
* An array of link relationships supported by this entity.
*/
public function uriRelationships();
/**
* Saves an entity permanently.
*

View File

@ -73,6 +73,13 @@ class EntityNG extends Entity {
*/
protected $fieldDefinitions;
/**
* Local cache for URI placeholder substitution values.
*
* @var array
*/
protected $uriPlaceholderReplacements;
/**
* Overrides Entity::__construct().
*/
@ -135,6 +142,61 @@ class EntityNG extends Entity {
return $this->get('uuid')->value;
}
/**
* {@inheritdoc}
*/
public function uri($rel = 'canonical') {
$entity_info = $this->entityInfo();
$link_templates = isset($entity_info['links']) ? $entity_info['links'] : array();
if (isset($link_templates[$rel])) {
$template = $link_templates[$rel];
$replacements = $this->uriPlaceholderReplacements();
$uri['path'] = str_replace(array_keys($replacements), array_values($replacements), $template);
// @todo Remove this once http://drupal.org/node/1888424 is in and we can
// move the BC handling of / vs. no-/ to the generator.
$uri['path'] = trim($uri['path'], '/');
// Pass the entity data to url() so that alter functions do not need to
// look up this entity again.
$uri['options']['entity_type'] = $this->entityType;
$uri['options']['entity'] = $this;
return $uri;
}
// For a canonical link (that is, a link to self), look up the stack for
// default logic. Other relationship types are not supported by parent
// classes.
if ($rel == 'canonical') {
return parent::uri();
}
}
/**
* Returns an array of placeholders for this entity.
*
* Individual entity classes may override this method to add additional
* placeholders if desired. If so, they should be sure to replicate the
* property caching logic.
*
* @return array
* An array of URI placeholders.
*/
protected function uriPlaceholderReplacements() {
if (empty($this->uriPlaceholderReplacements)) {
$this->uriPlaceholderReplacements = array(
'{entityType}' => $this->entityType(),
'{bundle}' => $this->bundle(),
'{id}' => $this->id(),
'{uuid}' => $this->uuid(),
'{' . $this->entityType() . '}' => $this->id(),
);
}
return $this->uriPlaceholderReplacements;
}
/**
* Implements \Drupal\Core\TypedData\ComplexDataInterface::get().
*/

View File

@ -1568,10 +1568,11 @@ function template_preprocess_comment(&$variables) {
}
$uri = $comment->uri();
$permalink_uri = $comment->permalink();
$uri['options'] += array('attributes' => array('class' => 'permalink', 'rel' => 'bookmark'));
$variables['title'] = l($comment->subject->value, $uri['path'], $uri['options']);
$variables['permalink'] = l(t('Permalink'), $uri['path'], $uri['options']);
$variables['permalink'] = l(t('Permalink'), $permalink_uri['path'], $permalink_uri['options']);
$variables['submitted'] = t('Submitted by !username on !datetime', array('!username' => $variables['author'], '!datetime' => $variables['created']));
if ($comment->pid->target_id) {
@ -1589,10 +1590,10 @@ function template_preprocess_comment(&$variables) {
else {
$variables['parent_changed'] = format_date($comment_parent->changed->value);
}
$uri_parent = $comment_parent->uri();
$uri_parent['options'] += array('attributes' => array('class' => 'permalink', 'rel' => 'bookmark'));
$variables['parent_title'] = l($comment_parent->subject->value, $uri_parent['path'], $uri_parent['options']);
$variables['parent_permalink'] = l(t('Parent permalink'), $uri_parent['path'], $uri_parent['options']);
$permalink_uri_parent = $comment_parent->permalink();
$permalink_uri_parent['options'] += array('attributes' => array('class' => array('permalink'), 'rel' => 'bookmark'));
$variables['parent_title'] = l($comment_parent->subject->value, $permalink_uri_parent['path'], $permalink_uri_parent['options']);
$variables['parent_permalink'] = l(t('Parent permalink'), $permalink_uri_parent['path'], $permalink_uri_parent['options']);
$variables['parent'] = t('In reply to !parent_title by !parent_username',
array('!parent_username' => $variables['parent_author'], '!parent_title' => $variables['parent_title']));
}

View File

@ -14,4 +14,13 @@ use Drupal\Core\Entity\ContentEntityInterface;
*/
interface CommentInterface extends ContentEntityInterface {
/**
* Returns the permalink URL for this comment.
*
* @return array
* An array containing the 'path' and 'options' keys used to build the URI
* of the comment, and matching the signature of
* UrlGenerator::generateFromPath().
*/
public function permalink();
}

View File

@ -41,6 +41,10 @@ use Drupal\Core\Language\Language;
* "bundle" = "node_type",
* "label" = "subject",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "/comment/{comment}",
* "edit-form" = "/comment/{comment}/edit"
* }
* )
*/
@ -219,4 +223,15 @@ class Comment extends EntityNG implements CommentInterface {
public function id() {
return $this->get('cid')->value;
}
/**
* {@inheritdoc}
*/
public function permalink() {
$url['path'] = 'node/' . $this->nid->value;
$url['options'] = array('fragment' => 'comment-' . $this->id());
return $url;
}
}

View File

@ -48,7 +48,12 @@ use Drupal\node\NodeBCDecorator;
* "bundle" = "type"
* },
* route_base_path = "admin/structure/types/manage/{bundle}",
* permission_granularity = "bundle"
* permission_granularity = "bundle",
* links = {
* "canonical" = "/node/{node}",
* "edit-form" = "/node/{node}/edit",
* "version-history" = "/node/{node}/revisions"
* }
* )
*/
class Node extends EntityNG implements NodeInterface {

View File

@ -2164,11 +2164,18 @@ function node_page_view(EntityInterface $node) {
// of the active trail, and the link name becomes the page title.
// Thus, we must explicitly set the page title to be the node title.
drupal_set_title($node->label());
$uri = $node->uri();
// Set the node path as the canonical URL to prevent duplicate content.
drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
// Set the non-aliased path as a default shortlink.
drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
foreach ($node->uriRelationships() as $rel) {
$uri = $node->uri($rel);
// Set the node path as the canonical URL to prevent duplicate content.
drupal_add_html_head_link(array('rel' => $rel, 'href' => url($uri['path'], $uri['options'])), TRUE);
if ($rel == 'canonical') {
// Set the non-aliased canonical path as a default shortlink.
drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
}
}
return node_show($node);
}

View File

@ -135,11 +135,11 @@ class CommentAttributesTest extends CommentTestBase {
$this->drupalLogin($this->web_user);
$comment_1 = $this->saveComment($this->node->nid, $this->web_user->uid);
$comment_1_uri = url('comment/' . $comment_1->id(), array('fragment' => 'comment-' . $comment_1->id(), 'absolute' => TRUE));
$comment_1_uri = url('comment/' . $comment_1->id(), array('absolute' => TRUE));
// Posts a reply to the first comment.
$comment_2 = $this->saveComment($this->node->nid, $this->web_user->uid, NULL, $comment_1->id());
$comment_2_uri = url('comment/' . $comment_2->id(), array('fragment' => 'comment-' . $comment_2->id(), 'absolute' => TRUE));
$comment_2_uri = url('comment/' . $comment_2->id(), array('absolute' => TRUE));
$parser = new \EasyRdf_Parser_Rdfa();
$graph = new \EasyRdf_Graph();
@ -176,7 +176,8 @@ class CommentAttributesTest extends CommentTestBase {
* An array containing information about an anonymous user.
*/
function _testBasicCommentRdfaMarkup($graph, $comment, $account = array()) {
$comment_uri = url('comment/' . $comment->id(), array('fragment' => 'comment-' . $comment->id(), 'absolute' => TRUE));
$uri = $comment->uri();
$comment_uri = url($uri['path'], $uri['options'] + array('absolute' => TRUE));
// Comment type.
$expected_value = array(
@ -235,7 +236,12 @@ class CommentAttributesTest extends CommentTestBase {
else {
// The author is expected to be a blank node.
$author_uri = $graph->get($comment_uri, '<http://rdfs.org/sioc/ns#has_creator>');
$this->assertTrue($author_uri->isBnode(), 'Comment relation to author found in RDF output (sioc:has_creator).');
if ($author_uri instanceof \EasyRdf_Resource) {
$this->assertTrue($author_uri->isBnode(), 'Comment relation to author found in RDF output (sioc:has_creator) and author is blank node.');
}
else {
$this->fail('Comment relation to author found in RDF output (sioc:has_creator).');
}
}
// Author name.

View File

@ -419,7 +419,7 @@ function rdf_comment_load($comments) {
$comment->rdf_data['date'] = rdf_rdfa_attributes($comment->rdf_mapping['created'], $comment->created->value);
$comment->rdf_data['nid_uri'] = url('node/' . $comment->nid->target_id);
if ($comment->pid->target_id) {
$comment->rdf_data['pid_uri'] = url('comment/' . $comment->pid->target_id, array('fragment' => 'comment-' . $comment->pid->target_id));
$comment->rdf_data['pid_uri'] = url('comment/' . $comment->pid->target_id);
}
}
}

View File

@ -43,6 +43,10 @@ use Drupal\taxonomy\TermInterface;
* bundle_keys = {
* "bundle" = "vid"
* },
* links = {
* "canonical" = "/taxonomy/term/{taxonomy_term}",
* "edit-form" = "/taxonomy/term/{taxonomy_term}/edit"
* },
* menu_base_path = "taxonomy/term/%taxonomy_term",
* route_base_path = "admin/structure/taxonomy/manage/{bundle}",
* permission_granularity = "bundle"

View File

@ -32,12 +32,16 @@ function taxonomy_term_page(Term $term) {
drupal_set_breadcrumb($breadcrumb);
drupal_add_feed('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->label());
$uri = $term->uri();
foreach ($term->uriRelationships() as $rel) {
$uri = $term->uri($rel);
// Set the term path as the canonical URL to prevent duplicate content.
drupal_add_html_head_link(array('rel' => $rel, 'href' => url($uri['path'], $uri['options'])), TRUE);
// Set the term path as the canonical URL to prevent duplicate content.
drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
// Set the non-aliased path as a default shortlink.
drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
if ($rel == 'canonical') {
// Set the non-aliased canonical path as a default shortlink.
drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
}
}
$build['taxonomy_terms'] = taxonomy_term_view_multiple(array($term->id() => $term));
if ($nids = taxonomy_select_nodes($term->id(), TRUE, config('node.settings')->get('items_per_page'))) {

View File

@ -41,6 +41,10 @@ use Drupal\Core\Language\Language;
* entity_keys = {
* "id" = "uid",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "/user/{user}",
* "edit-form" = "/user/{user}/edit"
* }
* )
*/

View File

@ -1135,4 +1135,13 @@ class ViewUI implements ViewStorageInterface {
public function onChange($property_name) {
$this->storage->onChange($property_name);
}
/**
* {@inheritdoc}
*/
public function uriRelationships() {
return $this->storage->uriRelationships();
}
}