Newer
Older
framework / system / Cookie / CookieStore.php
@MGatner MGatner on 18 May 2021 5 KB Release v4.1.2
<?php

/**
 * This file is part of the CodeIgniter 4 framework.
 *
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CodeIgniter\Cookie;

use ArrayIterator;
use CodeIgniter\Cookie\Exceptions\CookieException;
use Countable;
use IteratorAggregate;
use Traversable;

/**
 * The CookieStore object represents an immutable collection of `Cookie` value objects.
 *
 * @implements IteratorAggregate<string, Cookie>
 */
class CookieStore implements Countable, IteratorAggregate
{
	/**
	 * The cookie collection.
	 *
	 * @var array<string, Cookie>
	 */
	protected $cookies = [];

	/**
	 * Creates a CookieStore from an array of `Set-Cookie` headers.
	 *
	 * @param string[] $headers
	 * @param boolean  $raw
	 *
	 * @throws CookieException
	 *
	 * @return static
	 */
	public static function fromCookieHeaders(array $headers, bool $raw = false)
	{
		/**
		 * @var Cookie[] $cookies
		 */
		$cookies = array_filter(array_map(function (string $header) use ($raw) {
			try
			{
				return Cookie::fromHeaderString($header, $raw);
			}
			catch (CookieException $e)
			{
				log_message('error', $e->getMessage());
				return false;
			}
		}, $headers));

		return new static($cookies);
	}

	/**
	 * @param Cookie[] $cookies
	 *
	 * @throws CookieException
	 */
	final public function __construct(array $cookies)
	{
		$this->validateCookies($cookies);

		foreach ($cookies as $cookie)
		{
			$this->cookies[$cookie->getId()] = $cookie;
		}
	}

	/**
	 * Checks if a `Cookie` object identified by name and
	 * prefix is present in the collection.
	 *
	 * @param string      $name
	 * @param string      $prefix
	 * @param string|null $value
	 *
	 * @return boolean
	 */
	public function has(string $name, string $prefix = '', string $value = null): bool
	{
		$name = $prefix . $name;

		foreach ($this->cookies as $cookie)
		{
			if ($cookie->getPrefixedName() !== $name)
			{
				continue;
			}

			if ($value === null)
			{
				return true; // for BC
			}

			return $cookie->getValue() === $value;
		}

		return false;
	}

	/**
	 * Retrieves an instance of `Cookie` identified by a name and prefix.
	 * This throws an exception if not found.
	 *
	 * @param string $name
	 * @param string $prefix
	 *
	 * @throws CookieException
	 *
	 * @return Cookie
	 */
	public function get(string $name, string $prefix = ''): Cookie
	{
		$name = $prefix . $name;

		foreach ($this->cookies as $cookie)
		{
			if ($cookie->getPrefixedName() === $name)
			{
				return $cookie;
			}
		}

		throw CookieException::forUnknownCookieInstance([$name, $prefix]);
	}

	/**
	 * Store a new cookie and return a new collection. The original collection
	 * is left unchanged.
	 *
	 * @param Cookie $cookie
	 *
	 * @return static
	 */
	public function put(Cookie $cookie)
	{
		$store = clone $this;

		$store->cookies[$cookie->getId()] = $cookie;

		return $store;
	}

	/**
	 * Removes a cookie from a collection and returns an updated collection.
	 * The original collection is left unchanged.
	 *
	 * Removing a cookie from the store **DOES NOT** delete it from the browser.
	 * If you intend to delete a cookie *from the browser*, you must put an empty
	 * value cookie with the same name to the store.
	 *
	 * @param string $name
	 * @param string $prefix
	 *
	 * @return static
	 */
	public function remove(string $name, string $prefix = '')
	{
		$default = Cookie::setDefaults();

		$id = implode(';', [$prefix . $name, $default['path'], $default['domain']]);

		$store = clone $this;

		foreach (array_keys($store->cookies) as $index)
		{
			if ($index === $id)
			{
				unset($store->cookies[$index]);
			}
		}

		return $store;
	}

	/**
	 * Dispatches all cookies in store.
	 *
	 * @return void
	 */
	public function dispatch(): void
	{
		foreach ($this->cookies as $cookie)
		{
			$name    = $cookie->getPrefixedName();
			$value   = $cookie->getValue();
			$options = $cookie->getOptions();

			if ($cookie->isRaw())
			{
				$this->setRawCookie($name, $value, $options);
			}
			else
			{
				$this->setCookie($name, $value, $options);
			}
		}

		$this->clear();
	}

	/**
	 * Returns all cookie instances in store.
	 *
	 * @return array<string, Cookie>
	 */
	public function display(): array
	{
		return $this->cookies;
	}

	/**
	 * Clears the cookie collection.
	 *
	 * @return void
	 */
	public function clear(): void
	{
		$this->cookies = [];
	}

	/**
	 * Gets the Cookie count in this collection.
	 *
	 * @return integer
	 */
	public function count(): int
	{
		return count($this->cookies);
	}

	/**
	 * Gets the iterator for the cookie collection.
	 *
	 * @return Traversable<string, Cookie>
	 */
	public function getIterator()
	{
		return new ArrayIterator($this->cookies);
	}

	/**
	 * Validates all cookies passed to be instances of Cookie.
	 *
	 * @param array $cookies
	 *
	 * @throws CookieException
	 *
	 * @return void
	 */
	protected function validateCookies(array $cookies): void
	{
		foreach ($cookies as $index => $cookie)
		{
			$type = is_object($cookie) ? get_class($cookie) : gettype($cookie);

			if (! $cookie instanceof Cookie)
			{
				throw CookieException::forInvalidCookieInstance([static::class, Cookie::class, $type, $index]);
			}
		}
	}

	/**
	 * Extracted call to `setrawcookie()` in order to run unit tests on it.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $name
	 * @param string $value
	 * @param array  $options
	 *
	 * @return void
	 */
	protected function setRawCookie(string $name, string $value, array $options): void
	{
		setrawcookie($name, $value, $options);
	}

	/**
	 * Extracted call to `setcookie()` in order to run unit tests on it.
	 *
	 * @codeCoverageIgnore
	 *
	 * @param string $name
	 * @param string $value
	 * @param array  $options
	 *
	 * @return void
	 */
	protected function setCookie(string $name, string $value, array $options): void
	{
		setcookie($name, $value, $options);
	}
}