> * * @author Zaahid Bateson */ class PartHeaderContainer extends ErrorBag implements IteratorAggregate { /** * @var HeaderFactory the HeaderFactory object used for created headers */ protected HeaderFactory $headerFactory; /** * @var string[][] Each element in the array is an array with its first * element set to the header's name, and the second its value. */ private array $headers = []; /** * @var array Each element is an IHeader * representing the header at the same index in the $headers array. If * an IHeader has not been constructed for the header at that index, * the element would be set to null. */ private array $headerObjects = []; /** * @var array Maps header names by their "normalized" (lower-cased, * non-alphanumeric characters stripped) name to an array of indexes in * the $headers array. For example: * $headerMap['contenttype'] = [ 1, 4 ] * would indicate that the headers in $headers[1] and $headers[4] are * both headers with the name 'Content-Type' or 'contENTtype'. */ private array $headerMap = []; /** * @var int the next index to use for $headers and $headerObjects. */ private int $nextIndex = 0; /** * Pass a PartHeaderContainer as the second parameter. This is useful when * creating a new MimePart with this PartHeaderContainer and the original * container is needed for parsing and changes to the header in the part * should not affect parsing. * * @param PartHeaderContainer $cloneSource the original container to clone * from */ public function __construct( LoggerInterface $logger, HeaderFactory $headerFactory, ?PartHeaderContainer $cloneSource = null ) { parent::__construct($logger); $this->headerFactory = $headerFactory; if ($cloneSource !== null) { $this->headers = $cloneSource->headers; $this->headerObjects = $cloneSource->headerObjects; $this->headerMap = $cloneSource->headerMap; $this->nextIndex = $cloneSource->nextIndex; $this->copyErrorsFrom($cloneSource); } } /** * Returns true if the passed header exists in this collection. */ public function exists(string $name, int $offset = 0) : bool { $s = $this->headerFactory->getNormalizedHeaderName($name); return isset($this->headerMap[$s][$offset]); } /** * Returns an array of header indexes with names that more closely match * the passed $name if available: for instance if there are two headers in * an email, "Content-Type" and "ContentType", and the query is for a header * with the name "Content-Type", only headers that match exactly * "Content-Type" would be returned. * * @return int[]|null */ private function getAllWithOriginalHeaderNameIfSet(string $name) : ?array { $s = $this->headerFactory->getNormalizedHeaderName($name); if (isset($this->headerMap[$s])) { $filtered = \array_filter($this->headerMap[$s], fn($h) => \strcasecmp($this->headers[$h][0], $name) === 0); return (!empty($filtered)) ? $filtered : $this->headerMap[$s]; } return null; } /** * Returns the IHeader object for the header with the given $name, or null * if none exist. * * An optional offset can be provided, which defaults to the first header in * the collection when more than one header with the same name exists. * * Note that mime headers aren't case sensitive. */ public function get(string $name, int $offset = 0) : ?IHeader { $a = $this->getAllWithOriginalHeaderNameIfSet($name); if (!empty($a) && isset($a[$offset])) { return $this->getByIndex($a[$offset]); } return null; } /** * Returns the IHeader object for the header with the given $name, or null * if none exist, using the passed $iHeaderClass to construct it. * * An optional offset can be provided, which defaults to the first header in * the collection when more than one header with the same name exists. * * Note that mime headers aren't case sensitive. */ public function getAs(string $name, string $iHeaderClass, int $offset = 0) : ?IHeader { $a = $this->getAllWithOriginalHeaderNameIfSet($name); if (!empty($a) && isset($a[$offset])) { return $this->getByIndexAs($a[$offset], $iHeaderClass); } return null; } /** * Returns all headers with the passed name. * * @return IHeader[] */ public function getAll(string $name) : array { $a = $this->getAllWithOriginalHeaderNameIfSet($name); if (!empty($a)) { return \array_map(fn($index) => $this->getByIndex($index), $a); } return []; } /** * Returns the header in the headers array at the passed 0-based integer * index or null if one doesn't exist. */ private function getByIndex(int $index) : ?IHeader { if (!isset($this->headers[$index])) { return null; } if ($this->headerObjects[$index] === null) { $this->headerObjects[$index] = $this->headerFactory->newInstance( $this->headers[$index][0], $this->headers[$index][1] ); } return $this->headerObjects[$index]; } /** * Returns the header in the headers array at the passed 0-based integer * index or null if one doesn't exist, using the passed $iHeaderClass to * construct it. */ private function getByIndexAs(int $index, string $iHeaderClass) : ?IHeader { if (!isset($this->headers[$index])) { return null; } if ($this->headerObjects[$index] !== null && \get_class($this->headerObjects[$index]) === $iHeaderClass) { return $this->headerObjects[$index]; } return $this->headerFactory->newInstanceOf( $this->headers[$index][0], $this->headers[$index][1], $iHeaderClass ); } /** * Removes the header from the collection with the passed name. Defaults to * removing the first instance of the header for a collection that contains * more than one with the same passed name. * * @return bool true if a header was found and removed. */ public function remove(string $name, int $offset = 0) : bool { $s = $this->headerFactory->getNormalizedHeaderName($name); if (isset($this->headerMap[$s][$offset])) { $index = $this->headerMap[$s][$offset]; \array_splice($this->headerMap[$s], $offset, 1); unset($this->headers[$index], $this->headerObjects[$index]); return true; } return false; } /** * Removes all headers that match the passed name. * * @return bool true if one or more headers were removed. */ public function removeAll(string $name) : bool { $s = $this->headerFactory->getNormalizedHeaderName($name); if (!empty($this->headerMap[$s])) { foreach ($this->headerMap[$s] as $i) { unset($this->headers[$i], $this->headerObjects[$i]); } $this->headerMap[$s] = []; return true; } return false; } /** * Adds the header to the collection. */ public function add(string $name, string $value) : static { $s = $this->headerFactory->getNormalizedHeaderName($name); $this->headers[$this->nextIndex] = [$name, $value]; $this->headerObjects[$this->nextIndex] = null; if (!isset($this->headerMap[$s])) { $this->headerMap[$s] = []; } $this->headerMap[$s][] = $this->nextIndex; $this->nextIndex++; return $this; } /** * If a header exists with the passed name, and at the passed offset if more * than one exists, its value is updated. * * If a header with the passed name doesn't exist at the passed offset, it * is created at the next available offset (offset is ignored when adding). */ public function set(string $name, string $value, int $offset = 0) : static { $s = $this->headerFactory->getNormalizedHeaderName($name); if (!isset($this->headerMap[$s][$offset])) { $this->add($name, $value); return $this; } $i = $this->headerMap[$s][$offset]; $this->headers[$i] = [$name, $value]; $this->headerObjects[$i] = null; return $this; } /** * Returns an array of IHeader objects representing all headers in this * collection. * * @return IHeader[] */ public function getHeaderObjects() : array { return \array_filter(\array_map($this->getByIndex(...), \array_keys($this->headers))); } /** * Returns an array of headers in this collection. Each returned element in * the array is an array with the first element set to the name, and the * second its value: * * [ * [ 'Header-Name', 'Header Value' ], * [ 'Second-Header-Name', 'Second-Header-Value' ], * // etc... * ] * * @return string[][] */ public function getHeaders() : array { return \array_values(\array_filter($this->headers)); } /** * Returns an iterator to the headers in this collection. Each returned * element is an array with its first element set to the header's name, and * the second to its value: * * [ 'Header-Name', 'Header Value' ] * * @return Traversable> */ public function getIterator() : Traversable { return new ArrayIterator($this->getHeaders()); } public function getErrorBagChildren() : array { return \array_values(\array_filter($this->headerObjects)); } }