mustache_templates-8.x-1.0-beta4/src/Render/IterableMarkup.php
src/Render/IterableMarkup.php
<?php
namespace Drupal\mustache\Render;
use Drupal\Component\Render\MarkupInterface;
/**
* Passes multiple safe strings through the render system.
*
* This iterable markup allows for both concatenating the contained items,
* but also define a value that may be used instead of concatenating the items.
* For nesting iterable markup items, a child markup item may reference to its
* parent item through its "parent" property. All listed properties below are
* directly accessible from outside and can be manipulated.
*
* This object should only be constructed with known safe strings. If there is
* any risk that the string contains user-entered data that has not been
* filtered first, it must not be used.
*
* @internal
* This class must only be used inside the mustache_templates project.
*/
final class IterableMarkup implements MarkupInterface, \Countable, \IteratorAggregate, \ArrayAccess {
/**
* The iterable items.
*
* @var array
*/
private $items = [];
/**
* Whether the array of iterable items is primarily associative or not.
*
* @var bool
*/
private $isAssoc = TRUE;
/**
* The string value to use instead of concatenating the items.
*
* @var mixed
*/
private $value;
/**
* The parent.
*
* @var mixed
*/
private $parent;
/**
* The separator to use when concatenating as string.
*
* @var string
*/
private $separator = ', ';
/**
* The position of this item within its parent's list of items.
*
* Position counting starts at value 1.
*
* @var int
*/
private $position = 1;
/**
* Whether this item is the first one within its parent's list of items.
*
* @var bool
*/
private $first = TRUE;
/**
* Whether this item is the last one within its parent's list of items.
*
* @var bool
*/
private $last = TRUE;
/**
* Create an iterable markup object.
*
* @param array $items
* The items to iterate on.
* @param mixed $value
* (Optional) The string value to use instead of concatenating the items.
* @param mixed $parent
* (Optional) The parent, usually another IterableMarkup instance.
* @param string $separator
* (Optional) The separator to use on string concatenation.
* @param bool $wrap_scalars
* (Optional) Whether scalar values should be wrapped too. Default is TRUE.
* @param bool $wrap_arrays
* (Optional) Whether arrays should be wrapped too. Default is TRUE.
*
* @return static
* The iterable markup object.
*/
public static function create(array $items = [], $value = NULL, $parent = NULL, $separator = ', ', $wrap_scalars = TRUE, $wrap_arrays = TRUE) {
$instance = new static();
foreach ($items as $key => $item) {
if (is_int($key)) {
$instance->isAssoc = FALSE;
}
if ($wrap_arrays && is_array($item)) {
$instance->items[$key] = static::create($item, NULL, $instance, $separator, $wrap_scalars, $wrap_arrays);
}
elseif ($wrap_scalars && (is_scalar($item) || is_null($item))) {
$instance->items[$key] = static::create([], $item, $instance, $separator, $wrap_scalars, $wrap_arrays);
}
else {
$instance->items[$key] = $item;
}
}
$instance->value = $value;
$instance->parent = $parent;
$instance->separator = $separator;
return $instance;
}
/**
* {@inheritdoc}
*/
public function __toString() {
if ($this->value !== NULL && $this->value !== $this) {
return (string) $this->value;
}
$items = [];
foreach ($this as $item) {
if ($item === $this && !empty($this->items)) {
// This needs a special handling, to prevent infinite recursion.
$items = array_merge($items, $this->items);
}
else {
$items[] = $item;
}
}
return implode($this->separator, $items);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
if ($this->empty()) {
return [];
}
$values = [
'value' => $this->__toString(),
'isAssoc' => $this->isAssoc,
'items' => [],
'separator' => $this->separator,
'position' => $this->position,
'first' => $this->first,
'last' => $this->last,
];
foreach ($this->items as $i_key => $i_val) {
if ($i_val instanceof static) {
$i_val = $i_val->jsonSerialize();
}
elseif (is_object($i_val)) {
if (method_exists($i_val, '__toString')) {
$i_val = (string) $i_val;
}
elseif (method_exists($i_val, 'getString')) {
$i_val = $i_val->getString();
}
}
$values['items'][$i_key] = $i_val;
}
return $values;
}
/**
* {@inheritdoc}
*/
public function count(): int {
if ($this->items) {
$count = 0;
foreach ($this->items as $item) {
$count += $item instanceof static ? (int) $item->exists() : (int) ($item !== '');
}
if ($count) {
return $count;
}
}
if ($this->value !== NULL && $this->value !== $this) {
return is_numeric($this->value) ? (int) $this->value : strlen((string) $this->value);
}
return 0;
}
/**
* Whether the whole markup object is empty.
*
* @return bool
* Returns TRUE when it is empty.
*/
public function empty() {
return !$this->count();
}
/**
* Whether the whole markup object contains at least one value.
*
* @return bool
* Returns TRUE when it exists.
*/
public function exists() {
return (($this->value !== NULL) && ($this->value !== $this)) || !$this->empty();
}
/**
* {@inheritdoc}
*/
public function getIterator(): \Traversable {
if (!empty($this->items)) {
if (!$this->isAssoc) {
// As this item list contains numeric indices, make it a complete
// numeric list by filtering out any keys that are not numeric.
$target = [];
$i = 0;
while (array_key_exists($i, $this->items)) {
$target[] = &$this->items[$i];
$i++;
}
}
elseif (!isset($this->parent)) {
// Allow looping through the top-level array.
$target = &$this->items;
}
else {
// For associative arrays and objects, behave accordingly like
// Mustache.php handles associative arrays and objects.
// @see https://github.com/bobthecow/mustache.php/wiki/Variable-Resolution
return $this->exists() ? new \ArrayIterator([$this]) : new \ArrayIterator([]);
}
return new \CallbackFilterIterator(new \ArrayIterator($target), function ($current, $key, $iterator) {
return $this->stringIsEmpty($current);
});
}
return $this->exists() ? new \ArrayIterator([$this]) : new \ArrayIterator($this->items);
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value): void {
if (is_null($offset)) {
$this->items[] = $value;
}
else {
$this->items[$offset] = $value;
}
$this->updatePositions();
}
/**
* Sets an offset by reference.
*
* This extra method is needed to support setting by reference, as it is not
* officially supported by \ArrayAccess.
*
* @param mixed $offset
* The offset to assign the reference to.
* @param mixed &$value
* The value, passed by reference.
*/
public function offsetSetReference($offset, &$value) {
if (is_null($offset)) {
$this->items[] = &$value;
}
else {
$this->items[$offset] = &$value;
}
$this->updatePositions();
}
/**
* {@inheritdoc}
*/
public function offsetExists($offset): bool {
if (!isset($this->items[$offset])) {
return FALSE;
}
if ($this->items[$offset] instanceof static) {
return $this->items[$offset]->exists();
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset): void {
unset($this->items[$offset]);
$this->updatePositions();
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function &offsetGet($offset) {
if (isset($this->items[$offset])) {
return $this->items[$offset];
}
$null = static::create([], NULL, $this);
return $null;
}
/**
* Implements the magic __isset() method.
*
* @return bool
* Whether the property is set.
*/
public function __isset($name): bool {
if (is_null($name) || !is_scalar($name)) {
return FALSE;
}
if (isset($this->items[$name]) && $this->items[$name] instanceof static) {
return $this->items[$name]->exists();
}
if (isset($this->items[$name]) || (property_exists($this, $name) && !is_null($this->$name))) {
return TRUE;
}
if ($name === 'value') {
return isset($this->value) || !empty($this->items);
}
if ($name === 'string') {
return isset($this->value);
}
return FALSE;
}
/**
* Implements the magic __set() method.
*/
public function __set($name, $value): void {
$this->$name = &$value;
}
/**
* Implements the magic __get() method.
*
* @return mixed
* The value for the given name.
*/
public function &__get($name) {
if (isset($this->items[$name])) {
return $this->items[$name];
}
if ($name === 'value') {
if (empty($this->items)) {
$value = &$this->value;
return $value;
}
return $this;
}
if ($name === 'string') {
$return = $this->value !== $this ? (string) $this->value : '';
return $return;
}
if (property_exists($this, $name)) {
return $this->$name;
}
$return = static::create([], NULL, $this);
return $return;
}
/**
* Get the items list.
*
* @return array
* The items list.
*/
public function &getItems(): array {
return $this->items;
}
/**
* Updates the positions on the child items.
*/
public function updatePositions() {
reset($this->items);
$first = TRUE;
$i = 0;
$this->isAssoc = TRUE;
foreach ($this->items as $key => &$item) {
if (is_int($key)) {
$this->isAssoc = FALSE;
}
$i++;
if ($item instanceof static) {
$item->position = $i;
$item->first = $first;
$item->last = FALSE;
$item->parent = $this;
$item->updatePositions();
}
elseif (is_array($item)) {
$item['position'] = $i;
$item['first'] = $first;
$item['last'] = FALSE;
$item['parent'] = $this;
}
$first = FALSE;
}
if (isset($item)) {
if ($item instanceof static) {
$item->last = TRUE;
}
elseif (is_array($item)) {
$item['last'] = TRUE;
}
}
}
/**
* Get an item value that belongs to the given key.
*
* @param mixed &$item
* The item.
* @param mixed $key
* The key.
*
* @return mixed
* The item value. Might return NULL when no value was found for the given
* key, but also when the value of the given key is actually NULL.
*/
public function getItemValue(&$item, $key) {
if ($item instanceof static) {
if (isset($item->items[$key])) {
return $item->items[$key];
}
if ($key === 'value' && isset($item->value)) {
return $item->value;
}
if ($key === 'string') {
return $item->string;
}
}
elseif ((($item instanceof \ArrayAccess)|| is_array($item)) && !empty($item) && isset($item[$key])) {
return $item[$key];
}
elseif (is_object($item)) {
if (method_exists($item, 'isEmpty') && $item->isEmpty()) {
return NULL;
}
if (method_exists($item, 'empty') && $item->empty()) {
return NULL;
}
if (isset($item->$key)) {
return $item->$key;
}
}
return NULL;
}
/**
* Whether the string representation of the given item is empty.
*
* @param mixed &$item
* The item to check the emptyness of the string representation for. Skip
* the argument to check for the iterable object itself.
*
* @return bool
* Returns TRUE if empty, FALSE otherwise.
*/
public function stringIsEmpty(&$item = NULL) {
if ($item === NULL) {
unset($item);
$item = $this;
}
if ($item instanceof static) {
return $item->exists();
}
if (is_object($item)) {
if (!method_exists($item, '__toString')) {
return FALSE;
}
if (method_exists($item, 'isEmpty')) {
return !$item->isEmpty();
}
if (method_exists($item, 'empty')) {
return !$item->empty();
}
// @todo Consider not to rely on this conversion,
// as it could be too expensive in some cases.
return !empty((string) $item);
}
if (is_scalar($item)) {
return trim((string) $item) !== '';
}
return NULL;
}
}
