Issues (20)

src/file/Path.php (1 issue)

1
<?php declare(strict_types=1);
2
/**
3
 * This file is part of the Phootwork package.
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 *
7
 * @license MIT License
8
 * @copyright Thomas Gossmann
9
 */
10
namespace phootwork\file;
11
12
use phootwork\lang\ArrayObject;
13
use phootwork\lang\Text;
14
use Stringable;
15
16
class Path implements Stringable {
17
	/** @var ArrayObject */
18
	private ArrayObject $segments;
19
20
	/** @var string */
21
	private string $stream = '';
22
23
	/** @var Text */
24
	private Text $pathname;
25
26
	/** @var string */
27
	private string $dirname;
28
29
	/** @var string */
30
	private string $filename;
31
32
	/** @var string */
33
	private string $extension;
34
35
	/**
36
	 * Path constructor.
37
	 *
38
	 * @param string|Stringable $pathname
39
	 */
40 20
	public function __construct(Stringable|string $pathname) {
41 20
		$this->pathname = new Text($pathname);
42
43 20
		if ($this->pathname->match('/^[a-zA-Z]+:\/\//')) {
44 10
			$this->stream = $this->pathname->slice(0, (int) $this->pathname->indexOf('://') + 3)->toString();
45 10
			$this->pathname = $this->pathname->substring((int) $this->pathname->indexOf('://') + 3);
46
		}
47
48 20
		$this->segments = $this->pathname->split('/');
49 20
		$this->extension = pathinfo($this->pathname->toString(), PATHINFO_EXTENSION);
0 ignored issues
show
Documentation Bug introduced by
It seems like pathinfo($this->pathname...ile\PATHINFO_EXTENSION) can also be of type array. However, the property $extension is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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