Completed
Push — master ( 80d5dd...e32bbf )
by Vladimir
03:02
created

FileExplorer::matchesPattern()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.1666

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 10
cts 12
cp 0.8333
rs 8.8571
c 0
b 0
f 0
cc 6
eloc 9
nc 4
nop 1
crap 6.1666
1
<?php
2
3
/**
4
 * @copyright 2017 Vladimir Jimenez
5
 * @license   https://github.com/allejo/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx\Filesystem;
9
10
/**
11
 * The core class to handle reading files from directories on the filesystem.
12
 *
13
 * This class is the macOS Finder or Windows Explorer equivalent for stakx. New instances of this class should only be
14
 * created through the `FileExplorer::create()` helper function. To access the file iterator from this instance, use
15
 * `FileExplorer::getExplorer()` to retrieve File objects.
16
 *
17
 * @internal
18
 */
19
class FileExplorer extends \RecursiveFilterIterator implements \Iterator
20
{
21
    /**
22
     * A bitwise flag to have FileExplorer ignore all files unless its been explicitly included; all other files will be
23
     * ignored.
24
     */
25
    const INCLUDE_ONLY_FILES = 0x1;
26
27
    /**
28
     * A bitwise flag to have FileExplorer search files starting with a period as well.
29
     */
30
    const ALLOW_DOT_FILES = 0x2;
31
32
    /**
33
     * A list of common version control folders to ignore.
34
     *
35
     * The following folders should be ignored explicitly by the end user. Their usage isn't as popular so adding more
36
     * conditions to loop through will only slow down FileExplorer.
37
     *
38
     *   - 'CVS'
39
     *   - '_darcs'
40
     *   - '.arch-params'
41
     *   - '.monotone'
42
     *   - '.bzr'
43
     *
44
     * @var string[]
45
     */
46
    public static $vcsPatterns = ['.git', '.hg', '.svn', '_svn'];
47
48
    /**
49
     * A list of phrases to exclude from the search.
50
     *
51
     * @var string[]
52
     */
53
    private $excludes;
54
55
    /**
56
     * A list of phrases to explicitly include in the search.
57
     *
58
     * @var string[]
59
     */
60
    private $includes;
61
62
    /**
63
     * The bitwise sum of the flags applied to this FileExplorer instance.
64
     *
65
     * @var int|null
66
     */
67
    private $flags;
68
69
    /**
70
     * FileExplorer constructor.
71
     *
72
     * @param \RecursiveIterator $iterator
73
     * @param string[] $excludes
74
     * @param string[] $includes
75
     * @param int|null $flags
76
     */
77 41
    public function __construct(\RecursiveIterator $iterator, array $excludes = array(), array $includes = array(), $flags = null)
78
    {
79 41
        parent::__construct($iterator);
80
81 41
        $this->excludes = array_merge(self::$vcsPatterns, $excludes);
82 41
        $this->includes = $includes;
83 41
        $this->flags = $flags;
84 41
    }
85
86
    /**
87
     * @return string
88
     */
89
    public function __toString()
90
    {
91
        return $this->current()->getFilename();
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 41
    public function accept()
98
    {
99 41
        $filePath = $this->current()->getRelativeFilePath();
100
101 41
        return $this->matchesPattern($filePath);
102
    }
103
104
    /**
105
     * Get the current File object.
106
     *
107
     * @return File
108
     */
109 41
    public function current()
110
    {
111
        /** @var \SplFileInfo $current */
112 41
        $current = parent::current();
113
114 41
        return new File(
115 41
            $current->getPathname(),
116 41
            self::getRelativePath($current->getPath()),
117 41
            self::getRelativePath($current->getPathname())
118 41
        );
119
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124
    public function getChildren()
125
    {
126
        return new self(
127
            $this->getInnerIterator()->getChildren(),
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, PHP_CodeSniffer\Filters\ExactMatch, PHP_CodeSniffer\Filters\Filter, PHP_CodeSniffer\Filters\GitModified, 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...
128
            $this->excludes,
129
            $this->includes,
130
            $this->flags
131
        );
132
    }
133
134
    /**
135
     * Get an Iterator with all of the files that have met the search requirements.
136
     *
137
     * @return \RecursiveIteratorIterator
138
     */
139 41
    public function getExplorer()
140
    {
141 41
        return new \RecursiveIteratorIterator($this);
142
    }
143
144
    /**
145
     * Check whether or not a relative file path matches the definition given to this FileExplorer instance.
146
     *
147
     * @param string $filePath
148
     *
149
     * @return bool
150
     */
151 41
    public function matchesPattern($filePath)
152
    {
153 41
        if (self::strpos_array($filePath, $this->includes))
154 41
        {
155 6
            return true;
156
        }
157 39
        if (($this->flags & self::INCLUDE_ONLY_FILES) && !$this->current()->isDir())
158 39
        {
159
            return false;
160
        }
161
162 39
        if (!($this->flags & self::ALLOW_DOT_FILES) &&
163 39
            preg_match('#(^|\\\\|\/)\..+(\\\\|\/|$)#', $filePath) === 1)
164 39
        {
165
            return false;
166
        }
167
168 39
        return self::strpos_array($filePath, $this->excludes) === false;
169
    }
170
171
    /**
172
     * Create an instance of FileExplorer from a directory path as a string.
173
     *
174
     * @param string   $folder The path to the folder we're scanning
175
     * @param string[] $excludes
176
     * @param string[] $includes
177
     * @param int|null $flags
178
     *
179
     * @return FileExplorer
180
     */
181 41
    public static function create($folder, $excludes = array(), $includes = array(), $flags = null)
182
    {
183 41
        $folder = self::realpath($folder);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $folder. This often makes code more readable.
Loading history...
184 41
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
185
186 41
        return new self($iterator, $excludes, $includes, $flags);
187
    }
188
189
    /**
190
     * Search a given string for an array of possible elements.
191
     *
192
     * @param string   $haystack
193
     * @param string[] $needle
194
     * @param int      $offset
195
     *
196
     * @return bool True if an element from the given array was found in the string
197
     */
198 41
    private static function strpos_array($haystack, $needle, $offset = 0)
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

Loading history...
199
    {
200 41
        if (!is_array($needle))
201 41
        {
202
            $needle = array($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...
203
        }
204
205 41
        foreach ($needle as $query)
206
        {
207 41
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
208 41
            {
209 6
                return true;
210
            }
211
212 41
            if (strpos($haystack, $query, $offset) !== false)
213 41
            { // stop on first true result
214
                return true;
215
            }
216 41
        }
217
218 39
        return false;
219
    }
220
221
    /**
222
     * Strip the current working directory from an absolute path.
223
     *
224
     * @param string $path An absolute path
225
     *
226
     * @return string
227
     */
228 41
    private static function getRelativePath($path)
229
    {
230 41
        return str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $path);
231
    }
232
233
    /**
234
     * A vfsStream friendly way of getting the realpath() of something.
235
     *
236
     * @param string $path
237
     *
238
     * @return string
239
     */
240 41
    private static function realpath($path)
241
    {
242 41
        if (substr($path, 0, 6) == 'vfs://')
243 41
        {
244 6
            return $path;
245
        }
246
247 39
        return realpath($path);
248
    }
249
}
250