* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ namespace PrestaShop\PrestaShop\Adapter\Presenter; use ArrayAccess; use ArrayIterator; use ArrayObject; use Countable; use Iterator; use JsonSerializable; use PrestaShop\PrestaShop\Core\Util\Inflector; use ReflectionClass; use ReflectionException; use ReflectionMethod; use RuntimeException; /** * This class is useful to provide the same behaviour than an array, but which load the result of each key on demand * (LazyLoading). * * Example: * * If your want to define the ['addresses'] array access in your lazyArray object, just define the public method * getAddresses() and add the annotation arrayAccess to it. e.g: * * @arrayAccess * * @return array * * public function getAddresses() * * The method name should always be the index name converted to camelCase and prefixed with get. e.g: * * ['add_to_cart'] => getAddToCart() * * You can also add an array with already defined key to the lazyArray be calling the appendArray function. * e.g.: you have a $product array containing $product['id'] = 10; $product['url'] = 'foo'; * If you call ->appendArray($product) on the lazyArray, it will define the key ['id'] and ['url'] as well * for the lazyArray. * Note if the key already exists as a method, it will be skip. In our example, if getUrl() is defined with the * annotation @arrayAccess, the $product['url'] = 'foo'; will be ignored */ abstract class AbstractLazyArray implements Iterator, ArrayAccess, Countable, JsonSerializable { /** * @var ArrayObject */ private $arrayAccessList; /** * @var ArrayIterator */ private $arrayAccessIterator; /** * @var array */ private $methodCacheResults = []; /** * AbstractLazyArray constructor. * * @throws ReflectionException */ public function __construct() { $this->arrayAccessList = new ArrayObject(); $reflectionClass = new ReflectionClass(get_class($this)); $methods = $reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { $methodDoc = $method->getDocComment(); if (strpos($methodDoc, '@arrayAccess') !== false) { $this->arrayAccessList->offsetSet( $this->convertMethodNameToIndex($method->getName()), [ 'type' => 'method', 'value' => $method->getName(), ] ); } } $this->arrayAccessIterator = $this->arrayAccessList->getIterator(); } /** * Make the lazyArray serializable like an array. * * @return array * * @throws RuntimeException */ #[\ReturnTypeWillChange] public function jsonSerialize() { $arrayResult = []; foreach ($this->arrayAccessList as $key => $value) { $arrayResult[$key] = $this->offsetGet($key); } return $arrayResult; } /** * Set array key and values from $array into the LazyArray. * * @param array $array */ public function appendArray($array) { foreach ($array as $key => $value) { // do not override any existing method if (!$this->arrayAccessList->offsetExists($key)) { $this->arrayAccessList->offsetSet( $key, [ 'type' => 'variable', 'value' => $value, ] ); } } } /** * @param mixed $key * @param \Closure $closure */ public function appendClosure($key, \Closure $closure) { $this->arrayAccessList->offsetSet( $key, [ 'type' => 'closure', 'value' => $closure, ] ); } /** * The number of keys defined into the lazyArray. * * @return int */ public function count(): int { return $this->arrayAccessList->count(); } /** * The properties are provided as an array. But callers checking the type of this class (is_object === true) * think they must use the object syntax. * * Check if the index exists inside the lazyArray. * * @param string $index * * @return bool */ public function __isset($index) { return $this->offsetExists($index); } /** * The properties are provided as an array. But callers checking the type of this class (is_object === true) * think they must use the object syntax. * * Get the value associated with the $index from the lazyArray. * * @param mixed $index * * @return mixed * * @throws RuntimeException */ public function __get($index) { return $this->offsetGet($index); } /** * The properties are provided as an array. But callers checking the type of this class (is_object === true) * think they must use the object syntax. * * @param mixed $name * @param mixed $value * * @throws RuntimeException */ public function __set($name, $value) { $this->offsetSet($name, $value); } /** * The properties are provided as an array. But callers checking the type of this class (is_object === true) * think they must use the object syntax. * * @param mixed $name * * @throws RuntimeException */ public function __unset($name) { $this->offsetUnset($name); } /** * Needed to ensure that any changes to this object won't bleed to other instances */ public function __clone() { $this->arrayAccessList = clone $this->arrayAccessList; $this->arrayAccessIterator = clone $this->arrayAccessIterator; } /** * Get the value associated with the $index from the lazyArray. * * @param mixed $index * * @return mixed * * @throws RuntimeException */ #[\ReturnTypeWillChange] public function offsetGet($index) { if (isset($this->arrayAccessList[$index])) { $type = $this->arrayAccessList[$index]['type']; switch ($type) { case 'method': $isResultAvailableInCache = (isset($this->methodCacheResults[$index])); if (!$isResultAvailableInCache) { $methodName = $this->arrayAccessList[$index]['value']; $this->methodCacheResults[$index] = $this->{$methodName}(); } $result = $this->methodCacheResults[$index]; break; case 'closure': $isResultAvailableInCache = (isset($this->methodCacheResults[$index])); if (!$isResultAvailableInCache) { $methodName = $this->arrayAccessList[$index]['value']; $this->methodCacheResults[$index] = $methodName(); } $result = $this->methodCacheResults[$index]; break; default: $result = $this->arrayAccessList[$index]['value']; break; } return $result; } return []; } public function clearMethodCacheResults() { $this->methodCacheResults = []; } /** * Check if the index exists inside the lazyArray. * * @param mixed $index * * @return bool */ public function offsetExists($index): bool { return isset($this->arrayAccessList[$index]); } /** * Copy the lazyArray. * * @return AbstractLazyArray */ public function getArrayCopy() { return clone $this; } /** * Get the result associated with the current index. * * @return mixed * * @throws RuntimeException */ #[\ReturnTypeWillChange] public function current() { $key = $this->arrayAccessIterator->key(); return $this->offsetGet($key); } /** * Go to the next result inside the lazyArray. */ public function next(): void { $this->arrayAccessIterator->next(); } /** * Get the key associated with the current index. * * @return mixed|string */ #[\ReturnTypeWillChange] public function key() { return $this->arrayAccessIterator->key(); } /** * Check if we are at the end of the lazyArray. * * @return bool */ public function valid(): bool { return $this->arrayAccessIterator->valid(); } /** * Go back to the first element of the lazyArray. */ public function rewind(): void { $this->arrayAccessIterator->rewind(); } /** * Set the keys not present in the given $array to null. * * @param array $array * * @throws RuntimeException */ public function intersectKey($array) { $arrayCopy = $this->arrayAccessList->getArrayCopy(); foreach ($arrayCopy as $key => $value) { if (!array_key_exists($key, $array)) { $this->offsetUnset($key, true); } } } /** * @param mixed $offset * @param mixed $value * @param bool $force if set, allow override of an existing method * * @throws RuntimeException */ public function offsetSet($offset, $value, $force = false): void { if (!$force && $this->arrayAccessList->offsetExists($offset)) { $result = $this->arrayAccessList->offsetGet($offset); if ($result['type'] !== 'variable') { throw new RuntimeException('Trying to set the index ' . print_r($offset, true) . ' of the LazyArray ' . get_class($this) . ' already defined by a method is not allowed'); } } $this->arrayAccessList->offsetSet($offset, [ 'type' => 'variable', 'value' => $value, ]); } /** * @param mixed $offset * @param bool $force if set, allow unset of an existing method * * @throws RuntimeException */ public function offsetUnset($offset, $force = false): void { $result = $this->arrayAccessList->offsetGet($offset); if ($force || $result['type'] === 'variable') { $this->arrayAccessList->offsetUnset($offset); } else { throw new RuntimeException('Trying to unset the index ' . print_r($offset, true) . ' of the LazyArray ' . get_class($this) . ' already defined by a method is not allowed'); } } /** * @param string $methodName * * @return string */ private function convertMethodNameToIndex($methodName) { // remove "get" prefix from the function name $strippedMethodName = substr($methodName, 3); return Inflector::getInflector()->tableize($strippedMethodName); } }