Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like AbstractCollection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AbstractCollection, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
13 | abstract class AbstractCollection extends Arrayy implements CollectionInterface |
||
14 | { |
||
15 | /** |
||
16 | * The type of elements stored in this collection. |
||
17 | * |
||
18 | * @var string |
||
19 | */ |
||
20 | private $collectionType; |
||
21 | |||
22 | /** |
||
23 | * Constructs a collection object of the specified type, optionally with the |
||
24 | * specified data. |
||
25 | * |
||
26 | * @param mixed $data |
||
27 | * <p> |
||
28 | * The initial items to store in the collection. |
||
29 | * </p> |
||
30 | * <p> |
||
31 | * Should be an array or a generator, otherwise it will try |
||
32 | * to convert it into an array. |
||
33 | * </p> |
||
34 | */ |
||
35 | 11 | public function __construct($data = []) |
|
40 | |||
41 | /** |
||
42 | * @return static[] |
||
43 | */ |
||
44 | 3 | public function getCollection(): array |
|
48 | |||
49 | /** |
||
50 | * The type (FQCN) associated with this collection. |
||
51 | * |
||
52 | * @return string |
||
53 | */ |
||
54 | abstract public function getType(): string; |
||
55 | |||
56 | /** |
||
57 | * Merge current items and items of given collections into a new one. |
||
58 | * |
||
59 | * @param CollectionInterface ...$collections The collections to merge. |
||
60 | * |
||
61 | * @throws \InvalidArgumentException if any of the given collections are not of the same type |
||
62 | * |
||
63 | * @return CollectionInterface |
||
64 | */ |
||
65 | 1 | public function merge(CollectionInterface ...$collections): CollectionInterface |
|
66 | { |
||
67 | 1 | foreach ($collections as $collection) { |
|
68 | 1 | if ($collection instanceof Arrayy) { |
|
69 | 1 | foreach ($collection as $item) { |
|
70 | 1 | $this->append($item); |
|
71 | } |
||
72 | } |
||
73 | } |
||
74 | |||
75 | 1 | return $this; |
|
76 | } |
||
77 | |||
78 | /** |
||
79 | * Assigns a value to the specified offset + check the type. |
||
80 | * |
||
81 | * @param int|string|null $offset |
||
82 | * @param mixed $value |
||
83 | */ |
||
84 | 1 | View Code Duplication | public function offsetSet($offset, $value) |
|
|||
85 | { |
||
86 | 1 | if ($this->checkType($this->collectionType, $value) === false) { |
|
87 | 1 | throw new \InvalidArgumentException( |
|
88 | 1 | 'Value must be of type ' . $this->collectionType . '; value is ' . $this->valueToString($value) |
|
89 | ); |
||
90 | } |
||
91 | |||
92 | parent::offsetSet($offset, $value); |
||
93 | } |
||
94 | |||
95 | /** |
||
96 | * Prepend a (key) + value to the current array. |
||
97 | * |
||
98 | * @param mixed $value |
||
99 | * @param mixed $key |
||
100 | * |
||
101 | * @return static |
||
102 | * <p>(Mutable) Return this Arrayy object, with the prepended value.</p> |
||
103 | */ |
||
104 | 2 | View Code Duplication | public function prepend($value, $key = null): Arrayy |
105 | { |
||
106 | 2 | if ($this->checkType($this->collectionType, $value) === false) { |
|
107 | 2 | throw new \InvalidArgumentException( |
|
108 | 2 | 'Value must be of type ' . $this->collectionType . '; value is ' . $this->valueToString($value) |
|
109 | ); |
||
110 | } |
||
111 | |||
112 | return parent::prepend($value, $key); |
||
113 | } |
||
114 | |||
115 | /** |
||
116 | * Append a (key) + value to the current array. |
||
117 | * |
||
118 | * @param mixed $value |
||
119 | * @param mixed $key |
||
120 | * |
||
121 | * @return static |
||
122 | * <p>(Mutable) Return this Arrayy object, with the appended values.</p> |
||
123 | */ |
||
124 | 3 | View Code Duplication | public function append($value, $key = null): Arrayy |
125 | { |
||
126 | 3 | if ($this->checkType($this->collectionType, $value) === false) { |
|
127 | 1 | throw new \InvalidArgumentException( |
|
128 | 1 | 'Value must be of type ' . $this->collectionType . '; value is ' . $this->valueToString($value) |
|
129 | ); |
||
130 | } |
||
131 | |||
132 | 2 | return parent::append($value, $key); |
|
133 | } |
||
134 | |||
135 | /** |
||
136 | * Returns the values from given property or method. |
||
137 | * |
||
138 | * @param string $keyOrPropertyOrMethod the property or method name to filter by |
||
139 | * |
||
140 | * @throws \InvalidArgumentException if property or method is not defined |
||
141 | * |
||
142 | * @return array |
||
143 | */ |
||
144 | 1 | public function column(string $keyOrPropertyOrMethod): array |
|
145 | { |
||
146 | // init |
||
147 | 1 | $temp = []; |
|
148 | |||
149 | 1 | foreach ($this->array as $item) { |
|
150 | 1 | $temp[] = $this->extractValue($item, $keyOrPropertyOrMethod); |
|
151 | } |
||
152 | |||
153 | 1 | return $temp; |
|
154 | } |
||
155 | |||
156 | /** |
||
157 | * Returns a collection of matching items. |
||
158 | * |
||
159 | * @param string $keyOrPropertyOrMethod the property or method to evaluate |
||
160 | * @param mixed $value the value to match |
||
161 | * |
||
162 | * @throws \InvalidArgumentException if property or method is not defined |
||
163 | * |
||
164 | * @return CollectionInterface |
||
165 | */ |
||
166 | 1 | public function where(string $keyOrPropertyOrMethod, $value): CollectionInterface |
|
167 | { |
||
168 | 1 | return $this->filter( |
|
169 | function ($item) use ($keyOrPropertyOrMethod, $value) { |
||
170 | 1 | $accessorValue = $this->extractValue( |
|
171 | 1 | $item, |
|
172 | 1 | $keyOrPropertyOrMethod |
|
173 | ); |
||
174 | |||
175 | 1 | return $accessorValue === $value; |
|
176 | 1 | } |
|
177 | ); |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * Internal mechanic of set method. |
||
182 | * |
||
183 | * @param string|null $key |
||
184 | * @param mixed $value |
||
185 | * @param bool $checkProperties |
||
186 | * |
||
187 | * @return bool |
||
188 | */ |
||
189 | 10 | View Code Duplication | protected function internalSet($key, $value, $checkProperties = true): bool |
190 | { |
||
191 | 10 | if ($this->checkType($this->collectionType, $value) === false) { |
|
192 | 1 | throw new \InvalidArgumentException( |
|
193 | 1 | 'Value must be of type ' . $this->collectionType . '; value is ' . $this->valueToString($value) |
|
194 | ); |
||
195 | } |
||
196 | |||
197 | 9 | return parent::internalSet($key, $value, $checkProperties); |
|
198 | } |
||
199 | |||
200 | /** |
||
201 | * Extracts the value of the given property or method from the object. |
||
202 | * |
||
203 | * @param Arrayy $object the object to extract the value from |
||
204 | * @param string $keyOrPropertyOrMethod the property or method for which the |
||
205 | * value should be extracted |
||
206 | * |
||
207 | * @throws \InvalidArgumentException if the method or property is not defined |
||
208 | * |
||
209 | * @return mixed the value extracted from the specified property or method |
||
210 | */ |
||
211 | 2 | private function extractValue(Arrayy $object, string $keyOrPropertyOrMethod) |
|
212 | { |
||
213 | 2 | if (isset($object[$keyOrPropertyOrMethod])) { |
|
214 | 2 | $return = $object->get($keyOrPropertyOrMethod); |
|
215 | |||
216 | 2 | if ($return instanceof Arrayy) { |
|
217 | 1 | return $return->getArray(); |
|
218 | } |
||
219 | |||
220 | 1 | return $return; |
|
221 | } |
||
222 | |||
223 | if (\property_exists($object, $keyOrPropertyOrMethod)) { |
||
224 | return $object->{$keyOrPropertyOrMethod}; |
||
225 | } |
||
226 | |||
227 | if (\method_exists($object, $keyOrPropertyOrMethod)) { |
||
228 | return $object->{$keyOrPropertyOrMethod}(); |
||
229 | } |
||
230 | |||
231 | throw new \InvalidArgumentException( |
||
232 | \sprintf('array-key & property & method "%s" not defined in %s', $keyOrPropertyOrMethod, \gettype($object)) |
||
233 | ); |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Returns `true` if value is of the specified type. |
||
238 | * |
||
239 | * @param string $type the type to check the value against |
||
240 | * @param mixed $value the value to check |
||
241 | * |
||
242 | * @return bool |
||
243 | */ |
||
244 | 10 | private function checkType(string $type, $value): bool |
|
278 | |||
279 | /** |
||
280 | * @param mixed $value |
||
281 | * |
||
282 | * @return string |
||
283 | */ |
||
284 | 5 | private function valueToString($value): string |
|
314 | } |
||
315 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.