--- /dev/null
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved. |
+ | |
+ | This work is published under the GNU AGPLv3 license with some |
+ | permitted exceptions and without any warranty. For full license |
+ | and copyright information, see https://civicrm.org/licensing |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * A lazy-array works much like a regular array or ArrayObject. However, it is
+ * initially empty - and it is only populated if used.
+ */
+class CRM_Utils_LazyArray implements ArrayAccess, IteratorAggregate, Countable {
+
+ /**
+ * A function which generates a list of values.
+ *
+ * @var callable
+ * function(): iterable
+ */
+ private $func;
+
+ /**
+ * Cached values
+ *
+ * @var array|null
+ */
+ private $cache;
+
+ /**
+ * CRM_Utils_LazyList constructor.
+ *
+ * @param callable $func
+ * Function which provides a list of values (array/iterator/generator).
+ */
+ public function __construct($func) {
+ $this->func = $func;
+ }
+
+ /**
+ * Determine if the content has been fetched.
+ *
+ * @return bool
+ */
+ public function isLoaded() {
+ return $this->cache !== NULL;
+ }
+
+ public function load($force = FALSE) {
+ if ($this->cache === NULL || $force) {
+ $this->cache = CRM_Utils_Array::cast(call_user_func($this->func));
+ }
+ return $this;
+ }
+
+ public function offsetExists($offset) {
+ return isset($this->load()->cache[$offset]);
+ }
+
+ public function &offsetGet($offset) {
+ return $this->load()->cache[$offset];
+ }
+
+ public function offsetSet($offset, $value) {
+ if ($offset === NULL) {
+ $this->load()->cache[] = $value;
+ }
+ else {
+ $this->load()->cache[$offset] = $value;
+ }
+ }
+
+ public function offsetUnset($offset) {
+ unset($this->load()->cache[$offset]);
+ }
+
+ public function getIterator() {
+ return new ArrayIterator($this->load()->cache);
+ }
+
+ /**
+ * @return array
+ */
+ public function getArrayCopy() {
+ return $this->load()->cache;
+ }
+
+ public function count() {
+ return count($this->load()->cache);
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * Class CRM_Utils_LazyArrayTest
+ * @group headless
+ */
+class CRM_Utils_LazyArrayTest extends CiviUnitTestCase {
+
+ public function testAssoc() {
+ $l = $this->createFruitBasket();
+ $this->assertFalse($l->isLoaded());
+
+ $this->assertEquals('apple', $l['a']);
+ $this->assertEquals('banana', $l['b']);
+ $this->assertTrue($l->isLoaded());
+ $this->assertEquals(3, count($l));
+ $this->assertTrue(isset($l['c']));
+ $this->assertFalse(isset($l['d']));
+
+ $l['a'] = 'apricot';
+ $this->assertEquals('apricot', $l['a']);
+ $this->assertEquals(3, count($l));
+
+ $l['d'] = 'date';
+ $this->assertEquals('date', $l['d']);
+ $this->assertEquals(4, count($l));
+
+ $keys = [];
+ foreach ($l as $key => $value) {
+ $keys[] = $key;
+ }
+ $this->assertEquals(['a', 'b', 'c', 'd'], $keys);
+ $this->assertEquals(['a', 'b', 'c', 'd'], array_keys(CRM_Utils_Array::cast($l)));
+ }
+
+ public function testNumeric() {
+ $l = $this->createSeaRecords();
+ $this->assertFalse($l->isLoaded());
+
+ $this->assertEquals('aegean', $l[0]['name']);
+ $this->assertEquals('caspian', $l[2]['name']);
+ $this->assertTrue($l->isLoaded());
+ $this->assertEquals(3, count($l));
+ $this->assertTrue(isset($l[2]));
+ $this->assertFalse(isset($l[3]));
+
+ $l[2]['name'] = 'coral';
+ $this->assertEquals(['name' => 'coral', 'area' => 371], $l['2']);
+ $this->assertEquals(3, count($l));
+
+ $l[] = ['name' => 'weddell', 'area' => 2800];
+ $this->assertEquals('weddell', $l[3]['name']);
+ $this->assertEquals(4, count($l));
+
+ $keys = [];
+ foreach ($l as $key => $value) {
+ $keys[] = $key;
+ }
+ $this->assertEquals([0, 1, 2, 3], $keys);
+ $this->assertEquals([0, 1, 2, 3], array_keys(CRM_Utils_Array::cast($l)));
+ }
+
+ public function testBasicInspections() {
+ $l = $this->createFruitBasket();
+ $this->assertFalse($l->isLoaded());
+
+ $this->assertTrue($l !== NULL);
+ $this->assertTrue($l instanceof CRM_Utils_LazyArray);
+ $this->assertTrue(is_iterable($l));
+ $this->assertTrue(!is_array($l));
+
+ $this->assertFalse($l->isLoaded());
+
+ $this->assertEquals(3, count($l));
+ $this->assertTrue($l->isLoaded());
+ }
+
+ public function testCopy() {
+ $l = $this->createFruitBasket();
+ $copy = $l->getArrayCopy();
+ $copy['d'] = 'date';
+
+ $this->assertEquals(3, count($l));
+ $this->assertEquals(4, count($copy));
+ }
+
+ /**
+ * @return \CRM_Utils_LazyArray
+ */
+ private function createFruitBasket(): \CRM_Utils_LazyArray {
+ return new CRM_Utils_LazyArray(function () {
+ yield 'a' => 'apple';
+ yield 'b' => 'banana';
+ yield 'c' => 'cherry';
+ });
+ }
+
+ /**
+ * @return \CRM_Utils_LazyArray
+ */
+ private function createSeaRecords(): \CRM_Utils_LazyArray {
+ return new CRM_Utils_LazyArray(function () {
+ return [
+ ['name' => 'aegean', 'area' => 214],
+ ['name' => 'baltic', 'area' => 377],
+ ['name' => 'caspian', 'area' => 371],
+ ];
+ });
+ }
+
+}