Issue #1970360 by Crell, linclark, dawehner, YesCT, matt2000: Entities should define URI templates and standard links.
parent
4b23474b69
commit
342f132b35
|
@ -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
|
||||
|
|
|
@ -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().
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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().
|
||||
*/
|
||||
|
|
|
@ -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']));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'))) {
|
||||
|
|
|
@ -41,6 +41,10 @@ use Drupal\Core\Language\Language;
|
|||
* entity_keys = {
|
||||
* "id" = "uid",
|
||||
* "uuid" = "uuid"
|
||||
* },
|
||||
* links = {
|
||||
* "canonical" = "/user/{user}",
|
||||
* "edit-form" = "/user/{user}/edit"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue