ValueHasher   A
last analyzed

Complexity

Total Complexity 29

Size/Duplication

Total Lines 213
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 213
c 0
b 0
f 0
wmc 29
lcom 1
cbo 5
ccs 68
cts 68
cp 1
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C equals() 0 29 7
A getEquivalences() 0 4 1
A getEquivalence() 0 16 4
B equivalentArray() 0 20 6
B equivalentObject() 0 26 6
B hashObject() 0 23 4
1
<?php
2
3
/**
4
 * This file is part of the phpcommon/comparison package.
5
 *
6
 * (c) Marcos Passos <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE file
9
 * that was distributed with this source code.
10
 */
11
12
namespace PhpCommon\Comparison\Hasher;
13
14
use InvalidArgumentException;
15
use PhpCommon\Comparison\Equatable;
16
use PhpCommon\Comparison\Equivalence;
17
use PhpCommon\Comparison\Hasher;
18
use PhpCommon\Comparison\Hashable;
19
20
/**
21
 * Provides an external means for producing hash codes and comparing values for
22
 * equality.
23
 *
24
 * This equivalence relation delegates the equality check and hashing
25
 * strategy to the methods {@link PhpCommon\Comparison\Equatable::equals()} and
26
 * {@link PhpCommon\Comparison\Hashable::getHash()}, whenever the handled
27
 * values are instances of `Equatable` and `Hashable` respectively.
28
 *
29
 * @author Marcos Passos <[email protected]>
30
 */
