plus-8.x-4.x-dev/src/Utility/Element.php
src/Utility/Element.php
<?php
namespace Drupal\plus\Utility;
use Drupal\Core\Render\RenderContext;
use Drupal\plus\Plugin\Theme\ThemeInterface;
use Drupal\plus\Traits\RendererTrait;
use Drupal\plus\Plus;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element as CoreElement;
/**
* Provides helper methods for Drupal render elements.
*
* @ingroup utility
*
* @see \Drupal\Core\Render\Element
*/
class Element extends DrupalArray {
use RendererTrait;
/**
* The current state of the form.
*
* @var \Drupal\Core\Form\FormStateInterface
*/
protected $formState;
/**
* The element type.
*
* @var string
*/
protected $type = FALSE;
/**
* {@inheritdoc}
*/
protected $propertyPrefix = '#';
/**
* The current theme.
*
* @var \Drupal\plus\Plugin\Theme\ThemeInterface
*/
protected $theme;
/** @noinspection PhpMissingParentConstructorInspection */
/**
* {@inheritdoc}
*/
public function __construct(&$element = [], FormStateInterface $form_state = NULL, ThemeInterface $theme = NULL) {
if (!isset($theme)) {
$theme = Plus::getActiveTheme();
}
$this->theme = $theme;
if (!is_array($element)) {
$element = ['#markup' => $element instanceof MarkupInterface ? $element : new FormattableMarkup((string) $element, [])];
}
$this->__storage = &$element;
$this->formState = $form_state;
}
/**
* Implements the magic __toString() method.
*
* Note: this mimics ToStringTrait, but because this may be invoked outside
* of any RenderContext, it should use ::renderPlain instead of the trait's
* normal ::render method invocation.
*
* @see \Drupal\Component\Utility\ToStringTrait::__toString
*/
public function __toString() {
try {
return (string) $this->renderPlain();
}
catch (\Exception $e) {
// User errors in __toString() methods are considered fatal in the Drupal
// error handler.
trigger_error(get_class($e) . ' thrown while calling __toString on a ' . get_class($this) . ' object in ' . $e->getFile() . ' on line ' . $e->getLine() . ': ' . $e->getMessage(), E_USER_ERROR);
// In case there is a different error handler being used that did not
// fatal on E_USER_ERROR, terminate the PHP script execution. However,
// for test purposes allow a return value.
return $this->_die();
}
}
/**
* For test purposes, wrap die() in an overridable method.
*/
protected function _die() { // @codingStandardsIgnoreLine
die();
}
/**
* {@inheritdoc}
*
* @return \Drupal\plus\Utility\Element
* The newly created element instance.
*/
public static function create(...$arguments) {
foreach ([0, 1, 2] as $i) {
if (!isset($arguments[$i])) {
$arguments[$i] = $i === 0 ? [] : NULL;
}
}
// Immediately return a cloned version if element is already an Element.
$element = $arguments[0];
if ($element instanceof self) {
$clone = $element->copy();
}
elseif (is_object($element)) {
$clone = clone $element;
}
else {
$clone = $element;
}
$class = static::getElementClass();
return new $class($clone, $arguments[1], $arguments[2]);
}
/**
* {@inheritdoc}
*
* @return \Drupal\plus\Utility\Element
* The newly created element instance.
*/
public static function reference(&...$arguments) {
foreach ([0, 1, 2] as $i) {
if (!isset($arguments[$i])) {
$arguments[$i] = $i === 0 ? [] : NULL;
}
}
// Immediately return if already an Element instance.
$element = &$arguments[0];
if ($element instanceof self) {
return $element;
}
$class = static::getElementClass();
return new $class($element, $arguments[1], $arguments[2]);
}
/**
* Adds a callback to an array.
*
* @param string $property
* The name of the element property to add callback to, no # prefix.
* @param callable $callback
* The callback to add.
* @param array|string $replace
* If specified, the callback will instead replace the specified value
* instead of being appended to the $callbacks array.
* @param string $placement
* Flag that determines how to add the callback to the array.
* @param bool $default_info
* Flag indicating whether to merge in the default element info.
*
* @return bool
* TRUE if the callback was added, FALSE if $replace was specified but its
* callback could be found in the list of callbacks.
*
* @throws \InvalidArgumentException
* If $property contains a # prefix.
* If $placement is not a valid type.
*/
public function addCallback($property, callable $callback, $replace = NULL, $placement = 'append', $default_info = TRUE) {
// Ensure that the property name does not have a # prefix.
if (CoreElement::property($property)) {
throw new \InvalidArgumentException('Property name must not include a # prefix.');
}
// Before Drupal 8, most render array callbacks were invoked manually, not
// using call_user_func_array(), which makes it impossible to add static
// method callbacks from classes. Instead, this must specify a procedural
// function that correlates with the type of callback.
if ((int) \Drupal::VERSION[0] < 8) {
$element_callbacks = &$this->getProperty("plus_$property", []);
$element_callbacks[] = $callback;
$callback = 'plus_element_' . $property . '_callback';
}
// Only continue if callback is valid.
if (!is_callable($callback)) {
throw new \InvalidArgumentException(sprintf('Unknown callback: %s', is_array($callback) ? '[' . implode(', ', $callback) . ']' : (string) $callback));
}
// Retrieve the default element info.
$default = [];
if (($type = $this->getProperty('type')) && $default_info && !$this->hasProperty('defaults_loaded')) {
// Purposefully use element_info_property() for 7.x and 8.x compatibility.
$default = element_info_property($type, $property, []);
$this->setProperty('defaults_loaded', TRUE);
}
$existing = &$this->getProperty($property, $default);
// Add the callback.
return Plus::addCallback($existing, $callback, $replace, $placement);
}
/**
* Retrieves the class to use for constructing new Element instances.
*
* This, essentially, allows themes to sub-class this object.
*
* @return string
* The class name.
*/
public static function getElementClass() {
$class = Plus::getActiveTheme()->getElementClass();
if ($class !== self::class && !is_subclass_of($class, self::class)) {
throw new \LogicException('Element class provided by theme must subclass \Drupal\plus\Utility\Element.');
}
return $class;
}
/**
* {@inheritdoc}
*/
public function &__get($key) {
if (CoreElement::property($key)) {
throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Please use \Drupal\plus\Utility\Element::getProperty instead.');
}
$instance = new self($this->get($key, []));
return $instance;
}
/**
* {@inheritdoc}
*/
public function __set($key, $value) {
if (CoreElement::property($key)) {
throw new \InvalidArgumentException('Cannot dynamically retrieve element property. Use \Drupal\plus\Utility\Element::setProperty instead.');
}
$this->set($key, ($value instanceof Element ? $value->getArray() : $value));
}
/**
* {@inheritdoc}
*/
public function __isset($name) {
if (CoreElement::property($name)) {
throw new \InvalidArgumentException('Cannot dynamically check if an element has a property. Use \Drupal\plus\Utility\Element::unsetProperty instead.');
}
return parent::__isset($name);
}
/**
* {@inheritdoc}
*/
public function __unset($name) {
if (CoreElement::property($name)) {
throw new \InvalidArgumentException('Cannot dynamically unset an element property. Use \Drupal\plus\Utility\Element::hasProperty instead.');
}
parent::__unset($name);
}
/**
* Appends a property with a value.
*
* @param string $name
* The name of the property to set.
* @param mixed $value
* The value of the property to set, passed by reference.
*
* @return $this
*/
public function appendProperty($name, &$value) {
$property = &$this->getProperty($name);
$element = $value instanceof Element ? $value : Element::reference($value);
// If property isn't set, just set it.
if (!isset($property)) {
$property = $value;
return $this;
}
if (is_array($property)) {
$property[] = $element->getArray();
}
else {
$property .= (string) $element->renderPlain();
}
return $this;
}
/**
* Identifies the children of an element array, optionally sorted by weight.
*
* The children of a element array are those key/value pairs whose key does
* not start with a '#'. See drupal_render() for details.
*
* @param bool $sort
* Boolean to indicate whether the children should be sorted by weight.
*
* @return array
* The array keys of the element's children.
*/
public function childKeys($sort = FALSE) {
return CoreElement::children($this->__storage, $sort);
}
/**
* Retrieves the children of an element array, optionally sorted by weight.
*
* The children of a element array are those key/value pairs whose key does
* not start with a '#'. See drupal_render() for details.
*
* @param bool $sort
* Boolean to indicate whether the children should be sorted by weight.
*
* @return \Drupal\plus\Utility\Element[]
* An array child elements.
*/
public function children($sort = FALSE) {
$children = [];
foreach ($this->childKeys($sort) as $child) {
$children[$child] = new self($this->__storage[$child]);
}
return $children;
}
/**
* Retrieves the render array for the element.
*
* @return array
* The element render array, passed by reference.
*/
public function &getArray() {
return $this->__storage;
}
/**
* Retrieves a context value from the #context element property, if any.
*
* @param string $name
* The name of the context key to retrieve.
* @param mixed $default
* Optional. The default value to use if the context $name isn't set.
*
* @return mixed|null
* The context value or the $default value if not set.
*/
public function &getContext($name, $default = NULL) {
$context = &$this->getProperty('context', []);
if (!isset($context[$name])) {
$context[$name] = $default;
}
return $context[$name];
}
/**
* Returns the error message filed against the given form element.
*
* Form errors higher up in the form structure override deeper errors as well
* as errors on the element itself.
*
* @return string|null
* Either the error message for this element or NULL if there are no errors.
*
* @throws \BadMethodCallException
* When the element instance was not constructed with a valid form state
* object.
*/
public function getError() {
if (!$this->formState) {
throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
}
return $this->formState->getError($this->__storage);
}
/**
* Retrieves the render array for the element.
*
* @param string $name
* The name of the element property to retrieve, not including the # prefix.
* @param mixed $default
* The default to set if property does not exist.
*
* @return mixed
* The property value, NULL if not set.
*/
public function &getProperty($name, $default = NULL) {
return $this->get("#$name", $default);
}
/**
* Returns the visible children of an element.
*
* @return array
* The array keys of the element's visible children.
*/
public function getVisibleChildren() {
return CoreElement::getVisibleChildren($this->__storage);
}
/**
* Indicates whether the element has an error set.
*
* @throws \BadMethodCallException
* When the element instance was not constructed with a valid form state
* object.
*/
public function hasError() {
$error = $this->getError();
return isset($error);
}
/**
* Indicates whether the element has a specific property.
*
* @param string $name
* The property to check.
*
* @return bool
* TRUE or FALSE
*/
public function hasProperty($name) {
return $this->exists("#$name");
}
/**
* Indicates whether the element is a button.
*
* @return bool
* TRUE or FALSE.
*/
public function isButton() {
$button_types = ['button', 'submit', 'reset', 'image_button'];
return !empty($this->__storage['#is_button']) || $this->isType($button_types) || $this->hasClass('button');
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return CoreElement::isEmpty($this->__storage);
}
/**
* Indicates whether a property on the element is empty.
*
* @param string $name
* The property to check.
*
* @return bool
* Whether the given property on the element is empty.
*/
public function isPropertyEmpty($name) {
return $this->hasProperty($name) && empty($this->getProperty($name));
}
/**
* Checks if a value is a render array.
*
* @param mixed $value
* The value to check.
*
* @return bool
* TRUE if the given value is a render array, otherwise FALSE.
*/
public static function isRenderArray($value) {
return is_array($value) && (isset($value['#type']) ||
isset($value['#theme']) || isset($value['#theme_wrappers']) ||
isset($value['#markup']) || isset($value['#attached']) ||
isset($value['#cache']) || isset($value['#lazy_builder']) ||
isset($value['#create_placeholder']) || isset($value['#pre_render']) ||
isset($value['#post_render']) || isset($value['#process']));
}
/**
* Checks if the element is a specific type of element.
*
* @param string|array $type
* The element type(s) to check.
*
* @return bool
* TRUE if element is or one of $type.
*/
public function isType($type) {
$property = $this->getProperty('type');
return $property && in_array($property, (is_array($type) ? $type : [$type]));
}
/**
* Determines if an element is visible.
*
* @return bool
* TRUE if the element is visible, otherwise FALSE.
*/
public function isVisible() {
return CoreElement::isVisibleElement($this->__storage);
}
/**
* Maps an element's properties to its attributes array.
*
* @param array $map
* An associative array whose keys are element property names and whose
* values are the HTML attribute names to set on the corresponding
* property; e.g., array('#propertyname' => 'attributename'). If both names
* are identical except for the leading '#', then an attribute name value is
* sufficient and no property name needs to be specified.
*
* @return $this
*/
public function mapProperties(array $map) {
CoreElement::setAttributes($this->__storage, $map);
return $this;
}
/**
* Prepends a property with a value.
*
* @param string $name
* The name of the property to set.
* @param mixed $value
* The value of the property to set.
*
* @return $this
*/
public function prependProperty($name, $value) {
$property = &$this->getProperty($name);
$value = $value instanceof Element ? $value->getArray() : $value;
// If property isn't set, just set it.
if (!isset($property)) {
$property = $value;
return $this;
}
if (is_array($property)) {
array_unshift($property, Element::reference($value)->getArray());
}
else {
$property = (string) $value . (string) $property;
}
return $this;
}
/**
* Gets properties of a structured array element (keys beginning with '#').
*
* @return array
* An array of property keys for the element.
*/
public function properties() {
return CoreElement::properties($this->__storage);
}
/**
* Renders the final element HTML.
*
* @return \Drupal\Component\Render\MarkupInterface
* The rendered HTML.
*
* @throws \LogicException
* When called outside of a render context (i.e. outside of a renderRoot(),
* renderPlain() or executeInRenderContext() call).
* @throws \Exception
* If a #pre_render callback throws an exception, it is caught to mark the
* renderer as no longer being in a root render call, if any. Then the
* exception is rethrown.
*/
public function render() {
return static::getRenderer()->render($this->__storage);
}
/**
* Executes a callable within a render context.
*
* Only for very advanced use cases. Prefer using ::renderRoot() and
* ::renderPlain() instead.
*
* @param \Drupal\Core\Render\RenderContext $context
* The render context to execute the callable within.
* @param callable $callable
* (optional) The callable to execute. If not set, it will default to
* rendering the current element array.
*
* @return mixed
* The callable's return value.
*
* @throws \LogicException
* In case bubbling has failed, can only happen in case of broken code.
*
* @see \Drupal\Core\Render\RenderContext
* @see \Drupal\Core\Render\BubbleableMetadata
* @see \Drupal\Core\Render\Renderer::executeInRenderContext
*/
public function renderInContext(RenderContext $context, callable $callable = NULL) {
$renderer = static::getRenderer();
if (!isset($callable)) {
$build = &$this->__storage;
$callable = function () use (&$build, $renderer) {
return $renderer->render($build);
};
}
return $renderer->executeInRenderContext($context, $callable);
}
/**
* Renders the final element HTML, ignoring any current RenderContext.
*
* @return \Drupal\Component\Render\MarkupInterface
* The rendered HTML.
*/
public function renderPlain() {
return static::getRenderer()->renderPlain($this->__storage);
}
/**
* Renders the final element HTML.
*
* (Cannot be executed within another render context.)
*
* @return \Drupal\Component\Render\MarkupInterface
* The rendered HTML.
*/
public function renderRoot() {
return static::getRenderer()->renderRoot($this->__storage);
}
/**
* Flags an element as having an error.
*
* @param string $message
* (optional) The error message to present to the user.
*
* @return $this
*
* @throws \BadMethodCallException
* When the element instance was not constructed with a valid form state
* object.
*/
public function setError($message = '') {
if (!$this->formState) {
throw new \BadMethodCallException('The element instance must be constructed with a valid form state object to use this method.');
}
$this->formState->setError($this->__storage, $message);
return $this;
}
/**
* Sets the value for a property.
*
* @param string $name
* The name of the property to set.
* @param mixed $value
* The value of the property to set.
* @param bool $recurse
* Flag indicating wither to set the same property on child elements.
*
* @return $this
*/
public function setProperty($name, $value, $recurse = FALSE) {
$this->__storage["#$name"] = $value instanceof Element ? $value->getArray() : $value;
if ($recurse) {
foreach ($this->children() as $child) {
$child->setProperty($name, $value, $recurse);
}
}
return $this;
}
/**
* Removes a property from the element.
*
* @param string $name
* The name of the property to unset.
*
* @return $this
*/
public function unsetProperty($name) {
unset($this->__storage["#$name"]);
return $this;
}
}
