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, '\Drupal\Component\Utility\Unicode::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 if ($index == 0 && !isset($this->list[0]) && $this->definition->isComputed()) {
102 $this->list[0] = $this->createItem(0);
104 return isset($this->list[$index]) ? $this->list[$index] : NULL;
110 public function set($index, $value) {
111 if (!is_numeric($index)) {
112 throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
114 // Ensure indexes stay sequential. We allow assigning an item at an existing
115 // index, or at the next index available.
116 if ($index < 0 || $index > count($this->list)) {
117 throw new \InvalidArgumentException('Unable to set a value to a non-subsequent delta in a list.');
119 // Support setting values via typed data objects.
120 if ($value instanceof TypedDataInterface) {
121 $value = $value->getValue();
123 // If needed, create the item at the next position.
124 $item = isset($this->list[$index]) ? $this->list[$index] : $this->appendItem();
125 $item->setValue($value);
132 public function removeItem($index) {
133 if (isset($this->list) && array_key_exists($index, $this->list)) {
134 // Remove the item, and reassign deltas.
135 unset($this->list[$index]);
136 $this->rekey($index);
139 throw new \InvalidArgumentException('Unable to remove item at non-existing index.');
145 * Renumbers the items in the list.
147 * @param int $from_index
148 * Optionally, the index at which to start the renumbering, if it is known
149 * that items before that can safely be skipped (for example, when removing
150 * an item at a given index).
152 protected function rekey($from_index = 0) {
153 // Re-key the list to maintain consecutive indexes.
154 $this->list = array_values($this->list);
155 // Each item holds its own index as a "name", it needs to be updated
156 // according to the new list indexes.
157 for ($i = $from_index; $i < count($this->list); $i++) {
158 $this->list[$i]->setContext($i, $this);
165 public function first() {
166 return $this->get(0);
172 public function offsetExists($offset) {
173 // We do not want to throw exceptions here, so we do not use get().
174 return isset($this->list[$offset]);
180 public function offsetUnset($offset) {
181 $this->removeItem($offset);
187 public function offsetGet($offset) {
188 return $this->get($offset);
194 public function offsetSet($offset, $value) {
195 if (!isset($offset)) {
196 // The [] operator has been used.
197 $this->appendItem($value);
200 $this->set($offset, $value);
207 public function appendItem($value = NULL) {
208 $offset = count($this->list);
209 $item = $this->createItem($offset, $value);
210 $this->list[$offset] = $item;
215 * Helper for creating a list item object.
217 * @return \Drupal\Core\TypedData\TypedDataInterface
219 protected function createItem($offset = 0, $value = NULL) {
220 return $this->getTypedDataManager()->getPropertyInstance($this, $offset, $value);
226 public function getItemDefinition() {
227 return $this->definition->getItemDefinition();
233 public function getIterator() {
234 return new \ArrayIterator($this->list);
240 public function count() {
241 return count($this->list);
247 public function isEmpty() {
248 foreach ($this->list as $item) {
249 if ($item instanceof ComplexDataInterface || $item instanceof ListInterface) {
250 if (!$item->isEmpty()) {
254 // Other items are treated as empty if they have no value only.
255 elseif ($item->getValue() !== NULL) {
265 public function filter($callback) {
266 if (isset($this->list)) {
268 // Apply the filter, detecting if some items were actually removed.
269 $this->list = array_filter($this->list, function ($item) use ($callback, &$removed) {
270 if (call_user_func($callback, $item)) {
287 public function onChange($delta) {
288 // Notify the parent of changes.
289 if (isset($this->parent)) {
290 $this->parent->onChange($this->name);
295 * Magic method: Implements a deep clone.
297 public function __clone() {
298 foreach ($this->list as $delta => $item) {
299 $this->list[$delta] = clone $item;
300 $this->list[$delta]->setContext($delta, $this);