Completed
Push — master ( 4a4860...19bc37 )
by Vladimir
01:56
created

FileExplorer::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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