1<?php 2 3namespace Drupal\Core\TypedData\Plugin\DataType; 4 5use Drupal\Core\TypedData\ComplexDataInterface; 6use Drupal\Core\TypedData\ListInterface; 7use Drupal\Core\TypedData\TypedData; 8use Drupal\Core\TypedData\TypedDataInterface; 9 10/** 11 * A generic list class. 12 * 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. 17 * 18 * @ingroup typed_data 19 * 20 * @DataType( 21 * id = "list", 22 * label = @Translation("List of items"), 23 * definition_class = "\Drupal\Core\TypedData\ListDataDefinition" 24 * ) 25 */ 26class ItemList extends TypedData implements \IteratorAggregate, ListInterface { 27 28 /** 29 * Numerically indexed array of items. 30 * 31 * @var \Drupal\Core\TypedData\TypedDataInterface[] 32 */ 33 protected $list = []; 34 35 /** 36 * {@inheritdoc} 37 */ 38 public function getValue() { 39 $values = []; 40 foreach ($this->list as $delta => $item) { 41 $values[$delta] = $item->getValue(); 42 } 43 return $values; 44 } 45 46 /** 47 * Overrides \Drupal\Core\TypedData\TypedData::setValue(). 48 * 49 * @param array|null $values 50 * An array of values of the field items, or NULL to unset the field. 51 * @param bool $notify 52 * (optional) Whether to notify the parent object of the change. Defaults to 53 * TRUE. 54 */ 55 public function setValue($values, $notify = TRUE) { 56 if (!isset($values) || $values === []) { 57 $this->list = []; 58 } 59 else { 60 // Only arrays with numeric keys are supported. 61 if (!is_array($values)) { 62 throw new \InvalidArgumentException('Cannot set a list with a non-array value.'); 63 } 64 // Assign incoming values. Keys are renumbered to ensure 0-based 65 // sequential deltas. If possible, reuse existing items rather than 66 // creating new ones. 67 foreach (array_values($values) as $delta => $value) { 68 if (!isset($this->list[$delta])) { 69 $this->list[$delta] = $this->createItem($delta, $value); 70 } 71 else { 72 $this->list[$delta]->setValue($value, FALSE); 73 } 74 } 75 // Truncate extraneous pre-existing values. 76 $this->list = array_slice($this->list, 0, count($values)); 77 } 78 // Notify the parent of any changes. 79 if ($notify && isset($this->parent)) { 80 $this->parent->onChange($this->name); 81 } 82 } 83 84 /** 85 * {@inheritdoc} 86 */ 87 public function getString() { 88 $strings = []; 89 foreach ($this->list as $item) { 90 $strings[] = $item->getString(); 91 } 92 // Remove any empty strings resulting from empty items. 93 return implode(', ', array_filter($strings, 'mb_strlen')); 94 } 95 96 /** 97 * {@inheritdoc} 98 */ 99 public function get($index) { 100 if (!is_numeric($index)) { 101 throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.'); 102 } 103 return isset($this->list[$index]) ? $this->list[$index] : NULL; 104 } 105 106 /** 107 * {@inheritdoc} 108 */ 109 public function set($index, $value) { 110 if (!is_numeric($index)) { 111 throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.'); 112 } 113 // Ensure indexes stay sequential. We allow assigning an item at an existing 114 // index, or at the next index available. 115 if ($index < 0 || $index > count($this->list)) { 116 throw new \InvalidArgumentException('Unable to set a value to a non-subsequent delta in a list.'); 117 } 118 // Support setting values via typed data objects. 119 if ($value instanceof TypedDataInterface) { 120 $value = $value->getValue(); 121 } 122 // If needed, create the item at the next position. 123 $item = isset($this->list[$index]) ? $this->list[$index] : $this->appendItem(); 124 $item->setValue($value); 125 return $this; 126 } 127 128 /** 129 * {@inheritdoc} 130 */ 131 public function removeItem($index) { 132 if (isset($this->list) && array_key_exists($index, $this->list)) { 133 // Remove the item, and reassign deltas. 134 unset($this->list[$index]); 135 $this->rekey($index); 136 } 137 else { 138 throw new \InvalidArgumentException('Unable to remove item at non-existing index.'); 139 } 140 return $this; 141 } 142 143 /** 144 * Renumbers the items in the list. 145 * 146 * @param int $from_index 147 * Optionally, the index at which to start the renumbering, if it is known 148 * that items before that can safely be skipped (for example, when removing 149 * an item at a given index). 150 */ 151 protected function rekey($from_index = 0) { 152 // Re-key the list to maintain consecutive indexes. 153 $this->list = array_values($this->list); 154 // Each item holds its own index as a "name", it needs to be updated 155 // according to the new list indexes. 156 for ($i = $from_index; $i < count($this->list); $i++) { 157 $this->list[$i]->setContext($i, $this); 158 } 159 } 160 161 /** 162 * {@inheritdoc} 163 */ 164 public function first() { 165 return $this->get(0); 166 } 167 168 /** 169 * {@inheritdoc} 170 */ 171 public function offsetExists($offset) { 172 // We do not want to throw exceptions here, so we do not use get(). 173 return isset($this->list[$offset]); 174 } 175 176 /** 177 * {@inheritdoc} 178 */ 179 public function offsetUnset($offset) { 180 $this->removeItem($offset); 181 } 182 183 /** 184 * {@inheritdoc} 185 */ 186 public function offsetGet($offset) { 187 return $this->get($offset); 188 } 189 190 /** 191 * {@inheritdoc} 192 */ 193 public function offsetSet($offset, $value) { 194 if (!isset($offset)) { 195 // The [] operator has been used. 196 $this->appendItem($value); 197 } 198 else { 199 $this->set($offset, $value); 200 } 201 } 202 203 /** 204 * {@inheritdoc} 205 */ 206 public function appendItem($value = NULL) { 207 $offset = count($this->list); 208 $item = $this->createItem($offset, $value); 209 $this->list[$offset] = $item; 210 return $item; 211 } 212 213 /** 214 * Helper for creating a list item object. 215 * 216 * @return \Drupal\Core\TypedData\TypedDataInterface 217 */ 218 protected function createItem($offset = 0, $value = NULL) { 219 return $this->getTypedDataManager()->getPropertyInstance($this, $offset, $value); 220 } 221 222 /** 223 * {@inheritdoc} 224 */ 225 public function getItemDefinition() { 226 return $this->definition->getItemDefinition(); 227 } 228 229 /** 230 * {@inheritdoc} 231 */ 232 public function getIterator() { 233 return new \ArrayIterator($this->list); 234 } 235 236 /** 237 * {@inheritdoc} 238 */ 239 public function count() { 240 return count($this->list); 241 } 242 243 /** 244 * {@inheritdoc} 245 */ 246 public function isEmpty() { 247 foreach ($this->list as $item) { 248 if ($item instanceof ComplexDataInterface || $item instanceof ListInterface) { 249 if (!$item->isEmpty()) { 250 return FALSE; 251 } 252 } 253 // Other items are treated as empty if they have no value only. 254 elseif ($item->getValue() !== NULL) { 255 return FALSE; 256 } 257 } 258 return TRUE; 259 } 260 261 /** 262 * {@inheritdoc} 263 */ 264 public function filter($callback) { 265 if (isset($this->list)) { 266 $removed = FALSE; 267 // Apply the filter, detecting if some items were actually removed. 268 $this->list = array_filter($this->list, function ($item) use ($callback, &$removed) { 269 if (call_user_func($callback, $item)) { 270 return TRUE; 271 } 272 else { 273 $removed = TRUE; 274 } 275 }); 276 if ($removed) { 277 $this->rekey(); 278 } 279 } 280 return $this; 281 } 282 283 /** 284 * {@inheritdoc} 285 */ 286 public function onChange($delta) { 287 // Notify the parent of changes. 288 if (isset($this->parent)) { 289 $this->parent->onChange($this->name); 290 } 291 } 292 293 /** 294 * Magic method: Implements a deep clone. 295 */ 296 public function __clone() { 297 foreach ($this->list as $delta => $item) { 298 $this->list[$delta] = clone $item; 299 $this->list[$delta]->setContext($delta, $this); 300 } 301 } 302 303} 304