Completed
Push — master ( a41cf5...7efa4f )
by Vladimir
02:55
created

FileExplorer::realpath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 4
cts 4
cp 1
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
crap 2
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 53
    public function __construct(\RecursiveIterator $iterator, array $excludes = array(), array $includes = array(), $flags = null)
78
    {
79 53
        parent::__construct($iterator);
80
81 53
        $this->excludes = array_merge(self::$vcsPatterns, $excludes);
82 53
        $this->includes = $includes;
83 53
        $this->flags = $flags;
84 53
    }
85
86
    /**
87
     * @return string
88
     */
89
    public function __toString()
90
    {
91
        return $this->current()->getFilename();
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97 53
    public function accept()
98
    {
99 53
        $filePath = $this->current()->getRelativeFilePath();
100
101 53
        return $this->matchesPattern($filePath);
102
    }
103
104
    /**
105
     * Get the current File object.
106
     *
107
     * @return File
108
     */
109 53
    public function current()
110
    {
111
        /** @var \SplFileInfo $current */
112 53
        $current = parent::current();
113
114 53
        return new File($current->getPathname());
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 12
    public function getChildren()
121
    {
122 12
        return new self(
123 12
            $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...
124 12
            $this->excludes,
125 12
            $this->includes,
126 12
            $this->flags
127
        );
128
    }
129
130
    /**
131
     * Get an Iterator with all of the files that have met the search requirements.
132
     *
133
     * @return \RecursiveIteratorIterator
134
     */
135 53
    public function getExplorer()
136
    {
137 53
        return new \RecursiveIteratorIterator($this);
138
    }
139
140
    /**
141
     * Check whether or not a relative file path matches the definition given to this FileExplorer instance.
142
     *
143
     * @param string $filePath
144
     *
145
     * @return bool
146
     */
147 53
    public function matchesPattern($filePath)
148
    {
149 53
        if (self::strpos_array($filePath, $this->includes))
150
        {
151 18
            return true;
152
        }
153 51
        if (($this->flags & self::INCLUDE_ONLY_FILES) && !$this->current()->isDir())
154
        {
155
            return false;
156
        }
157
158 51
        if (!($this->flags & self::ALLOW_DOT_FILES) &&
159 51
            preg_match('#(^|\\\\|\/)\..+(\\\\|\/|$)#', $filePath) === 1)
160
        {
161
            return false;
162
        }
163
164 51
        return self::strpos_array($filePath, $this->excludes) === false;
165
    }
166
167
    /**
168
     * Create an instance of FileExplorer from a directory path as a string.
169
     *
170
     * @param string   $folder The path to the folder we're scanning
171
     * @param string[] $excludes
172
     * @param string[] $includes
173
     * @param int|null $flags
174
     *
175
     * @return FileExplorer
176
     */
177 53
    public static function create($folder, $excludes = array(), $includes = array(), $flags = null)
178
    {
179 53
        $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...
180 53
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
181
182 53
        return new self($iterator, $excludes, $includes, $flags);
183
    }
184
185
    /**
186
     * Search a given string for an array of possible elements.
187
     *
188
     * @param string   $haystack
189
     * @param string[] $needle
190
     * @param int      $offset
191
     *
192
     * @return bool True if an element from the given array was found in the string
193
     */
194 53
    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...
195
    {
196 53
        if (!is_array($needle))
197
        {
198
            $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...
199
        }
200
201 53
        foreach ($needle as $query)
202
        {
203 53
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
204
            {
205 18
                return true;
206
            }
207
208 53
            if (strpos($haystack, $query, $offset) !== false)
209
            { // stop on first true result
210 53
                return true;
211
            }
212
        }
213
214 51
        return false;
215
    }
216
217
    /**
218
     * Strip the current working directory from an absolute path.
219
     *
220
     * @param string $path An absolute path
221
     *
222
     * @return string
223
     */
224
    private static function getRelativePath($path)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
225
    {
226
        return str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $path);
227
    }
228
229
    /**
230
     * A vfsStream friendly way of getting the realpath() of something.
231
     *
232
     * @param string $path
233
     *
234
     * @return string
235
     */
236 53
    private static function realpath($path)
237
    {
238 53
        if (substr($path, 0, 6) == 'vfs://')
239
        {
240 18
            return $path;
241
        }
242
243 39
        return realpath($path);
244
    }
245
}
246