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 Share 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 Share, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
17 | class Share extends AbstractShare { |
||
18 | /** |
||
19 | * @var Server $server |
||
20 | */ |
||
21 | private $server; |
||
22 | |||
23 | /** |
||
24 | * @var string $name |
||
25 | */ |
||
26 | private $name; |
||
27 | |||
28 | /** |
||
29 | * @var Connection $connection |
||
30 | */ |
||
31 | public $connection; |
||
32 | |||
33 | /** |
||
34 | * @var \Icewind\SMB\Parser |
||
35 | */ |
||
36 | protected $parser; |
||
37 | |||
38 | /** |
||
39 | * @var \Icewind\SMB\System |
||
40 | */ |
||
41 | private $system; |
||
42 | |||
43 | /** |
||
44 | * @param Server $server |
||
45 | * @param string $name |
||
46 | * @param System $system |
||
47 | */ |
||
48 | public function __construct($server, $name, System $system = null) { |
||
49 | parent::__construct(); |
||
50 | $this->server = $server; |
||
51 | $this->name = $name; |
||
52 | $this->system = (!is_null($system)) ? $system : new System(); |
||
53 | $this->parser = new Parser(new TimeZoneProvider($this->server->getHost(), $this->system)); |
||
54 | } |
||
55 | |||
56 | protected function getConnection() { |
||
57 | $workgroupArgument = ($this->server->getWorkgroup()) ? ' -W ' . escapeshellarg($this->server->getWorkgroup()) : ''; |
||
58 | $smbClientPath = $this->system->getSmbclientPath(); |
||
59 | if (!$smbClientPath) { |
||
60 | throw new DependencyException('Can\'t find smbclient binary in path'); |
||
61 | } |
||
62 | $command = sprintf('%s%s %s --authentication-file=%s %s', |
||
63 | $this->system->hasStdBuf() ? 'stdbuf -o0 ' : '', |
||
64 | $this->system->getSmbclientPath(), |
||
65 | $workgroupArgument, |
||
66 | System::getFD(3), |
||
67 | escapeshellarg('//' . $this->server->getHost() . '/' . $this->name) |
||
68 | ); |
||
69 | $connection = new Connection($command, $this->parser); |
||
70 | $connection->writeAuthentication($this->server->getUser(), $this->server->getPassword()); |
||
71 | $connection->connect(); |
||
72 | if (!$connection->isValid()) { |
||
73 | throw new ConnectionException($connection->readLine()); |
||
74 | } |
||
75 | // some versions of smbclient add a help message in first of the first prompt |
||
76 | $connection->clearTillPrompt(); |
||
77 | return $connection; |
||
78 | } |
||
79 | |||
80 | /** |
||
81 | * @throws \Icewind\SMB\Exception\ConnectionException |
||
82 | * @throws \Icewind\SMB\Exception\AuthenticationException |
||
83 | * @throws \Icewind\SMB\Exception\InvalidHostException |
||
84 | */ |
||
85 | protected function connect() { |
||
86 | if ($this->connection and $this->connection->isValid()) { |
||
87 | return; |
||
88 | } |
||
89 | $this->connection = $this->getConnection(); |
||
90 | } |
||
91 | |||
92 | protected function reconnect() { |
||
93 | $this->connection->reconnect(); |
||
94 | if (!$this->connection->isValid()) { |
||
95 | throw new ConnectionException(); |
||
96 | } |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * Get the name of the share |
||
101 | * |
||
102 | * @return string |
||
103 | */ |
||
104 | public function getName() { |
||
105 | return $this->name; |
||
106 | } |
||
107 | |||
108 | protected function simpleCommand($command, $path) { |
||
109 | $escapedPath = $this->escapePath($path); |
||
110 | $cmd = $command . ' ' . $escapedPath; |
||
111 | $output = $this->execute($cmd); |
||
112 | return $this->parseOutput($output, $path); |
||
113 | } |
||
114 | |||
115 | /** |
||
116 | * List the content of a remote folder |
||
117 | * |
||
118 | * @param $path |
||
119 | * @return \Icewind\SMB\IFileInfo[] |
||
120 | * |
||
121 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
122 | * @throws \Icewind\SMB\Exception\InvalidTypeException |
||
123 | */ |
||
124 | public function dir($path) { |
||
125 | $escapedPath = $this->escapePath($path); |
||
126 | $output = $this->execute('cd ' . $escapedPath); |
||
127 | //check output for errors |
||
128 | $this->parseOutput($output, $path); |
||
129 | $output = $this->execute('dir'); |
||
130 | |||
131 | $this->execute('cd /'); |
||
132 | |||
133 | return $this->parser->parseDir($output, $path); |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * @param string $path |
||
138 | * @return \Icewind\SMB\IFileInfo |
||
139 | */ |
||
140 | public function stat($path) { |
||
141 | $escapedPath = $this->escapePath($path); |
||
142 | $output = $this->execute('allinfo ' . $escapedPath); |
||
143 | // Windows and non Windows Fileserver may respond different |
||
144 | // to the allinfo command for directories. If the result is a single |
||
145 | // line = error line, redo it with a different allinfo parameter |
||
146 | if ($escapedPath == '""' && count($output) < 2) { |
||
147 | $output = $this->execute('allinfo ' . '"."'); |
||
148 | } |
||
149 | if (count($output) < 3) { |
||
150 | $this->parseOutput($output, $path); |
||
151 | } |
||
152 | $stat = $this->parser->parseStat($output); |
||
153 | return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode']); |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * Create a folder on the share |
||
158 | * |
||
159 | * @param string $path |
||
160 | * @return bool |
||
161 | * |
||
162 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
163 | * @throws \Icewind\SMB\Exception\AlreadyExistsException |
||
164 | */ |
||
165 | public function mkdir($path) { |
||
166 | return $this->simpleCommand('mkdir', $path); |
||
167 | } |
||
168 | |||
169 | /** |
||
170 | * Remove a folder on the share |
||
171 | * |
||
172 | * @param string $path |
||
173 | * @return bool |
||
174 | * |
||
175 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
176 | * @throws \Icewind\SMB\Exception\InvalidTypeException |
||
177 | */ |
||
178 | public function rmdir($path) { |
||
179 | return $this->simpleCommand('rmdir', $path); |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * Delete a file on the share |
||
184 | * |
||
185 | * @param string $path |
||
186 | * @param bool $secondTry |
||
187 | * @return bool |
||
188 | * @throws InvalidTypeException |
||
189 | * @throws NotFoundException |
||
190 | * @throws \Exception |
||
191 | */ |
||
192 | public function del($path, $secondTry = false) { |
||
193 | //del return a file not found error when trying to delete a folder |
||
194 | //we catch it so we can check if $path doesn't exist or is of invalid type |
||
195 | try { |
||
196 | return $this->simpleCommand('del', $path); |
||
197 | } catch (NotFoundException $e) { |
||
198 | //no need to do anything with the result, we just check if this throws the not found error |
||
199 | try { |
||
200 | $this->simpleCommand('ls', $path); |
||
201 | } catch (NotFoundException $e2) { |
||
202 | throw $e; |
||
203 | } catch (\Exception $e2) { |
||
204 | throw new InvalidTypeException($path); |
||
205 | } |
||
206 | throw $e; |
||
207 | } catch (FileInUseException $e) { |
||
208 | if ($secondTry) { |
||
209 | throw $e; |
||
210 | } |
||
211 | $this->reconnect(); |
||
212 | return $this->del($path, true); |
||
213 | } |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * Rename a remote file |
||
218 | * |
||
219 | * @param string $from |
||
220 | * @param string $to |
||
221 | * @return bool |
||
222 | * |
||
223 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
224 | * @throws \Icewind\SMB\Exception\AlreadyExistsException |
||
225 | */ |
||
226 | View Code Duplication | public function rename($from, $to) { |
|
227 | $path1 = $this->escapePath($from); |
||
228 | $path2 = $this->escapePath($to); |
||
229 | $output = $this->execute('rename ' . $path1 . ' ' . $path2); |
||
230 | return $this->parseOutput($output, $to); |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * Upload a local file |
||
235 | * |
||
236 | * @param string $source local file |
||
237 | * @param string $target remove file |
||
238 | * @return bool |
||
239 | * |
||
240 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
241 | * @throws \Icewind\SMB\Exception\InvalidTypeException |
||
242 | */ |
||
243 | View Code Duplication | public function put($source, $target) { |
|
|
|||
244 | $path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping |
||
245 | $path2 = $this->escapePath($target); |
||
246 | $output = $this->execute('put ' . $path1 . ' ' . $path2); |
||
247 | return $this->parseOutput($output, $target); |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * Download a remote file |
||
252 | * |
||
253 | * @param string $source remove file |
||
254 | * @param string $target local file |
||
255 | * @return bool |
||
256 | * |
||
257 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
258 | * @throws \Icewind\SMB\Exception\InvalidTypeException |
||
259 | */ |
||
260 | View Code Duplication | public function get($source, $target) { |
|
261 | $path1 = $this->escapePath($source); |
||
262 | $path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping |
||
263 | $output = $this->execute('get ' . $path1 . ' ' . $path2); |
||
264 | return $this->parseOutput($output, $source); |
||
265 | } |
||
266 | |||
267 | /** |
||
268 | * Open a readable stream to a remote file |
||
269 | * |
||
270 | * @param string $source |
||
271 | * @return resource a read only stream with the contents of the remote file |
||
272 | * |
||
273 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
274 | * @throws \Icewind\SMB\Exception\InvalidTypeException |
||
275 | */ |
||
276 | public function read($source) { |
||
277 | $source = $this->escapePath($source); |
||
278 | // since returned stream is closed by the caller we need to create a new instance |
||
279 | // since we can't re-use the same file descriptor over multiple calls |
||
280 | $connection = $this->getConnection(); |
||
281 | |||
282 | $connection->write('get ' . $source . ' ' . System::getFD(5)); |
||
283 | $connection->write('exit'); |
||
284 | $fh = $connection->getFileOutputStream(); |
||
285 | stream_context_set_option($fh, 'file', 'connection', $connection); |
||
286 | return $fh; |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * Open a writable stream to a remote file |
||
291 | * |
||
292 | * @param string $target |
||
293 | * @return resource a write only stream to upload a remote file |
||
294 | * |
||
295 | * @throws \Icewind\SMB\Exception\NotFoundException |
||
296 | * @throws \Icewind\SMB\Exception\InvalidTypeException |
||
297 | */ |
||
298 | public function write($target) { |
||
299 | $target = $this->escapePath($target); |
||
300 | // since returned stream is closed by the caller we need to create a new instance |
||
301 | // since we can't re-use the same file descriptor over multiple calls |
||
302 | $connection = $this->getConnection(); |
||
303 | |||
304 | $fh = $connection->getFileInputStream(); |
||
305 | $connection->write('put ' . System::getFD(4) . ' ' . $target); |
||
306 | $connection->write('exit'); |
||
307 | |||
308 | // use a close callback to ensure the upload is finished before continuing |
||
309 | // this also serves as a way to keep the connection in scope |
||
310 | return CallbackWrapper::wrap($fh, null, null, function () use ($connection, $target) { |
||
311 | $connection->close(false); // dont terminate, give the upload some time |
||
312 | }); |
||
313 | } |
||
314 | |||
315 | /** |
||
316 | * @param string $path |
||
317 | * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL |
||
318 | * @return mixed |
||
319 | */ |
||
320 | public function setMode($path, $mode) { |
||
321 | $modeString = ''; |
||
322 | $modeMap = array( |
||
323 | FileInfo::MODE_READONLY => 'r', |
||
324 | FileInfo::MODE_HIDDEN => 'h', |
||
325 | FileInfo::MODE_ARCHIVE => 'a', |
||
326 | FileInfo::MODE_SYSTEM => 's' |
||
327 | ); |
||
328 | foreach ($modeMap as $modeByte => $string) { |
||
329 | if ($mode & $modeByte) { |
||
330 | $modeString .= $string; |
||
331 | } |
||
332 | } |
||
333 | $path = $this->escapePath($path); |
||
334 | |||
335 | // first reset the mode to normal |
||
336 | $cmd = 'setmode ' . $path . ' -rsha'; |
||
337 | $output = $this->execute($cmd); |
||
338 | $this->parseOutput($output, $path); |
||
339 | |||
340 | if ($mode !== FileInfo::MODE_NORMAL) { |
||
341 | // then set the modes we want |
||
342 | $cmd = 'setmode ' . $path . ' ' . $modeString; |
||
343 | $output = $this->execute($cmd); |
||
344 | return $this->parseOutput($output, $path); |
||
345 | } else { |
||
346 | return true; |
||
347 | } |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * @param string $path |
||
352 | * @return INotifyHandler |
||
353 | * @throws ConnectionException |
||
354 | * @throws DependencyException |
||
355 | */ |
||
356 | public function notify($path) { |
||
357 | if (!$this->system->hasStdBuf()) { //stdbuf is required to disable smbclient's output buffering |
||
358 | throw new DependencyException('stdbuf is required for usage of the notify command'); |
||
359 | } |
||
360 | $connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process |
||
361 | $command = 'notify ' . $this->escapePath($path); |
||
362 | $connection->write($command . PHP_EOL); |
||
363 | return new NotifyHandler($connection, $path); |
||
364 | } |
||
365 | |||
366 | /** |
||
367 | * @param string $command |
||
368 | * @return array |
||
369 | */ |
||
370 | protected function execute($command) { |
||
371 | $this->connect(); |
||
372 | $this->connection->write($command . PHP_EOL); |
||
373 | return $this->connection->read(); |
||
374 | } |
||
375 | |||
376 | /** |
||
377 | * check output for errors |
||
378 | * |
||
379 | * @param string[] $lines |
||
380 | * @param string $path |
||
381 | * |
||
382 | * @throws NotFoundException |
||
383 | * @throws \Icewind\SMB\Exception\AlreadyExistsException |
||
384 | * @throws \Icewind\SMB\Exception\AccessDeniedException |
||
385 | * @throws \Icewind\SMB\Exception\NotEmptyException |
||
386 | * @throws \Icewind\SMB\Exception\InvalidTypeException |
||
387 | * @throws \Icewind\SMB\Exception\Exception |
||
388 | * @return bool |
||
389 | */ |
||
390 | protected function parseOutput($lines, $path = '') { |
||
391 | if (count($lines) === 0) { |
||
392 | return true; |
||
393 | } else { |
||
394 | $this->parser->checkForError($lines, $path); |
||
395 | return false; |
||
396 | } |
||
397 | } |
||
398 | |||
399 | /** |
||
400 | * @param string $string |
||
401 | * @return string |
||
402 | */ |
||
403 | protected function escape($string) { |
||
406 | |||
407 | /** |
||
408 | * @param string $path |
||
409 | * @return string |
||
410 | */ |
||
411 | protected function escapePath($path) { |
||
412 | $this->verifyPath($path); |
||
413 | if ($path === '/') { |
||
414 | $path = ''; |
||
415 | } |
||
416 | $path = str_replace('/', '\\', $path); |
||
417 | $path = str_replace('"', '^"', $path); |
||
418 | $path = ltrim($path, '\\'); |
||
419 | return '"' . $path . '"'; |
||
420 | } |
||
421 | |||
422 | /** |
||
423 | * @param string $path |
||
424 | * @return string |
||
425 | */ |
||
426 | protected function escapeLocalPath($path) { |
||
427 | $path = str_replace('"', '\"', $path); |
||
430 | |||
431 | public function __destruct() { |
||
434 | } |
||
435 |
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.