4f756dadf2540b9b15e9d4e37f5d353f1b02c67e
[yaffs-website] / ItemList.php
1 <?php
2
3 namespace Drupal\Core\TypedData\Plugin\DataType;
4
5 use Drupal\Core\TypedData\ComplexDataInterface;
6 use Drupal\Core\TypedData\ListInterface;
7 use Drupal\Core\TypedData\TypedData;
8 use 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  */
26 class 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    */
52   public function setValue($values, $notify = TRUE) {
53     if (!isset($values) || $values === []) {
54       $this->list = [];
55     }
56     else {
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.');
60       }
61       // Assign incoming values. Keys are renumbered to ensure 0-based
62       // sequential deltas. If possible, reuse existing items rather than
63       // creating new ones.
64       foreach (array_values($values) as $delta => $value) {
65         if (!isset($this->list[$delta])) {
66           $this->list[$delta] = $this->createItem($delta, $value);
67         }
68         else {
69           $this->list[$delta]->setValue($value, FALSE);
70         }
71       }
72       // Truncate extraneous pre-existing values.
73       $this->list = array_slice($this->list, 0, count($values));
74     }
75     // Notify the parent of any changes.
76     if ($notify && isset($this->parent)) {
77       $this->parent->onChange($this->name);
78     }
79   }
80
81   /**
82    * {@inheritdoc}
83    */
84   public function getString() {
85     $strings = [];
86     foreach ($this->list as $item) {
87       $strings[] = $item->getString();
88     }
89     // Remove any empty strings resulting from empty items.
90     return implode(', ', array_filter($strings, '\Drupal\Component\Utility\Unicode::strlen'));
91   }
92
93   /**
94    * {@inheritdoc}
95    */
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.');
99     }
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);
103     }
104     return isset($this->list[$index]) ? $this->list[$index] : NULL;
105   }
106
107   /**
108    * {@inheritdoc}
109    */
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.');
113     }
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.');
118     }
119     // Support setting values via typed data objects.
120     if ($value instanceof TypedDataInterface) {
121       $value = $value->getValue();
122     }
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);
126     return $this;
127   }
128
129   /**
130    * {@inheritdoc}
131    */
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);
137     }
138     else {
139       throw new \InvalidArgumentException('Unable to remove item at non-existing index.');
140     }
141     return $this;
142   }
143
144   /**
145    * Renumbers the items in the list.
146    *
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).
151    */
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);
159     }
160   }
161
162   /**
163    * {@inheritdoc}
164    */
165   public function first() {
166     return $this->get(0);
167   }
168
169   /**
170    * {@inheritdoc}
171    */
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]);
175   }
176
177   /**
178    * {@inheritdoc}
179    */
180   public function offsetUnset($offset) {
181     $this->removeItem($offset);
182   }
183
184   /**
185    * {@inheritdoc}
186    */
187   public function offsetGet($offset) {
188     return $this->get($offset);
189   }
190
191   /**
192    * {@inheritdoc}
193    */
194   public function offsetSet($offset, $value) {
195     if (!isset($offset)) {
196       // The [] operator has been used.
197       $this->appendItem($value);
198     }
199     else {
200       $this->set($offset, $value);
201     }
202   }
203
204   /**
205    * {@inheritdoc}
206    */
207   public function appendItem($value = NULL) {
208     $offset = count($this->list);
209     $item = $this->createItem($offset, $value);
210     $this->list[$offset] = $item;
211     return $item;
212   }
213
214   /**
215    * Helper for creating a list item object.
216    *
217    * @return \Drupal\Core\TypedData\TypedDataInterface
218    */
219   protected function createItem($offset = 0, $value = NULL) {
220     return $this->getTypedDataManager()->getPropertyInstance($this, $offset, $value);
221   }
222
223   /**
224    * {@inheritdoc}
225    */
226   public function getItemDefinition() {
227     return $this->definition->getItemDefinition();
228   }
229
230   /**
231    * {@inheritdoc}
232    */
233   public function getIterator() {
234     return new \ArrayIterator($this->list);
235   }
236
237   /**
238    * {@inheritdoc}
239    */
240   public function count() {
241     return count($this->list);
242   }
243
244   /**
245    * {@inheritdoc}
246    */
247   public function isEmpty() {
248     foreach ($this->list as $item) {
249       if ($item instanceof ComplexDataInterface || $item instanceof ListInterface) {
250         if (!$item->isEmpty()) {
251           return FALSE;
252         }
253       }
254       // Other items are treated as empty if they have no value only.
255       elseif ($item->getValue() !== NULL) {
256         return FALSE;
257       }
258     }
259     return TRUE;
260   }
261
262   /**
263    * {@inheritdoc}
264    */
265   public function filter($callback) {
266     if (isset($this->list)) {
267       $removed = FALSE;
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)) {
271           return TRUE;
272         }
273         else {
274           $removed = TRUE;
275         }
276       });
277       if ($removed) {
278         $this->rekey();
279       }
280     }
281     return $this;
282   }
283
284   /**
285    * {@inheritdoc}
286    */
287   public function onChange($delta) {
288     // Notify the parent of changes.
289     if (isset($this->parent)) {
290       $this->parent->onChange($this->name);
291     }
292   }
293
294   /**
295    * Magic method: Implements a deep clone.
296    */
297   public function __clone() {
298     foreach ($this->list as $delta => $item) {
299       $this->list[$delta] = clone $item;
300       $this->list[$delta]->setContext($delta, $this);
301     }
302   }
303
304 }