mustache_templates-8.x-1.0-beta4/src/MustacheTokenIterate.php
src/MustacheTokenIterate.php
<?php
namespace Drupal\mustache;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\Utility\Token;
use Drupal\mustache\Render\IterableMarkup;
/**
* Service for iterating through nested token data.
*/
class MustacheTokenIterate {
/**
* The token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* The token entity mapper.
*
* @var \Drupal\token\TokenEntityMapperInterface|null
*/
protected $tokenEntityMapper;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The MustacheTokenIterate constructor.
*
* @param \Drupal\Core\Utility\Token $token
* The token service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(Token $token, EntityTypeManagerInterface $entity_type_manager) {
$this->token = $token;
$this->tokenEntityMapper = static::getTokenEntityMapper();
$this->entityTypeManager = $entity_type_manager;
}
/**
* Get the iterable target that is addressed by a token.
*
* @param string|array $token_name
* The token name without operation prefix (i.e. not prefixed with
* "iterate.<operation>.") that addresses the desired iterable target.
* Can also be passed as array if the token name already was split up
* into its parts.
* @param array $data
* Provided token data.
* @param array $options
* Provided token options.
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
* The bubbleable metadata. This is passed to the token replacement
* implementations so that they can attach their metadata.
* @param string|bool $check_access
* (Optional) The type of access check to perform on the addressed target.
* Set to FALSE if access check should be skipped.
*
* @return \Drupal\mustache\Render\IterableMarkup
* The iterable target, starting at root level.
*/
public function getIterableTarget($token_name, array $data, array $options, BubbleableMetadata $bubbleable_metadata, $check_access = 'view') {
$token_keys = is_string($token_name) ? explode(':', $token_name) : $token_name;
$target = IterableMarkup::create();
if (!empty($token_keys)) {
$build = $this->buildRecursive($data, $options, $bubbleable_metadata, $token_keys, $check_access);
if ($build->exists()) {
$build->parent = $target;
$target[reset($token_keys)] = $build;
}
}
return $target;
}
/**
* Recusively builds up the iterable target data.
*
* @param array $data
* Provided token data.
* @param array $options
* Provided token options.
* @param \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata
* The bubbleable metadata. This is passed to the token replacement
* implementations so that they can attach their metadata.
* @param array $token_keys
* The token keys that address the desired target data.
* @param string|bool $check_access
* (Optional) The type of access check to perform on the addressed target.
* Set to FALSE if access check should be skipped.
*
* @return \Drupal\mustache\Render\IterableMarkup
* The target data. May hold empty leaves if requested target was either
* not found, or if not an iterable data type, or if the current user has
* no permissions to access the data.
*/
protected function buildRecursive(array $data, array $options, BubbleableMetadata $bubbleable_metadata, array $token_keys, $check_access = 'view') {
$target = IterableMarkup::create();
if (empty($data) && empty($token_keys)) {
return $target;
}
$key = array_shift($token_keys);
$property = reset($token_keys);
if (!isset($data[$key])) {
if (!empty($data)) {
if (strpos($key, '_') !== FALSE) {
$key_hyphened = str_replace('_', '-', $key);
if (isset($data[$key_hyphened])) {
array_unshift($token_keys, $key_hyphened);
return $this->buildRecursive($data, $options, $bubbleable_metadata, $token_keys, $check_access);
}
}
elseif (strpos($key, '-') !== FALSE) {
$key_underscore = str_replace('-', '_', $key);
if (isset($data[$key_underscore])) {
array_unshift($token_keys, $key_underscore);
return $this->buildRecursive($data, $options, $bubbleable_metadata, $token_keys, $check_access);
}
}
}
$data[$key] = NULL;
}
if (!isset($data['_token_keys'])) {
$data['_token_keys'] = $token_keys;
}
$candidate = $data[$key];
if ($candidate instanceof CacheableDependencyInterface) {
$bubbleable_metadata->addCacheableDependency($candidate);
}
if ($check_access && $candidate instanceof AccessibleInterface) {
/** @var \Drupal\Core\Access\AccessResultInterface $access_result */
$access_result = $candidate->access($check_access, NULL, TRUE);
$bubbleable_metadata->addCacheableDependency($access_result);
if (!$access_result->isAllowed()) {
return $target;
}
}
if ($property === FALSE) {
// Let us see whether a token replacement value exists for the given
// target. If there is one, use it. If not, fall back to unprocessed
// property values instead.
$token_type = $key;
$token_candidates = [];
$entity = isset($data['_entity']) ? $data['_entity'] : NULL;
if ($candidate instanceof TypedDataInterface) {
$root = $candidate->getRoot()->getValue();
if ($root instanceof CacheableDependencyInterface) {
$bubbleable_metadata->addCacheableDependency($root);
}
if ($check_access && $root instanceof AccessibleInterface) {
/** @var \Drupal\Core\Access\AccessResultInterface $access_result */
$access_result = $root->access($check_access, NULL, TRUE);
$bubbleable_metadata->addCacheableDependency($access_result);
if (!$access_result->isAllowed()) {
return $target;
}
}
if ($root instanceof EntityInterface) {
$entity = $root;
}
else {
$token_type = $candidate->getRoot()->getDataDefinition()->getDataType();
$data[$token_type] = $root;
}
// Use the data value itself as candidate.
$data[$candidate->getDataDefinition()->getDataType()] = $candidate->getValue();
}
if ($entity) {
$token_type = $this->getTokenTypeForEntity($entity);
$data[$token_type] = $entity;
}
$token_candidate = '';
if (isset($data['_field_name'])) {
$token_candidate = implode(':', array_merge([$data['_field_name']], $data['_token_keys']));
}
elseif (!empty($data['_token_keys'])) {
$token_candidate = implode(':', $data['_token_keys']);
}
if (!empty($token_candidate)) {
$token_candidates[$token_candidate] = '[' . $token_type . ':' . $token_candidate . ']';
}
if ((count($token_candidates) === 2) && strlen(end($token_candidates)) > strlen(reset($token_candidates))) {
// The token with the longer name should get first precedence.
$token_candidates = array_reverse($token_candidates, TRUE);
}
if (!empty($token_candidates)) {
$token_replacements = $this->token->generate($token_type, $token_candidates, $data, $options, $bubbleable_metadata);
foreach ($token_candidates as $token) {
if (isset($token_replacements[$token]) && (is_scalar($token_replacements[$token]) || (is_object($token_replacements[$token]) && method_exists($token_replacements[$token], '__toString')))) {
$target->value = $token_replacements[$token];
break;
}
}
}
if (empty($target->value)) {
if ($candidate instanceof TypedDataInterface) {
$property_path = $candidate->getPropertyPath();
if (empty($property_path) && empty($data['_token_keys'])) {
$target->value = $candidate->getString();
}
elseif (!empty($property_path)) {
$parts = explode('.', $property_path);
if ((!empty($data['_token_keys']) && $parts == $data['_token_keys']) || (isset($data['_field_name']) && $parts == array_merge([$data['_field_name']], $data['_token_keys']))) {
$target->value = $candidate->getString();
}
}
}
elseif (empty($data['_token_keys']) && (is_scalar($candidate) || (is_object($candidate) && method_exists($candidate, '__toString')))) {
$target->value = (string) $candidate;
}
}
// Some iterations start on a higher level (for example, the first level
// of a multidimensional array), and access would happen on subsets within
// the scope of that iteration level. Therefore, load the children too.
if (is_iterable($candidate) && empty($data['_halt_at_key_level'])) {
foreach ($candidate as $c_key => $c_value) {
$data = [
'_entity' => $entity,
$c_key => $c_value,
];
if ($c_value instanceof TypedDataInterface) {
$root = $c_value->getRoot()->getValue();
$data['_entity'] = $root instanceof EntityInterface ? $root : $entity;
$data['_token_keys'] = explode('.', $c_value->getPropertyPath());
}
$build = $this->buildRecursive($data, $options, $bubbleable_metadata, [$c_key], $check_access);
if ($build->exists()) {
$build->parent = $target;
$target[$c_key] = $build;
}
}
}
return $target;
}
if ($candidate instanceof FieldableEntityInterface) {
$data['_entity'] = $candidate;
$data['_token_keys'] = $token_keys;
// From here on, work with the entity wrapped as typed data object.
$candidate = $candidate->getTypedData();
// If any, unset the previously stored field name. Looking at an entity
// at this point indicates that we either are at the beginning, or
// we step down one level deeper, that "resets" our scope.
unset($data['_field_name']);
}
if ($candidate instanceof FieldItemListInterface) {
$data['_entity'] = $candidate->getEntity();
$data['_field_name'] = $candidate->getFieldDefinition()->getName();
$data['_token_keys'] = $token_keys;
}
if ($candidate instanceof FieldItemInterface) {
$data['_entity'] = $candidate->getEntity();
$data['_field_name'] = $candidate->getFieldDefinition()->getName();
$data['_token_keys'] = array_merge([$key], $token_keys);
}
if ($candidate instanceof ComplexDataInterface) {
$properties = $candidate->getProperties(TRUE);
if (isset($properties[$property])) {
unset($data[$key]);
$data[$property] = $candidate->get($property);
$build = $this->buildRecursive($data, $options, $bubbleable_metadata, $token_keys, $check_access);
if ($build->exists()) {
$build->parent = $target;
$target[$property] = $build;
return $target;
}
}
if ($candidate instanceof EntityReferenceItem) {
$target_type = $candidate->getFieldDefinition()->getSetting('target_type');
if ($property == $target_type) {
$item_value = $candidate->getValue();
if (isset($item_value['target_id']) || isset($item_value['entity'])) {
unset($data[$key]);
$data[$property] = !empty($item_value['entity']) ? $item_value['entity'] : $this->entityTypeManager->getStorage($target_type)->load($item_value['target_id']);
$build = $this->buildRecursive($data, $options, $bubbleable_metadata, $token_keys, $check_access);
if ($build->exists()) {
$build->parent = $target;
$target[$property] = $build;
return $target;
}
}
}
}
}
if (is_array($candidate) || $candidate instanceof ListInterface) {
$target = $this->buildRecursive([
'_token_keys' => [$key],
'_halt_at_key_level' => TRUE,
$key => $candidate,
], $options, $bubbleable_metadata, [$key], $check_access);
$data['_token_keys'] = $token_keys;
$fixed_delta = is_numeric($property) && (int) $property == $property;
if ($fixed_delta) {
array_shift($token_keys);
}
elseif ($candidate instanceof EntityReferenceFieldItemListInterface) {
$target_type = $candidate->getFieldDefinition()->getSetting('target_type');
if ($property == $target_type) {
array_shift($token_keys);
$candidate = $candidate->referencedEntities();
}
}
foreach ($candidate as $delta_item => $item) {
if (!$fixed_delta || $delta_item == $property) {
array_unshift($token_keys, $delta_item);
$data[$delta_item] = $item;
$build = $this->buildRecursive($data, $options, $bubbleable_metadata, $token_keys, $check_access);
if ($build->exists()) {
$build->parent = $target;
$target[$delta_item] = $build;
}
array_shift($token_keys);
}
}
return $target;
}
if (is_object($candidate)) {
foreach (['get' . ucfirst($property), $property] as $method) {
if (method_exists($candidate, $method)) {
$data[$property] = call_user_func([$candidate, $method]);
break;
}
}
if ($candidate instanceof ConfigEntityInterface) {
$property_values = [];
if ($property === $candidate->getEntityTypeId()) {
array_shift($token_keys);
}
$property_name = implode('.', $token_keys);
if ($value = $candidate->get($property_name)) {
NestedArray::setValue($property_values, $token_keys, $value, TRUE);
$target[$property] = IterableMarkup::create($property_values, NULL, $target);
return $target;
}
}
if (!isset($data[$property]) && isset($candidate->$property)) {
$data[$property] = $candidate->$property;
}
if (isset($data[$property])) {
$build = $this->buildRecursive($data, $options, $bubbleable_metadata, $token_keys, $check_access);
if ($build->exists()) {
$build->parent = $target;
$target[$property] = $build;
return $target;
}
}
}
if (!isset($target[$property])) {
$property_values = [];
while (TRUE) {
// Pass in the first key without any subsequent token keys, to indicate
// that the token replacement is the last chance to get a value.
$data['_token_keys'] = $token_keys;
$build = $this->buildRecursive(['_halt_at_key_level' => TRUE] + $data, $options, $bubbleable_metadata, [$key], $check_access);
array_shift($token_keys);
if ($build->exists()) {
NestedArray::setValue($property_values, $token_keys, $build, TRUE);
$target[$property] = $property_values instanceof IterableMarkup ? $property_values : IterableMarkup::create($property_values, NULL, $target);
break;
}
if (empty($token_keys)) {
break;
}
}
}
return $target;
}
/**
* Get the token entity mapper.
*
* @return \Drupal\token\TokenEntityMapperInterface|null
* The token type mapper, or NULL if not available.
*/
protected static function getTokenEntityMapper() {
return \Drupal::hasService('token.entity_mapper') ? \Drupal::service('token.entity_mapper') : NULL;
}
/**
* Get the token type for the given entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return string
* The token type.
*/
protected function getTokenTypeForEntity(EntityInterface $entity) {
if ($this->tokenEntityMapper) {
$token_type = $this->tokenEntityMapper->getTokenTypeForEntityType($entity->getEntityTypeId(), TRUE);
}
else {
$definition = $this->entityTypeManager->getDefinition($entity->getEntityTypeId());
$token_type = $definition->get('token_type') ?: $entity->getEntityTypeId();
}
return $token_type;
}
}
