Completed
Push — master ( e306e7...5688ab )
by Bekh-Ivanov
23:04 queued 19:48
created

HashArray   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 80%

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 1
dl 0
loc 324
ccs 76
cts 95
cp 0.8
rs 9
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
getObjectType() 0 1 ?
B __construct() 0 13 5
A getNewOffset() 0 7 2
A preSetElement() 0 14 2
A hasElementHash() 0 3 1
A hasElement() 0 3 1
A removeElement() 0 3 1
A removeByElementHash() 0 6 2
A addElement() 0 9 2
A getByElementHash() 0 9 2
A offsetUnset() 0 14 2
A equals() 0 8 3
A append() 0 3 1
A offsetSet() 0 3 1
A hasValidType() 0 4 1
B setElement() 0 15 5
A serialize() 0 6 1
A unserialize() 0 11 2
A isEmpty() 0 3 1
1
<?php
2
3
namespace Wikibase\DataModel;
4
5
use ArrayObject;
6
use Comparable;
7
use Hashable;
8
use InvalidArgumentException;
9
use Traversable;
10
11
/**
12
 * Generic array object with lookups based on hashes of the elements.
13
 *
14
 * Elements need to implement Hashable.
15
 *
16
 * Note that by default the getHash method uses @see MapValueHashesr
17
 * which returns a hash based on the contents of the list, regardless
18
 * of order and keys.
19
 *
20
 * Also note that if the Hashable elements are mutable, any modifications
21
 * made to them via their mutator methods will not cause an update of
22
 * their associated hash in this array.
23
 *
24
 * @since 0.1
25
 *
26
 * @license GPL-2.0+
27
 * @author Jeroen De Dauw < [email protected] >
28
 */
