Completed
Pull Request — master (#21)
by Greg
02:18
created

CommandFileDiscovery::setIncludeFilesAtBase()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
use Symfony\Component\Finder\Finder;
5
6
/**
7
 * Do discovery presuming that the namespace of the command will
8
 * contain the last component of the path.  This is the convention
9
 * that should be used when searching for command files that are
10
 * bundled with the modules of a framework.  The convention used
11
 * is that the namespace for a module in a framework should start with
12
 * the framework name followed by the module name.
13
 *
14
 * For example, if base namespace is "Drupal", then a command file in
15
 * modules/default_content/src/CliTools/ExampleCommands.cpp
16
 * will be in the namespace Drupal\default_content\CliTools.
17
 *
18
 * For global locations, the middle component of the namespace is
19
 * omitted.  For example, if the base namespace is "Drupal", then
20
 * a command file in __DRUPAL_ROOT__/CliTools/ExampleCommands.cpp
21
 * will be in the namespace Drupal\CliTools.
22
 *
23
 * To discover namespaced commands in modules:
24
 *
25
 * $commandFiles = $discovery->discoverNamespaced($moduleList, '\Drupal');
26
 *
27
 * To discover global commands:
28
 *
29
 * $commandFiles = $discovery->discover($drupalRoot, '\Drupal');
30
 */
31
class CommandFileDiscovery
32
{
33
    protected $excludeList;
34
    protected $searchLocations;
35
    protected $searchPattern = '*Commands.php';
36
    protected $includeFilesAtBase = true;
37
38
    public function __construct()
39
    {
40
        $this->excludeList = ['Exclude'];
41
        $this->searchLocations = [
42
            'Command',
43
            'CliTools', // TODO: Maybe remove
44
        ];
45
    }
46
47
    /**
48
     * Specify whether to search for files at the base directory
49
     * ($directoryList parameter to discover and discoverNamespaced
50
     * methods), or only in the directories listed in the search paths.
51
     *
52
     * @param type $includeFilesAtBase
53
     */
54
    public function setIncludeFilesAtBase($includeFilesAtBase)
55
    {
56
        $this->includeFilesAtBase = $includeFilesAtBase;
0 ignored issues
show
Documentation Bug introduced by
It seems like $includeFilesAtBase of type object<Consolidation\AnnotatedCommand\type> is incompatible with the declared type boolean of property $includeFilesAtBase.

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...
57
        return $this;
58
    }
59
60
    /**
61
     * Set the list of excludes to add to the finder, replacing
62
     * whatever was there before.
63
     *
64
     * @param array $excludeList The list of directory names to skip when
65
     *   searching for command files.
66
     */
67
    public function setExcludeList($excludeList)
68
    {
69
        $this->excludeList = $excludeList;
70
        return $this;
71
    }
72
73
    /**
74
     * Add one more location to the exclude list.
75
     *
76
     * @param string $exclude One directory name to skip when searching
77
     *   for command files.
78
     */
79
    public function addExclude($exclude)
80
    {
81
        $this->excludeList[] = $exclude;
82
        return $this;
83
    }
84
85
    /**
86
     * Set the list of search locations to examine in each directory where
87
     * command files may be found.  This replaces whatever was there before.
88
     *
89
     * @param array $searchLocations The list of locations to search for command files.
90
     */
91
    public function setSearchLocations($searchLocations)
92
    {
93
        $this->searchLocations = $searchLocations;
94
        return $this;
95
    }
96
97
    /**
98
     * Add one more location to the search location list.
99
     *
100
     * @param string $location One more relative path to search
101
     *   for command files.
102
     */
103
    public function addSearchLocation($location)
104
    {
105
        $this->searchLocations[] = $location;
106
        return $this;
107
    }
108
109
    /**
110
     * Specify the pattern / regex used by the finder to search for
111
     * command files.
112
     */
113
    public function setSearchPattern($searchPattern)
114
    {
115
        $this->searchPattern = $searchPattern;
116
        return $this;
117
    }
118
119
    /**
120
     * Given a list of directories, e.g. Drupal modules like:
121
     *
122
     *    core/modules/block
123
     *    core/modules/dblog
124
     *    modules/default_content
125
     *
126
     * Discover command files in any of these locations.
127
     *
128
     * @param string|string[] $directoryList Places to search for commands.
129
     *
130
     * @return array
131
     */
132
    public function discoverNamespaced($directoryList, $baseNamespace = '')
133
    {
134
        return $this->discover($this->convertToNamespacedList((array)$directoryList), $baseNamespace);
135
    }
136
137
    /**
138
     * Given a simple list containing paths to directories, where
139
     * the last component of the path should appear in the namespace,
140
     * after the base namespace, this function will return an
141
     * associative array mapping the path's basename (e.g. the module
142
     * name) to the directory path.
143
     *
144
     * Module names must be unique.
145
     *
146
     * @param string[] $directoryList A list of module locations
147
     *
148
     * @return array
149
     */
150
    public function convertToNamespacedList($directoryList)
151
    {
152
        $namespacedArray = [];
153
        foreach ((array)$directoryList as $directory) {
154
            $namespacedArray[basename($directory)] = $directory;
155
        }
156
        return $namespacedArray;
157
    }
158
159
    /**
160
     * Search for command files in the specified locations. This is the function that
161
     * should be used for all locations that are NOT modules of a framework.
162
     *
163
     * @param string|string[] $directoryList Places to search for commands.
164
     * @return array
165
     */
166
    public function discover($directoryList, $baseNamespace = '')
167
    {
168
        $commandFiles = [];
169
        foreach ((array)$directoryList as $key => $directory) {
170
            $itemsNamespace = $this->joinNamespace([$baseNamespace, $key]);
171
            $commandFiles = array_merge(
172
                $commandFiles,
173
                $this->discoverCommandFiles($directory, $itemsNamespace),
174
                $this->discoverCommandFiles("$directory/src", $itemsNamespace)
175
            );
176
        }
177
        return $commandFiles;
178
    }
179
180
    /**
181
     * Search for command files in specific locations within a single directory.
182
     *
183
     * In each location, we will accept only a few places where command files
184
     * can be found. This will reduce the need to search through many unrelated
185
     * files.
186
     *
187
     * The valid search locations include:
188
     *
189
     *    .
190
     *    CliTools
191
     *    src/CliTools
192
     *
193
     * The pattern we will look for is any file whose name ends in 'Commands.php'.
194
     * A list of paths to found files will be returned.
195
     */
196
    protected function discoverCommandFiles($directory, $baseNamespace)
197
    {
198
        $commandFiles = [];
199
        // In the search location itself, we will search for command files
200
        // immediately inside the directory only.
201
        if ($this->includeFilesAtBase) {
202
            $commandFiles = $this->discoverCommandFilesInLocation($directory, '== 0', $baseNamespace);
203
        }
204
205
        // In the other search locations,
206
        foreach ($this->searchLocations as $location) {
207
            $itemsNamespace = $this->joinNamespace([$baseNamespace, $location]);
208
            $commandFiles = array_merge(
209
                $commandFiles,
210
                $this->discoverCommandFilesInLocation("$directory/$location", '< 2', $itemsNamespace)
211
            );
212
        }
213
        return $commandFiles;
214
    }
215
216
    /**
217
     * Search for command files in just one particular location.  Returns
218
     * an associative array mapping from the pathname of the file to the
219
     * classname that it contains.  The pathname may be ignored if the search
220
     * location is included in the autoloader.
221
     *
222
     * @param string $directory The location to search
223
     * @param string $depth How deep to search (e.g. '== 0' or '< 2')
224
     * @param string $baseNamespace Namespace to prepend to each classname
225
     *
226
     * @return array
227
     */
228
    protected function discoverCommandFilesInLocation($directory, $depth, $baseNamespace)
229
    {
230
        if (!is_dir($directory)) {
231
            return [];
232
        }
233
        $finder = $this->createFinder($directory, $depth);
234
235
        $commands = [];
236
        foreach ($finder as $file) {
237
            $relativeNamespaceAndClassname = str_replace(
238
                ['/', '.php'],
239
                ['\\', ''],
240
                $file->getRelativePathname()
241
            );
242
            $classname = $this->joinNamespace([$baseNamespace, $relativeNamespaceAndClassname]);
243
            $commandFilePath = $this->joinPaths([$directory, $file->getRelativePathname()]);
244
            $commands[$commandFilePath] = $classname;
245
        }
246
247
        return $commands;
248
    }
249
250
    /**
251
     * Create a Finder object for use in searching a particular directory
252
     * location.
253
     *
254
     * @param string $directory The location to search
255
     * @param string $depth The depth limitation
256
     *
257
     * @return Finder
258
     */
259
    protected function createFinder($directory, $depth)
260
    {
261
        $finder = new Finder();
262
        $finder->files()
263
            ->name($this->searchPattern)
264
            ->in($directory)
265
            ->depth($depth);
266
267
        foreach ($this->excludeList as $item) {
268
            $finder->exclude($item);
269
        }
270
271
        return $finder;
272
    }
273
274
    /**
275
     * Combine the items of the provied array into a backslash-separated
276
     * namespace string.  Empty and numeric items are omitted.
277
     *
278
     * @param array $namespaceParts List of components of a namespace
279
     *
280
     * @return string
281
     */
282
    protected function joinNamespace(array $namespaceParts)
283
    {
284
        return $this->joinParts(
285
            '\\',
286
            $namespaceParts,
287
            function ($item) {
288
                return !is_numeric($item) && !empty($item);
289
            }
290
        );
291
    }
292
293
    /**
294
     * Combine the items of the provied array into a slash-separated
295
     * pathname.  Empty items are omitted.
296
     *
297
     * @param array $pathParts List of components of a path
298
     *
299
     * @return string
300
     */
301
    protected function joinPaths(array $pathParts)
302
    {
303
        return $this->joinParts(
304
            '/',
305
            $pathParts,
306
            function ($item) {
307
                return !empty($item);
308
            }
309
        );
310
    }
311
312
    /**
313
     * Simple wrapper around implode and array_filter.
314
     *
315
     * @param string $delimiter
316
     * @param array $parts
317
     * @param callable $filterFunction
318
     */
319
    protected function joinParts($delimiter, $parts, $filterFunction)
320
    {
321
        return implode(
322
            $delimiter,
323
            array_filter($parts, $filterFunction)
324
        );
325
    }
326
}
327