diff --git a/tests/src/Kernel/FieldTest.php b/tests/src/Kernel/FieldTest.php index d418088..bb28dfd 100644 --- a/tests/src/Kernel/FieldTest.php +++ b/tests/src/Kernel/FieldTest.php @@ -8,10 +8,15 @@ use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\Core\Render\Markup; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; use Drupal\filter\Entity\FilterFormat; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\contact\Entity\Message; +use Drupal\Component\Utility\Html; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\taxonomy\Tests\TaxonomyTestTrait; +use Drupal\Core\Render\BubbleableMetadata; /** * Tests field tokens. @@ -20,17 +25,28 @@ use Drupal\contact\Entity\Message; */ class FieldTest extends KernelTestBase { + use TaxonomyTestTrait; + use EntityReferenceTestTrait; + /** * @var \Drupal\filter\FilterFormatInterface */ protected $testFormat; + + /** + * Vocabulary for testing chained token support. + * + * @var \Drupal\taxonomy\VocabularyInterface + */ + protected $vocabulary; + /** * Modules to enable. * * @var array */ - public static $modules = ['node', 'text', 'field', 'filter', 'contact', 'options']; + public static $modules = ['node', 'text', 'field', 'filter', 'contact', 'options', 'taxonomy']; /** * {@inheritdoc} @@ -40,6 +56,7 @@ class FieldTest extends KernelTestBase { $this->installEntitySchema('user'); $this->installEntitySchema('node'); + $this->installEntitySchema('taxonomy_term'); // Create the article content type with a text field. $node_type = NodeType::create([ @@ -107,6 +124,40 @@ class FieldTest extends KernelTestBase { 'entity_type' => 'node', 'bundle' => 'article', ])->save(); + + + // Add a node reference field. + $this->createEntityReferenceField('node', 'article', 'test_reference', 'Test reference', 'node'); + + // Add a taxonomy term reference field. + $this->vocabulary = $this->createVocabulary(); + $this->createEntityReferenceField( + 'node', + 'article', + 'test_term_reference', + 'Test term reference', + 'taxonomy_term', + 'default:taxonomy_term', + [ + 'target_bundles' => [ + $this->vocabulary->id() => $this->vocabulary->id(), + ], + ] + ); + + // Add a field to the vocabulary. + $storage = FieldStorageConfig::create([ + 'field_name' => 'term_field', + 'entity_type' => 'taxonomy_term', + 'type' => 'text', + ]); + $storage->save(); + $field = FieldConfig::create([ + 'field_name' => 'term_field', + 'entity_type' => 'taxonomy_term', + 'bundle' => $this->vocabulary->id(), + ]); + $field->save(); } /** @@ -316,4 +367,118 @@ class FieldTest extends KernelTestBase { // Verify that node entity type doesn't have a uid token. $this->assertNull($tokenService->getTokenInfo('node', 'uid')); } + + /* + * Tests chaining tokens. + */ + public function testChainedTokens() { + $reference = Node::create([ + 'title' => 'Test node to reference', + 'type' => 'article', + 'test_field' => [ + 'value' => 'foo', + 'format' => $this->testFormat->id(), + ] + ]); + $reference->save(); + $term_reference_field_value = $this->randomString(); + $term_reference = $this->createTerm($this->vocabulary, [ + 'name' => 'Term to reference', + 'term_field' => [ + 'value' => $term_reference_field_value, + 'format' => $this->testFormat->id(), + ], + ]); + $entity = Node::create([ + 'title' => 'Test entity reference', + 'type' => 'article', + 'test_reference' => ['target_id' => $reference->id()], + 'test_term_reference' => ['target_id' => $term_reference->id()], + ]); + $entity->save(); + + $this->assertTokens('node', ['node' => $entity], [ + 'test_reference:entity:title' => Markup::create('Test node to reference'), + 'test_reference:entity:test_field' => Markup::create('foo'), + 'test_term_reference:entity:term_field' => Html::escape($term_reference_field_value), + 'test_reference:target_id' => $reference->id(), + 'test_term_reference:target_id' => $term_reference->id(), + 'test_term_reference:entity:url:path' => '/' . $term_reference->toUrl('canonical')->getInternalPath(), + ]); + + // Test the :entity tokens. + $input = $this->mapTokenNames('node', ['test_term_reference:entity']); + $bubbleable_metadata = new BubbleableMetadata(); + $replacement = \Drupal::token()->generate('node', $input, ['node' => $entity], [], $bubbleable_metadata); + $replacement_term_field_value = $replacement['[node:test_term_reference:entity]']->get('term_field')->getValue()[0]['value']; + $this->assertTrue($replacement_term_field_value == $term_reference_field_value); + + // Test some non existent tokens. + $this->assertNoTokens('node', ['node' => $entity], [ + 'test_reference:1:title', + 'test_term_reference:do_not_exists', + 'test_term_reference:do:not:exists', + 'test_term_reference:do_not_exists:0', + ]); + } + + /** + * Tests support for cardinality > 1 chaining. + */ + public function testMultivalueCardinality() { + /** @var \Drupal\field\FieldStorageConfigInterface $storage */ + $storage = FieldStorageConfig::load('node.test_term_reference'); + $storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $storage->save(); + + // Add a few terms. + $terms = []; + $terms_value = []; + foreach (range(1, 3) as $i) { + $terms_value[$i] = $this->randomString(); + $terms[$i] = $this->createTerm($this->vocabulary, [ + 'name' => $this->randomString(), + 'term_field' => [ + 'value' => $terms_value[$i], + 'format' => $this->testFormat->id(), + ], + ]); + } + + $entity = Node::create([ + 'title' => 'Test multivalue chained tokens', + 'type' => 'article', + 'test_term_reference' => [ + ['target_id' => $terms[1]->id()], + ['target_id' => $terms[2]->id()], + ['target_id' => $terms[3]->id()], + ], + ]); + $entity->save(); + + $this->assertTokens('node', ['node' => $entity], [ + 'test_term_reference:0:entity:term_field' => Html::escape($terms[1]->term_field->value), + 'test_term_reference:1:entity:term_field' => Html::escape($terms[2]->term_field->value), + 'test_term_reference:2:entity:term_field' => Html::escape($terms[3]->term_field->value), + 'test_term_reference:0:target_id' => $terms[1]->id(), + 'test_term_reference:1:target_id' => $terms[2]->id(), + 'test_term_reference:2:target_id' => $terms[3]->id(), + ]); + + // Test some non existent tokens. + $this->assertNoTokens('node', ['node' => $entity], [ + 'test_term_reference:3:term_field', + 'test_term_reference:0:do_not_exists', + 'test_term_reference:1:do:not:exists', + 'test_term_reference:1:2:do_not_exists', + ]); + + // Test the :entity tokens. + $input = $this->mapTokenNames('node', ['test_term_reference:0:entity']); + $bubbleable_metadata = new BubbleableMetadata(); + $replacement = \Drupal::token()->generate('node', $input, ['node' => $entity], [], $bubbleable_metadata); + $replacement_term_field_value = $replacement['[node:test_term_reference:0:entity]']->get('term_field')->getValue()[0]['value']; + $this->assertTrue($replacement_term_field_value == $terms_value[1]); + } + } diff --git a/token.tokens.inc b/token.tokens.inc index 68e2755..15f1e76 100644 --- a/token.tokens.inc +++ b/token.tokens.inc @@ -5,6 +5,8 @@ * Token callbacks for the token module. */ use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; @@ -13,6 +15,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Xss; +use Drupal\Core\TypedData\DataReferenceDefinitionInterface; use Drupal\Core\Url; use Drupal\field\FieldConfigInterface; use Drupal\field\FieldStorageConfigInterface; @@ -24,6 +27,7 @@ use Drupal\Core\TypedData\PrimitiveInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; + /** * Implements hook_token_info_alter(). */ @@ -1276,6 +1280,16 @@ function field_token_info_alter(&$info) { 'module' => 'token', ]; } + elseif (($property_definition instanceof DataReferenceDefinitionInterface) && ($property_definition->getTargetDefinition() instanceof EntityDataDefinitionInterface)) { + $referenced_entity_type = $property_definition->getTargetDefinition()->getEntityTypeId(); + $referenced_token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($referenced_entity_type); + $info['tokens'][$field_token_name][$property] = [ + 'name' => 'Referenced entity', + 'description' => t('The entity referenced by this entity reference field.'), + 'module' => 'token', + 'type' => $referenced_token_type, + ]; + } } } } @@ -1345,7 +1359,7 @@ function field_tokens($type, $tokens, array $data = array(), array $options = ar list($field_name, $delta) = explode(':', $name, 2); if (!is_numeric($delta)) { unset($delta); - } + } $token_name = $field_name; } if (_token_module($data['token_type'], $token_name) != 'token') { @@ -1395,12 +1409,14 @@ function field_tokens($type, $tokens, array $data = array(), array $options = ar } // Handle [entity:field_name:value] and [entity:field_name:0:value] // tokens. - else if ($field_tokens = \Drupal::token()->findWithPrefix($tokens, $field_name)) { + else { + $field_tokens = \Drupal::token()->findWithPrefix($tokens, $field_name); $property_token_data = [ 'field_property' => TRUE, $data['entity_type'] . '-' . $field_name => $data['entity']->$field_name, 'field_name' => $data['entity_type'] . '-' . $field_name, ]; + $replacements += \Drupal::token()->generate($field_name, $field_tokens, $property_token_data, $options, $bubbleable_metadata); } } @@ -1411,16 +1427,43 @@ function field_tokens($type, $tokens, array $data = array(), array $options = ar elseif (!empty($data['field_property'])) { foreach ($tokens as $name => $original) { // Handle [entity:field_name:0:value] tokens. + $filtered_tokens = $tokens; if (strpos($name, ':') !== FALSE) { list($delta, $rest) = explode(':', $name, 2); - $name = is_numeric($delta) ? $rest : $name; + if (is_numeric($delta)) { + $name = $rest; + // Pre-filter the tokens to select those with the correct delta. + $filtered_tokens = \Drupal::token()->findWithPrefix($tokens, $delta); + } + else { + $delta = 0; + } } // [entity:field_name:value] is treated as [entity:field_name:0:value]. else { $delta = 0; } - if (isset($data[$data['field_name']][$delta]) && isset($data[$data['field_name']][$delta]->$name)) { - $replacements[$original] = $data[$data['field_name']][$delta]->$name; + + if (isset($data[$data['field_name']][$delta])) { + $field_item = $data[$data['field_name']][$delta]; + } + else { + // The field has no such delta. + continue; + } + + if (strpos($name, ':') !== FALSE) { + list($property_name, $rest) = explode(':', $name, 2); + if (isset($field_item->$property_name) && ($field_item->$property_name instanceof FieldableEntityInterface)) { + // Entity reference field. + $entity = $field_item->$property_name; + $field_tokens = \Drupal::token()->findWithPrefix($filtered_tokens, $property_name); + $token_type = \Drupal::service('token.entity_mapper')->getTokenTypeForEntityType($entity->getEntityTypeId(), TRUE); + $replacements += \Drupal::token()->generate($token_type, $field_tokens, [$token_type => $entity], $options, $bubbleable_metadata); + } + } + elseif (isset($field_item->$name)) { + $replacements[$original] = $field_item->$name; } } }