Stratadox/IdentityMap

View on GitHub
src/IdentityMap.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
declare(strict_types=1);

namespace Stratadox\IdentityMap;

use function array_keys;
use function array_merge as these;
use function array_values as allThese;
use function assert as makeSureThat;
use function get_class as theClassOfThe;
use function is_null as weDidNotYetList;
use function is_object as itIsAn;
use function spl_object_id as theInstanceIdOf;

/**
 * Contains objects by class and id.
 *
 * @author Stratadox
 */
final class IdentityMap implements MapsObjectsByIdentity
{
    private $objectWith;
    private $entityIdFor;
    private $objects;

    private function __construct(array $objectsByClassAndId, array $idsByObject)
    {
        $this->objectWith = $objectsByClassAndId;
        $this->entityIdFor = $idsByObject;
    }

    /**
     * Produces a new identity map that contains the objects.
     *
     * @param object[] $theseObjects The objects to add, as [id => object]
     * @return MapsObjectsByIdentity The map of objects.
     */
    public static function with(array $theseObjects): MapsObjectsByIdentity
    {
        $objects = [];
        $entityIds = [];
        foreach ($theseObjects as $id => $object) {
            $objects = IdentityMap::addTo($objects, (string) $id, $object);
            $entityIds[theInstanceIdOf($object)] = (string) $id;
        }
        return new self($objects, $entityIds);
    }

    /**
     * Produces an empty identity map.
     *
     * @return MapsObjectsByIdentity The empty map of objects.
     */
    public static function startEmpty(): MapsObjectsByIdentity
    {
        return new self([], []);
    }

    /** @inheritdoc */
    public function has(string $class, string $id): bool
    {
        return isset($this->objectWith[$class][$id]);
    }

    /** @inheritdoc */
    public function hasThe(object $object): bool
    {
        return isset($this->entityIdFor[theInstanceIdOf($object)]);
    }

    /** @inheritdoc */
    public function get(string $class, string $id): object
    {
        $this->mustHave($class, $id);
        return $this->objectWith[$class][$id];
    }

    /** @inheritdoc */
    public function add(string $id, object $object): MapsObjectsByIdentity
    {
        $class = theClassOfThe($object);
        $this->mayNotAlreadyHave($class, $id);

        $new = clone $this;
        $new->objectWith[$class][$id] = $object;
        $new->entityIdFor[theInstanceIdOf($object)] = $id;
        return $new;
    }

    /** @inheritdoc */
    public function remove(string $class, string $id): MapsObjectsByIdentity
    {
        $this->mustHave($class, $id);
        $entityIdFor = $this->entityIdFor;
        $objectWith = $this->objectWith;
        unset(
            $entityIdFor[theInstanceIdOf($objectWith[$class][$id])],
            $objectWith[$class][$id]
        );
        return new IdentityMap($objectWith, $entityIdFor);
    }

    /** @inheritdoc */
    public function removeThe(object $object): MapsObjectsByIdentity
    {
        return $this->remove(theClassOfThe($object), $this->idOf($object));
    }

    /** @inheritdoc */
    public function idOf(object $object): string
    {
        if (!isset($this->entityIdFor[theInstanceIdOf($object)])) {
            throw IdentityNotFound::forThe($object);
        }
        return $this->entityIdFor[theInstanceIdOf($object)];
    }

    public function objects(): array
    {
        if (weDidNotYetList($this->objects)) {
            $objects = [];
            foreach ($this->objectWith as $class => $objectsById) {
                $objects[] = allThese($objectsById);
            }
            $this->objects = these(...$objects);
        }
        return $this->objects;
    }

    /** @throws NoSuchObject */
    private function mustHave(string $class, string $id): void
    {
        if ($this->has($class, $id)) {
            return;
        }
        throw IdentityNotFound::requesting($class, $id);
    }

    /** @throws AlreadyThere */
    private function mayNotAlreadyHave(string $class, string $id): void
    {
        if ($this->has($class, $id)) {
            throw DuplicationDetected::in($class, $id);
        }
    }

    private static function addTo(
        array $objectsBy,
        string $withId,
        object $object
    ): array {
        $objectsBy[theClassOfThe($object)][$withId] = $object;
        return $objectsBy;
    }
}