eca-1.0.x-dev/src/Plugin/DataType/DataTransferObject.php
src/Plugin/DataType/DataTransferObject.php
<?php
namespace Drupal\eca\Plugin\DataType;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\Attribute\DataType;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\Plugin\DataType\Map;
use Drupal\Core\TypedData\PrimitiveInterface;
use Drupal\Core\TypedData\TraversableTypedDataInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\Url;
use Drupal\eca\TypedData\DataTransferObjectDefinition;
/**
* Defines the "dto" data type.
*
* A Data Transfer Object (DTO) allows attachment of arbitrary properties.
* A DTO can also be used as a list: items may be dynamically added by using '+'
* and removed by using '-'. Example: $dto->set('+', $value).
*/
#[DataType(
id: 'dto',
label: new TranslatableMarkup('Data Transfer Object'),
description: new TranslatableMarkup('Data Transfer Objects (DTOs) which may contain arbitrary and user-defined properties of data.'),
definition_class: DataTransferObjectDefinition::class
)]
class DataTransferObject extends Map {
/**
* A manually set string representation of this object.
*
* @var string|null
*/
protected ?string $stringRepresentation = NULL;
/**
* Flag to indicate whether rekeying is enabled or disabled.
*
* @var bool
*/
protected bool $disableRekey = FALSE;
/**
* Creates a new instance of a DTO.
*
* @param mixed $value
* (optional) The value to set, in conformance to ::setValue(). May also
* be a content entity, whose fields will be used. When the given value is a
* scalar, it will be set in conformance to ::setStringRepresentation().
* @param \Drupal\Core\TypedData\TypedDataInterface|null $parent
* (optional) If known, the parent object.
* @param string|null $name
* (optional) If the parent is given, the property name of the parent.
* @param bool $notify
* (optional) Whether to notify the parent object of the change.
*
* @return \Drupal\eca\Plugin\DataType\DataTransferObject
* The DTO instance.
*/
public static function create(mixed $value = NULL, ?TypedDataInterface $parent = NULL, ?string $name = NULL, bool $notify = TRUE): DataTransferObject {
$manager = \Drupal::typedDataManager();
if ($parent && $name) {
/**
* @var \Drupal\eca\Plugin\DataType\DataTransferObject $dto
*/
$dto = $manager->createInstance('dto', [
'data_definition' => DataTransferObjectDefinition::create('dto'),
'name' => $name,
'parent' => $parent,
]);
}
else {
/**
* @var \Drupal\eca\Plugin\DataType\DataTransferObject $dto
*/
$dto = $manager->create(DataTransferObjectDefinition::create('dto'));
}
if (isset($value)) {
if ($value instanceof EntityInterface) {
$dto->setStringRepresentation($value->id());
}
elseif ($value instanceof Config) {
$dto->setStringRepresentation($value->getName());
}
if (is_scalar($value)) {
$dto->setStringRepresentation($value);
}
else {
$dto->setValue($value, $notify);
}
}
return $dto;
}
/**
* Creates a DTO from user input.
*
* User input may be a Yaml-formatted hash of values, or an unformatted
* sequence of values, separated with commas and optionally with a colon for
* keyed values. Plain values without separator (comma or new line) will use
* the string representation instead of an array of properties.
*
* @param string $user_input
* The user input as string.
*
* @return \Drupal\eca\Plugin\DataType\DataTransferObject
* A DTO instance, holding values from the user input.
*/
public static function fromUserInput(string $user_input): DataTransferObject {
if (mb_strpos($user_input, PHP_EOL)) {
try {
$values = Yaml::decode($user_input);
if (is_string($values)) {
// Only care for trying conversion of nested structures. For any other
// values, apply the other section below.
$values = [];
}
}
catch (InvalidDataTypeException) {
$values = [];
}
}
else {
$values = [];
}
if (empty($values) && ($user_input !== '')) {
$option = strtok($user_input, "," . PHP_EOL);
while ($option !== FALSE) {
$option = trim($option);
[$key, $value] = array_merge(explode(':', $option, 2), [$option]);
$key = trim($key);
$value = trim($value);
if (mb_substr($key, 0, 1) === '[' && mb_substr($value, -1, 1) === ']') {
// Prevent tokens from being split off.
$key = $value = $option;
}
if ($key !== '' && $value !== '') {
$values[$key] = $value;
}
$option = strtok("," . PHP_EOL);
}
}
// Use the string representation directly if no sequence was provided.
if ((count($values) === 1) && (key($values) === current($values))) {
$values = current($values);
}
return static::create($values);
}
/**
* Shorthand method for building an array from user input.
*
* @param string $user_input
* The user input as string.
*
* @return array
* The built array, holding values from the user input.
*
* @see ::fromUserInput
*/
public static function buildArrayFromUserInput(string $user_input): array {
return static::fromUserInput($user_input)->toArray();
}
/**
* {@inheritdoc}
*/
public function __construct(DataDefinitionInterface $definition, $name = NULL, ?TypedDataInterface $parent = NULL) {
parent::__construct($definition, $name, $parent);
// Make sure that the data definition reflects dynamically added properties.
$this->definition = DataTransferObjectDefinition::create($definition->getDataType(), $this);
}
/**
* {@inheritdoc}
*/
public function toArray(): array {
$values = [];
foreach ($this->getProperties() as $name => $property) {
$values[$name] = $property instanceof ComplexDataInterface ? $property->toArray() : $property->getValue();
}
if (empty($values) && isset($this->stringRepresentation)) {
$values[] = $this->stringRepresentation;
}
return $values;
}
/**
* {@inheritdoc}
*/
public function getValue() {
$value = [];
// Build up an associative array that holds both the data types and the
// corresponding contained values, so that the property list holding
// typed data objects may be restored at any subsequent processing.
foreach ($this->properties as $name => $property) {
$definition = $property->getDataDefinition();
if (!$definition->isComputed()) {
$value['types'][$name] = $definition->getDataType();
$value['values'][$name] = $property->getValue();
}
}
if (isset($this->stringRepresentation)) {
if ($value) {
$value['_string_representation'] = $this->stringRepresentation;
}
else {
$value = $this->stringRepresentation;
}
}
return $value;
}
/**
* Overrides \Drupal\Core\TypedData\Plugin\DataType\Map::setValue().
*
* A DTO allows arbitrary properties. In order to know about the correct data
* types of given properties, passed values should be typed data objects.
* Alternatively, scalar values may be passed in directly in case it's also
* not that critical that a given value may be (wrongly) treated as a string.
* Otherwise, an additional types key should be provided (see description of
* the $values argument).
*
* @param mixed|null $values
* An array of property values as typed data objects, scalars or entities.
* Alternatively, if typed data objects are not available at this point, the
* values may be an associative array keyed by 'types' and 'values'. Both
* array values are a sequence that match with their array keys,
* which are in turn property names. Set to NULL to make this object empty.
* @param bool $notify
* (optional) Whether to notify the parent object of the change. Defaults to
* TRUE. If a property is updated from a parent object, set it to FALSE to
* avoid being notified again.
*/
public function setValue($values, $notify = TRUE): void {
if ($values instanceof TypedDataInterface) {
if (($values instanceof TraversableTypedDataInterface) && ($elements = static::traverseElements($values))) {
$values = $elements;
}
else {
$values = $values->getValue();
}
}
if ($values instanceof EntityInterface) {
$values = $values->getTypedData()->getProperties();
}
elseif ($values instanceof Config) {
/**
* @var \Drupal\Core\TypedData\TraversableTypedDataInterface $typed_config
*/
$typed_config = \Drupal::service('config.typed')->createFromNameAndData($values->getName(), $values->getRawData());
$values = static::traverseElements($typed_config);
}
if (is_null($values)) {
// Shortcut to make this DTO empty.
$this->stringRepresentation = NULL;
$this->properties = [];
$this->values = [];
}
elseif (is_scalar($values) || ($values instanceof MarkupInterface)) {
// Internally forward this argument to set it as string representation.
// This is not officially allowed by this method, but included here
// to reduce possible hurdles when working with a DTO.
$this->setStringRepresentation($values);
}
elseif (!is_array($values)) {
throw new \InvalidArgumentException("Invalid values given. Values must be represented as an associative array.");
}
else {
if (isset($values['_string_representation'])) {
$this->setStringRepresentation($values['_string_representation']);
unset($values['_string_representation']);
}
if (empty($values['types']) || empty($values['values'])) {
foreach ($values as $name => $value) {
if (!($value instanceof TypedDataInterface)) {
if ($value instanceof EntityInterface) {
$values[$name] = $this->wrapEntityValue($name, $value);
}
elseif (is_scalar($value)) {
$values[$name] = $this->wrapScalarValue($name, $value);
}
elseif (is_iterable($value)) {
$values[$name] = $this->wrapIterableValue($name, $value);
}
elseif (is_null($value)) {
unset($values[$name]);
}
elseif ($value instanceof MarkupInterface) {
$values[$name] = $this->wrapScalarValue($name, (string) $value);
}
elseif ($value instanceof Url) {
$values[$name] = $this->wrapUrlValue($name, $value);
}
elseif (is_object($value) && method_exists($value, '__toString')) {
$values[$name] = $this->wrapAnyValue($name, $value);
}
else {
throw new \InvalidArgumentException("Invalid values given. Values must be of scalar types, entities, stringable or typed data objects.");
}
}
}
}
else {
$manager = $this->getTypedDataManager();
$instances = [];
foreach ($values['types'] as $name => $type) {
$instance = $manager->createInstance($type, [
'data_definition' => $manager->createDataDefinition($type),
'name' => $name,
'parent' => $this,
]);
$instance->setValue($values['values'][$name], FALSE);
$instances[$name] = $instance;
}
$values = $instances;
}
// Update any existing property objects.
foreach ($this->properties as $name => $property) {
if (isset($values[$name])) {
$property->setValue($values[$name]->getValue(), FALSE);
}
else {
// Property does not exist anymore, thus remove it.
unset($this->properties[$name]);
}
// Remove the value from $this->values to ensure it does not contain any
// value for computed properties.
unset($this->values[$name]);
}
// Add new properties.
$this->properties += $values;
}
// Notify the parent of any changes.
if ($notify && isset($this->parent)) {
$this->parent->onChange($this->name);
}
}
/**
* Set a string representation of this object.
*
* @param mixed $value
* A scalar value.
*/
public function setStringRepresentation(mixed $value): void {
$this->stringRepresentation = is_null($value) ? NULL : (string) $value;
}
/**
* {@inheritdoc}
*/
public function getString(): ?string {
if (isset($this->stringRepresentation)) {
return $this->stringRepresentation;
}
if (isset($this->properties['#type']) || isset($this->properties['#theme'])) {
// Attached data is a renderable array, so render it.
$renderer = static::renderer();
$build = $this->toArray();
if ($renderer->hasRenderContext()) {
return $renderer->render($build);
}
return $renderer->executeInRenderContext(new RenderContext(), static function () use (&$build, $renderer) {
return $renderer->render($build);
});
}
$values = [];
$is_assoc = FALSE;
foreach ($this->getProperties() as $name => $property) {
$value = $property instanceof ComplexDataInterface ? $property->toArray() : $property->getValue();
if (is_object($value)) {
// Objects are not supported for being encoded to Yaml.
$value = $property->getString();
}
if (($value === NULL) || ($value === '') || (is_iterable($value) && !count($value))) {
// Skip empty items.
continue;
}
if (is_array($value)) {
// Convert entities to arrays for Yaml encoding below.
foreach ($value as $k => $v) {
if ($v instanceof EntityInterface) {
$value[$k] = $v->toArray();
}
}
}
if (is_int($name) || ctype_digit($name)) {
$values[] = $value;
}
else {
$values[$name] = $value;
if ($name !== $value) {
$is_assoc = TRUE;
}
}
}
if (!$is_assoc) {
$values = array_values($values);
}
return $values ? Yaml::encode($values) : '';
}
/**
* Implements magic __toString() method.
*/
public function __toString(): string {
return $this->getString();
}
/**
* {@inheritdoc}
*/
public function getProperties($include_computed = FALSE): array {
$properties = [];
foreach ($this->properties as $name => $property) {
$definition = $property->getDataDefinition();
if ($include_computed || !$definition->isComputed()) {
$properties[$name] = $property;
}
}
return $properties;
}
/**
* {@inheritdoc}
*/
protected function writePropertyValue($property_name, mixed $value): void {
if ($property_name === '-') {
if ($value === NULL) {
array_pop($this->properties);
}
else {
foreach ($this->properties as $name => $property) {
if ($property === $value || $property->getValue() === $value) {
unset($this->properties[$name]);
if (is_int($name) || ctype_digit($name)) {
$this->rekey($name);
}
}
}
}
}
elseif ($value instanceof TypedDataInterface) {
if (isset($this->properties[$property_name])) {
$this->properties[$property_name]->setValue($value->getValue());
}
elseif ($property_name === '+') {
$this->properties[] = $value;
}
else {
$this->properties[$property_name] = $value;
// @todo $property name can never be integer, can it?
// @phpstan-ignore-next-line
if (is_int($property_name) || ctype_digit((string) $property_name)) {
$this->rekey((int) $property_name);
}
}
}
elseif ($value === NULL) {
// When receiving NULL as unwrapped $value, then handle this just like
// removing the property from the list.
unset($this->properties[$property_name]);
// @todo $property name can never be integer, can it?
// @phpstan-ignore-next-line
if (is_int($property_name) || ctype_digit((string) $property_name)) {
$this->rekey((int) $property_name);
}
}
elseif ($value instanceof EntityInterface) {
$this->writePropertyValue($property_name, $this->wrapEntityValue($property_name, $value));
}
elseif ($value instanceof Config) {
$this->writePropertyValue($property_name, $this->wrapConfigValue($property_name, $value));
}
elseif (is_scalar($value)) {
$this->writePropertyValue($property_name, $this->wrapScalarValue($property_name, $value));
}
elseif (is_iterable($value)) {
$this->writePropertyValue($property_name, $this->wrapIterableValue($property_name, $value));
}
elseif ($value instanceof MarkupInterface) {
$this->writePropertyValue($property_name, $this->wrapScalarValue($property_name, (string) $value));
}
elseif ($value instanceof Url) {
$this->writePropertyValue($property_name, $this->wrapUrlValue($property_name, $value));
}
elseif (is_object($value) && method_exists($value, '__toString')) {
$this->writePropertyValue($property_name, $this->wrapAnyValue($property_name, $value));
}
else {
throw new \InvalidArgumentException("Invalid value given. Value must be of a scalar type, an entity, stringable or a typed data object.");
}
}
/**
* Magic method: Gets a property value.
*
* @param int|string $name
* The name of the property to get; e.g., 'title' or 'name'.
*
* @return mixed
* The property value.
*
* @throws \InvalidArgumentException
* If a non-existent property is accessed.
*/
public function __get(int|string $name) {
// There is either a property object or a plain value - possibly for a
// not-defined property. If we have a plain value, directly return it.
if (isset($this->properties[$name])) {
return $this->properties[$name] instanceof PrimitiveInterface ? $this->properties[$name]->getValue() : $this->properties[$name];
}
}
/**
* Magic method: Sets a property value.
*
* @param int|string $name
* The name of the property to set; e.g., 'title' or 'name'.
* @param mixed $value
* The value as typed data object to set, or NULL to unset the property.
*
* @throws \InvalidArgumentException
* If the given argument is not typed data or not NULL.
*/
public function __set(int|string $name, mixed $value) {
$this->set($name, $value);
}
/**
* Magic method: Determines whether a property is set.
*
* @param int|string $name
* The name of the property to get; e.g., 'title' or 'name'.
*
* @return bool
* Returns TRUE if the property exists and is set, FALSE otherwise.
*/
public function __isset(int|string $name) {
if (isset($this->properties[$name])) {
return $this->properties[$name]->getValue() !== NULL;
}
return FALSE;
}
/**
* Magic method: Unsets a property.
*
* @param int|string $name
* The name of the property to get; e.g., 'title' or 'name'.
*/
public function __unset(int|string $name) {
if ($this->definition->getPropertyDefinition($name)) {
$this->set($name, NULL);
}
else {
// Explicitly unset the property in $this->values if a non-defined
// property is unset, such that its key is removed from $this->values.
unset($this->values[$name]);
}
}
/**
* Saves contained data, that belongs to a saveable resource.
*
* This operation is being performed as one database transaction.
*/
public function saveData(): void {
if (!($saveables = $this->getSaveables())) {
return;
}
$transaction = static::databaseConnection()->startTransaction();
foreach ($saveables as $saveable) {
try {
$saveable->save();
}
catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
}
}
/**
* Deletes contained data, that belongs to a saveable resource.
*
* This operation is being performed as one database transaction.
*/
public function deleteData(): void {
if (!($saveables = $this->getSaveables())) {
return;
}
$transaction = static::databaseConnection()->startTransaction();
foreach ($saveables as $saveable) {
try {
$saveable->delete();
}
catch (\Exception $e) {
$transaction->rollBack();
throw $e;
}
}
}
/**
* Get contained data items, that can be saved.
*
* @return array
* The saveable data items.
*/
public function getSaveables(): array {
$saveables = [];
foreach ($this->properties as $property) {
$value = $property->getValue();
if ((($value instanceof EntityInterface) || (($value instanceof Config) && !($value instanceof ImmutableConfig))) && !in_array($value, $saveables, TRUE)) {
$saveables[] = $value;
continue;
}
$parent = NULL;
while (($property->getParent() !== $parent) && ($parent = $property->getParent())) {
$parent_value = $parent->getValue();
if ((($parent_value instanceof EntityInterface) || (($parent_value instanceof Config) && !($parent_value instanceof ImmutableConfig))) && !in_array($parent_value, $saveables, TRUE)) {
$saveables[] = $parent_value;
break;
}
}
}
return $saveables;
}
/**
* Shift the first item from the beginning of the object's list of properties.
*
* @return \Drupal\Core\TypedData\TypedDataInterface|null
* The removed item, or NULL if the DTO is empty.
*/
public function shift(): ?TypedDataInterface {
$properties = $this->properties;
reset($properties);
$key = key($properties);
$item = array_shift($this->properties);
if (is_int($key) || ctype_digit((string) $key)) {
$this->rekey($key);
}
return $item;
}
/**
* Pop the last item from the end of the object's list of properties.
*
* @return \Drupal\Core\TypedData\TypedDataInterface|null
* The removed item, or NULL if the DTO is empty.
*/
public function pop(): ?TypedDataInterface {
return array_pop($this->properties);
}
/**
* Remove the given value from the object's list of properties.
*
* @param mixed $value
* The value to remove.
*
* @return \Drupal\Core\TypedData\TypedDataInterface|null
* The removed item, or NULL if the DTO does not contain the given value.
*/
public function remove(mixed $value): ?TypedDataInterface {
$item = NULL;
foreach ($this->properties as $name => $property) {
$property_value = $property->getValue();
$value_matches = ($property_value === $value || $property === $value);
if (!$value_matches && ($value instanceof EntityInterface) && ($property_value instanceof EntityInterface)) {
// Many times, entity objects are cloned. Take another look, whether the
// identifier matches.
$identifier = $identifier ?? ($value->uuid() ?? $value->id());
$value_matches = isset($identifier) && ($identifier === ($property_value->uuid() ?? $property_value->id()))
&& ($value->language()->getId() === $property_value->language()->getId())
// @phpstan-ignore-next-line
&& (!($value instanceof RevisionableInterface) || ($value->getRevisionId() === $property_value->getRevisionId()))
&& ($value->getEntityTypeId() === $property_value->getEntityTypeId());
}
if ($value_matches) {
$item = $this->properties[$name];
unset($this->properties[$name]);
if (is_int($name) || ctype_digit($name)) {
$this->rekey($name);
}
}
}
return $item;
}
/**
* Remove an item from the object's list of properties by the given name.
*
* @param int|string $name
* The property name.
*
* @return \Drupal\Core\TypedData\TypedDataInterface|null
* The removed item, or NULL if the DTO does not contain an item by the
* given property name.
*/
public function removeByName(int|string $name): ?TypedDataInterface {
$item = NULL;
if (isset($this->properties[$name])) {
$item = $this->properties[$name];
unset($this->properties[$name]);
if (is_int($name) || ctype_digit($name)) {
$this->rekey($name);
}
}
return $item;
}
/**
* Adds a value to the beginning of the object's list of properties.
*
* @param mixed $value
* The value to add, preferable as typed data or an entity.
*
* @return int
* The index of the added value.
*/
public function unshift(mixed $value): int {
$index = $this->push($value);
$property = $this->properties[$index];
unset($this->properties[$index]);
array_unshift($this->properties, $property);
$properties = $this->properties;
reset($properties);
$index = key($properties);
$this->rekey();
return $index;
}
/**
* Pushes a value to the end of the object's list of properties.
*
* @param mixed $value
* The value to add, preferable as typed data or an entity.
*
* @return int
* The index of the added value.
*/
public function push(mixed $value): int {
$properties = $this->properties;
$properties[] = $value;
end($properties);
$index = key($properties);
$this->writePropertyValue($index, $value);
$this->rekey();
return $index;
}
/**
* Returns the number of property items.
*
* @return int
* The number of property items.
*/
public function count(): int {
return count($this->properties);
}
/**
* {@inheritdoc}
*
* Also considers the string representation for being empty.
*/
public function isEmpty(): bool {
return (is_null($this->stringRepresentation) || $this->stringRepresentation === '') && parent::isEmpty();
}
/**
* Wraps the scalar value by a Typed Data object.
*
* @param int|string $name
* The property name.
* @param mixed $value
* The scalar value.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The Typed Data object.
*/
protected function wrapScalarValue(int|string $name, mixed $value): TypedDataInterface {
$manager = $this->getTypedDataManager();
$scalar_type = 'string';
if (is_numeric($value)) {
$scalar_type = is_int($value) || ctype_digit((string) $value) ? 'integer' : 'float';
}
elseif (is_bool($value)) {
$scalar_type = 'boolean';
}
$instance = $manager->createInstance($scalar_type, [
'data_definition' => $manager->createDataDefinition($scalar_type),
'name' => $name,
'parent' => $this,
]);
$instance->setValue($value, FALSE);
return $instance;
}
/**
* Wraps the entity by a Typed Data object.
*
* @param int|string $name
* The property name.
* @param \Drupal\Core\Entity\EntityInterface $value
* The entity.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The Typed Data object.
*/
protected function wrapEntityValue(int|string $name, EntityInterface $value): TypedDataInterface {
$manager = $this->getTypedDataManager();
$instance = $manager->createInstance('entity', [
'data_definition' => EntityDataDefinition::create($value->getEntityTypeId(), $value->bundle()),
'name' => $name,
'parent' => $this,
]);
$instance->setValue($value, FALSE);
return $instance;
}
/**
* Wraps the config by a Typed Data object.
*
* @param int|string $name
* The property name.
* @param \Drupal\Core\Config\Config $value
* The config.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The Typed Data object.
*/
protected function wrapConfigValue(int|string $name, Config $value) : TypedDataInterface {
/** @var \Drupal\Core\Config\TypedConfigManager $manager */
$manager = \Drupal::service('config.typed');
/** @var \Drupal\Core\TypedData\TraversableTypedDataInterface $typed_config */
$typed_config = $manager->createFromNameAndData($value->getName(), $value->getRawData());
return $manager->create($typed_config->getDataDefinition(), $value->getRawData(), $name, $this);
}
/**
* Wraps an iterable value by a Typed Data object.
*
* @param int|string $name
* The property name.
* @param mixed $value
* The iterable value.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The Typed Data object.
*/
protected function wrapIterableValue(int|string $name, mixed $value): TypedDataInterface {
$instance = static::create(NULL, $this, $name, FALSE);
foreach ($value as $k => $v) {
$instance->set($k, $v, FALSE);
}
return $instance;
}
/**
* Wraps a URL by a Typed Data object.
*
* @param int|string $name
* The property name.
* @param \Drupal\Core\Url $value
* The URL value.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The Typed Data object.
*/
protected function wrapUrlValue(int|string $name, Url $value): TypedDataInterface {
$manager = $this->getTypedDataManager();
$instance = $manager->createInstance('eca_url', [
'data_definition' => $manager->createDataDefinition('eca_url'),
'name' => $name,
'parent' => $this,
]);
$instance->setValue($value, FALSE);
return $instance;
}
/**
* Wraps any unspecified value by a non-specific ("any") Typed Data object.
*
* @param int|string $name
* The property name.
* @param mixed $value
* The unspecified value.
*
* @return \Drupal\Core\TypedData\TypedDataInterface
* The Typed Data object.
*/
protected function wrapAnyValue(int|string $name, mixed $value): TypedDataInterface {
$manager = $this->getTypedDataManager();
$instance = $manager->createInstance('any', [
'data_definition' => $manager->createDataDefinition('any'),
'name' => $name,
'parent' => $this,
]);
$instance->setValue($value, FALSE);
return $instance;
}
/**
* Renumbers the items in the property list.
*
* @param int $from_index
* Optionally, the index at which to start the renumbering, if it is known
* that items before that can safely be skipped (for example, when removing
* an item at a given index).
*/
protected function rekey(int $from_index = 0): void {
if ($this->disableRekey) {
return;
}
$assoc = [];
$sequence = [];
foreach ($this->properties as $p_name => $p_val) {
if (is_int($p_name) || ctype_digit($p_name)) {
$sequence[] = $p_val;
}
else {
$assoc[$p_name] = $p_val;
}
}
$this->properties = array_merge($assoc, $sequence);
// Each item holds its own index as a "name", it needs to be updated
// according to the new list indexes.
$countSequence = count($sequence);
for ($i = $from_index; $i < $countSequence; $i++) {
$this->properties[$i]->setContext((string) $i, $this);
}
}
/**
* Helper method to traverse and collect the traversed elements.
*
* @param \Drupal\Core\TypedData\TraversableTypedDataInterface $traversable
* The traversable object.
*
* @return \Drupal\Core\TypedData\TypedDataInterface[]
* The traversed elements.
*/
protected static function traverseElements(TraversableTypedDataInterface $traversable): array {
$elements = [];
foreach ($traversable as $key => $element) {
$elements[$key] = $element;
}
return $elements;
}
/**
* Get the database connection.
*
* @return \Drupal\Core\Database\Connection
* The database connection.
*/
protected static function databaseConnection(): Connection {
return \Drupal::database();
}
/**
* Get the renderer.
*
* @return \Drupal\Core\Render\RendererInterface
* The renderer.
*/
protected static function renderer(): RendererInterface {
return \Drupal::service('renderer');
}
/**
* Gets the current disabled status of rekeying.
*
* @return bool
* The current disabled status of rekeying.
*/
public function rekeyDisabledStatus(): bool {
return $this->disableRekey;
}
/**
* Disables rekeying for this DTO entirely.
*/
public function disableRekey(): void {
$this->disableRekey = TRUE;
}
/**
* Enables rekeying for this DTO entirely.
*/
public function enableRekey(): void {
$this->disableRekey = FALSE;
}
}
