Completed
Push — master ( f87d1c...560885 )
by Greg
02:43
created

CommandFileDiscovery::addSearchLocation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
c 0
b 0
f 0
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
    /** @var string[] */
34
    protected $excludeList;
35
    /** @var string[] */
36
    protected $searchLocations;
37
    /** @var string */
38
    protected $searchPattern = '*Commands.php';
39
    /** @var boolean */
40
    protected $includeFilesAtBase = true;
41
    /** @var integer */
42
    protected $searchDepth = 2;
43
44
    public function __construct()
45
    {
46
        $this->excludeList = ['Exclude'];
47
        $this->searchLocations = [
48
            'Command',
49
            'CliTools', // TODO: Maybe remove
50
        ];
51
    }
52
53
    /**
54
     * Specify whether to search for files at the base directory
55
     * ($directoryList parameter to discover and discoverNamespaced
56
     * methods), or only in the directories listed in the search paths.
57
     *
58
     * @param boolean $includeFilesAtBase
59
     */
60
    public function setIncludeFilesAtBase($includeFilesAtBase)
61
    {
62
        $this->includeFilesAtBase = $includeFilesAtBase;
63
        return $this;
64
    }
65
66
    /**
67
     * Set the list of excludes to add to the finder, replacing
68
     * whatever was there before.
69
     *
70
     * @param array $excludeList The list of directory names to skip when
71
     *   searching for command files.
72
     */
73
    public function setExcludeList($excludeList)
74
    {
75
        $this->excludeList = $excludeList;
76
        return $this;
77
    }
78
79
    /**
80
     * Add one more location to the exclude list.
81
     *
82
     * @param string $exclude One directory name to skip when searching
83
     *   for command files.
84
     */
85
    public function addExclude($exclude)
86
    {
87
        $this->excludeList[] = $exclude;
88
        return $this;
89
    }
90
91
    /**
92
     * Set the search depth.  By default, fills immediately in the
93
     * base directory are searched, plus all of the search locations
94
     * to this specified depth.  If the search locations is set to
95
     * an empty array, then the base directory is searched to this
96
     * depth.
97
     */
98
    public function setSearchDepth($searchDepth)
99
    {
100
        $this->searchDepth = $searchDepth;
101
    }
102
103
    /**
104
     * Set the list of search locations to examine in each directory where
105
     * command files may be found.  This replaces whatever was there before.
106
     *
107
     * @param array $searchLocations The list of locations to search for command files.
108
     */
109
    public function setSearchLocations($searchLocations)
110
    {
111
        $this->searchLocations = $searchLocations;
112
        return $this;
113
    }
114
115
    /**
116
     * Add one more location to the search location list.
117
     *
118
     * @param string $location One more relative path to search
119
     *   for command files.
120
     */
121
    public function addSearchLocation($location)
122
    {
123
        $this->searchLocations[] = $location;
124
        return $this;
125
    }
126
127
    /**
128
     * Specify the pattern / regex used by the finder to search for
129
     * command files.
130
     */
131
    public function setSearchPattern($searchPattern)
132
    {
133
        $this->searchPattern = $searchPattern;
134
        return $this;
135
    }
136
137
    /**
138
     * Given a list of directories, e.g. Drupal modules like:
139
     *
140
     *    core/modules/block
141
     *    core/modules/dblog
142
     *    modules/default_content
143
     *
144
     * Discover command files in any of these locations.
145
     *
146
     * @param string|string[] $directoryList Places to search for commands.
147
     *
148
     * @return array
149
     */
150
    public function discoverNamespaced($directoryList, $baseNamespace = '')
151
    {
152
        return $this->discover($this->convertToNamespacedList((array)$directoryList), $baseNamespace);
153
    }
154
155
    /**
156
     * Given a simple list containing paths to directories, where
157
     * the last component of the path should appear in the namespace,
158
     * after the base namespace, this function will return an
159
     * associative array mapping the path's basename (e.g. the module
160
     * name) to the directory path.
161
     *
162
     * Module names must be unique.
163
     *
164
     * @param string[] $directoryList A list of module locations
165
     *
166
     * @return array
167
     */
168
    public function convertToNamespacedList($directoryList)
169
    {
170
        $namespacedArray = [];
171
        foreach ((array)$directoryList as $directory) {
172
            $namespacedArray[basename($directory)] = $directory;
173
        }
174
        return $namespacedArray;
175
    }
176
177
    /**
178
     * Search for command files in the specified locations. This is the function that
179
     * should be used for all locations that are NOT modules of a framework.
180
     *
181
     * @param string|string[] $directoryList Places to search for commands.
182
     * @return array
183
     */
184
    public function discover($directoryList, $baseNamespace = '')
185
    {
186
        $commandFiles = [];
187
        foreach ((array)$directoryList as $key => $directory) {
188
            $itemsNamespace = $this->joinNamespace([$baseNamespace, $key]);
189
            $commandFiles = array_merge(
190
                $commandFiles,
191
                $this->discoverCommandFiles($directory, $itemsNamespace),
192
                $this->discoverCommandFiles("$directory/src", $itemsNamespace)
193
            );
194
        }
195
        return $commandFiles;
196
    }
