Passed
Pull Request — 4 (#10150)
by Maxime
08:05
created

ManifestFileFinder::findModuleCIPhpConfiguration()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 27
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 3
dl 0
loc 27
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use RuntimeException;
6
use SilverStripe\Assets\FileFinder;
7
8
/**
9
 * An extension to the default file finder with some extra filters to faciliate
10
 * autoload and template manifest generation:
11
 *   - Only modules with _config.php files are scanned.
12
 *   - If a _manifest_exclude file is present inside a directory it is ignored.
13
 *   - Assets and module language directories are ignored.
14
 *   - Module tests directories are skipped if the ignore_tests option is not
15
 *     set to false.
16
 */
17
class ManifestFileFinder extends FileFinder
18
{
19
20
    const CONFIG_FILE = '_config.php';
21
    const CONFIG_DIR = '_config';
22
    const EXCLUDE_FILE = '_manifest_exclude';
23
    const LANG_DIR = 'lang';
24
    const TESTS_DIR = 'tests';
25
    const VENDOR_DIR = 'vendor';
26
27
    /**
28
     * @deprecated 4.4.0:5.0.0 Use global `RESOURCES_DIR` instead.
29
     */
30
    const RESOURCES_DIR = RESOURCES_DIR;
31
32
    protected static $default_options = [
33
        'include_themes' => false,
34
        'ignore_tests' => true,
35
        'min_depth' => 1,
36
        'ignore_dirs' => ['node_modules'],
37
        'ignore_ci_configs' => []
38
    ];
39
40
    public function acceptDir($basename, $pathname, $depth)
41
    {
42
        // Skip if ignored
43
        if ($this->isInsideIgnored($basename, $pathname, $depth)) {
44
            return false;
45
        }
46
47
        // Keep searching inside vendor
48
        $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
49
        if ($inVendor) {
50
            // Skip nested vendor folders (e.g. vendor/silverstripe/framework/vendor)
51
            if ($depth == 4 && basename($pathname) === self::VENDOR_DIR) {
52
                return false;
53
            }
54
55
            // Keep searching if we could have a subdir module
56
            if ($depth < 3) {
57
                return true;
58
            }
59
60
            // Stop searching if we are in a non-module library
61
            $libraryPath = $this->upLevels($pathname, $depth - 3);
62
            $libraryBase = basename($libraryPath);
63
            if (!$this->isDirectoryModule($libraryBase, $libraryPath, 3)) {
64
                return false;
65
            }
66
        }
67
68
        // Include themes
69
        if ($this->getOption('include_themes') && $this->isInsideThemes($basename, $pathname, $depth)) {
70
            return true;
71
        }
72
73
        // Skip if not in module
74
        if (!$this->isInsideModule($basename, $pathname, $depth)) {
75
            return false;
76
        }
77
78
        // Skip if test dir inside vendor module with unexpected CI Configuration
79
        if ($depth > 3 && $basename === self::TESTS_DIR && $ignoreCIConfig = $this->getOption('ignore_ci_configs')) {
80
            $ciLib = $this->findModuleCIPhpConfiguration($basename, $pathname, $depth);
81
            if (in_array($ciLib, $ignoreCIConfig)) {
82
                return false;
83
            }
84
        }
85
86
        return parent::acceptDir($basename, $pathname, $depth);
87
    }
88
89
    /**
90
     * Check if the given dir is, or is inside the vendor folder
91
     *
92
     * @param string $basename
93
     * @param string $pathname
94
     * @param int $depth
95
     * @return bool
96
     */
97
    public function isInsideVendor($basename, $pathname, $depth)
98
    {
99
        $base = basename($this->upLevels($pathname, $depth - 1));
100
        return $base === self::VENDOR_DIR;
101
    }
102
103
    /**
104
     * Check if the given dir is, or is inside the themes folder
105
     *
106
     * @param string $basename
107
     * @param string $pathname
108
     * @param int $depth
109
     * @return bool
110
     */
111
    public function isInsideThemes($basename, $pathname, $depth)
112
    {
113
        $base = basename($this->upLevels($pathname, $depth - 1));
114
        return $base === THEMES_DIR;
115
    }
116
117
    /**
118
     * Check if this folder or any parent is ignored
119
     *
120
     * @param string $basename
121
     * @param string $pathname
122
     * @param int $depth
123
     * @return bool
124
     */
125
    public function isInsideIgnored($basename, $pathname, $depth)
126
    {
127
        return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
128
            return $this->isDirectoryIgnored($basename, $pathname, $depth);
129
        });
130
    }
131
132
    /**
133
     * Check if this folder is inside any module
134
     *
135
     * @param string $basename
136
     * @param string $pathname
137
     * @param int $depth
138
     * @return bool
139
     */
140
    public function isInsideModule($basename, $pathname, $depth)
141
    {
142
        return $this->anyParents($basename, $pathname, $depth, function ($basename, $pathname, $depth) {
143
            return $this->isDirectoryModule($basename, $pathname, $depth);
144
        });
145
    }
