Skip to content

Instantly share code, notes, and snippets.

@pscheit
Created May 9, 2025 03:23
Show Gist options
  • Select an option

  • Save pscheit/8c36380e09c566baf8603eb14ffec74a to your computer and use it in GitHub Desktop.

Select an option

Save pscheit/8c36380e09c566baf8603eb14ffec74a to your computer and use it in GitHub Desktop.

Revisions

  1. pscheit created this gist May 9, 2025.
    132 changes: 132 additions & 0 deletions yay-map.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,132 @@
    <?php declare(strict_types=1);

    namespace YAY;

    use Countable;
    use IteratorAggregate;
    use Traversable;
    use Webmozart\Assert\Assert;

    /**
    * @template T of object
    * @implements IteratorAggregate<string, T>
    */
    final class Map implements IteratorAggregate, Countable
    {
    /**
    * @var array<string, T>
    */
    private array $objects;

    /**
    * @param array<T> $objects
    * @param class-string<T> $type
    * @param \Closure(T $object): non-empty-string $indexer
    */
    private function __construct(
    private readonly string $type,
    private readonly \Closure $indexer,
    array $objects,
    ) {
    $this->objects = [];
    foreach ($objects as $object) {
    $this->add($object);
    }
    }

    /**
    * @template TCreate of object
    * @param array<TCreate> $objects
    * @param class-string<TCreate> $type
    * @return Map<TCreate>
    */
    public static function byProperty(string $propertyName, string $type, array $objects): self
    {
    $indexer = function (object $object) use ($type, $propertyName): string {
    $value = $object->{$propertyName};

    if (!is_string($value) && !is_int($value)) {
    throw new \LogicException('Cannot use property ' . $propertyName . ' of ' . $object::class . ' as index, because I must be able to cast it to string.');
    }

    $stringedValue = (string) $value;

    if ($stringedValue === "") {
    throw new \RuntimeException('The indexer did not index an object correctly: ' . $type . '. The string was empty.');
    }

    return $stringedValue;
    };

    return new self($type, $indexer, $objects);
    }

    /**
    * @template TCreate of object
    * @param array<TCreate> $objects
    * @param class-string<TCreate> $type
    * @return Map<TCreate>
    */
    public static function byMethod(string $methodName, string $type, array $objects): self
    {
    $indexer = function (object $object) use ($type, $methodName): string {
    $value = $object->{$methodName}();

    if (!is_string($value) && !is_int($value)) {
    throw new \LogicException('Cannot use return value from method ' . $methodName . ' of ' . $object::class . ' as index, because I must be able to cast it to string.');
    }

    $stringedValue = (string) $value;

    if ($stringedValue === "") {
    throw new \RuntimeException('The indexer did not index an object correctly: ' . $type . '. The string was empty.');
    }

    return $stringedValue;
    };

    return new self($type, $indexer, $objects);
    }

    public function has(string $key): bool
    {
    return array_key_exists($key, $this->objects);
    }

    /**
    * @param T $object
    */
    public function add(object $object): void
    {
    Assert::isInstanceOf($object, $this->type); // @phpstan-ignore staticMethod.alreadyNarrowedType
    $index = ($this->indexer)($object);
    $this->objects[$index] = $object;
    }

    /**
    * @return T
    */
    public function get(string $key)
    {
    if (!$this->has($key)) {
    throw new \OutOfBoundsException('Object not found by indexed ' . $key);
    }

    return $this->objects[$key];
    }

    public function getIterator(): Traversable
    {
    return new \ArrayIterator($this->objects);
    }

    public function count(): int
    {
    return count($this->objects);
    }

    public function remove(string $key): void
    {
    unset($this->objects[$key]);
    }
    }