Completed
Push — master ( fb692e...c26f50 )
by ignace nyamagana
03:48
created

HierarchicalPath::createFromArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 2
1
<?php
2
/**
3
 * League.Uri (http://uri.thephpleague.com)
4
 *
5
 * @package   League.uri
6
 * @author    Ignace Nyamagana Butera <[email protected]>
7
 * @copyright 2013-2015 Ignace Nyamagana Butera
8
 * @license   https://github.com/thephpleague/uri/blob/master/LICENSE (MIT License)
9
 * @version   4.2.0
10
 * @link      https://github.com/thephpleague/uri/
11
 */
12
namespace League\Uri\Components;
13
14
use InvalidArgumentException;
15
use League\Uri\Interfaces\HierarchicalComponent;
16
use League\Uri\Interfaces\HierarchicalPath as HierarchicalPathInterface;
17
18
/**
19
 * Value object representing a URI path component.
20
 *
21
 * @package League.uri
22
 * @author  Ignace Nyamagana Butera <[email protected]>
23
 * @since   1.0.0
24
 */
25
class HierarchicalPath extends AbstractHierarchicalComponent implements HierarchicalPathInterface
26
{
27
    use PathTrait;
28
29
    /**
30
     * @inheritdoc
31
     */
32
    protected static $separator = '/';
33
34
    /**
35
     * return a new instance from an array or a traversable object
36
     *
37
     * @param \Traversable|string[] $data The segments list
38
     * @param int                   $type one of the constant IS_ABSOLUTE or IS_RELATIVE
39
     *
40
     * @throws InvalidArgumentException If $type is not a recognized constant
41
     *
42
     * @return static
43
     */
44 234
    public static function createFromSegments($data, $type = self::IS_RELATIVE)
45
    {
46 234
        static $type_list = [self::IS_ABSOLUTE => 1, self::IS_RELATIVE => 1];
47
48 234
        if (!isset($type_list[$type])) {
49 3
            throw new InvalidArgumentException('Please verify the submitted constant');
50
        }
51
52 231
        return new static(static::formatComponentString($data, $type));
53
    }
54
55
    /**
56
     * Return a formatted component string according to its type
57
     *
58
     * @param \Traversable|string[] $data The segments list
59
     * @param int                   $type
60
     *
61
     * @throws InvalidArgumentException If $data is invalid
62
     *
63
     * @return string
64
     */
65 231
    protected static function formatComponentString($data, $type)
66
    {
67 231
        $path = implode(static::$separator, static::validateIterator($data));
68 219
        if (self::IS_ABSOLUTE == $type) {
69 168
            return static::$separator.$path;
70
        }
71
72 51
        return $path;
73
    }
74
75
    /**
76
     * DEPRECATION WARNING! This method will be removed in the next major point release
77
     *
78
     * @deprecated deprecated since version 4.2
79
     *
80
     * return a new instance from an array or a traversable object
81
     *
82
     * @param \Traversable|string[] $data The segments list
83
     * @param int                   $type one of the constant IS_ABSOLUTE or IS_RELATIVE
84
     *
85
     * @throws InvalidArgumentException If $type is not a recognized constant
86
     *
87
     * @return static
88
     */
89
    public static function createFromArray($data, $type = self::IS_RELATIVE)
90
    {
91
        return static::createFromSegments($data, $type);
92
    }
93
94
    /**
95
     * New Instance
96
     *
97
     * @param string $path
98
     */
99 1009
    public function __construct($path = '')
100
    {
101 1009
        $path = $this->validateString($path);
102 1000
        $this->isAbsolute = self::IS_RELATIVE;
103 1000
        if (static::$separator == substr($path, 0, 1)) {
104 821
            $this->isAbsolute = self::IS_ABSOLUTE;
105 821
            $path = substr($path, 1, strlen($path));
106 547
        }
107
108 1000
        $append_delimiter = false;
109 1000
        if (static::$separator === substr($path, -1, 1)) {
110 138
            $path = substr($path, 0, -1);
111 138
            $append_delimiter = true;
112 92
        }
113
114 1000
        $this->data = $this->validate($path);
115 1000
        if ($append_delimiter) {
116 138
            $this->data[] = '';
117 92
        }
118 1000
    }
119
120
    /**
121
     * validate the submitted data
122
     *
123
     * @param string $data
124
     *
125
     * @return array
126
     */
127
    protected function validate($data)
128
    {
129 1000
        $filterSegment = function ($segment) {
130 1000
            return isset($segment);
131 1000
        };
132
133 1000
        $data = $this->decodePath($data);
134
135 1000
        return array_filter(explode(static::$separator, $data), $filterSegment);
136
    }
137
138
    /**
139
     * Return a new instance when needed
140
     *
141
     * @param array $data
142
     *
143
     * @return static
144
     */
145 183
    protected function newCollectionInstance(array $data)
146
    {
147 183
        if ($data == $this->data) {
148 6
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (League\Uri\Components\HierarchicalPath) is incompatible with the return type declared by the abstract method League\Uri\Components\Ab...::newCollectionInstance of type League\Uri\Types\ImmutableCollectionTrait.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
149
        }
150
151 177
        return $this->createFromSegments($data, $this->isAbsolute);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->createFrom...ta, $this->isAbsolute); (League\Uri\Components\HierarchicalPath) is incompatible with the return type declared by the abstract method League\Uri\Components\Ab...::newCollectionInstance of type League\Uri\Types\ImmutableCollectionTrait.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
152
    }