146
147
    /**
148
     * Check if any parents match the given callback
149
     *
150
     * @param string $basename
151
     * @param string $pathname
152
     * @param int $depth
153
     * @param callable $callback
154
     * @return bool
155
     */
156
    protected function anyParents($basename, $pathname, $depth, $callback)
157
    {
158
        // Check all ignored dir up the path
159
        while ($depth >= 0) {
160
            $ignored = $callback($basename, $pathname, $depth);
161
            if ($ignored) {
162
                return true;
163
            }
164
            $pathname = dirname($pathname);
165
            $basename = basename($pathname);
166
            $depth--;
167
        }
168
        return false;
169
    }
170
171
    /**
172
     * Check if the given dir is a module root (not a subdir)
173
     *
174
     * @param string $basename
175
     * @param string $pathname
176
     * @param string $depth
177
     * @return bool
178
     */
179
    public function isDirectoryModule($basename, $pathname, $depth)
180
    {
181
        // Depth can either be 0, 1, or 3 (if and only if inside vendor)
182
        $inVendor = $this->isInsideVendor($basename, $pathname, $depth);
0 ignored issues
show
Bug introduced by
$depth of type string is incompatible with the type integer expected by parameter $depth of SilverStripe\Core\Manife...inder::isInsideVendor(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

182
        $inVendor = $this->isInsideVendor($basename, $pathname, /** @scrutinizer ignore-type */ $depth);
Loading history...
183
        if ($depth > 0 && $depth !== ($inVendor ? 3 : 1)) {
184
            return false;
185
        }
186
187
        // True if config file exists
188
        if (file_exists($pathname . '/' . self::CONFIG_FILE)) {
189
            return true;
190
        }
191
192
        // True if config dir exists
193
        if (file_exists($pathname . '/' . self::CONFIG_DIR)) {
194
            return true;
195
        }
196
197
        return false;
198
    }
199
200
    /**
201
     * Get a parent path the given levels above
202
     *
203
     * @param string $pathname
204
     * @param int $depth Number of parents to rise
205
     * @return string
206
     */
207
    protected function upLevels($pathname, $depth)
208
    {
209
        if ($depth < 0) {
210
            return null;
211
        }
212
        while ($depth--) {
213
            $pathname = dirname($pathname);
214
        }
215
        return $pathname;
216
    }
217
218
    /**
219
     * Get all ignored directories
220
     *
221
     * @return array
222
     */
223
    protected function getIgnoredDirs()
224
    {
225
        $ignored = [self::LANG_DIR, 'node_modules'];
226
        if ($this->getOption('ignore_tests')) {
227
            $ignored[] = self::TESTS_DIR;
228
        }
229
        return $ignored;
230
    }
231
232
    /**
233
     * Check if the given directory is ignored
234
     * @param string $basename
235
     * @param string $pathname
236
     * @param string $depth
237
     * @return bool
238
     */
239
    public function isDirectoryIgnored($basename, $pathname, $depth)
240
    {
241
        // Don't ignore root
242
        if ($depth === 0) {
0 ignored issues
show
introduced by
The condition $depth === 0 is always false.
Loading history...
243
            return false;
244
        }
245
246
        // Check if manifest-ignored is present
247
        if (file_exists($pathname . '/' . self::EXCLUDE_FILE)) {
248
            return true;
249
        }
250
251
        // Check if directory name is ignored
252
        $ignored = $this->getIgnoredDirs();
253
        if (in_array($basename, $ignored)) {
254
            return true;
255
        }
256
257
        // Ignore these dirs in the root only
258
        if ($depth === 1 && in_array($basename, [ASSETS_DIR, RESOURCES_DIR])) {
0 ignored issues
show
introduced by
The condition $depth === 1 is always false.
Loading history...
259
            return true;
260
        }
261
262
        return false;
263
    }
264
265
    /**
266
     * Find out the root of the current module and read the PHP CI configuration from tho composer file
267
     *
268
     * @param string $basename Name of the current folder
269
     * @param string $pathname Full path the parent folder
270
     * @param string $depth Depth of the current folder
271
     */
272
    private function findModuleCIPhpConfiguration(string $basename, string $pathname, int $depth): string
273
    {
274
        if ($depth < 1) {
275
            // We went all the way back to the root of the project
276
            return Module::CI_UNKNOWN;
277
        }
278
279
        // We pop the current folder and use the next entry the pathname
280
        $newBasename = basename($pathname);
281
        $newPathname = dirname($pathname);
282
        $newDepth = $depth - 1;
283
284
        if ($this->isDirectoryModule($newBasename, $newPathname, $newDepth)) {
285
            // We've reached the root of the module folder, we can read the PHP CI config now
286
            $module = new Module($newPathname, $this->upLevels($newPathname, $newDepth));
287
            $config = $module->getCIConfig();
288
289
            if (empty($config['PHP'])) {
290
                // This should never happen
291
                throw new RuntimeException('Module::getCIConfig() did not return a PHP CI value');
292
            }
293
294
            return $config['PHP'];
295
        }
296
297
        // We haven't reach our module root yet ... let's look up one more level
298
        return $this->findModuleCIPhpConfiguration($newBasename, $newPathname, $newDepth);
299
    }
300
}
301