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:
1 | <?php |
||
17 | class path |
||
18 | { |
||
19 | protected static $collapseCache = array(); |
||
20 | |||
21 | /** |
||
22 | * This method returns all the parent paths for a given path, starting at the root and including the |
||
23 | * given path itself. |
||
24 | * |
||
25 | * Usage: |
||
26 | * \arc\path::parents( '/foo/bar/doh/', '/foo/' ); // => [ '/foo/', '/foo/bar/', '/foo/bar/doh/' ] |
||
27 | * |
||
28 | * @param string $path The path to derive all parent paths from. |
||
29 | * @param string $root The root or topmost parent to return. Defaults to '/'. |
||
30 | * @return array Array of all parent paths, starting at the root and ending with the given path. |
||
31 | * Note: It includes the given path! |
||
32 | * Note: when $path is not a child of $root, and empty array is returned |
||
33 | */ |
||
34 | 10 | public static function parents($path, $root = '/') |
|
35 | { |
||
36 | 10 | $parents = []; |
|
37 | 10 | if (self::isChild($path, $root)) { |
|
38 | 10 | $subpath = substr($path, strlen($root)); |
|
39 | // returns all parents starting at the root, up to and including the path itself |
||
40 | 10 | $prevpath = ''; |
|
41 | 10 | $parents = self::reduce( $subpath, function ($result, $entry) use ($root, &$prevpath) { |
|
42 | 10 | $prevpath .= $entry . '/'; |
|
43 | 10 | $result[] = $root . $prevpath; |
|
44 | |||
45 | 10 | return $result; |
|
46 | 10 | }, [ $root ] ); |
|
47 | } |
||
48 | |||
49 | 10 | return $parents; |
|
50 | } |
||
51 | |||
52 | /** |
||
53 | * This method parses a path which may contain '..' or '.' or '//' entries and returns the resulting |
||
54 | * absolute path. |
||
55 | * |
||
56 | * Usage: |
||
57 | * \arc\path::collapse( '../', '/foo/bar/' ); // => '/foo/' |
||
58 | * \arc\path::collapse( '\\foo\\.\\bar/doh/../' ); // => '/foo/bar/' |
||
59 | * |
||
60 | * @param string $path The input path, which may be relative. If this path starts with a '/' it is |
||
61 | * considered to start in the root. |
||
62 | * @param string $cwd The current working directory. For relative paths this is the starting point. |
||
63 | * @return string The absolute path, without '..', '.' or '//' entries. |
||
64 | */ |
||
65 | 16 | public static function collapse($path, $cwd = null) |
|
66 | { |
||
67 | // removes '.', changes '//' to '/', changes '\\' to '/', calculates '..' up to '/' |
||
68 | 16 | if ( $path instanceof \arc\path\Value ) { |
|
69 | 6 | return $path; |
|
70 | } |
||
71 | 16 | if ( !isset($path[0]) ) { |
|
72 | 2 | return $cwd; |
|
73 | } |
||
74 | 16 | if ( isset($cwd) && $cwd && $path[0] !== '/' && $path[0] !== '\\' ) { |
|
75 | 2 | $path = $cwd . '/' . $path; |
|
76 | } |
||
77 | 16 | if ( isset(self::$collapseCache[$path]) ) { // cache hit - so return that |
|
78 | 14 | return self::$collapseCache[$path]; |
|
79 | } else { |
||
80 | 14 | $value = new \arc\path\Value($path); |
|
81 | 14 | if ( isset(self::$collapseCache[(string)$value]) ) { |
|
82 | 2 | self::$collapseCache[$path] = self::$collapseCache[(string)$value]; |
|
83 | 2 | return self::$collapseCache[(string)$value]; |
|
84 | } else { |
||
85 | 14 | self::$collapseCache[$path] = self::$collapseCache[(string) $value] = $value; |
|
86 | 14 | return $value; |
|
87 | } |
||
88 | } |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * This method cleans the input path with the given filter method. You can specify any of the |
||
93 | * sanitize methods valid for filter_var or you can use your own callback function. By default |
||
94 | * it url encodes each filename in the path. |
||
95 | * |
||
96 | * Usage: |
||
97 | * \arc\path::clean( '/a path/to somewhere/' ); // => '/a%20path/to%20somewhere/' |
||
98 | * |
||
99 | * @param string $path The path to clean. |
||
100 | * @param mixed $filter Either one of the sanitize filters for filter_var or a callback method as |
||
101 | * in \arc\path::map |
||
102 | * @param mixed $flags Optional list of flags for the sanitize filter. |
||
103 | * @return string The cleaned path. |
||
104 | */ |
||
105 | 2 | public static function clean($path, $filter = null, $flags = null) |
|
106 | { |
||
107 | 2 | if (is_callable( $filter )) { |
|
108 | 2 | $callback = $filter; |
|
109 | } else { |
||
110 | 2 | if (!isset( $filter )) { |
|
111 | 2 | $filter = FILTER_SANITIZE_ENCODED; |
|
112 | } |
||
113 | 2 | if (!isset($flags)) { |
|
114 | 2 | $flags = FILTER_FLAG_ENCODE_LOW | FILTER_FLAG_ENCODE_HIGH; |
|
115 | } |
||
116 | 2 | $callback = function ($entry) use ($filter, $flags) { |
|
117 | 2 | return filter_var( $entry, $filter, $flags); |
|
118 | 2 | }; |
|
119 | } |
||
120 | |||
121 | 2 | return self::map( $path, $callback ); |
|
122 | } |
||
123 | |||
124 | /** |
||
125 | * Returns either the immediate parent path for the given path, or null if it is outside the |
||
126 | * root path. Differs with dirname() in that it will not return '/' as a parent of '/', but |
||
127 | * null instead. |
||
128 | * |
||
129 | * Usage: |
||
130 | * \arc\path::parent( '/foo/bar/' ); // => '/foo/' |
||
131 | * |
||
132 | * @param string $path The path from which to get the parent path. |
||
133 | * @param string $root Optional root path, defaults to '/' |
||
134 | * @return string|null The parent of the given path or null if the parent is outside the root path. |
||
135 | */ |
||
136 | 4 | public static function parent($path, $root = '/') |
|
137 | { |
||
138 | 4 | if ($path == $root) { |
|
139 | 2 | return null; |
|
140 | } |
||
141 | 4 | $parent = dirname( $path ); |
|
142 | 4 | if (isset($parent[1])) { // fast check to see if there is a dirname |
|
143 | 4 | $parent .= '/'; |
|
144 | } |
||
145 | 4 | $parent[0] = '/'; // dirname('/something/') returns '\' in windows. |
|
146 | 4 | if (strpos( (string)$parent, (string)$root ) !== 0) { // parent is outside of the root |
|
147 | |||
148 | 2 | return null; |
|
149 | } |
||
150 | |||
151 | 4 | return $parent; |
|
152 | } |
||
153 | |||
154 | |||
155 | /** |
||
156 | * Returns the root entry of the given path. |
||
157 | * |
||
158 | * Usage: |
||
159 | * $rootEntry = \arc\path::head( '/root/of/a/path/' ); // => 'root' |
||
160 | * |
||
161 | * @param string $path The path to get the root entry of. |
||
162 | * @return string The root entry of the given path, without slashes. |
||
163 | */ |
||
164 | View Code Duplication | public static function head($path) |
|
|
|||
165 | { |
||
166 | if (!\arc\path::isAbsolute($path)) { |
||
167 | $path = '/' . $path; |
||
168 | } |
||
169 | |||
170 | return substr( (string)$path, 1, strpos( (string)$path, '/', 1) - 1 ); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * Returns the path without its root entry. |
||
175 | * |
||
176 | * Usage: |
||
177 | * $remainder = \arc\path::tail( '/root/of/a/path/' ); // => '/of/a/path/' |
||
178 | * |
||
179 | * @param string $path The path to get the tail of. |
||
180 | * @return string The path without its root entry. |
||
181 | */ |
||
182 | View Code Duplication | public static function tail($path) |
|
183 | { |
||
184 | if (!\arc\path::isAbsolute($path)) { |
||
185 | $path = '/' . $path; |
||
186 | } |
||
187 | |||
188 | return substr( (string)$path, strpos( (string)$path, '/', 1) ); |
||
189 | } |
||
190 | |||
191 | /** |
||
192 | * Returns the difference between sourcePath and targetPath as a relative path in |
||
193 | * such a way that if you append the relative path to the source path and collapse |
||
194 | * that, the result is the targetPath. |
||
195 | * @param string $targetPath The target path to map to. |
||
196 | * @param string $sourcePath The source path to start with. |
||
197 | * @return string The relative path from source to target. |
||
198 | */ |
||
199 | 6 | public static function diff($sourcePath, $targetPath) |
|
200 | { |
||
201 | 6 | $diff = ''; |
|
202 | 6 | $targetPath = \arc\path::collapse( $targetPath ); |
|
203 | 6 | $sourcePath = \arc\path::collapse( $sourcePath ); |
|
204 | 6 | $commonParent = \arc\path::search( $sourcePath, function ($path) use ($targetPath, &$diff) { |
|
205 | 6 | if (!\arc\path::isChild( $targetPath, $path )) { |
|
206 | 6 | $diff .= '../'; |
|
207 | } else { |
||
208 | 6 | return $path; |
|
209 | } |
||
210 | 6 | }, false); |
|
211 | 6 | $diff .= substr( $targetPath, strlen( $commonParent ) ); |
|
212 | |||
213 | 6 | return $diff; |
|
214 | } |
||
215 | |||
216 | /** |
||
217 | * Returns true if the path is a child or descendant of the parent. |
||
218 | * @param string $path The path to check |
||
219 | * @param string $parent The parent to check. |
||
220 | * @return bool True if path is a child or descendant of parent |
||
221 | */ |
||
222 | 12 | public static function isChild($path, $parent) |
|
228 | |||
229 | /** |
||
230 | * Returns true if the given path starts with a '/'. |
||
231 | * @param string $path The path to check |
||
232 | * @return bool True is the path starts with a '/' |
||
233 | */ |
||
234 | 8 | public static function isAbsolute($path) |
|
235 | { |
||
238 | |||
239 | 32 | protected static function getSplitPath($path) |
|
243 | |||
244 | /** |
||
245 | * Applies a callback function to each filename in a path. The result will be the new filename. |
||
246 | * |
||
247 | * Usage: |
||
248 | * /arc/path::map( '/foo>bar/', function ($filename) { |
||
249 | * return htmlentities( $filename, ENT_QUOTES ); |
||
250 | * } ); // => '/foo>bar/' |
||
251 | * |
||
252 | * @param string $path The path to alter. |
||
253 | * @param Callable $callback |
||
254 | * @return string A path with all filenames changed as by the callback method. |
||
255 | */ |
||
256 | 4 | public static function map($path, $callback) |
|
267 | |||
268 | /** |
||
269 | * Applies a callback function to each filename in a path, but the result of the callback is fed back |
||
270 | * to the next call to the callback method as the first argument. |
||
271 | * |
||
272 | * Usage: |
||
273 | * /arc/path::reduce( '/foo/bar/', function ($previousResult, $filename) { |
||
274 | * return $previousResult . $filename . '\\'; |
||
275 | * }, '\\' ); // => '\\foo\\bar\\' |
||
276 | * |
||
277 | * @param string $path The path to reduce. |
||
278 | * @param Callable $callback The method to apply to each filename of the path |
||
279 | * @param mixed $initial Optional. The initial reduced value to start the callback with. |
||
280 | * @return mixed The final result of the callback method |
||
281 | */ |
||
282 | 30 | public static function reduce($path, $callback, $initial = null) |
|
286 | |||
287 | /** |
||
288 | * Applies a callback function to each parent of a path, in order. Starting at the root by default, |
||
289 | * but optionally in reverse order. Will continue while the callback method returns null, otherwise |
||
290 | * returns the result of the callback method. |
||
291 | * |
||
292 | * Usage: |
||
293 | * $result = \arc\path::search( '/foo/bar/', function ($parent) { |
||
294 | * if ($parent == '/foo/') { // silly test |
||
295 | * return true; |
||
296 | * } |
||
297 | * }); |
||
298 | * |
||
299 | * @param string $path Each parent of this path will be passed to the callback method. |
||
300 | * @param Callable $callback The method to apply to each parent |
||
301 | * @param bool $startAtRoot Optional. If set to false, root will be called last, otherwise first. |
||
302 | * Defaults to true. |
||
303 | * @param string $root Optional. Specify another root, no parents above the root will be called. |
||
304 | * Defaults to '/'. |
||
305 | * @return mixed The first non-null result of the callback method |
||
306 | */ |
||
307 | 8 | public static function search($path, $callback, $startAtRoot = true, $root = '/') |
|
320 | } |
||
321 |
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.