Created
May 9, 2025 03:23
-
-
Save pscheit/8c36380e09c566baf8603eb14ffec74a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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]); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Makes code so much more readable than array_key_exists, isset and using plan arrays: