Path::equals()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.5222
c 0
b 0
f 0
cc 5
nc 6
nop 1
1
<?php
2
namespace phootwork\file;
3
4
use phootwork\lang\ArrayObject;
5
use phootwork\lang\Text;
6
7
class Path {
8
	
9
	/** @var ArrayObject */
10
	private $segments;
11
12
	/** @var string */
13
	private $stream;
14
	
15
	/** @var Text */
16
	private $pathname;
17
	
18
	/** @var string */
19
	private $dirname;
20
	
21
	/** @var string */
22
	private $filename;
23
	
24
	/** @var string */
25
	private $extension;
26
	
27
	
28
	public function __construct($pathname) {
29
		$this->init($pathname);
30
	}
31
32
	private function init($pathname) {
33
		$this->pathname = $pathname instanceof Text ? $pathname : new Text($pathname);
34
35
		if ($this->pathname->match('/^[a-zA-Z]+:\/\//')) {
36
			$this->stream = $this->pathname->slice(0, $this->pathname->indexOf('://') + 3)->toString();
37
			$this->pathname = $this->pathname->substring($this->pathname->indexOf('://') + 3);
38
		}
39
40
		$this->segments = $this->pathname->split('/');
41
		$this->extension = pathinfo($this->pathname, PATHINFO_EXTENSION);
42
		$this->filename = basename($this->pathname);
43
		$this->dirname = dirname($this->pathname);
44
	}
45
46
	/**
47
	 * Returns the extension
48
	 * 
49
	 * @return string the extension
50
	 */
51
	public function getExtension() {
52
		return $this->extension;
53
	}
54
	
55
	/**
56
	 * Returns the filename
57
	 *
58
	 * @return string the filename
59
	 */
60
	public function getFilename() {
61
		return $this->filename;
62
	}
63
	
64
	/**
65
	 * Gets the path without filename
66
	 *
67
	 * @return string
68
	 */
69
	public function getDirname() {
70
		return $this->stream . $this->dirname;
71
	}
72
	
73
	/**
74
	 * Gets the full pathname
75
	 *
76
	 * @return Text
77
	 */
78
	public function getPathname() {
79
		return new Text ($this->stream . $this->pathname);
80
	}
81
82
	/**
83
	 * @return bool
84
	 */
85
	public function isStream() {
86
		return (null !== $this->stream);
87
	}
88
	
89
	/**
90
	 * Changes the extension of this path
91
	 * 
92
	 * @param string $extension the new extension
93
	 * @return $this
94
	 */
95
	public function setExtension($extension) {
96
		$pathinfo = pathinfo($this->pathname);
97
		
98
		$pathname = new Text($pathinfo['dirname']);
99
		if (!empty($pathinfo['dirname'])) {
100
			$pathname = $pathname->append('/');
101
		}
102
		
103
		$this->init($pathname
104
			->append($pathinfo['filename'])
105
			->append('.')
106
			->append($extension))
107
		;
108
109
		return $this;
110
	}
111
	
112
	/**
113
	 * Returns a path with the same segments as this path but with a 
114
	 * trailing separator added (if not already existent).
115
	 * 
116
	 * @return $this
117
	 */
118
	public function addTrailingSeparator() {
119
		if (!$this->hasTrailingSeparator()) {
120
			$this->pathname = $this->pathname->append('/');
121
		}
122
		return $this;
123
	}
124
125
	/**
126
	 * Returns the path obtained from the concatenation of the given path's 
127
	 * segments/string to the end of this path.
128
	 * 
129
	 * @param string|Text|Path $path
130
	 * @return Path
131
	 */
132
	public function append($path) {
133
		if ($path instanceof Path) {
134
			$path = $path->getPathname();
135
		}
136
		
137
		if (!$this->hasTrailingSeparator()) {
138
			$this->addTrailingSeparator();
139
		}
140
		
141
		return new Path($this->getPathname()->append($path));
142
	}
143
144
	/**
145
	 * Returns whether this path has a trailing separator.
146
	 * 
147
	 * @return boolean
148
	 */
149
	public function hasTrailingSeparator() {
150
		return $this->pathname->endsWith('/');
151
	}
152
	
153
	/**
154
	 * Returns whether this path is empty
155
	 * 
156
	 * @return boolean
157
	 */
158
	public function isEmpty() {
159
		return $this->pathname->isEmpty();
160
	}
161
	
162
	/**
163
	 * Returns whether this path is an absolute path.
164
	 * 
165
	 * @return boolean
166
	 */
167
	public function isAbsolute() {
168
		//Stream urls are always absolute
169
		if ($this->isStream()) {
170
			return true;
171
		}
172
173
		if (realpath($this->pathname->toString()) == $this->pathname->toString()) {
174
			return true;
175
		}
176
		
177
		if ($this->pathname->length() == 0 || $this->pathname->startsWith('.')) {
178
			return false;
179
		}
180
		
181
		// Windows allows absolute paths like this.
182
		if ($this->pathname->match('#^[a-zA-Z]:\\\\#')) {
183
			return true;
184
		}
185
186
		// A path starting with / or \ is absolute; anything else is relative.
187
		return $this->pathname->startsWith('/') || $this->pathname->startsWith('\\');
188
	}
189
	
190
	/**
191
	 * Checks whether this path is the prefix of another path
192
	 * 
193
	 * @param Path $anotherPath
194
	 * @return boolean
195
	 */
196
	public function isPrefixOf(Path $anotherPath) {
197
		return $anotherPath->getPathname()->startsWith($this->pathname);
198
	}
199
200
	/**
201
	 * Returns the last segment of this path, or null if it does not have any segments.
202
	 * 
203
	 * @return Text
204
	 */
205
	public function lastSegment() {
206
		return new Text($this->segments[count($this->segments) - 1]);
207
	}
208
209
	/**
210
	 * Makes the path relative to another given path
211
	 * 
212
	 * @param Path $base
213
	 * @return Path the new relative path
214
	 */
215
	public function makeRelativeTo(Path $base) {
216
		$pathname = clone $this->pathname;
217
		return new Path($pathname->replace($base->removeTrailingSeparator()->getPathname(), ''));
218
	}
219
	
220
	/**
221
	 * Returns a count of the number of segments which match in this 
222
	 * path and the given path, comparing in increasing segment number order.
223
	 * 
224
	 * @param Path $anotherPath
225
	 * @return int
226
	 */
227
	public function matchingFirstSegments(Path $anotherPath) {
228
		$segments = $anotherPath->segments();
229
		$count = 0;
230
		foreach ($this->segments as $i => $segment) {
231
			if ($segment != $segments[$i]) {
232
				break;
233
			}
234
			$count++;
235
		}
236
		
237
		return $count;
238
	}
239
	
240
	/**
241
	 * Returns a new path which is the same as this path but with the file extension removed.
242
	 * 
243
	 * @return Path
244
	 */
245
	public function removeExtension() {
246
		return new Path($this->pathname->replace('.' . $this->getExtension(), ''));
247
	}
248
	
249
	/**
250
	 * Returns a copy of this path with the given number of segments removed from the beginning.
251
	 * 
252
	 * @param int $count
253
	 * @return Path
254
	 */
255
	public function removeFirstSegments($count) {
256
		$segments = new ArrayObject();
257
		for ($i = $count; $i < $this->segmentCount(); $i++) {
258
			$segments->push($this->segments[$i]);
259
		}
260
		return new Path($segments->join('/'));
261
	}
262
	
263
	/**
264
	 * Returns a copy of this path with the given number of segments removed from the end.
265
	 * 
266
	 * @param int $count
267
	 * @return Path
268
	 */
269
	public function removeLastSegments($count) {
270
		$segments = new ArrayObject();
271
		for ($i = 0; $i < $this->segmentCount() - $count; $i++) {
272
			$segments->push($this->segments[$i]);
273
		}
274
		return new Path($segments->join('/'));
275
	}
276
	
277
	/**
278
	 * Returns a copy of this path with the same segments as this path but with a trailing separator removed.
279
	 * 
280
	 * @return $this
281
	 */
282
	public function removeTrailingSeparator() {
283
		if ($this->hasTrailingSeparator()) {
284
			$this->pathname = $this->pathname->substring(0, -1);
285
		}
286
		return $this;
287
	}
288
	
289
	/**
290
	 * Returns the specified segment of this path, or null if the path does not have such a segment.
291
	 * 
292
	 * @param int $index
293
	 * @return string
294
	 */
295
	public function segment($index) {
296
		if (isset($this->segments[$index])) {
297
			return $this->segments[$index];
298
		}
299
300
		return null;
301
	}
302
	
303
	/**
304
	 * Returns the number of segments in this path.
305
	 * 
306
	 * @return int
307
	 */
308
	public function segmentCount() {
309
		return $this->segments->count();
310
	}
311
	
312
	/**
313
	 * Returns the segments in this path in order.
314
	 * 
315
	 * @return ArrayObject<string>
316
	 */
317
	public function segments() {
318
		return $this->segments;
319
	}
320
	
321
	/**
322
	 * Returns a FileDescriptor corresponding to this path.
323
	 * 
324
	 * @return FileDescriptor
325
	 */
326
	public function toFileDescriptor() {
327
		return new FileDescriptor($this->getPathname());
328
	}
329
	
330
	/**
331
	 * Returns a string representation of this path
332
	 * 
333
	 * @return string A string representation of this path
334
	 */
335
	public function toString() {
336
		return $this->stream . $this->pathname;
337
	}
338
	
339
	/**
340
	 * String representation as pathname
341
	 */
342
	public function __toString() {
343
		return $this->toString();
344
	}
345
	
346
	/**
347
	 * Returns a copy of this path truncated after the given number of segments.
348
	 * 
349
	 * @param int $count
350
	 * @return Path
351
	 */
352
	public function upToSegment($count) {
353
		$segments = new ArrayObject();
354
		for ($i = 0; $i < $count; $i++) {
355
			$segments->push($this->segments[$i]);
356
		}
357
358
		return new Path($segments->join('/'));
359
	}
360
	
361
	/**
362
	 * Checks whether both paths point to the same location
363
	 * 
364
	 * @param Path|string $anotherPath
365
	 * @return boolean true if the do, false if they don't
366
	 */
367
	public function equals($anotherPath) {
368
		$anotherPath = $anotherPath instanceof Path ? $anotherPath : new Path($anotherPath);
369
370
		if ($this->isStream() ^ $anotherPath->isStream()) {
371
			return false;
372
		}
373
374
		if ($this->isStream() && $anotherPath->isStream()) {
375
			return $this->toString() === $anotherPath->toString();
376
		}
377
378
		return realpath($this->pathname->toString()) == realpath($anotherPath->toString());
379
	}
380
}
381