Completed
Pull Request — master (#41)
by Vladimir
02:38
created

FileExplorer::realpath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 9.6666
1
<?php
2
3
/**
4
 * @copyright 2016 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 36
    public function __construct(\RecursiveIterator $iterator, array $excludes = array(), array $includes = array(), $flags = null)
71
    {
72 36
        parent::__construct($iterator);
73
74 36
        $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 36
        $this->includes = $includes;
76 36
        $this->flags = $flags;
77 36
    }
78
79
    /**
80
     * @return string
81
     */
82
    public function __toString()
83
    {
84
        return $this->current()->getFilename();
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 36
    public function accept ()
91
    {
92 36
        $filePath = $this->current()->getRelativePathname();
93
94 36
        return $this->matchesPattern($filePath);
95
    }
96
97
    /**
98
     * Get the current SplFileInfo object
99
     *
100
     * @return SplFileInfo
101
     */
102 36
    public function current()
103
    {
104
        /** @var \SplFileInfo $current */
105 36
        $current = parent::current();
106
107 36
        return (new SplFileInfo(
108 36
            $current->getPathname(),
109 36
            self::getRelativePath($current->getPath()),
110 36
            self::getRelativePath($current->getPathname())
111 36
        ));
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 36
    public function getExplorer ()
133
    {
134 36
        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 36
    public function matchesPattern ($filePath)
145
    {
146 36
        if (self::strpos_array($filePath, $this->includes)) { return true; }
147 34
        if (($this->flags & self::INCLUDE_ONLY_FILES) && !$this->current()->isDir()) { return false; }
148
149 34
        if (!($this->flags & self::ALLOW_DOT_FILES) &&
150 34
            preg_match('#(^|\\\\|\/)\..+(\\\\|\/|$)#', $filePath) === 1) { return false; }
151
152 34
        return (self::strpos_array($filePath, $this->excludes) === false);
153
    }
154
155
    /**
156
     * Create an instance of FileExplorer from a directory path as a string
157
     *
158
     * @param  string   $folder   The path to the folder we're scanning
159
     * @param  string[] $excludes
160
     * @param  string[] $includes
161
     * @param  int|null $flags
162
     *
163
     * @return FileExplorer
164
     */
165 36
    public static function create ($folder, $excludes = array(), $includes = array(), $flags = null)
166
    {
167 36
        $folder = self::realpath($folder);
168 36
        $iterator = new \RecursiveDirectoryIterator($folder, \RecursiveDirectoryIterator::SKIP_DOTS);
169
170 36
        return (new self($iterator, $excludes, $includes, $flags));
171
    }
172
173
    /**
174
     * Search a given string for an array of possible elements
175
     *
176
     * @param  string   $haystack
177
     * @param  string[] $needle
178
     * @param  int      $offset
179
     *
180
     * @return bool True if an element from the given array was found in the string
181
     */
182 36
    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...
183
    {
184 36
        if (!is_array($needle))
185 36
        {
186
            $needle = array($needle);
187
        }
188
189 36
        foreach ($needle as $query)
190
        {
191 36
            if (substr($query, 0, 1) == '/' && substr($query, -1, 1) == '/' && preg_match($query, $haystack) === 1)
192 36
            {
193 6
                return true;
194
            }
195
196 36
            if (strpos($haystack, $query, $offset) !== false) // stop on first true result
197 36
            {
198
                return true;
199
            }
200 36
        }
201
202 34
        return false;
203
    }
204
205
    /**
206
     * Strip the current working directory from an absolute path
207
     *
208
     * @param  string $path An absolute path
209
     *
210
     * @return string
211
     */
212 36
    private static function getRelativePath ($path)
213
    {
214 36
        return str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $path);
215
    }
216
217
    /**
218
     * A vfsStream friendly way of getting the realpath() of something
219
     *
220
     * @param  string $path
221
     *
222
     * @return string
223
     */
224 36
    private static function realpath ($path)
225
    {
226 36
        if (substr($path, 0, 6) == 'vfs://')
227 36
        {
228 6
            return $path;
229
        }
230
231 34
        return realpath($path);
232
    }
233
}
234