ItemList.php 8.1 KB

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