31
class ValueHasher extends IdentityHasher
32
{
33
    /**
34
     * Maps a class name to an equivalence relation.
35
     *
36
     * @var Equivalence[]
37
     */
38
    protected $equivalences = [];
39
40
    /**
41
     * Creates a new value based equivalence relation.
42
     *
43
     * @param array $equivalences The equivalence relation mapping, with class
44
     *                            names as keys and relations as value.
45
     */
46 156
    public function __construct(array $equivalences = [])
47
    {
48 156
        $this->equivalences = $equivalences;
49 156
    }
50
51
    /**
52
     * {@inheritdoc}
53
     *
54
     * Two instances are considered equals if they are of the same
55
     * and if every type-specific relation defined in one is equal to the
56
     * corresponding relation in the other.
57
     */
58 20
    public function equals(Equatable $other)
59
    {
60 20
        if ($this === $other) {
61 4
            return true;
62
        }
63
64 16
        if (!parent::equals($other)) {
65 2
            return false;
66
        }
67
68
        /** @var ValueHasher $other */
69 14
        $equivalences = $other->getEquivalences();
70
71 14
        if (count($this->equivalences) !== count($equivalences)) {
72 2
            return false;
73
        }
74
75 12
        foreach ($this->equivalences as $type => $equivalence) {
76 8
            if (!isset($equivalences[$type])) {
77 2
                return false;
78
            }
79
80 6
            if (!$equivalence->equals($equivalences[$type])) {
81 2
                return false;
82
            }
83 8
        }
84
85 8
        return true;
86
    }
87
88
    /**
89
     * Returns the type-specific equivalence relations.
90
     *
91
     * @return Equivalence[] The type-specific relations, with class names as
92
     *                       keys and relations as values.
93
     */
94 14
    public function getEquivalences()
95
    {
96 14
        return $this->equivalences;
97
    }
98
99
    /**
100
     * Returns an equivalence relation suitable for comparing objects of the
101
     * specified class, if any.
102
     *
103
     * When no relation is explicitly defined for the specified class, this
104
     * method traverses up the class hierarchy to find the nearest ancestor for
105
     * which a relation is specified. For example, a relation specified for the
106
     * class `Vehicle` is used to compare instances of its subclass `Car`, when
107
     * no relation is explicitly specified for it.
108
     *
109
     * @param string $className The fully qualified name of the class for which
110
     *                          the relation should be suitable for.
111
     *
112
     * @return Equivalence|boolean The relation suitable for comparing objects
113
     *                             of the specified class, or `null` no
114
     *                             suitable relation is found.
115
     */
116 34
    protected function getEquivalence($className)
117
    {
118 34
        if (empty($this->equivalences)) {
119 28
            return false;
120
        }
121
122 6
        if (isset($this->equivalences[$className])) {
123 6
            return $this->equivalences[$className];
124
        }
125
126 6
        if (($parent = get_parent_class($className)) !== false) {
127 4
            return $this->getEquivalence($parent);
128
        }
129
130 2
        return false;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 72
    protected function equivalentArray(array $left, $right)
137
    {
138 72
        if (!is_array($right) || count($left) !== count($right)) {
139 8
            return false;
140
        }
141
142 64
        foreach ($left as $key => $value) {
143 54
            if (!$this->equivalentString($key, key($right))) {
144 4
                return false;
145
            }
146
147 50
            if (!$this->equivalent($value, current($right))) {
148 28
                return false;
149
            }
150
151 26
            next($right);
152 36
        }
153
154 32
        return true;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     *
160
     * The values are considered equivalent if any of the following conditions
161
     * hold:
162
     *
163
     * 1. The reference value is an instance of {@link Equatable} and the
164
     *    expression `$left->equals($right)` is evaluated to `true`
165
     * 2. A specific equivalence relation is mapped to the type of the left-hand
166
     *    value and the expression `$relation->equivalent($left, $right)` is
167
     *    evaluated to `true`
168
     * 3. Both values refer to the same instance of the same class (in a
169
     *    particular namespace)
170
     */
171 62
    protected function equivalentObject($left, $right)
172
    {
173 62
        if ($left instanceof Equatable xor $right instanceof Equatable) {
174 6
            return false;
175
        }
176
177 56
        if ($left instanceof Equatable) {
178 24
            return $left->equals($right);
179
        }
180
181 32
        $equivalence = $this->getEquivalence(get_class($left));
182
183 32
        if ($equivalence !== false) {
184 4
            return $equivalence->equivalent($left, $right);
185
        }
186
187 30
        if (is_object($right)) {
188 26
            $equivalence = $this->getEquivalence(get_class($right));
189
190 26
            if ($equivalence !== false) {
191 2
                return $equivalence->equivalent($right, $left);
192
            }
193 24
        }
194
195 28
        return parent::equivalentObject($left, $right);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     *
201
     * The resulting hash code is guaranteed to be _consistent_ with the
202
     * {@link equivalentObject()} method, which means that for any references
203
     * `$x` and `$y`, if `equivalentObject($x, $y)`, then
204
     * `hashObject($x) === hashObject($y)`.
205
     *
206
     * The hash code is computed as follows:
207
     *
208
     * 1. If the specified object is an instance of {@link Hashable}, delegates
209
     *    the hashing strategy to the object being hashed.
210
     * 2. If a specific equivalence relation of type {@link Hasher} is mapped
211
     *    to the type of the given object, then uses the method
212
     *    {@link PhpCommon\Comparison\Hasher::hash()} as the hashing function
213
     * 3. If none of the previous rules apply, uses the method
214
     *    {@link PhpCommon\Comparison\Hasher\IdentityHasher::hashObject()} as
215
     *    the hash function
216
     *
217
     * @see hashObject()
218
     * @see PhpCommon\Comparison\Hashable::getHash()
219
     */
220 14
    protected function hashObject($value)
221
    {
222 14
        if ($value instanceof Hashable) {
223 6
            return self::HASH_OBJECT + $value->getHash();
224 8
        } elseif ($value instanceof Equatable) {
225 2
            throw new InvalidArgumentException(sprintf(
226
                'Any object implementing %s interface must also implement %s ' .
227 2
                'interface, otherwise the resulting hash code cannot be ' .
228 2
                'guaranteed by %s to be distributable across equivalences.',
229 2
                Equatable::class,
230 2
                Hashable::class,
231 2
                static::class
232 2
            ));
233
        }
234
235 6
        $equivalence = $this->getEquivalence(get_class($value));
236
237 6
        if ($equivalence instanceof Hasher) {
238 2
            return self::HASH_OBJECT + $equivalence->hash($value);
239
        }
240
241 4
        return parent::hashObject($value);
242
    }
243
}
244