197
198
    /**
199
     * Search for command files in specific locations within a single directory.
200
     *
201
     * In each location, we will accept only a few places where command files
202
     * can be found. This will reduce the need to search through many unrelated
203
     * files.
204
     *
205
     * The default search locations include:
206
     *
207
     *    .
208
     *    CliTools
209
     *    src/CliTools
210
     *
211
     * The pattern we will look for is any file whose name ends in 'Commands.php'.
212
     * A list of paths to found files will be returned.
213
     */
214
    protected function discoverCommandFiles($directory, $baseNamespace)
215
    {
216
        $commandFiles = [];
217
        // In the search location itself, we will search for command files
218
        // immediately inside the directory only.
219
        if ($this->includeFilesAtBase) {
220
            $commandFiles = $this->discoverCommandFilesInLocation(
221
                $directory,
222
                $this->getBaseDirectorySearchDepth(),
223
                $baseNamespace
224
            );
225
        }
226
227
        // In the other search locations,
228
        foreach ($this->searchLocations as $location) {
229
            $itemsNamespace = $this->joinNamespace([$baseNamespace, $location]);
230
            $commandFiles = array_merge(
231
                $commandFiles,
232
                $this->discoverCommandFilesInLocation(
233
                    "$directory/$location",
234
                    $this->getSearchDepth(),
235
                    $itemsNamespace
236
                )
237
            );
238
        }
239
        return $commandFiles;
240
    }
241
242
    /**
243
     * Return a Finder search depth appropriate for our selected search depth.
244
     *
245
     * @return string
246
     */
247
    protected function getSearchDepth()
248
    {
249
        return $this->searchDepth <= 0 ? '== 0' : '< ' . $this->searchDepth;
250
    }
251
252
    /**
253
     * Return a Finder search depth for the base directory.  If the
254
     * searchLocations array has been populated, then we will only search
255
     * for files immediately inside the base directory; no traversal into
256
     * deeper directories will be done, as that would conflict with the
257
     * specification provided by the search locations.  If there is no
258
     * search location, then we will search to whatever depth was specified
259
     * by the client.
260
     *
261
     * @return string
262
     */
263
    protected function getBaseDirectorySearchDepth()
264
    {
265
        if (!empty($this->searchLocations)) {
266
            return '== 0';
267
        }
268
        return $this->getSearchDepth();
269
    }
270
271
    /**
272
     * Search for command files in just one particular location.  Returns
273
     * an associative array mapping from the pathname of the file to the
274
     * classname that it contains.  The pathname may be ignored if the search
275
     * location is included in the autoloader.
276
     *
277
     * @param string $directory The location to search
278
     * @param string $depth How deep to search (e.g. '== 0' or '< 2')
279
     * @param string $baseNamespace Namespace to prepend to each classname
280
     *
281
     * @return array
282
     */
283
    protected function discoverCommandFilesInLocation($directory, $depth, $baseNamespace)
284
    {
285
        if (!is_dir($directory)) {
286
            return [];
287
        }
288
        $finder = $this->createFinder($directory, $depth);
289
290
        $commands = [];
291
        foreach ($finder as $file) {
292
            $relativePathName = $file->getRelativePathname();
293
            $relativeNamespaceAndClassname = str_replace(
294
                ['/', '.php'],
295
                ['\\', ''],
296
                $relativePathName
297
            );
298
            $classname = $this->joinNamespace([$baseNamespace, $relativeNamespaceAndClassname]);
299
            $commandFilePath = $this->joinPaths([$directory, $relativePathName]);
300
            $commands[$commandFilePath] = $classname;
301
        }
302
303
        return $commands;
304
    }
305
306
    /**
307
     * Create a Finder object for use in searching a particular directory
308
     * location.
309
     *
310
     * @param string $directory The location to search
311
     * @param string $depth The depth limitation
312
     *
313
     * @return Finder
314
     */
315
    protected function createFinder($directory, $depth)
316
    {
317
        $finder = new Finder();
318
        $finder->files()
319
            ->name($this->searchPattern)
320
            ->in($directory)
321
            ->depth($depth);
322
323
        foreach ($this->excludeList as $item) {
324
            $finder->exclude($item);
325
        }
326
327
        return $finder;
328
    }
329
330
    /**
331
     * Combine the items of the provied array into a backslash-separated
332
     * namespace string.  Empty and numeric items are omitted.
333
     *
334
     * @param array $namespaceParts List of components of a namespace
335
     *
336
     * @return string
337
     */
338
    protected function joinNamespace(array $namespaceParts)
339
    {
340
        return $this->joinParts(
341
            '\\',
342
            $namespaceParts,
343
            function ($item) {
344
                return !is_numeric($item) && !empty($item);
345
            }
346
        );
347
    }
348
349
    /**
350
     * Combine the items of the provied array into a slash-separated
351
     * pathname.  Empty items are omitted.
352
     *
353
     * @param array $pathParts List of components of a path
354
     *
355
     * @return string
356
     */
357
    protected function joinPaths(array $pathParts)
358
    {
359
        return $this->joinParts(
360
            '/',
361
            $pathParts,
362
            function ($item) {
363
                return !empty($item);
364
            }
365
        );
366
    }
367
368
    /**
369
     * Simple wrapper around implode and array_filter.
370
     *
371
     * @param string $delimiter
372
     * @param array $parts
373
     * @param callable $filterFunction
374
     */
375
    protected function joinParts($delimiter, $parts, $filterFunction)
376
    {
377
        return implode(
378
            $delimiter,
379
            array_filter($parts, $filterFunction)
380
        );
381
    }
382
}
383