mustache_templates-8.x-1.0-beta4/src/Plugin/mustache/Magic/Introspection.php
src/Plugin/mustache/Magic/Introspection.php
<?php
namespace Drupal\mustache\Plugin\mustache\Magic;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\Url;
use Drupal\mustache\Magic\LazyMagic;
use Drupal\mustache\Plugin\MustacheMagic;
use Drupal\mustache\Plugin\MustacheMagicInterface;
use Drupal\mustache\Render\IterableMarkup;
use Drupal\mustache\Render\Markup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Introspect available resources (keys, partials) within Mustache templates.
*
* Usage:
*
* You can use the "magical" {{show.<resource>}} variable anywhere Mustache
* template syntax is allowed. For example, the Mustache input filter plugin or
* - when "mustache_token" is installed - any input that supports tokens.
*
* When that variable is used in a template, the contents will not be printed
* out within the rendered template, but instead printed out as a message
* on the currently opened page. Printed messages are only shown for users who
* have the "view mustache debug messages" permission.
*
* @code
* {{show.keys}}
* {{show.partials}}
* @endcode
*
* @MustacheMagic(
* id = "show",
* label = @Translation("Introspection"),
* description = @Translation("Use the <b>{{show.keys}}</b> variable to print out available data keys, and <b>{{show.partials}}</b> to print out a list of available reusable templates. Requires the <em>"View Mustache debug messages"</em> permission."),
* lambda = FALSE
* )
*/
class Introspection extends MustacheMagic {
/**
* The module handler, if any.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|null
*/
protected $moduleHandler;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
if (isset($instance->messenger)) {
$instance->moduleHandler = $container->get('module_handler');
}
return $instance;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
$trusted = parent::trustedCallbacks();
$trusted[] = 'keys';
$trusted[] = 'partials';
return $trusted;
}
/**
* Prints out a list of available data keys.
*
* @return string
* An empty string is returned, so that rendered template output is not
* affected when using a {{show}} variable.
*/
public function keys() {
if (!$this->messenger) {
$this->logger->warning(sprintf("A {{show}} variable is set for debugging purposes in a Mustache template (%s). Remove that variable from the template when it is not needed anymore.", $this->element['#template']['name']));
return $this->__toString();
}
$with_tokens = $this->withTokens();
$keys = [];
$data = $this->data;
if ($data instanceof IterableMarkup) {
$items = isset($data->items) ? $data->items : [];
if (!empty($data->value) && (empty($items) || !isset($items['value']))) {
$items['value'] = $data->value;
}
$data = $items;
}
if (is_iterable($data)) {
foreach ($data as $key => $value) {
if ($key === '') {
continue;
}
$this->buildKeysInfo($keys, $value, $key);
}
}
$endpoint = NULL;
if (isset($this->element['#endpoint'])) {
$endpoint = $this->element['#endpoint'] instanceof Url ? $this->element['#endpoint']->toString() : (string) $this->element['#endpoint'];
}
if (isset($endpoint, $this->element['#success'])) {
$received = FALSE;
if ($this->element['#success']) {
/** @var \Drupal\mustache\MustacheHttp $http */
$http = \Drupal::service('mustache.http');
$received = $http->getData($this->element['#endpoint']);
}
if ($received === FALSE) {
$this->messenger->addError(t('Could not fetch data from Json URL endpoint @url. Is that endpoint reachable?', ['@url' => $endpoint]));
}
elseif (!is_array($received)) {
$this->messenger->addError(t('Unexpected data type received from Json endpoint @url. Mustache Templates always expect an array, got @type.', [
'@url' => $endpoint,
'@type' => gettype($received),
]));
}
else {
$this->messenger->addMessage(t('Successfully received data (@type, containing @num items) from endpoint @url.', [
'@url' => $endpoint,
'@type' => gettype($received),
'@num' => count($received),
]));
$received_keys = [];
foreach ($received as $r_k => $r_v) {
$this->buildKeysInfo($received_keys, $r_v, $r_k);
}
$list = Markup::create('<ul><li>' . implode('</li><li>', $received_keys) . '</li></ul>');
$this->messenger->addMessage(t('That endpoint returned the following data keys:<br />@list', ['@list' => $list]));
}
}
if ($with_tokens) {
$token_settings = &$this->element['#with_tokens'];
$token_data = isset($token_settings['data']) ? $token_settings['data'] : [];
/** @var \Drupal\mustache\MustacheTokenProcessor $token_processor */
$token_processor = \Drupal::service('mustache.token_processor');
$token_processor->processData($token_data);
if (!empty($token_data)) {
$token_info = \Drupal::token()->getInfo();
foreach ($token_info['tokens'] as $type => $tokens) {
if (isset($token_data[$type])) {
$this->buildKeysInfo($keys, $token_data[$type], $type);
foreach ($tokens as $key => $token) {
$key = $type . '.' . str_replace(':', '.', $key);
$keys[$key] = '<b>{{' . $key . '}}</b> <em>' . $token['name'] . '</em>';
}
}
}
}
}
foreach (array_keys($keys) as $key) {
$key_parts = explode('.', $key);
if (is_numeric(end($key_parts))) {
// Encourage the usage concrete property names. This also lowers the
// possibly huge amount of suggestions, at least for a little bit.
unset($keys[$key]);
}
}
if (!empty($keys)) {
$list = Markup::create('<ul><li>' . implode('</li><li>', $keys) . '</li></ul>');
if (isset($received_keys) && $with_tokens) {
$this->messenger->addMessage(t('Further available:<br />@list', ['@list' => $list]));
}
else {
$this->messenger->addMessage(t('All available data keys:<br />@list', ['@list' => $list]));
}
if ($this->moduleHandler->moduleExists('mustache_token')) {
$this->messenger->addMessage(t('If you want to iterate through any of the above mentioned keys, you may prefix the key with "iterate.<operation>.*", for example {{iterate.loop.node.field_mytodolist}}. Available <b><operation></b> keys:<br /><ul><li>{{iterate.<b>loop</b>.*}}</li><li>{{iterate.<b>explode</b>.*}}</li><li>{{iterate.<b>first</b>.*}}</li><li>{{iterate.<b>last</b>.*}}</li><li>{{iterate.<b>count</b>.*}}</li><li>{{iterate.<b>empty</b>.*}}</li><li>{{iterate.<b>sum</b>.*}}</li><li>{{iterate.<b>min</b>.*}}</li><li>{{iterate.<b>max</b>.*}}</li><li>{{iterate.<b>avg</b>.*}}</li></ul>'));
}
$this->messenger->addMessage(t('It is sometimes possible to access previous data with {{previous.<b>*</b>}}. Also conditions are possible regards previous data, for example <em>{{#if.node.field_mytodolist.greaterthan.previous}}Even more to do...{{/if.node.field_mytodolist.greaterthan.previous}}</em>. This mechanic is not reliable though and should not be used for business critical data.'));
}
elseif (!$with_tokens) {
$this->messenger->addWarning(t('No explicitly set data keys are available.'));
}
if ($with_tokens) {
if ($this->moduleHandler->moduleExists('token') && $this->moduleHandler->moduleExists('help')) {
$this->messenger->addMessage(t('<a href="/admin/help/token" target="_blank">Global tokens</a> may be available for you. Available tokens like "[site:name]" may be used as Mustache variables like "{{site.name}}".'));
}
else {
$this->messenger->addMessage(t('Global tokens may be available for you. Available tokens like "[site:name]" may be used as Mustache variables like "{{site.name}}".'));
}
}
$this->messenger->addMessage(t('When form binding is enabled, you may access submitted form values with {{form.<b><name></b>}}'));
$this->messenger->addWarning(t('Above messages are printed because a {{show}} variable has been set in a Mustache template. Remove that variable from the template so that these messages will not show up anymore.'));
return "";
}
/**
* Prints out a list of reusable templates.
*
* @return string
* An empty string is returned, so that rendered template output is not
* affected when using a {{show}} variable.
*/
public function partials() {
if (!$this->messenger) {
$this->logger->warning(sprintf("A {{show}} variable is set for debugging purposes in a Mustache template (%s). Remove that variable from the template when it is not needed anymore.", $this->element['#template']['name']));
return $this->__toString();
}
/** @var \Drupal\mustache\MustacheTemplates */
$templates = \Drupal::service('mustache.templates');
$available = [];
$all_templates = array_keys($templates->findAll());
$num = count($all_templates);
foreach ($all_templates as $name) {
if ($num < 100) {
$available[$name] = '<b>' . $name . '</b> :<br />' . trim(nl2br(htmlentities($templates->getContent($name))));
$len = strlen($available[$name]);
if ($len > 400) {
$available[$name] = function_exists('text_summary') ? text_summary($available[$name], NULL, 400) : substr($available[$name], 0, 400);
$available[$name] .= '<br/><em><b>... (+ ' . ($len - 400) . ' more characters)</b></em>';
}
}
else {
$available[$name] = '<b>' . $name . '</b>';
}
}
$list = Markup::create('<ul><li>' . implode('</li><li>', $available) . '</li></ul>');
if (empty($available)) {
$this->messenger->addWarning(t('No reusable templates were found.'));
}
else {
$this->messenger->addMessage(t('You can reuse templates as partials, for example <b>{{>template_name}}</b>.'));
$this->messenger->addMessage(t('Available templates (<b>@num</b> total, names are shown first and are <b>bold</b>):<br/>@list', ['@num' => $num, '@list' => $list]));
}
$this->messenger->addWarning(t('Above messages are printed because a {{show}} variable has been set in a Mustache template. Remove that variable from the template so that these messages will not show up anymore.'));
return "";
}
/**
* Recursively builds up the keys info for a given value.
*
* @param array &$keys
* Already added keys.
* @param mixed $data
* The currently scoped data value.
* @param mixed $parent_key
* The parent key.
* @param int $level
* (Optional) The current nesting level. This method aborts at level 3.
* @param array $additional_info
* (Optional) Additional info.
*/
protected function buildKeysInfo(array &$keys, $data, $parent_key, $level = 0, array $additional_info = []) {
if ($level > 3) {
return;
}
$level++;
if ($data instanceof EntityReferenceFieldItemListInterface) {
$target_type = $data->getFieldDefinition()->getSetting('target_type');
$entities = $data->referencedEntities();
$count = count($entities);
$cardinality = (int) $data->getFieldDefinition()->getFieldStorageDefinition()->getCardinality();
$name = $data->getName();
foreach ($entities as $delta => $entity) {
$additional_info = [];
if ($cardinality !== 1) {
$additional_info = [
t('number of items within <em>@name</em>: <b>@num</b>', [
'@num' => $count,
'@name' => $name,
]),
];
}
$this->buildKeysInfo($keys, $entity, $parent_key . '.' . $target_type, $level, $additional_info);
if ($cardinality !== 1) {
$additional_info = [];
if ($count > 1) {
$additional_info = [t('available <delta> keys: <b>@keys</b>', ['@keys' => implode(',', array_keys($entities))])];
$this->buildKeysInfo($keys, $entity, $parent_key . '.<delta>' . '.' . $target_type, $level, $additional_info);
}
else {
$this->buildKeysInfo($keys, $entity, $parent_key . '.' . $delta . '.' . $target_type, $level, $additional_info);
}
}
}
}
elseif ($data instanceof EntityReferenceItem) {
$target_type = $data->getFieldDefinition()->getSetting('target_type');
$item_value = $data->getValue();
if (isset($item_value['target_id']) || isset($item_value['entity'])) {
$entity = !empty($item_value['entity']) ? $item_value['entity'] : \Drupal::entityTypeManager()->getStorage($target_type)->load($item_value['target_id']);
if ($entity) {
$this->buildKeysInfo($keys, $entity, $parent_key . '.' . $target_type, $level);
}
}
}
if ($data instanceof ListInterface) {
$count = $data->count();
$cardinality = $data instanceof FieldItemListInterface ? $data->getFieldDefinition()->getFieldStorageDefinition()->getCardinality() : $count;
$name = $data->getName();
foreach ($data as $delta => $item) {
$additional_info = [];
if ($cardinality !== 1) {
$additional_info = [
t('number of items within <em>@name</em>: <b>@num</b>', [
'@num' => $count,
'@name' => $name,
]),
];
}
$this->buildKeysInfo($keys, $item, $parent_key, $level, $additional_info);
if ($cardinality !== 1) {
$additional_info = [];
if ($count > 1) {
$additional_info = [t('available <b><delta></b> keys: <b>@keys</b>', ['@keys' => implode(',', array_keys($data->getValue()))])];
$this->buildKeysInfo($keys, $item, $parent_key . '.<delta>', $level, $additional_info);
}
else {
$this->buildKeysInfo($keys, $item, $parent_key . '.' . $delta, $level, $additional_info);
}
}
return;
}
}
$is_iterable = is_iterable($data);
if (!$is_iterable) {
$data = [$parent_key => $data];
}
foreach ($data as $key => $value) {
$key = $is_iterable ? implode('.', [$parent_key, $key]) : $key;
if ($value instanceof IterableMarkup) {
// Exclude "background noise" that comes from our beloved
// MustacheTokenProcessor, which generates a stringable data tree.
continue;
}
if (($value instanceof AccessibleInterface) && !$value->access('view')) {
continue;
}
if ($value instanceof FieldableEntityInterface) {
// From here on, work with the entity wrapped as typed data object.
$value = $value->getTypedData();
}
if ($value instanceof TypedDataInterface) {
$data_definition = $value->getDataDefinition();
if (!($data_desc = (string) $data_definition->getLabel())) {
$data_type = explode(':', $data_definition->getDataType());
$data_desc = end($data_type);
}
$data_desc = !empty($data_desc) ? [$data_desc] : [];
$keys[$key] = '<b>{{' . $key . '}}</b> <em>' . implode(', ', $data_desc) . '</em>';
}
elseif ($value instanceof ConfigEntityInterface) {
// Some config objects don't give any insight about their properties,
// thus work with the array representation from here instead.
$keys[$key] = '<b>{{' . $key . '}}</b> <em>' . $value->getEntityType()->getLabel() . ' (' . t('not a scalar value') . ')</em>';
$value = $value->toArray();
}
elseif ($value instanceof LazyMagic || $value instanceof MustacheMagicInterface) {
$plugin = $value instanceof LazyMagic ? $value->plugin() : $value;
$definition = $plugin->getPluginDefinition();
$lambda = !empty($definition['lambda']) ? '#' : '';
$keys[$key] = '<b>{{' . $lambda . $key . '}}</b> <em>' . $definition['label'] . ': ' . $definition['description'] . '</em>';
}
elseif (is_object($value)) {
if (!method_exists($value, '__toString')) {
$keys[$key] = '<b>{{' . $key . '}}</b> <em>' . gettype($value) . ' (' . t('not a scalar value') . ')</em>';
}
else {
$keys[$key] = '<b>{{' . $key . '}}</b> <em>' . gettype($value) . '</em>';
}
$value_array = [];
foreach (get_class_methods($value) as $method) {
$reflection = new \ReflectionMethod($value, $method);
if (!$reflection->getNumberOfRequiredParameters()) {
$value_array[$method] = $value->$method();
}
}
$value_array += array_merge(get_class_vars(get_class($value)), get_object_vars($value));
$value = $value_array;
}
else {
$keys[$key] = '<b>{{' . $key . '}}</b> <em>' . gettype($value) . '</em>';
}
if (!empty($additional_info)) {
$keys[$key] .= ', ' . implode(', ', $additional_info);
}
if (is_iterable($value) || $value instanceof TypedDataInterface) {
$this->buildKeysInfo($keys, $value, $key, $level, $additional_info);
}
}
}
}
