Completed
Pull Request — master (#87)
by Vladimir
02:24
created

FileExplorer::getExplorer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
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::getFileIterator()` 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 64
    public function __construct(\RecursiveIterator $iterator, array $excludes = [], array $includes = [], $flags = null)
83
    {
84 64
        parent::__construct($iterator);
85
86 64
        $this->excludes = array_merge(self::$vcsPatterns, $excludes);
87 64
        $this->includes = $includes;
88 64
        $this->flags = $flags;
89 64
    }
90
91
    /**
92
     * @return string
93
     */
94
    public function __toString()
95
    {
96
        return $this->current()->getFilename();
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 64
    public function accept()
103
    {
104 64
        $filePath = $this->current()->getRelativeFilePath();
105
106 64
        return $this->matchesPattern($filePath);
107
    }
108
109
    /**
110
     * Get the current File object.
111
     *
112
     * @return File
113
     */
114 64
    public function current()
115
    {
116
        /** @var \SplFileInfo $current */
117 64
        $current = parent::current();
118
119 64
        return new File($current->getPathname());
120
    }
121
122
    /**
123
     * {@inheritdoc}
124
     */
125 1
    public function getChildren()
126
    {
127 1
        return new self(
128 1
            $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 1
            $this->excludes,
130 1
            $this->includes,
131 1
            $this->flags
132 1
        );
133
    }
134
135
    /**
136
     * Get an Iterator with all of the files (and *only* files) that have met the search requirements.
137
     *
138
     * @return \RecursiveIteratorIterator
139
     */
140 62
    public function getFileIterator()
141
    {
142 62
        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 64
    public function matchesPattern($filePath)
153
    {
154 64
        if (self::strpos_array($filePath, $this->includes))
155 64
        {
156 21
            return true;
157
        }
158 49
        if (($this->flags & self::INCLUDE_ONLY_FILES) && !$this->current()->isDir())
159 49
        {
160 3
            return false;
161
        }
162 46
        if (($this->flags & self::IGNORE_DIRECTORIES) && $this->current()->isDir())
163 46
        {
164 1
            return false;
165
        }
166
167 46
        if (!($this->flags & self::ALLOW_DOT_FILES) &&
168 45
            preg_match('#(^|\\\\|\/)\..+(\\\\|\/|$)#', $filePath) === 1)
169 46
        {
170 1
            return false;
171
        }
172
173 46
        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 64
    public static function create($folder, $excludes = [], $includes = [], $flags = null)
187
    {
188 64
        $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 64
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
190
191 64
        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 64
    private static function strpos_array($haystack, $needle, $offset = 0)
204
    {
205 64
        if (!is_array($needle))
206 64
        {
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 64
        foreach ($needle as $query)
211
        {
212 63
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
213 63
            {
214 21
                return true;
215
            }
216
217 62
            if (strpos($haystack, $query, $offset) !== false)
218 62
            { // stop on first true result
219 2
                return true;
220
            }
221 63
        }
222
223 49
        return false;
224
    }
225
}
226