From 48872a5738c50268b9761dadf248c5885a63e4c8 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 19 May 2021 19:34:16 -0700 Subject: [PATCH] LazyArray - Add helper class for lazily-loaded lists --- CRM/Utils/Array.php | 25 +++++ CRM/Utils/LazyArray.php | 95 ++++++++++++++++++ tests/phpunit/CRM/Utils/LazyArrayTest.php | 111 ++++++++++++++++++++++ 3 files changed, 231 insertions(+) create mode 100644 CRM/Utils/LazyArray.php create mode 100644 tests/phpunit/CRM/Utils/LazyArrayTest.php diff --git a/CRM/Utils/Array.php b/CRM/Utils/Array.php index f2260f4502..06aa5a8faf 100644 --- a/CRM/Utils/Array.php +++ b/CRM/Utils/Array.php @@ -17,6 +17,31 @@ */ class CRM_Utils_Array { + /** + * Cast a value to an array. + * + * This is similar to PHP's `(array)`, but it also converts iterators. + * + * @param mixed $value + * @return array + */ + public static function cast($value) { + if (is_array($value)) { + return $value; + } + if ($value instanceof CRM_Utils_LazyArray || $value instanceof ArrayObject) { + // iterator_to_array() would work here, but getArrayCopy() doesn't require actual iterations. + return $value->getArrayCopy(); + } + if (is_iterable($value)) { + return iterator_to_array($value); + } + if (is_scalar($value)) { + return [$value]; + } + throw new \RuntimeException(sprintf("Cannot cast %s to array", gettype($value))); + } + /** * Returns $list[$key] if such element exists, or a default value otherwise. * diff --git a/CRM/Utils/LazyArray.php b/CRM/Utils/LazyArray.php new file mode 100644 index 0000000000..4ea6ab1659 --- /dev/null +++ b/CRM/Utils/LazyArray.php @@ -0,0 +1,95 @@ +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); + } + +} diff --git a/tests/phpunit/CRM/Utils/LazyArrayTest.php b/tests/phpunit/CRM/Utils/LazyArrayTest.php new file mode 100644 index 0000000000..c48b1455db --- /dev/null +++ b/tests/phpunit/CRM/Utils/LazyArrayTest.php @@ -0,0 +1,111 @@ +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], + ]; + }); + } + +} -- 2.25.1