Completed
Push — master ( 7b9015...c5b311 )
by Vladimir
26s queued 11s
created

FileExplorer::getChildren()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
ccs 5
cts 6
cp 0.8333
crap 2.0185
1
<?php
2
3
/**
4
 * @copyright 2018 Vladimir Jimenez
5
 * @license   https://github.com/stakx-io/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\Filesystem;
9
10
use allejo\stakx\Filesystem\FilesystemLoader as fs;
11
12
/**
13
 * The core class to handle reading files from directories on the filesystem.
14
 *
15
 * This class is the macOS Finder or Windows Explorer equivalent for stakx. New instances of this class should only be
16
 * created through the `FileExplorer::create()` helper function. To access the file iterator from this instance, use
17
 * `FileExplorer::getFileIterator()` to retrieve File objects.
18
 *
19
 * @internal
20
 */
21
class FileExplorer extends \RecursiveFilterIterator implements \Iterator
22
{
23
    /**
24
     * A bitwise flag to have FileExplorer ignore all files unless its been explicitly included; all other files will be
25
     * ignored.
26
     */
27
    const INCLUDE_ONLY_FILES = 0x1;
28
29
    /**
30
     * A bitwise flag to have FileExplorer search files starting with a period as well.
31
     */
32
    const ALLOW_DOT_FILES = 0x2;
33
34
    /**
35
     * A bitwise flag to have FileExplorer ignore any directories.
36
     */
37
    const IGNORE_DIRECTORIES = 0x4;
38
39
    /**
40
     * A list of common version control folders to ignore.
41
     *
42
     * The following folders should be ignored explicitly by the end user. Their usage isn't as popular so adding more
43
     * conditions to loop through will only slow down FileExplorer.
44
     *
45
     *   - 'CVS'
46
     *   - '_darcs'
47
     *   - '.arch-params'
48
     *   - '.monotone'
49
     *   - '.bzr'
50
     *
51
     * @var string[]
52
     */
53
    public static $vcsPatterns = ['.git', '.hg', '.svn', '_svn'];
54
55
    /**
56
     * A custom callable that will be used in the `accept()` method. If null, the default matcher will be used.
57
     *
58
     * @var callable[]
59
     */
60
    private $matchers;
61
62
    /**
63
     * A list of phrases to exclude from the search.
64
     *
65
     * @var string[]
66
     */
67
    private $excludes;
68
69
    /**
70
     * A list of phrases to explicitly include in the search.
71
     *
72
     * @var string[]
73
     */
74
    private $includes;
75
76
    /**
77
     * The bitwise sum of the flags applied to this FileExplorer instance.
78
     *
79
     * @var int|null
80
     */
81
    private $flags;
82
83
    /**
84
     * FileExplorer constructor.
85
     *
86
     * @param \RecursiveIterator $iterator
87
     * @param string[] $includes
88
     * @param string[] $excludes
89
     * @param int|null $flags
90
     */
91 65
    public function __construct(\RecursiveIterator $iterator, array $includes = [], array $excludes = [], $flags = null)
92
    {
93 65
        parent::__construct($iterator);
94
95 65
        $this->excludes = array_merge(self::$vcsPatterns, $excludes);
96 65
        $this->includes = $includes;
97 65
        $this->flags = $flags;
98 65
        $this->matchers = [];
99 65
    }
100
101
    /**
102
     * @return string
103
     */
104
    public function __toString()
105
    {
106
        return $this->current()->getFilename();
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112 65
    public function accept()
113
    {
114 65
        if (!empty($this->matchers))
115
        {
116 1
            foreach ($this->matchers as $matcher)
117
            {
118 1
                $result = call_user_func($matcher, $this->current());
119
120
                // If any custom matchers return false, let's exit immediately
121 1
                if ($result === false)
122
                {
123 1
                    return false;
124
                }
125
            }
126
        }
127
128 65
        $filePath = $this->current()->getRelativeFilePath();
129
130 65
        return $this->matchesPattern($filePath);
131
    }
132
133
    /**
134
     * Get the current File object.
135
     *
136
     * @return File|Folder
137
     */
138 65
    public function current()
139
    {
140
        /** @var \SplFileInfo $current */
141 65
        $current = parent::current();
142 65
        $path = new FilesystemPath($current->getPathname());
143
144 65
        if ($current->isDir())
145
        {
146 3
            return new Folder($path);
147
        }
148
149 65
        return new File($path);
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 1
    public function getChildren()
156
    {
157 1
        $explorer = new self(
158 1
            $this->getInnerIterator()->getChildren(), $this->includes, $this->excludes, $this->flags
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Iterator as the method getChildren() does only exist in the following implementations of said interface: DoctrineTest\InstantiatorTestAsset\PharAsset, PHPUnit_Runner_Filter_GroupFilterIterator, PHPUnit_Runner_Filter_Group_Exclude, PHPUnit_Runner_Filter_Group_Include, PHPUnit_Runner_Filter_Test, PHPUnit_Util_TestSuiteIterator, PHP_CodeCoverage_Report_Node_Iterator, ParentIterator, Phar, PharData, RecursiveArrayIterator, RecursiveCachingIterator, RecursiveCallbackFilterIterator, RecursiveDirectoryIterator, RecursiveFilterIterator, RecursiveRegexIterator, SimpleXMLIterator, SplFileObject, SplTempFileObject, Symfony\Component\Finder...DirectoryFilterIterator, Symfony\Component\Finder...ursiveDirectoryIterator, allejo\stakx\Filesystem\FileExplorer.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
159
        );
160
161 1
        foreach ($this->matchers as $matcher)
162
        {
163
            $explorer->addMatcher($matcher);
164
        }
165
166 1
        return $explorer;
167
    }
168
169
    /**
170
     * Get an Iterator with all of the files (and *only* files) that have met the search requirements.
171
     *
172
     * @return \RecursiveIteratorIterator
173
     */
174 63
    public function getFileIterator()
175
    {
176 63
        return new \RecursiveIteratorIterator($this);
177
    }
178
179
    /**
180
     * Check whether or not a relative file path matches the definition given to this FileExplorer instance.
181
     *
182
     * @param string $filePath
183
     *
184
     * @return bool
185
     */
186 65
    public function matchesPattern($filePath)
187
    {
188 65
        if (self::strpos_array($filePath, $this->includes))
189
        {
190 53
            return true;
191
        }
192 14
        if (($this->flags & self::INCLUDE_ONLY_FILES) && !$this->current()->isDir())
193
        {
194 3
            return false;
195
        }
196 11
        if (($this->flags & self::IGNORE_DIRECTORIES) && $this->current()->isDir())
197
        {
198 1
            return false;
199
        }
200
201 11
        if (!($this->flags & self::ALLOW_DOT_FILES) &&
202 11
            preg_match('#(^|\\\\|\/)\..+(\\\\|\/|$)#', $filePath) === 1)
203
        {
204 1
            return false;
205
        }
206
207 11
        return self::strpos_array($filePath, $this->excludes) === false;
208
    }
209
210
    /**
211
     * Add a custom matcher that will be executed before the default matcher that uses file names and paths.
212
     *
213
     * @param callable $callable
214
     */
215 1
    public function addMatcher(callable $callable)
216
    {
217 1
        $this->matchers[] = $callable;
218 1
    }
219
220
    /**
221
     * Create an instance of FileExplorer from a directory path as a string.
222
     *
223
     * @deprecated Use `FileExplorer::createFromDefinition()` instead.
224
     *
225
     * @param string $folder The path to the folder we're scanning
226
     * @param string[] $includes
227
     * @param string[] $excludes
228
     * @param int|null $flags
229
     *
230
     * @return FileExplorer
231
     */
232
    public static function create($folder, $includes = [], $excludes = [], $flags = null)
233
    {
234
        $folder = fs::realpath($folder);
0 ignored issues
show
Documentation introduced by
$folder is of type string, but the function expects a object<string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Coding Style introduced by
Consider using a different name than the parameter $folder. This often makes code more readable.
Loading history...
235
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
236
237
        return new self($iterator, $includes, $excludes, $flags);
238
    }
239
240
    /**
241
     * @param FileExplorerDefinition $definition
242
     *
243
     * @return FileExplorer
244
     */
245 52
    public static function createFromDefinition(FileExplorerDefinition $definition)
246
    {
247 52
        return self::create($definition->folder, $definition->includes, $definition->excludes, $definition->flags);
0 ignored issues
show
Deprecated Code introduced by
The method allejo\stakx\Filesystem\FileExplorer::create() has been deprecated with message: Use `FileExplorer::createFromDefinition()` instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
248
    }
249
250
    /**
251
     * Search a given string for an array of possible elements.
252
     *
253
     * @param string   $haystack
254
     * @param string[] $needle
255
     * @param int      $offset
256
     *
257
     * @return bool True if an element from the given array was found in the string
258
     */
259 65
    private static function strpos_array($haystack, $needle, $offset = 0)
260
    {
261 65
        if (!is_array($needle))
262
        {
263
            $needle = [$needle];
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $needle. This often makes code more readable.
Loading history...
264
        }
265
266 65
        foreach ($needle as $query)
267
        {
268 64
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
269
            {
270 53
                return true;
271
            }
272
273 31
            if (strpos($haystack, $query, $offset) !== false)
274
            { // stop on first true result
275 31
                return true;
276
            }
277
        }
278
279 14
        return false;
280
    }
281
}
282