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 Filesystem 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 Filesystem, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
13 | class Filesystem |
||
14 | { |
||
15 | protected $adapter; |
||
16 | |||
17 | /** |
||
18 | * Contains File objects created with $this->createFile() method. |
||
19 | * |
||
20 | * @var array |
||
21 | */ |
||
22 | protected $fileRegister = array(); |
||
23 | |||
24 | /** |
||
25 | * @param Adapter $adapter A configured Adapter instance |
||
26 | */ |
||
27 | public function __construct(Adapter $adapter) |
||
31 | |||
32 | /** |
||
33 | * Returns the adapter. |
||
34 | * |
||
35 | * @return Adapter |
||
36 | */ |
||
37 | public function getAdapter() |
||
41 | |||
42 | /** |
||
43 | * Indicates whether the file matching the specified key exists. |
||
44 | * |
||
45 | * @param string $key |
||
46 | * |
||
47 | * @return bool TRUE if the file exists, FALSE otherwise |
||
48 | * |
||
49 | * @throws \InvalidArgumentException If $key is invalid |
||
50 | */ |
||
51 | public function has($key) |
||
52 | { |
||
53 | self::assertValidKey($key); |
||
54 | |||
55 | return $this->adapter->exists($key); |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * Renames a file. |
||
60 | * |
||
61 | * @param string $sourceKey |
||
62 | * @param string $targetKey |
||
63 | * |
||
64 | * @return bool TRUE if the rename was successful |
||
65 | * |
||
66 | * @throws Exception\FileNotFound when sourceKey does not exist |
||
67 | * @throws Exception\UnexpectedFile when targetKey exists |
||
68 | * @throws \RuntimeException when cannot rename |
||
69 | * @throws \InvalidArgumentException If $sourceKey or $targetKey are invalid |
||
70 | */ |
||
71 | public function rename($sourceKey, $targetKey) |
||
93 | |||
94 | /** |
||
95 | * Returns the file matching the specified key. |
||
96 | * |
||
97 | * @param string $key Key of the file |
||
98 | * @param bool $create Whether to create the file if it does not exist |
||
99 | * |
||
100 | * @throws Exception\FileNotFound |
||
101 | * @throws \InvalidArgumentException If $key is invalid |
||
102 | * |
||
103 | * @return File |
||
104 | */ |
||
105 | public function get($key, $create = false) |
||
106 | { |
||
107 | self::assertValidKey($key); |
||
108 | |||
109 | if (!$create) { |
||
110 | $this->assertHasFile($key); |
||
111 | } |
||
112 | |||
113 | return $this->createFile($key); |
||
114 | } |
||
115 | |||
116 | /** |
||
117 | * Writes the given content into the file. |
||
118 | * |
||
119 | * @param string $key Key of the file |
||
120 | * @param string $content Content to write in the file |
||
121 | * @param bool $overwrite Whether to overwrite the file if exists |
||
122 | * |
||
123 | * @throws Exception\FileAlreadyExists When file already exists and overwrite is false |
||
124 | * @throws \RuntimeException When for any reason content could not be written |
||
125 | * @throws \InvalidArgumentException If $key is invalid |
||
126 | * |
||
127 | * @return int The number of bytes that were written into the file |
||
128 | */ |
||
129 | public function write($key, $content, $overwrite = false) |
||
130 | { |
||
131 | self::assertValidKey($key); |
||
132 | |||
133 | if (!$overwrite && $this->has($key)) { |
||
134 | throw new Exception\FileAlreadyExists($key); |
||
135 | } |
||
136 | |||
137 | $numBytes = $this->adapter->write($key, $content); |
||
138 | |||
139 | if (false === $numBytes) { |
||
140 | throw new \RuntimeException(sprintf('Could not write the "%s" key content.', $key)); |
||
141 | } |
||
142 | |||
143 | return $numBytes; |
||
144 | } |
||
145 | |||
146 | /** |
||
147 | * Reads the content from the file. |
||
148 | * |
||
149 | * @param string $key Key of the file |
||
150 | * |
||
151 | * @throws Exception\FileNotFound when file does not exist |
||
152 | * @throws \RuntimeException when cannot read file |
||
153 | * @throws \InvalidArgumentException If $key is invalid |
||
154 | * |
||
155 | * @return string |
||
156 | */ |
||
157 | View Code Duplication | public function read($key) |
|
|
|||
158 | { |
||
159 | self::assertValidKey($key); |
||
160 | |||
161 | $this->assertHasFile($key); |
||
162 | |||
163 | $content = $this->adapter->read($key); |
||
164 | |||
165 | if (false === $content) { |
||
166 | throw new \RuntimeException(sprintf('Could not read the "%s" key content.', $key)); |
||
167 | } |
||
168 | |||
169 | return $content; |
||
170 | } |
||
171 | |||
172 | /** |
||
173 | * Deletes the file matching the specified key. |
||
174 | * |
||
175 | * @param string $key |
||
176 | * |
||
177 | * @throws \RuntimeException when cannot read file |
||
178 | * @throws \InvalidArgumentException If $key is invalid |
||
179 | * |
||
180 | * @return bool |
||
181 | */ |
||
182 | View Code Duplication | public function delete($key) |
|
183 | { |
||
184 | self::assertValidKey($key); |
||
185 | |||
186 | $this->assertHasFile($key); |
||
187 | |||
188 | if ($this->adapter->delete($key)) { |
||
189 | $this->removeFromRegister($key); |
||
190 | |||
191 | return true; |
||
192 | } |
||
193 | |||
194 | throw new \RuntimeException(sprintf('Could not remove the "%s" key.', $key)); |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | * Returns an array of all keys. |
||
199 | * |
||
200 | * @return array |
||
201 | */ |
||
202 | public function keys() |
||
206 | |||
207 | /** |
||
208 | * Lists keys beginning with given prefix |
||
209 | * (no wildcard / regex matching). |
||
210 | * |
||
211 | * if adapter implements ListKeysAware interface, adapter's implementation will be used, |
||
212 | * in not, ALL keys will be requested and iterated through. |
||
213 | * |
||
214 | * @param string $prefix |
||
215 | * |
||
216 | * @return array |
||
217 | */ |
||
218 | public function listKeys($prefix = '') |
||
242 | |||
243 | /** |
||
244 | * Returns the last modified time of the specified file. |
||
245 | * |
||
246 | * @param string $key |
||
247 | * |
||
248 | * @return int An UNIX like timestamp |
||
249 | * |
||
250 | * @throws \InvalidArgumentException If $key is invalid |
||
251 | */ |
||
252 | public function mtime($key) |
||
253 | { |
||
254 | self::assertValidKey($key); |
||
255 | |||
256 | $this->assertHasFile($key); |
||
257 | |||
258 | return $this->adapter->mtime($key); |
||
259 | } |
||
260 | |||
261 | /** |
||
262 | * Returns the checksum of the specified file's content. |
||
263 | * |
||
264 | * @param string $key |
||
265 | * |
||
266 | * @return string A MD5 hash |
||
267 | * |
||
268 | * @throws \InvalidArgumentException If $key is invalid |
||
269 | */ |
||
270 | View Code Duplication | public function checksum($key) |
|
271 | { |
||
272 | self::assertValidKey($key); |
||
273 | |||
274 | $this->assertHasFile($key); |
||
275 | |||
276 | if ($this->adapter instanceof Adapter\ChecksumCalculator) { |
||
277 | return $this->adapter->checksum($key); |
||
278 | } |
||
279 | |||
280 | return Util\Checksum::fromContent($this->read($key)); |
||
281 | } |
||
282 | |||
283 | /** |
||
284 | * Returns the size of the specified file's content. |
||
285 | * |
||
286 | * @param string $key |
||
287 | * |
||
288 | * @return int File size in Bytes |
||
289 | * |
||
290 | * @throws \InvalidArgumentException If $key is invalid |
||
291 | */ |
||
292 | View Code Duplication | public function size($key) |
|
293 | { |
||
294 | self::assertValidKey($key); |
||
295 | |||
296 | $this->assertHasFile($key); |
||
297 | |||
298 | if ($this->adapter instanceof Adapter\SizeCalculator) { |
||
299 | return $this->adapter->size($key); |
||
300 | } |
||
301 | |||
302 | return Util\Size::fromContent($this->read($key)); |
||
303 | } |
||
304 | |||
305 | /** |
||
306 | * Gets a new stream instance of the specified file. |
||
307 | * |
||
308 | * @param $key |
||
309 | * |
||
310 | * @return Stream|Stream\InMemoryBuffer |
||
311 | * |
||
312 | * @throws \InvalidArgumentException If $key is invalid |
||
313 | */ |
||
314 | public function createStream($key) |
||
315 | { |
||
316 | self::assertValidKey($key); |
||
317 | |||
318 | if ($this->adapter instanceof Adapter\StreamFactory) { |
||
319 | return $this->adapter->createStream($key); |
||
320 | } |
||
321 | |||
322 | return new Stream\InMemoryBuffer($this, $key); |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | * Creates a new file in a filesystem. |
||
327 | * |
||
328 | * @param $key |
||
329 | * |
||
330 | * @return File |
||
331 | * |
||
332 | * @throws \InvalidArgumentException If $key is invalid |
||
333 | */ |
||
334 | public function createFile($key) |
||
348 | |||
349 | /** |
||
350 | * Get the mime type of the provided key. |
||
351 | * |
||
352 | * @param string $key |
||
353 | * |
||
354 | * @return string |
||
355 | * |
||
356 | * @throws \InvalidArgumentException If $key is invalid |
||
357 | */ |
||
358 | public function mimeType($key) |
||
359 | { |
||
360 | self::assertValidKey($key); |
||
361 | |||
362 | $this->assertHasFile($key); |
||
363 | |||
364 | if ($this->adapter instanceof Adapter\MimeTypeProvider) { |
||
365 | return $this->adapter->mimeType($key); |
||
366 | } |
||
367 | |||
368 | throw new \LogicException(sprintf( |
||
369 | 'Adapter "%s" cannot provide MIME type', |
||
370 | get_class($this->adapter) |
||
371 | )); |
||
372 | } |
||
373 | |||
374 | /** |
||
375 | * Checks if matching file by given key exists in the filesystem. |
||
376 | * |
||
377 | * Key must be non empty string, otherwise it will throw Exception\FileNotFound |
||
378 | * {@see http://php.net/manual/en/function.empty.php} |
||
379 | * |
||
380 | * @param string $key |
||
381 | * |
||
382 | * @throws Exception\FileNotFound when sourceKey does not exist |
||
383 | */ |
||
384 | private function assertHasFile($key) |
||
385 | { |
||
386 | if (!$this->has($key)) { |
||
387 | throw new Exception\FileNotFound($key); |
||
388 | } |
||
389 | } |
||
390 | |||
391 | /** |
||
392 | * Checks if matching File object by given key exists in the fileRegister. |
||
393 | * |
||
394 | * @param string $key |
||
395 | * |
||
396 | * @return bool |
||
397 | */ |
||
398 | private function isFileInRegister($key) |
||
402 | |||
403 | /** |
||
404 | * Clear files register. |
||
405 | */ |
||
406 | public function clearFileRegister() |
||
410 | |||
411 | /** |
||
412 | * Removes File object from register. |
||
413 | * |
||
414 | * @param string $key |
||
415 | */ |
||
416 | public function removeFromRegister($key) |
||
422 | |||
423 | /** |
||
424 | * @param string $key |
||
425 | * |
||
426 | * @return bool |
||
427 | */ |
||
428 | public function isDirectory($key) |
||
432 | |||
433 | /** |
||
434 | * @param string $key |
||
435 | * |
||
436 | * @throws \InvalidArgumentException Given $key should not be empty |
||
437 | */ |
||
438 | private static function assertValidKey($key) |
||
439 | { |
||
440 | if (empty($key)) { |
||
441 | throw new \InvalidArgumentException('Object path is empty.'); |
||
442 | } |
||
443 | } |
||
444 | } |
||
445 |
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.