153
154
    /**
155
     * Retrieves a single path segment.
156
     *
157
     * Retrieves a single path segment. If the segment offset has not been set,
158
     * returns the default value provided.
159
     *
160
     * @param string $offset  the segment offset
161
     * @param mixed  $default Default value to return if the offset does not exist.
162
     *
163
     * @return mixed
164
     */
165 9
    public function getSegment($offset, $default = null)
166
    {
167 9
        if (isset($this->data[$offset])) {
168 6
            return $this->data[$offset];
169
        }
170
171 3
        return $default;
172
    }
173
174
    /**
175
     * @inheritdoc
176
     */
177 9
    public static function __set_state(array $properties)
178
    {
179 9
        return static::createFromSegments($properties['data'], $properties['isAbsolute']);
180
    }
181
182
    /**
183
     * @inheritdoc
184
     */
185 967
    public function getContent()
186
    {
187 967
        $front_delimiter = '';
188 967
        if ($this->isAbsolute == self::IS_ABSOLUTE) {
189 779
            $front_delimiter = static::$separator;
190 519
        }
191
192 967
        return $this->encodePath(
193 967
            $front_delimiter.implode(static::$separator, $this->data)
194 644
        );
195
    }
196
197
    /**
198
     * @inheritdoc
199
     */
200 967
    public function __toString()
