Completed
Push — master ( 505030...fbe7e8 )
by Vladimir
02:23
created

FileExplorer::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
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 array              $excludes
67
     * @param array              $includes
68
     * @param int|null           $flags
69
     */
70 37
    public function __construct(\RecursiveIterator $iterator, array $excludes = array(), array $includes = array(), $flags = null)
71
    {
72 37
        parent::__construct($iterator);
73
74 37
        $this->excludes = array_merge(self::$vcsPatterns, $excludes);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge(self::$vcsPatterns, $excludes) of type array is incompatible with the declared type array<integer,string> of property $excludes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
75 37
        $this->includes = $includes;
76 37
        $this->flags = $flags;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
77 37
    }
78
79
    /**
80
     * @return string
81
     */
82
    public function __toString()
83
    {
84
        return $this->current()->getFilename();
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 37
    public function accept()
91
    {
92 37
        $filePath = $this->current()->getRelativePathname();
93
94 37
        return $this->matchesPattern($filePath);
95
    }
96
97
    /**
98
     * Get the current SplFileInfo object.
99
     *
100
     * @return SplFileInfo
101
     */
102 37
    public function current()
103
    {
104
        /** @var \SplFileInfo $current */
105 37
        $current = parent::current();
106
107 37
        return new SplFileInfo(
108 37
            $current->getPathname(),
109 37
            self::getRelativePath($current->getPath()),
110 37
            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, 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 37
    public function getExplorer()
133
    {
134 37
        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 37
    public function matchesPattern($filePath)
0 ignored issues
show
Coding Style introduced by
function matchesPattern() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
145
    {
146 37
        if (self::strpos_array($filePath, $this->includes))
147
        {
148 6
            return true;
149
        }
150 35
        if (($this->flags & self::INCLUDE_ONLY_FILES) && !$this->current()->isDir())
151
        {
152
            return false;
153
        }
154
155 35
        if (!($this->flags & self::ALLOW_DOT_FILES) &&
156 35
            preg_match('#(^|\\\\|\/)\..+(\\\\|\/|$)#', $filePath) === 1)
157
        {
158
            return false;
159
        }
160
161 35
        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 37
    public static function create($folder, $excludes = array(), $includes = array(), $flags = null)
175
    {
176 37
        $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...
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
177 37
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
178
179 37
        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 37
    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...
Coding Style introduced by
function strpos_array() does not seem to conform to the naming convention (^(?:[a-z]|__)[a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
Coding Style introduced by
function strpos_array() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
192
    {
193 37
        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 37
        foreach ($needle as $query)
199
        {
200 37
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
201
            {
202 6
                return true;
203
            }
204
205 37
            if (strpos($haystack, $query, $offset) !== false)
206
            { // stop on first true result
207
                return true;
208
            }
209
        }
210
211 35
        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 37
    private static function getRelativePath($path)
222
    {
223 37
        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 37
    private static function realpath($path)
234
    {
235 37
        if (substr($path, 0, 6) == 'vfs://')
236
        {
237 6
            return $path;
238
        }
239
240 35
        return realpath($path);
241
    }
242
}
243