Passed
Push — 4.6 ( 85242b...27c1c7 )
by Steve
08:07
created

ModuleManifest::getModule()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 4
nop 1
dl 0
loc 17
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Core\Manifest;
4
5
use LogicException;
6
use Psr\SimpleCache\CacheInterface;
7
use SilverStripe\Core\Cache\CacheFactory;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Injector\Injector;
10
11
/**
12
 * A utility class which builds a manifest of configuration items
13
 */
14
class ModuleManifest
15
{
16
    use Configurable;
17
18
    const PROJECT_KEY = '$project';
19
20
    /**
21
     * The base path used when building the manifest
22
     *
23
     * @var string
24
     */
25
    protected $base;
26
27
    /**
28
     * A string to prepend to all cache keys to ensure all keys are unique to just this $base
29
     *
30
     * @var string
31
     */
32
    protected $cacheKey;
33
34
    /**
35
     * Factory to use to build cache
36
     *
37
     * @var CacheFactory
38
     */
39
    protected $cacheFactory;
40
41
    /**
42
     * @var CacheInterface
43
     */
44
    protected $cache;
45
46
    /**
47
     * List of all modules.
48
     *
49
     * @var Module[]
50
     */
51
    protected $modules = [];
52
53
    /**
54
     * List of modules sorted by priority
55
     *
56
     * @config
57
     * @var array
58
     */
59
    private static $module_priority = [];
0 ignored issues
show
introduced by
The private property $module_priority is not used, and could be removed.
Loading history...
60
61
    /**
62
     * Project name
63
     *
64
     * @config
65
     * @var string
66
     */
67
    private static $project = null;
0 ignored issues
show
introduced by
The private property $project is not used, and could be removed.
Loading history...
68
69
    /**
70
     * Constructs and initialises a new configuration object, either loading
71
     * from the cache or re-scanning for classes.
72
     *
73
     * @param string $base The project base path.
74
     * @param CacheFactory $cacheFactory Cache factory to use
75
     */
76
    public function __construct($base, CacheFactory $cacheFactory = null)
77
    {
78
        $this->base = $base;
79
        $this->cacheKey = sha1($base) . '_modules';
80
        $this->cacheFactory = $cacheFactory;
81
    }
82
83
    /**
84
     * Adds a path as a module
85
     *
86
     * @param string $path
87
     */
88
    public function addModule($path)
89
    {
90
        $module = new Module($path, $this->base);
91
        $name = $module->getName();
92
93
        // Save if not already added
94
        if (empty($this->modules[$name])) {
95
            $this->modules[$name] = $module;
96
            return;
97
        }
98
99
        // Validate duplicate module
100
        $path = $module->getPath();
101
        $otherPath = $this->modules[$name]->getPath();
102
        if ($otherPath !== $path) {
103
            throw new LogicException(
104
                "Module {$name} is in two places - {$path} and {$otherPath}"
105
            );
106
        }
107
    }
108
109
    /**
110
     * Returns true if the passed module exists
111
     *
112
     * @param string $name Either full composer name or short name
113
     * @return bool
114
     */
115
    public function moduleExists($name)
116
    {
117
        $module = $this->getModule($name);
118
        return !empty($module);
119
    }
120
121
    /**
122
     * @param bool $includeTests
123
     * @param bool $forceRegen Force the manifest to be regenerated.
124
     */
125
    public function init($includeTests = false, $forceRegen = false)
126
    {
127
        // build cache from factory
128
        if ($this->cacheFactory) {
129
            $this->cache = $this->cacheFactory->create(
130
                CacheInterface::class . '.modulemanifest',
131
                ['namespace' => 'modulemanifest' . ($includeTests ? '_tests' : '')]
132
            );
133
        }
134
135
        // Unless we're forcing regen, try loading from cache
136
        if (!$forceRegen && $this->cache) {
137
            $this->modules = $this->cache->get($this->cacheKey) ?: [];
138
        }
139
        if (empty($this->modules)) {
140
            $this->regenerate($includeTests);
141
        }
142
    }
143
144
    /**
145
     * Includes all of the php _config.php files found by this manifest.
146
     */
147
    public function activateConfig()
148
    {
149
        $modules = $this->getModules();
150
        // Work in reverse priority, so the higher priority modules get later execution
151
        /** @var Module $module */
152
        foreach (array_reverse($modules) as $module) {
153
            $module->activate();
154
        }
155
    }
156
157
    /**
158
     * Completely regenerates the manifest file. Scans through finding all php _config.php and yaml _config/*.ya?ml
159
     * files,parses the yaml files into fragments, sorts them and figures out what values need to be checked to pick
160
     * the correct variant.
161
     *
162
     * Does _not_ build the actual variant
163
     *
164
     * @param bool $includeTests
165
     */
166
    public function regenerate($includeTests = false)
167
    {
168
        $this->modules = [];
169
170
        $finder = new ManifestFileFinder();
171
        $finder->setOptions([
172
            'min_depth' => 0,
173
            'ignore_tests' => !$includeTests,
174
            'dir_callback' => function ($basename, $pathname, $depth) use ($finder) {
175
                if ($finder->isDirectoryModule($basename, $pathname, $depth)) {
176
                    $this->addModule($pathname);
177
                }
178
            },
179
        ]);
180
        $finder->find($this->base);
181
182
        // Include root itself if module
183
        if ($finder->isDirectoryModule(basename($this->base), $this->base, 0)) {
184
            $this->addModule($this->base);
185
        }
186
187
        if ($this->cache) {
188
            $this->cache->set($this->cacheKey, $this->modules);
189
        }
190
    }
191
192
    /**
193
     * Get module by name
194
     *
195
     * @param string $name
196
     * @return Module
197
     */
198
    public function getModule($name)
199
    {
200
        // Optimised find
201
        if (isset($this->modules[$name])) {
202
            return $this->modules[$name];
203
        }
204
205
        // Fall back to lookup by shortname
206
        if (!strstr($name, '/')) {
207
            foreach ($this->modules as $module) {
208
                if (strcasecmp($module->getShortName(), $name) === 0) {
209
                    return $module;
210
                }
211
            }
212
        }
213
214
        return null;
215
    }
216
217
    /**
218
     * Get modules found
219
     *
220
     * @return Module[]
221
     */
222
    public function getModules()
223
    {
224
        return $this->modules;
225
    }
226
227
    /**
228
     * Sort modules sorted by priority
229
     */
230
    public function sort()
231
    {
232
        $order = static::config()->uninherited('module_priority');
233
        $project = static::config()->get('project');
234
235
        /** @var PrioritySorter $sorter */
236
        $sorter = Injector::inst()->createWithArgs(
237
            PrioritySorter::class . '.modulesorter',
238
            [
239
                $this->modules,
240
                $order ?: [],
241
            ]
242
        );
243
244
        if ($project) {
245
            $sorter->setVariable(self::PROJECT_KEY, $project);
246
        }
247
248
        $this->modules = $sorter->getSortedList();
249
    }
250
251
    /**
252
     * Get module that contains the given path
253
     *
254
     * @param string $path Full filesystem path to the given file
255
     * @return Module The module, or null if not a path in any module
256
     */
257
    public function getModuleByPath($path)
258
    {
259
        // Ensure path exists
260
        $path = realpath($path);
261
        if (!$path) {
262
            return null;
263
        }
264
265
        /** @var Module $rootModule */
266
        $rootModule = null;
267
268
        // Find based on loaded modules
269
        $modules = ModuleLoader::inst()->getManifest()->getModules();
270
271
        foreach ($modules as $module) {
272
            // Check if path is in module
273
            $modulePath = realpath($module->getPath());
274
            // if there is a real path
275
            if ($modulePath) {
276
                // we remove separator to ensure that we are comparing fairly
277
                $modulePath = rtrim($modulePath, DIRECTORY_SEPARATOR);
278
                $path = rtrim($path, DIRECTORY_SEPARATOR);
279
                // if the paths are not the same
280
                if ($modulePath !== $path) {
281
                    //add separator to avoid mixing up, for example:
282
                    //silverstripe/framework and silverstripe/framework-extension
283
                    $modulePath .= DIRECTORY_SEPARATOR;
284
                    $path .= DIRECTORY_SEPARATOR;
285
                    // if the module path is not the same as the start of the module path being tested
286
                    if (stripos($path, $modulePath) !== 0) {
287
                        // then we need to test the next module
288
                        continue;
289
                    }
290
                }
291
            }
292
            // If this is the root module, keep looking in case there is a more specific module later
293
            if (empty($module->getRelativePath())) {
294
                $rootModule = $module;
295
            } else {
296
                return $module;
297
            }
298
        }
299
300
        // Fall back to top level module
301
        return $rootModule;
302
    }
303
}
304