201
    {
202 967
        return (string) $this->getContent();
203
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208 30
    public function prepend($component)
209
    {
210 30
        return $this->createFromSegments(
211 30
                $this->validateComponent($component),
212 30
                $this->isAbsolute
213 30
            )->append($this);
214
    }
215
216
    /**
217
     * Returns an instance with the specified component appended
218
     *
219
     * This method MUST retain the state of the current instance, and return
220
     * an instance that contains the modified component with the appended data
221
     *
222
     * @param HierarchicalComponent|string $component the component to append
223
     *
224
     * @return static
225
     */
226 72
    public function append($component)
227
    {
228 72
        $source = $this->toArray();
229 72
        if (!empty($source) && '' === end($source)) {
230 30
            array_pop($source);
231 20
        }
232
233 72
        return $this->newCollectionInstance(array_merge(
234 48
            $source,
235 72
            $this->validateComponent($component)->toArray()
236 48
        ));
237
    }
238
239
    /**
240
     * Returns the path basename
241
     *
242
     * @return string
243
     */
244 63
    public function getBasename()
245
    {
246 63
        $data = $this->data;
247
248 63
        return (string) array_pop($data);
249
    }
250
251
    /**
252
     * Returns parent directory's path
253
     *
254
     * @return string
255
     */
256 24
    public function getDirname()
257
    {
258 24
        return str_replace(
259 24
            ['\\', "\0"],
260 24
            [static::$separator, '\\'],
261 24
            dirname(str_replace('\\', "\0", $this->__toString()))
262 16
        );
263
    }
264
265
    /**
266
     * Returns the basename extension
267
     *
268
     * @return string
269
     */
270 57
    public function getExtension()
271
    {
272 57
        list($basename, ) = explode(';', $this->getBasename(), 2);
273
274 57
        return pathinfo($basename, PATHINFO_EXTENSION);
275
    }
276
277
    /**
278
     * Returns an instance with the specified basename extension
279
     *
280
     * This method MUST retain the state of the current instance, and return
281
     * an instance that contains the extension basename modified.
282
     *
283
     * @param string $extension the new extension
284
     *                          can preceeded with or without the dot (.) character
285
     *
286
     * @throws InvalidArgumentException If the extension is invalid
287
     *
288
     * @return static
289
     */
290 63
    public function withExtension($extension)
291
    {
292 63
        $extension = $this->formatExtension($extension);
293 57
        $segments = $this->toArray();
294 57
        $basename = array_pop($segments);
295 57
        $parts = explode(';', $basename, 2);
296 57
        $basenamePart = array_shift($parts);
297 57
        if ('' === $basenamePart || is_null($basenamePart)) {
298 6
            return $this;
299
        }
300
301 51
        $newBasename = $this->buildBasename($basenamePart, $extension, array_shift($parts));
302 51
        if ($basename === $newBasename) {
303 3
            return $this;
304
        }
305 48
        $segments[] = $newBasename;
306
307 48
        return $this->newCollectionInstance($segments);
308
    }
309
310
    /**
311
     * create a new basename with a new extension
312
     *
313
     * @param string $basenamePart  the basename file part
314
     * @param string $extension     the new extension to add
315
     * @param string $parameterPart the basename parameter part
316
     *
317
     * @return string
318
     */
319 51
    protected function buildBasename($basenamePart, $extension, $parameterPart)
320
    {
321 51
        $length = mb_strrpos($basenamePart, '.'.pathinfo($basenamePart, PATHINFO_EXTENSION), 'UTF-8');
322 51
        if (false !== $length) {
323 45
            $basenamePart = mb_substr($basenamePart, 0, $length, 'UTF-8');
324 30
        }
325
326 51
        $parameterPart = trim($parameterPart);
327 51
        if ('' !== $parameterPart) {
328 24
            $parameterPart = ";$parameterPart";
329 16
        }
330
331 51
        $extension = trim($extension);
332 51
        if ('' !== $extension) {
333 36
            $extension = ".$extension";
334 24
        }
335
336 51
        return $basenamePart.$extension.$parameterPart;
337
    }
338
339
    /**
340
     * validate and format the given extension
341
     *
342
     * @param string $extension the new extension to use
343
     *
344
     * @throws InvalidArgumentException If the extension is not valid
345
     *
346
     * @return string
347
     */
348 63
    protected function formatExtension($extension)
349
    {
350 63
        if (0 === strpos($extension, '.')) {
351 3
            throw new InvalidArgumentException('an extension sequence can not contain a leading `.` character');
352
        }
353
354 60
        if (strpos($extension, static::$separator)) {
355 3
            throw new InvalidArgumentException('an extension sequence can not contain a path delimiter');
356
        }
357
358 57
        return implode(static::$separator, $this->validate($extension));
359
    }
360
}
361