Completed
Pull Request — master (#69)
by Vladimir
05:36
created

FileExplorer::strpos_array()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 7.5375

Importance

Changes 0
Metric Value
dl 0
loc 22
c 0
b 0
f 0
rs 8.6346
ccs 7
cts 9
cp 0.7778
cc 7
nc 8
nop 3
crap 7.5375
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 38
    public function __construct(\RecursiveIterator $iterator, array $excludes = [], array $includes = [], $flags = null)
83
    {
84 38
        parent::__construct($iterator);
85
86 38
        $this->excludes = array_merge(self::$vcsPatterns, $excludes);
87 38
        $this->includes = $includes;
88 38
        $this->flags = $flags;
89 38
    }
90
91
    /**
92
     * @return string
93
     */
94
    public function __toString()
95
    {
96
        return $this->current()->getFilename();
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 38
    public function accept()
103
    {
104 38
        $filePath = $this->current()->getRelativeFilePath();
105
106 38
        return $this->matchesPattern($filePath);
107
    }
108
109
    /**
110
     * Get the current File object.
111
     *
112
     * @return File
113
     */
114 38
    public function current()
115
    {
116
        /** @var \SplFileInfo $current */
117 38
        $current = parent::current();
118
119 38
        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 38
    public function getExplorer()
141
    {
142 38
        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 38
    public function matchesPattern($filePath)
153
    {
154 38
        if (self::strpos_array($filePath, $this->includes))
155
        {
156
            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 38
    public static function create($folder, $excludes = [], $includes = [], $flags = null)
187
    {
188 38
        $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 38
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
190
191 38
        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 38
    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 38
        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 38
        foreach ($needle as $query)
211
        {
212 38
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
213
            {
214
                return true;
215
            }
216
217 38
            if (strpos($haystack, $query, $offset) !== false)
218
            { // stop on first true result
219 38
                return true;
220
            }
221
        }
222
223 38
        return false;
224
    }
225
}
226