Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.45% covered (success)
95.45%
42 / 44
84.62% covered (warning)
84.62%
11 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
ItemHashmap
95.45% covered (success)
95.45%
42 / 44
84.62% covered (warning)
84.62%
11 / 13
23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 first
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetGet
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 offsetCheck
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 typeCheck
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 offsetSet
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 offsetUnset
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
1<?php
2namespace Apie\Core\Lists;
3
4use Apie\Core\Exceptions\IndexNotFoundException;
5use Apie\Core\Exceptions\InvalidTypeException;
6use Apie\Core\Exceptions\ObjectIsEmpty;
7use Apie\Core\Exceptions\ObjectIsImmutable;
8use Apie\Core\TypeUtils;
9use Apie\Core\ValueObjects\Utils;
10use ArrayIterator;
11use Iterator;
12use ReflectionClass;
13use ReflectionType;
14use stdClass;
15
16/**
17 * @template T
18 * @implements HashmapInterface<T>
19 */
20class ItemHashmap implements HashmapInterface
21{
22    protected stdClass $internal;
23    /**
24     * @var array<string|int, T>
25     */
26    protected array $internalArray = [];
27
28    /** @var ReflectionType[] */
29    private static $typeMapping = [];
30
31    protected bool $mutable = true;
32
33    /**
34     * @param array<string|int, T>|stdClass $input
35     */
36    final public function __construct(array|stdClass $input = [])
37    {
38        $this->internal = new stdClass();
39        $oldMutable = $this->mutable;
40        $this->mutable = true;
41        foreach ($input as $key => $item) {
42            $this->offsetSet($key, $item);
43        }
44        $this->mutable = $oldMutable;
45    }
46
47    /**
48     * @return T
49     */
50    public function first(): mixed
51    {
52        if (empty($this->internalArray)) {
53            throw ObjectIsEmpty::createForHashmap();
54        }
55        return reset($this->internalArray);
56    }
57
58    public function count(): int
59    {
60        return count($this->internalArray);
61    }
62
63    /**
64     * @return array<string|int, T>
65     */
66    public function toArray(): array
67    {
68        return $this->internalArray;
69    }
70
71    /**
72     * @return Iterator<string|int, T>
73     */
74    public function getIterator(): Iterator
75    {
76        return new ArrayIterator($this->internalArray);
77    }
78
79    /**
80     * @return stdClass
81     */
82    public function jsonSerialize(): stdClass
83    {
84        return $this->internal;
85    }
86
87    public function offsetExists(mixed $offset): bool
88    {
89        return array_key_exists($offset, $this->internalArray);
90    }
91
92    public function offsetGet(mixed $offset): mixed
93    {
94        $offset = Utils::toString($offset);
95        if (!array_key_exists($offset, $this->internalArray)) {
96            throw new IndexNotFoundException($offset);
97        }
98        return $this->internalArray[$offset];
99    }
100
101    protected function getType(): ReflectionType
102    {
103        $currentClass = static::class;
104        if (!isset(self::$typeMapping[$currentClass])) {
105            self::$typeMapping[$currentClass] = (new ReflectionClass($currentClass))->getMethod('offsetGet')->getReturnType();
106        }
107        return self::$typeMapping[$currentClass];
108    }
109
110    protected function offsetCheck(mixed $value): string
111    {
112        if ($value === null) { // append
113            return (string) count($this->internalArray);
114        }
115        return Utils::toString($value);
116    }
117
118    protected function typeCheck(mixed $value): void
119    {
120        if (static::class === ItemHashmap::class) {
121            return;
122        }
123        $type = $this->getType();
124        if (!TypeUtils::matchesType($type, $value)) {
125            throw new InvalidTypeException($value, $type->__toString());
126        }
127    }
128    
129    public function offsetSet(mixed $offset, mixed $value): void
130    {
131        if (!$this->mutable) {
132            throw new ObjectIsImmutable($this);
133        }
134        $offset = $this->offsetCheck($offset);
135        $this->typeCheck($value);
136        $this->internal->{$offset} = $value;
137        $this->internalArray[$offset] = $value;
138    }
139
140    public function offsetUnset(mixed $offset): void
141    {
142        if (!$this->mutable) {
143            throw new ObjectIsImmutable($this);
144        }
145        $offset = Utils::toString($offset);
146        if (array_key_exists($offset, $this->internalArray)) {
147            unset($this->internalArray[$offset]);
148            unset($this->internal->{$offset});
149            return;
150        }
151        
152        throw new IndexNotFoundException($offset);
153    }
154}