3 namespace Drupal\Core\TypedData\Plugin\DataType;
5 use Drupal\Core\TypedData\ComplexDataInterface;
6 use Drupal\Core\TypedData\ListInterface;
7 use Drupal\Core\TypedData\TypedData;
8 use Drupal\Core\TypedData\TypedDataInterface;
11 * A generic list class.
13 * This class can serve as list for any type of items and is used by default.
14 * Data types may specify the default list class in their definition, see
15 * Drupal\Core\TypedData\Annotation\DataType.
16 * Note: The class cannot be called "List" as list is a reserved PHP keyword.
22 * label = @Translation("List of items"),
23 * definition_class = "\Drupal\Core\TypedData\ListDataDefinition"
26 class ItemList extends TypedData implements \IteratorAggregate, ListInterface {
29 * Numerically indexed array of items.
31 * @var \Drupal\Core\TypedData\TypedDataInterface[]
38 public function getValue() {
40 foreach ($this->list as $delta => $item) {
41 $values[$delta] = $item->getValue();
47 * Overrides \Drupal\Core\TypedData\TypedData::setValue().
49 * @param array|null $values
50 * An array of values of the field items, or NULL to unset the field.
52 public function setValue($values, $notify = TRUE) {
53 if (!isset($values) || $values === []) {
57 // Only arrays with numeric keys are supported.
58 if (!is_array($values)) {
59 throw new \InvalidArgumentException('Cannot set a list with a non-array value.');
61 // Assign incoming values. Keys are renumbered to ensure 0-based
62 // sequential deltas. If possible, reuse existing items rather than
64 foreach (array_values($values) as $delta => $value) {
65 if (!isset($this->list[$delta])) {
66 $this->list[$delta] = $this->createItem($delta, $value);
69 $this->list[$delta]->setValue($value, FALSE);
72 // Truncate extraneous pre-existing values.
73 $this->list = array_slice($this->list, 0, count($values));
75 // Notify the parent of any changes.
76 if ($notify && isset($this->parent)) {
77 $this->parent->onChange($this->name);
84 public function getString() {
86 foreach ($this->list as $item) {
87 $strings[] = $item->getString();
89 // Remove any empty strings resulting from empty items.
90 return implode(', ', array_filter($strings, 'mb_strlen'));
96 public function get($index) {
97 if (!is_numeric($index)) {
98 throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
100 // Automatically create the first item for computed fields.
101 // @deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0.
102 // Use \Drupal\Core\TypedData\ComputedItemListTrait instead.
103 if ($index == 0 && !isset($this->list[0]) && $this->definition->isComputed()) {
104 @trigger_error('Automatically creating the first item for computed fields is deprecated in Drupal 8.5.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\TypedData\ComputedItemListTrait instead.', E_USER_DEPRECATED);
105 $this->list[0] = $this->createItem(0);
107 return isset($this->list[$index]) ? $this->list[$index] : NULL;
113 public function set($index, $value) {
114 if (!is_numeric($index)) {
115 throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
117 // Ensure indexes stay sequential. We allow assigning an item at an existing
118 // index, or at the next index available.
119 if ($index < 0 || $index > count($this->list)) {
120 throw new \InvalidArgumentException('Unable to set a value to a non-subsequent delta in a list.');
122 // Support setting values via typed data objects.
123 if ($value instanceof TypedDataInterface) {
124 $value = $value->getValue();
126 // If needed, create the item at the next position.
127 $item = isset($this->list[$index]) ? $this->list[$index] : $this->appendItem();
128 $item->setValue($value);
135 public function removeItem($index) {
136 if (isset($this->list) && array_key_exists($index, $this->list)) {
137 // Remove the item, and reassign deltas.
138 unset($this->list[$index]);
139 $this->rekey($index);
142 throw new \InvalidArgumentException('Unable to remove item at non-existing index.');
148 * Renumbers the items in the list.
150 * @param int $from_index
151 * Optionally, the index at which to start the renumbering, if it is known
152 * that items before that can safely be skipped (for example, when removing
153 * an item at a given index).
155 protected function rekey($from_index = 0) {
156 // Re-key the list to maintain consecutive indexes.
157 $this->list = array_values($this->list);
158 // Each item holds its own index as a "name", it needs to be updated
159 // according to the new list indexes.
160 for ($i = $from_index; $i < count($this->list); $i++) {
161 $this->list[$i]->setContext($i, $this);
168 public function first() {
169 return $this->get(0);
175 public function offsetExists($offset) {
176 // We do not want to throw exceptions here, so we do not use get().
177 return isset($this->list[$offset]);
183 public function offsetUnset($offset) {
184 $this->removeItem($offset);
190 public function offsetGet($offset) {
191 return $this->get($offset);
197 public function offsetSet($offset, $value) {
198 if (!isset($offset)) {
199 // The [] operator has been used.
200 $this->appendItem($value);
203 $this->set($offset, $value);
210 public function appendItem($value = NULL) {
211 $offset = count($this->list);
212 $item = $this->createItem($offset, $value);
213 $this->list[$offset] = $item;
218 * Helper for creating a list item object.
220 * @return \Drupal\Core\TypedData\TypedDataInterface
222 protected function createItem($offset = 0, $value = NULL) {
223 return $this->getTypedDataManager()->getPropertyInstance($this, $offset, $value);
229 public function getItemDefinition() {
230 return $this->definition->getItemDefinition();
236 public function getIterator() {
237 return new \ArrayIterator($this->list);
243 public function count() {
244 return count($this->list);
250 public function isEmpty() {
251 foreach ($this->list as $item) {
252 if ($item instanceof ComplexDataInterface || $item instanceof ListInterface) {
253 if (!$item->isEmpty()) {
257 // Other items are treated as empty if they have no value only.
258 elseif ($item->getValue() !== NULL) {
268 public function filter($callback) {
269 if (isset($this->list)) {
271 // Apply the filter, detecting if some items were actually removed.
272 $this->list = array_filter($this->list, function ($item) use ($callback, &$removed) {
273 if (call_user_func($callback, $item)) {
290 public function onChange($delta) {
291 // Notify the parent of changes.
292 if (isset($this->parent)) {
293 $this->parent->onChange($this->name);
298 * Magic method: Implements a deep clone.
300 public function __clone() {
301 foreach ($this->list as $delta => $item) {
302 $this->list[$delta] = clone $item;
303 $this->list[$delta]->setContext($delta, $this);