29
abstract class HashArray extends ArrayObject implements Comparable {
30
31
	/**
32
	 * Maps element hashes to their offsets.
33
	 *
34
	 * @since 0.1
35
	 *
36
	 * @var array [ element hash (string) => element offset (string|int) ]
37
	 */
38
	protected $offsetHashes = [];
39
40
	/**
41
	 * @var integer
42
	 */
43
	protected $indexOffset = 0;
44
45
	/**
46
	 * Returns the name of an interface/class that the element should implement/extend.
47
	 *
48
	 * @since 0.4
49
	 *
50
	 * @return string
51
	 */
52
	abstract public function getObjectType();
53
54
	/**
55
	 * @see ArrayObject::__construct
56
	 *
57
	 * @param array|Traversable|null $input
58
	 * @param int $flags
59
	 * @param string $iteratorClass
60
	 *
61
	 * @throws InvalidArgumentException
62
	 */
63
	public function __construct( $input = null, $flags = 0, $iteratorClass = 'ArrayIterator' ) {
64
		parent::__construct( [], $flags, $iteratorClass );
65
66
		if ( $input !== null ) {
67
			if ( !is_array( $input ) && !( $input instanceof Traversable ) ) {
68
				throw new InvalidArgumentException( '$input must be an array or Traversable' );
69
			}
70
71
			foreach ( $input as $offset => $value ) {
72
				$this->offsetSet( $offset, $value );
73
			}
74
		}
75
	}
76
77
	/**
78
	 * Finds a new offset for when appending an element.
79
	 * The base class does this, so it would be better to integrate,
80
	 * but there does not appear to be any way to do this...
81
	 *
82
	 * @return integer
83
	 */
84
	protected function getNewOffset() {
85
		while ( $this->offsetExists( $this->indexOffset ) ) {
86
			$this->indexOffset++;
87
		}
88
89
		return $this->indexOffset;
90
	}
91
92
	/**
93
	 * Gets called before a new element is added to the ArrayObject.
94
	 *
95
	 * At this point the index is always set (ie not null) and the
96
	 * value is always of the type returned by @see getObjectType.
97
	 *
98
	 * Should return a boolean. When false is returned the element
99
	 * does not get added to the ArrayObject.
100 8
	 *
101 8
	 * @since 0.1
102 8
	 *
103 8
	 * @param int|string $index
104
	 * @param Hashable $hashable
105 8
	 *
106
	 * @return bool
107
	 */
108
	protected function preSetElement( $index, $hashable ) {
109
		$hash = $hashable->getHash();
110
111
		$hasHash = $this->hasElementHash( $hash );
112
113
		if ( $hasHash ) {
114
			return false;
115
		}
116
		else {
117
			$this->offsetHashes[$hash] = $index;
118
119
			return true;
120
		}
121
	}
122
123
	/**
124 8
	 * Returns if there is an element with the provided hash.
125 8
	 *
126
	 * @since 0.1
127 8
	 *
128
	 * @param string $elementHash
129 8
	 *
130
	 * @return bool
131
	 */
132
	public function hasElementHash( $elementHash ) {
133 8
		return array_key_exists( $elementHash, $this->offsetHashes );
134 4
	}
135 2
136 2
	/**
137
	 * Returns if there is an element with the same hash as the provided element in the list.
138 4
	 *
139 4
	 * @since 0.1
140
	 *
141 4
	 * @param Hashable $element
142
	 *
143
	 * @return bool
144 8
	 */
145
	public function hasElement( Hashable $element ) {
146
		return $this->hasElementHash( $element->getHash() );
147
	}
148
149
	/**
150
	 * Removes the element with the hash of the provided element, if there is such an element in the list.
151
	 *
152
	 * @since 0.1
153
	 *
154
	 * @param Hashable $element
155
	 */
156
	public function removeElement( Hashable $element ) {
157 21
		$this->removeByElementHash( $element->getHash() );
158 21
	}
159
160
	/**
161
	 * Removes the element with the provided hash, if there is such an element in the list.
162
	 *
163
	 * @since 0.1
164
	 *
165
	 * @param string $elementHash
166
	 */
167
	public function removeByElementHash( $elementHash ) {
168
		if ( $this->hasElementHash( $elementHash ) ) {
169
			$offset = $this->offsetHashes[$elementHash];
170 12
			$this->offsetUnset( $offset );
171 12
		}
172
	}
173
174
	/**
175
	 * Adds the provided element to the list if there is no element with the same hash yet.
176
	 *
177
	 * @since 0.1
178
	 *
179
	 * @param Hashable $element
180
	 *
181 8
	 * @return bool Indicates if the element was added or not.
182 8
	 */
183 8
	public function addElement( Hashable $element ) {
184
		$append = !$this->hasElementHash( $element->getHash() );
185
186
		if ( $append ) {
187
			$this->append( $element );
188
		}
189
190
		return $append;
191
	}
192 9
193 9
	/**
194 9
	 * Returns the element with the provided hash or false if there is no such element.
195
	 *
196 9
	 * @since 0.1
197 3
	 *
198 3
	 * @param string $elementHash
199
	 *
200 9
	 * @return mixed|bool
201 9
	 */
202 9
	public function getByElementHash( $elementHash ) {
203
		if ( $this->hasElementHash( $elementHash ) ) {
204
			$offset = $this->offsetHashes[$elementHash];
205
			return $this->offsetGet( $offset );
206
		}
207
		else {
208
			return false;
209
		}
210
	}
211
212
	/**
213 12
	 * @see ArrayObject::offsetUnset
214 12
	 *
215
	 * @since 0.1
216 12
	 *
217 8
	 * @param mixed $index
218 8
	 */
219
	public function offsetUnset( $index ) {
220 12
		if ( $this->offsetExists( $index ) ) {
221
			/**
222
			 * @var Hashable $element
223
			 */
224
			$element = $this->offsetGet( $index );
225
226
			$hash = $element->getHash();
227
228
			unset( $this->offsetHashes[$hash] );
229
230
			parent::offsetUnset( $index );
231
		}
232
	}
233
234
	/**
235
	 * @see Comparable::equals
236
	 *
237
	 * The comparison is done purely value based, ignoring the order of the elements in the array.
238
	 *
239
	 * @since 0.3
240
	 *
241
	 * @param mixed $target
242
	 *
243
	 * @return bool
244
	 */
245
	public function equals( $target ) {
246
		if ( $this === $target ) {
247
			return true;
248
		}
249
250
		return $target instanceof self
251
			&& $this->getHash() === $target->getHash();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Wikibase\DataModel\HashArray as the method getHash() does only exist in the following sub-classes of Wikibase\DataModel\HashArray: Wikibase\DataModel\Snak\SnakList. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
252
	}
253
254 13
	/**
255 13
	 * @see ArrayObject::append
256
	 *
257
	 * @param mixed $value
258
	 */
259 13
	public function append( $value ) {
260
		$this->setElement( null, $value );
261 13
	}
262
263 13
	/**
264 13
	 * @see ArrayObject::offsetSet()
265 13
	 *
266 3
	 * @param mixed $index
267 3
	 * @param mixed $value
268 3
	 */
269 3
	public function offsetSet( $index, $value ) {
270
		$this->setElement( $index, $value );
271 3
	}
272 3
273
	/**
274 12
	 * Returns if the provided value has the same type as the elements
275
	 * that can be added to this ArrayObject.
276
	 *
277 13
	 * @param mixed $value
278 13
	 *
279 13
	 * @return bool
280
	 */
281
	protected function hasValidType( $value ) {
282
		$class = $this->getObjectType();
283
		return $value instanceof $class;
284
	}
285
286
	/**
287
	 * Method that actually sets the element and holds
288
	 * all common code needed for set operations, including
289
	 * type checking and offset resolving.
290 4
	 *
291 4
	 * If you want to do additional indexing or have code that
292 4
	 * otherwise needs to be executed whenever an element is added,
293
	 * you can overload @see preSetElement.
294
	 *
295
	 * @param mixed $index
296
	 * @param mixed $value
297
	 *
298
	 * @throws InvalidArgumentException
299
	 */
300
	protected function setElement( $index, $value ) {
301
		if ( !$this->hasValidType( $value ) ) {
302
			$type = is_object( $value ) ? get_class( $value ) : gettype( $value );
303
304
			throw new InvalidArgumentException( '$value must be an instance of ' . $this->getObjectType() . '; got ' . $type );
305
		}
306 4
307 4
		if ( $index === null ) {
308 4
			$index = $this->getNewOffset();
309
		}
310
311
		if ( $this->preSetElement( $index, $value ) ) {
312 4
			parent::offsetSet( $index, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of setElement()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
313
		}
314
	}
315
316
	/**
317
	 * @see Serializable::serialize
318
	 *
319
	 * @return string
320 12
	 */
321 12
	public function serialize() {
322
		return serialize( [
323
			'data' => $this->getArrayCopy(),
324
			'index' => $this->indexOffset,
325
		] );
326 12
	}
327 12
328
	/**
329 12
	 * @see Serializable::unserialize
330 3
	 *
331 3
	 * @param string $serialized
332
	 */
333 12
	public function unserialize( $serialized ) {
334
		$serializationData = unserialize( $serialized );
335 12
336 12
		foreach ( $serializationData['data'] as $offset => $value ) {
337
			// Just set the element, bypassing checks and offset resolving,
338
			// as these elements have already gone through this.
339
			parent::offsetSet( $offset, $value );
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (offsetSet() instead of unserialize()). Are you sure this is correct? If so, you might want to change this to $this->offsetSet().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
340
		}
341
342
		$this->indexOffset = $serializationData['index'];
343
	}
344
345
	/**
346
	 * @return bool
347
	 */
348
	public function isEmpty() {
349
		return !$this->getIterator()->valid();
350 4
	}
351 4
352
}
353