Passed
Branch master (018ba4)
by Mathieu
06:43
created

FileLoader::basePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Charcoal\Loader;
4
5
use InvalidArgumentException;
6
7
// From PSR-3
8
use Psr\Log\LoggerAwareInterface;
9
use Psr\Log\LoggerAwareTrait;
10
use Psr\Log\NullLogger;
11
12
/**
13
 * Base File Loader
14
 */
15
class FileLoader implements
16
    LoggerAwareInterface
17
{
18
    use LoggerAwareTrait;
19
20
    /**
21
     * The loader's identifier (for caching found paths).
22
     *
23
     * @var string
24
     */
25
    protected $ident;
26
27
    /**
28
     * The paths to search in.
29
     *
30
     * @var string[]
31
     */
32
    protected $paths = [];
33
34
    /**
35
     * The base path to prepend to any relative paths to search in.
36
     *
37
     * @var string
38
     */
39
    private $basePath = '';
40
41
    /**
42
     * Return new FileLoader object.
43
     *
44
     * @param array $data The loader's dependencies.
45
     */
46
    public function __construct(array $data = null)
47
    {
48
        if (isset($data['base_path'])) {
49
            $this->setBasePath($data['base_path']);
50
        }
51
52
        if (isset($data['paths'])) {
53
            $this->addPaths($data['paths']);
0 ignored issues
show
Bug introduced by
It seems like $data['paths'] can also be of type string; however, parameter $paths of Charcoal\Loader\FileLoader::addPaths() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

53
            $this->addPaths(/** @scrutinizer ignore-type */ $data['paths']);
Loading history...
54
        }
55
56
        if (!isset($data['logger'])) {
57
            $data['logger'] = new NullLogger();
58
        }
59
60
        $this->setLogger($data['logger']);
61
    }
62
63
    /**
64
     * Retrieve the loader's identifier.
65
     *
66
     * @return string
67
     */
68
    public function ident()
69
    {
70
        return $this->ident;
71
    }
72
73
    /**
74
     * Set the loader's identifier.
75
     *
76
     * @param  mixed $ident A subset of language identifiers.
77
     * @throws InvalidArgumentException If the ident is invalid.
78
     * @return self
79
     */
80
    public function setIdent($ident)
81
    {
82
        if (!is_string($ident)) {
83
            throw new InvalidArgumentException(
84
                sprintf(
85
                    'Identifier for [%1$s] must be a string.',
86
                    get_called_class()
87
                )
88
            );
89
        }
90
91
        $this->ident = $ident;
92
93
        return $this;
94
    }
95
96
    /**
97
     * Retrieve the base path for relative search paths.
98
     *
99
     * @return string
100
     */
101
    public function basePath()
102
    {
103
        return $this->basePath;
104
    }
105
106
    /**
107
     * Assign a base path for relative search paths.
108
     *
109
     * @param  string $basePath The base path to use.
110
     * @throws InvalidArgumentException If the base path parameter is not a string.
111
     * @return self
112
     */
113 View Code Duplication
    public function setBasePath($basePath)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
114
    {
115
        if (!is_string($basePath)) {
116
            throw new InvalidArgumentException(
117
                'Base path must be a string'
118
            );
119
        }
120
121
        $basePath = realpath($basePath);
122
123
        $this->basePath = rtrim($basePath, '/\\').DIRECTORY_SEPARATOR;
0 ignored issues
show
Bug introduced by
It seems like $basePath can also be of type false; however, parameter $str of rtrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

123
        $this->basePath = rtrim(/** @scrutinizer ignore-type */ $basePath, '/\\').DIRECTORY_SEPARATOR;
Loading history...
124
125
        return $this;
126
    }
127
128
    /**
129
     * Returns the content of the first file found in search path.
130
     *
131
     * @param  string|null $ident Optional. A file to search for.
132
     * @return string The file's content or an empty string.
133
     */
134
    public function load($ident = null)
135
    {
136
        if ($ident === null) {
137
            return '';
138
        }
139
140
        $fileContent = $this->loadFirstFromSearchPath($ident);
141
142
        if ($fileContent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileContent of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
143
            return $fileContent;
144
        }
145
146
        return '';
147
    }
148
149
    /**
150
     * Load the first match from search paths.
151
     *
152
     * @param  string $filename A file to search for.
153
     * @return string|null The matched file's content or an empty string.
154
     */
155
    protected function loadFirstFromSearchPath($filename)
156
    {
157
        $file = $this->firstMatchingFilename($filename);
158
159
        if ($file) {
160
            return file_get_contents($file);
0 ignored issues
show
Bug Best Practice introduced by
The expression return file_get_contents($file) could also return false which is incompatible with the documented return type null|string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
161
        }
162
163
        return null;
164
    }
165
166
    /**
167
     * Retrieve the first match from search paths.
168
     *
169
     * @param  string $filename A file to search for.
170
     * @return string The full path to the matched file.
171
     */
172 View Code Duplication
    protected function firstMatchingFilename($filename)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
173
    {
174
        if (file_exists($filename)) {
175
            return $filename;
176
        }
177
178
        $paths = $this->paths();
179
180
        if (empty($paths)) {
181
            return null;
182
        }
183
184
        foreach ($paths as $path) {
185
            $file = $path.DIRECTORY_SEPARATOR.$filename;
186
            if (file_exists($file)) {
187
                return $file;
188
            }
189
        }
190
191
        return null;
192
    }
193
194
    /**
195
     * Retrieve all matches from search paths.
196
     *
197
     * @param  string $filename A file to search for.
198
     * @return array An array of matches.
199
     */
200
    protected function allMatchingFilenames($filename)
201
    {
202
        $matches = [];
203
204
        if (file_exists($filename)) {
205
            $matches[] = $filename;
206
        }
207
208
        $paths = $this->paths();
209
210
        if (empty($paths)) {
211
            return $matches;
212
        }
213
214
        foreach ($paths as $path) {
215
            $file = $path.DIRECTORY_SEPARATOR.$filename;
216
            if (file_exists($file)) {
217
                $matches[] = $file;
218
            }
219
        }
220
221
        return $matches;
222
    }
223
    /**
224
     * Load the contents of a JSON file.
225
     *
226
     * @param  mixed $filename The file path to retrieve.
227
     * @throws InvalidArgumentException If a JSON decoding error occurs.
228
     * @return array|null
229
     */
230 View Code Duplication
    protected function loadJsonFile($filename)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
231
    {
232
        $content = file_get_contents($filename);
233
234
        if ($content === null) {
235
            return null;
236
        }
237
238
        $data  = json_decode($content, true);
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type false; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

238
        $data  = json_decode(/** @scrutinizer ignore-type */ $content, true);
Loading history...
239
        $error = json_last_error();
240
241
        if ($error == JSON_ERROR_NONE) {
242
            return $data;
243
        }
244
245
        switch ($error) {
246
            case JSON_ERROR_NONE:
247
                break;
248
            case JSON_ERROR_DEPTH:
249
                $issue = 'Maximum stack depth exceeded';
250
                break;
251
            case JSON_ERROR_STATE_MISMATCH:
252
                $issue = 'Underflow or the modes mismatch';
253
                break;
254
            case JSON_ERROR_CTRL_CHAR:
255
                $issue = 'Unexpected control character found';
256
                break;
257
            case JSON_ERROR_SYNTAX:
258
                $issue = 'Syntax error, malformed JSON';
259
                break;
260
            case JSON_ERROR_UTF8:
261
                $issue = 'Malformed UTF-8 characters, possibly incorrectly encoded';
262
                break;
263
            default:
264
                $issue = 'Unknown error';
265
                break;
266
        }
267
268
        throw new InvalidArgumentException(
269
            sprintf('JSON %s could not be parsed: "%s"', $filename, $issue)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $issue does not seem to be defined for all execution paths leading up to this point.
Loading history...
270
        );
271
    }
272
273
    /**
274
     * Retrieve the searchable paths.
275
     *
276
     * @return string[]
277
     */
278
    public function paths()
279
    {
280
        return $this->paths;
281
    }
282
283
    /**
284
     * Assign a list of paths.
285
     *
286
     * @param  string[] $paths The list of paths to add.
287
     * @return self
288
     */
289
    public function setPaths(array $paths)
290
    {
291
        $this->paths = [];
292
        $this->addPaths($paths);
293
294
        return $this;
295
    }
296
297
    /**
298
     * Append a list of paths.
299
     *
300
     * @param  string[] $paths The list of paths to add.
301
     * @return self
302
     */
303
    public function addPaths(array $paths)
304
    {
305
        foreach ($paths as $path) {
306
            $this->addPath($path);
307
        }
308
309
        return $this;
310
    }
311
312
    /**
313
     * Append a path.
314
     *
315
     * @param  string $path A file or directory path.
316
     * @throws InvalidArgumentException If the path does not exist or is invalid.
317
     * @return self
318
     */
319 View Code Duplication
    public function addPath($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
320
    {
321
        $path = $this->resolvePath($path);
322
323
        if ($path && $this->validatePath($path)) {
324
            $this->paths[] = $path;
325
        }
326
327
        return $this;
328
    }
329
330
    /**
331
     * Prepend a path.
332
     *
333
     * @param  string $path A file or directory path.
334
     * @return self
335
     */
336 View Code Duplication
    public function prependPath($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
337
    {
338
        $path = $this->resolvePath($path);
339
340
        if ($path && $this->validatePath($path)) {
341
            array_unshift($this->paths, $path);
342
        }
343
344
        return $this;
345
    }
346
347
    /**
348
     * Parse a relative path using the base path if needed.
349
     *
350
     * @param  string $path The path to resolve.
351
     * @throws InvalidArgumentException If the path is invalid.
352
     * @return string
353
     */
354 View Code Duplication
    public function resolvePath($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
355
    {
356
        if (!is_string($path)) {
357
            throw new InvalidArgumentException(
358
                'Path needs to be a string'
359
            );
360
        }
361
362
        $basePath = $this->basePath();
363
        $path = ltrim($path, '/\\');
364
365
        if ($basePath && strpos($path, $basePath) === false) {
366
            $path = $basePath.$path;
367
        }
368
369
        return $path;
370
    }
371
372
    /**
373
     * Validate a resolved path.
374
     *
375
     * @param  string $path The path to validate.
376
     * @return boolean Returns TRUE if the path is valid otherwise FALSE.
377
     */
378
    public function validatePath($path)
379
    {
380
        return file_exists($path);
381
    }
382
}
383