*/ abstract class AbstractContainer extends ArrayObject { /** * Container name * * @var string */ protected $name; /** @var Manager */ protected $manager; /** * Default manager class to use if no manager has been provided * * @var string */ protected static $managerDefaultClass = SessionManager::class; /** * Default manager to use when instantiating a container without providing a ManagerInterface * * @var Manager */ protected static $defaultManager; /** * Default value to return by reference from offsetGet * * @var mixed */ private $defaultValue; /** * Constructor * * Provide a name ('Default' if none provided) and a ManagerInterface instance. * * @param null|string $name * @throws Exception\InvalidArgumentException */ public function __construct($name = 'Default', ?Manager $manager = null) { if (! preg_match('/^[a-z0-9][a-z0-9_\\\\]+$/i', $name)) { throw new Exception\InvalidArgumentException( 'Name passed to container is invalid; must consist of alphanumerics, backslashes and underscores only' ); } $this->name = $name; $this->setManager($manager); // Create namespace parent::__construct([], ArrayObject::ARRAY_AS_PROPS); // Start session $this->getManager()->start(); } /** * Set the default ManagerInterface instance to use when none provided to constructor * * @return void */ public static function setDefaultManager(?Manager $manager = null) { static::$defaultManager = $manager; } /** * Get the default ManagerInterface instance * * If none provided, instantiates one of type {@link $managerDefaultClass} * * @return Manager * @throws Exception\InvalidArgumentException If invalid manager default class provided. */ public static function getDefaultManager() { if (null === static::$defaultManager) { $manager = new static::$managerDefaultClass(); if (! $manager instanceof Manager) { throw new Exception\InvalidArgumentException( 'Invalid default manager type provided; must implement ManagerInterface' ); } static::$defaultManager = $manager; } return static::$defaultManager; } /** * Get container name * * @return string */ public function getName() { return $this->name; } /** * Set session manager * * @return Container * @throws Exception\InvalidArgumentException */ protected function setManager(?Manager $manager = null) { if (null === $manager) { $manager = static::getDefaultManager(); if (! $manager instanceof Manager) { throw new Exception\InvalidArgumentException( 'Manager provided is invalid; must implement ManagerInterface' ); } } $this->manager = $manager; return $this; } /** * Get manager instance * * @return Manager */ public function getManager() { return $this->manager; } /** * Get session storage object * * Proxies to ManagerInterface::getStorage() * * @return Storage */ protected function getStorage() { return $this->getManager()->getStorage(); } /** * Create a new container object on which to act * * @return ArrayObject */ protected function createContainer() { return new ArrayObject([], ArrayObject::ARRAY_AS_PROPS); } /** * Verify container namespace * * Checks to see if a container exists within the Storage object already. * If not, one is created; if so, checks to see if it's an ArrayObject. * If not, it raises an exception; otherwise, it returns the Storage * object. * * @param bool $createContainer Whether or not to create the container for the namespace * @return Storage|null Returns null only if $createContainer is false * @throws Exception\RuntimeException */ protected function verifyNamespace($createContainer = true) { $storage = $this->getStorage(); $name = $this->getName(); if (! isset($storage[$name])) { if (! $createContainer) { return; } $storage[$name] = $this->createContainer(); } if (! is_array($storage[$name]) && ! $storage[$name] instanceof Traversable) { throw new Exception\RuntimeException('Container cannot write to storage due to type mismatch'); } return $storage; } /** * Determine whether a given key needs to be expired * * Returns true if the key has expired, false otherwise. * * @param null|string $key * @return bool */ protected function expireKeys($key = null) { $storage = $this->verifyNamespace(); $name = $this->getName(); // Return early if key not found if ((null !== $key) && ! isset($storage[$name][$key])) { return true; } if ($this->expireByExpiryTime($storage, $name, $key)) { return true; } if ($this->expireByHops($storage, $name, $key)) { return true; } return false; } /** * Expire a key by expiry time * * Checks to see if the entire container has expired based on TTL setting, * or the individual key. * * @param string $name Container name * @param string $key Key in container to check * @return bool */ protected function expireByExpiryTime(Storage $storage, $name, $key) { $metadata = $storage->getMetadata($name); // Global container expiry if ( is_array($metadata) && isset($metadata['EXPIRE']) && ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE']) ) { unset($metadata['EXPIRE']); $storage->setMetadata($name, $metadata, true); $storage[$name] = $this->createContainer(); return true; } // Expire individual key if ( (null !== $key) && is_array($metadata) && isset($metadata['EXPIRE_KEYS']) && isset($metadata['EXPIRE_KEYS'][$key]) && ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE_KEYS'][$key]) ) { unset($metadata['EXPIRE_KEYS'][$key]); $storage->setMetadata($name, $metadata, true); unset($storage[$name][$key]); return true; } // Find any keys that have expired if ( (null === $key) && is_array($metadata) && isset($metadata['EXPIRE_KEYS']) ) { foreach (array_keys($metadata['EXPIRE_KEYS']) as $key) { if ($_SERVER['REQUEST_TIME'] > $metadata['EXPIRE_KEYS'][$key]) { unset($metadata['EXPIRE_KEYS'][$key]); if (isset($storage[$name][$key])) { unset($storage[$name][$key]); } } } $storage->setMetadata($name, $metadata, true); return true; } return false; } /** * Expire key by session hops * * Determines whether the container or an individual key within it has * expired based on session hops * * @param string $name * @param string $key * @return bool */ protected function expireByHops(Storage $storage, $name, $key) { $ts = $storage->getRequestAccessTime(); $metadata = $storage->getMetadata($name); // Global container expiry if ( is_array($metadata) && isset($metadata['EXPIRE_HOPS']) && ($ts > $metadata['EXPIRE_HOPS']['ts']) ) { $metadata['EXPIRE_HOPS']['hops']--; if (-1 === $metadata['EXPIRE_HOPS']['hops']) { unset($metadata['EXPIRE_HOPS']); $storage->setMetadata($name, $metadata, true); $storage[$name] = $this->createContainer(); return true; } $metadata['EXPIRE_HOPS']['ts'] = $ts; $storage->setMetadata($name, $metadata, true); return false; } // Single key expiry if ( (null !== $key) && is_array($metadata) && isset($metadata['EXPIRE_HOPS_KEYS']) && isset($metadata['EXPIRE_HOPS_KEYS'][$key]) && ($ts > $metadata['EXPIRE_HOPS_KEYS'][$key]['ts']) ) { $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']--; if (-1 === $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']) { unset($metadata['EXPIRE_HOPS_KEYS'][$key]); $storage->setMetadata($name, $metadata, true); unset($storage[$name][$key]); return true; } $metadata['EXPIRE_HOPS_KEYS'][$key]['ts'] = $ts; $storage->setMetadata($name, $metadata, true); return false; } // Find all expired keys if ( (null === $key) && is_array($metadata) && isset($metadata['EXPIRE_HOPS_KEYS']) ) { foreach (array_keys($metadata['EXPIRE_HOPS_KEYS']) as $key) { if ($ts > $metadata['EXPIRE_HOPS_KEYS'][$key]['ts']) { $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']--; if (-1 === $metadata['EXPIRE_HOPS_KEYS'][$key]['hops']) { unset($metadata['EXPIRE_HOPS_KEYS'][$key]); $storage->setMetadata($name, $metadata, true); unset($storage[$name][$key]); continue; } $metadata['EXPIRE_HOPS_KEYS'][$key]['ts'] = $ts; } } $storage->setMetadata($name, $metadata, true); return false; } return false; } /** * Store a value within the container * * @param string $offset * @param mixed $value * @return void */ public function offsetSet($offset, $value) { $this->expireKeys($offset); $storage = $this->verifyNamespace(); $name = $this->getName(); $storage[$name][$offset] = $value; } /** * Determine if the key exists * * @param string $key * @return bool */ public function offsetExists($key) { // If no container exists, we can't inspect it if (null === ($storage = $this->verifyNamespace(false))) { return false; } $name = $this->getName(); // Return early if the key isn't set if (! isset($storage[$name][$key])) { return false; } $expired = $this->expireKeys($key); return ! $expired; } /** * Retrieve a specific key in the container * * @param string $key * @return mixed */ public function &offsetGet($key) { if (! $this->offsetExists($key)) { return $this->defaultValue; } $storage = $this->getStorage(); $name = $this->getName(); return $storage[$name][$key]; } /** * Unset a single key in the container * * @param string $offset * @return void */ public function offsetUnset($offset) { if (! $this->offsetExists($offset)) { return; } $storage = $this->getStorage(); $name = $this->getName(); unset($storage[$name][$offset]); } /** @inheritDoc */ public function exchangeArray($input) { // handle arrayobject, iterators and the like: if (is_object($input) && ($input instanceof ArrayObject || $input instanceof \ArrayObject)) { $input = $input->getArrayCopy(); } if (! is_array($input)) { $input = (array) $input; } $storage = $this->verifyNamespace(); $name = $this->getName(); $old = $storage[$name]; $storage[$name] = $input; if ($old instanceof ArrayObject) { return $old->getArrayCopy(); } return $old; } /** @inheritDoc */ public function getIterator() { $this->expireKeys(); $storage = $this->getStorage(); $container = $storage[$this->getName()]; if ($container instanceof Traversable) { return $container; } return new ArrayIterator($container); } /** * Set expiration TTL * * Set the TTL for the entire container, a single key, or a set of keys. * * @param int $ttl TTL in seconds * @param string|array|null $vars * @return Container * @throws Exception\InvalidArgumentException */ public function setExpirationSeconds($ttl, $vars = null) { $storage = $this->getStorage(); $ts = time() + $ttl; if (is_scalar($vars) && null !== $vars) { $vars = (array) $vars; } if (null === $vars) { $this->expireKeys(); // first we need to expire global key, since it can already be expired $data = ['EXPIRE' => $ts]; } elseif (is_array($vars)) { // Cannot pass "$this" to a lambda $container = $this; // Filter out any items not in our container $expires = array_filter($vars, static fn($value): bool => $container->offsetExists($value)); // Map item keys => timestamp $expires = array_flip($expires); $expires = array_map(static fn() => $ts, $expires); // Create metadata array to merge in $data = ['EXPIRE_KEYS' => $expires]; } else { throw new Exception\InvalidArgumentException( 'Unknown data provided as second argument to ' . __METHOD__ ); } $storage->setMetadata( $this->getName(), $data ); return $this; } /** * Set expiration hops for the container, a single key, or set of keys * * @param int $hops * @param null|string|array $vars * @throws Exception\InvalidArgumentException * @return Container */ public function setExpirationHops($hops, $vars = null) { $storage = $this->getStorage(); $ts = $storage->getRequestAccessTime(); if (is_scalar($vars)) { $vars = (array) $vars; } if (null === $vars) { $this->expireKeys(); // first we need to expire global key, since it can already be expired $data = ['EXPIRE_HOPS' => ['hops' => $hops, 'ts' => $ts]]; } elseif (is_array($vars)) { // Cannot pass "$this" to a lambda $container = $this; // FilterInterface out any items not in our container $expires = array_filter($vars, static fn($value): bool => $container->offsetExists($value)); // Map item keys => timestamp $expires = array_flip($expires); $expires = array_map(static fn() => ['hops' => $hops, 'ts' => $ts], $expires); // Create metadata array to merge in $data = ['EXPIRE_HOPS_KEYS' => $expires]; } else { throw new Exception\InvalidArgumentException( 'Unknown data provided as second argument to ' . __METHOD__ ); } $storage->setMetadata( $this->getName(), $data ); return $this; } /** @inheritDoc */ public function getArrayCopy() { $storage = $this->verifyNamespace(); $container = $storage[$this->getName()]; return $container instanceof ArrayObject ? $container->getArrayCopy() : $container; } }