Completed
Pull Request — master (#69)
by Vladimir
02:48
created

FileExplorer::accept()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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
/**
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 bitwise flag to have FileExplorer ignore any directories.
34
     */
35
    const IGNORE_DIRECTORIES = 0x4;
36
37
    /**
38
     * A list of common version control folders to ignore.
39
     *
40
     * The following folders should be ignored explicitly by the end user. Their usage isn't as popular so adding more
41
     * conditions to loop through will only slow down FileExplorer.
42
     *
43
     *   - 'CVS'
44
     *   - '_darcs'
45
     *   - '.arch-params'
46
     *   - '.monotone'
47
     *   - '.bzr'
48
     *
49
     * @var string[]
50
     */
51
    public static $vcsPatterns = ['.git', '.hg', '.svn', '_svn'];
52
53
    /**
54
     * A list of phrases to exclude from the search.
55
     *
56
     * @var string[]
57
     */
58
    private $excludes;
59
60
    /**
61
     * A list of phrases to explicitly include in the search.
62
     *
63
     * @var string[]
64
     */
65
    private $includes;
66
67
    /**
68
     * The bitwise sum of the flags applied to this FileExplorer instance.
69
     *
70
     * @var int|null
71
     */
72
    private $flags;
73
74
    /**
75
     * FileExplorer constructor.
76
     *
77
     * @param \RecursiveIterator $iterator
78
     * @param string[]           $excludes
79
     * @param string[]           $includes
80
     * @param int|null           $flags
81
     */
82 52
    public function __construct(\RecursiveIterator $iterator, array $excludes = [], array $includes = [], $flags = null)
83
    {
84 52
        parent::__construct($iterator);
85
86 52
        $this->excludes = array_merge(self::$vcsPatterns, $excludes);
87 52
        $this->includes = $includes;
88 52
        $this->flags = $flags;
89 52
    }
90
91
    /**
92
     * @return string
93
     */
94
    public function __toString()
95
    {
96
        return $this->current()->getFilename();
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 52
    public function accept()
103
    {
104 52
        $filePath = $this->current()->getRelativeFilePath();
105
106 52
        return $this->matchesPattern($filePath);
107
    }
108
109
    /**
110
     * Get the current File object.
111
     *
112
     * @return File
113
     */
114 52
    public function current()
115
    {
116
        /** @var \SplFileInfo $current */
117 52
        $current = parent::current();
118
119 52
        return new File($current->getPathname());
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125
    public function getChildren()
126
    {
127
        return new self(
128
            $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, 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...
129
            $this->excludes,
130
            $this->includes,
131
            $this->flags
132
        );
133
    }
134
135
    /**
136
     * Get an Iterator with all of the files that have met the search requirements.
137
     *
138
     * @return \RecursiveIteratorIterator
139
     */
140 52
    public function getExplorer()
141
    {
142 52
        return new \RecursiveIteratorIterator($this);
143
    }
144
145
    /**
146
     * Check whether or not a relative file path matches the definition given to this FileExplorer instance.
147
     *
148
     * @param string $filePath
149
     *
150
     * @return bool
151
     */
152 52
    public function matchesPattern($filePath)
153
    {
154 52
        if (self::strpos_array($filePath, $this->includes))
155
        {
156 18
            return true;
157
        }
158 38
        if (($this->flags & self::INCLUDE_ONLY_FILES) && !$this->current()->isDir())
159
        {
160
            return false;
161
        }
162 38
        if (($this->flags & self::IGNORE_DIRECTORIES) && $this->current()->isDir())
163
        {
164
            return false;
165
        }
166
167 38
        if (!($this->flags & self::ALLOW_DOT_FILES) &&
168 38
            preg_match('#(^|\\\\|\/)\..+(\\\\|\/|$)#', $filePath) === 1)
169
        {
170
            return false;
171
        }
172
173 38
        return self::strpos_array($filePath, $this->excludes) === false;
174
    }
175
176
    /**
177
     * Create an instance of FileExplorer from a directory path as a string.
178
     *
179
     * @param string   $folder   The path to the folder we're scanning
180
     * @param string[] $excludes
181
     * @param string[] $includes
182
     * @param int|null $flags
183
     *
184
     * @return FileExplorer
185
     */
186 52
    public static function create($folder, $excludes = [], $includes = [], $flags = null)
187
    {
188 52
        $folder = File::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...
189 52
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
190
191 52
        return new self($iterator, $excludes, $includes, $flags);
192
    }
193
194
    /**
195
     * Search a given string for an array of possible elements.
196
     *
197
     * @param string   $haystack
198
     * @param string[] $needle
199
     * @param int      $offset
200
     *
201
     * @return bool True if an element from the given array was found in the string
202
     */
203 52
    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...
204
    {
205 52
        if (!is_array($needle))
206
        {
207
            $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...
208
        }
209
210 52
        foreach ($needle as $query)
211
        {
212 52
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
213
            {
214 18
                return true;
215
            }
216
217 52
            if (strpos($haystack, $query, $offset) !== false)
218
            { // stop on first true result
219 52
                return true;
220
            }
221
        }
222
223 38
        return false;
224
    }
225
}
226