Completed
Push — 2.0 ( a506c7...300469 )
by Christopher
04:09
created

PluginPackage   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 395
Duplicated Lines 4.56 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 18
loc 395
rs 6.0975
wmc 60
lcom 2
cbo 7

10 Methods

Rating   Name   Duplication   Size   Complexity  
A name() 0 4 1
B permissions() 12 32 4
A __get() 0 4 1
F info() 0 30 12
C _getKey() 0 43 13
B composer() 6 12 5
C settings() 0 35 8
B requiredBy() 0 30 5
C version() 0 51 10
A __debugInfo() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like PluginPackage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PluginPackage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace CMS\Core\Package;
13
14
use Cake\ORM\TableRegistry;
15
use Cake\Utility\Inflector;
16
use CMS\Core\Plugin;
17
18
/**
19
 * Represents a QuickAppsCMS plugin or theme.
20
 *
21
 * @property string $name CamelizedName of the plugin, e.g. `Megamenu`
22
 * @property string $humanName Human readable name of the plugin, e.g. `Megamenu Builder`
23
 * @property string $package Composer package name as `vendor/name`, e.g. `quickapps/megamenu`
24
 * @property string $path Full path to plugin root directory, where the `src` directory can be found
25
 * @property bool $isTheme Whether or not this plugin is actually a theme
26
 * @property bool $hasHelp Whether or not this plugin provides help documentation
27
 * @property bool $hasSettings Whether or not this plugin provides configurable values
28
 * @property bool $status Whether this plugin is enabled or not
29
 * @property array $aspects List of aspect classes used by AOP API
30
 * @property array $eventListeners List of event listener classes
31
 * @property array $settings Array of configurable values
32
 * @property array $composer Composer's json file as an array
33
 * @property array $permissions Plugin permissions tree indexed by role
34
 */
35
class PluginPackage extends BasePackage
36
{
37
38
    /**
39
     * Plugin information.
40
     *
41
     * @var array
42
     */
43
    protected $_info = [];
44
45
    /**
46
     * Permissions tree for this plugin.
47
     *
48
     * @var null|array
49
     */
50
    protected $_permissions = null;
51
52
    /**
53
     * {@inheritDoc}
54
     *
55
     * @return string CamelizedName plugin name
56
     */
57
    public function name()
58
    {
59
        return (string)Inflector::camelize(str_replace('-', '_', parent::name()));
60
    }
61
62
    /**
63
     * Gets plugin's permissions tree.
64
     *
65
     * ### Output example:
66
     *
67
     * ```php
68
     * [
69
     *     'administrator' => [
70
     *         'Plugin/Controller/action',
71
     *         'Plugin/Controller/action2',
72
     *         ...
73
     *     ],
74
     *     'role-machine-name' => [
75
     *         'Plugin/Controller/anotherAction',
76
     *         'Plugin/Controller/anotherAction2',
77
     *     ],
78
     *     ...
79
     * ]
80
     * ```
81
     *
82
     * @return array Permissions index by role's machine-name
83
     */
84
    public function permissions()
85
    {
86
        if (is_array($this->_permissions)) {
87
            return $this->_permissions;
88
        }
89
90
        $out = [];
91
        $acosTable = TableRegistry::get('User.Acos');
92
        $permissions = $acosTable
93
            ->Permissions
94
            ->find()
95
            ->where(['Acos.plugin' => $this->name])
96
            ->contain(['Acos', 'Roles'])
97
            ->all();
98
99 View Code Duplication
        foreach ($permissions as $permission) {
100
            if (!isset($out[$permission->role->slug])) {
101
                $out[$permission->role->slug] = [];
102
            }
103
            $out[$permission->role->slug][] = implode(
104
                '/',
105
                $acosTable
106
                ->find('path', ['for' => $permission->aco->id])
107
                ->extract('alias')
108
                ->toArray()
109
            );
110
        }
111
112
        $this->_permissions = $out;
113
114
        return $out;
115
    }
116
117
    /**
118
     * Magic getter to access properties that exists on info().
119
     *
120
     * @param string $property Name of the property to access
121
     * @return mixed
122
     */
123
    public function &__get($property)
124
    {
125
        return $this->info($property);
126
    }
127
128
    /**
129
     * Gets information for this plugin.
130
     *
131
     * When `$full` is set to true some additional keys will be repent in the
132
     * resulting array:
133
     *
134
     * - `settings`: Plugin's settings info fetched from DB.
135
     * - `composer`: Composer JSON information, converted to an array.
136
     * - `permissions`: Permissions tree for this plugin, see `PluginPackage::permissions()`
137
     *
138
     * ### Example:
139
     *
140
     * Reading full information:
141
     *
142
     * ```php
143
     * $plugin->info();
144
     *
145
     * // returns an array as follow:
146
     * [
147
     *     'name' => 'User,
148
     *     'isTheme' => false,
149
     *     'hasHelp' => true,
150
     *     'hasSettings' => false,
151
     *     'eventListeners' => [ ... ],
152
     *     'status' => 1,
153
     *     'path' => '/path/to/plugin',
154
     *     'settings' => [ ... ], // only when $full = true
155
     *     'composer' => [ ... ], // only when $full = true
156
     *     'permissions' => [ ... ], // only when $full = true
157
     * ]
158
     * ```
159
     *
160
     * Additionally the first argument, $key, can be used to get an specific value
161
     * using a dot syntax path:
162
     *
163
     * ```php
164
     * $plugin->info('isTheme');
165
     * $plugin->info('settings.some_key');
166
     * ```
167
     *
168
     * If the given path is not found NULL will be returned
169
     *
170
     * @param string $key Optional path to read from the resulting array
171
     * @return mixed Plugin information as an array if no key is given, or the
172
     *  requested value if a valid $key was provided, or NULL if $key path is not
173
     *  found
174
     */
175
    public function &info($key = null)
176
    {
177
        $plugin = $this->name();
178
        if (empty($this->_info)) {
179
            $this->_info = (array)quickapps("plugins.{$plugin}");
180
        }
181
182
        $parts = explode('.', $key);
183
        $getComposer = in_array('composer', $parts) || $key === null;
184
        $getSettings = in_array('settings', $parts) || $key === null;
185
        $getPermissions = in_array('permissions', $parts) || $key === null;
186
187
        if ($getComposer && !isset($this->_info['composer'])) {
188
            $this->_info['composer'] = $this->composer();
189
        }
190
191
        if ($getSettings && !isset($this->_info['settings'])) {
192
            $this->_info['settings'] = (array)$this->settings();
193
        }
194
195
        if ($getPermissions && !isset($this->_info['permissions'])) {
196
            $this->_info['permissions'] = (array)$this->permissions();
197
        }
198
199
        if ($key === null) {
200
            return $this->_info;
201
        }
202
203
        return $this->_getKey($parts);
204
    }
205
206
    /**
207
     * Gets info value for the given key.
208
     *
209
     * @param string|array $key The path to read. String using a dot-syntax, or an
210
     *  array result of exploding by `.` symbol
211
     * @return mixed
212
     */
213
    protected function &_getKey($key)
214
    {
215
        $default = null;
216
        $parts = is_string($key) ? explode('.', $key) : $key;
217
218
        switch (count($parts)) {
219
            case 1:
220
                if (isset($this->_info[$parts[0]])) {
221
                    return $this->_info[$parts[0]];
222
                }
223
224
                return $default;
225
            case 2:
226
                if (isset($this->_info[$parts[0]][$parts[1]])) {
227
                    return $this->_info[$parts[0]][$parts[1]];
228
                }
229
230
                return $default;
231
            case 3:
232
                if (isset($this->_info[$parts[0]][$parts[1]][$parts[2]])) {
233
                    return $this->_info[$parts[0]][$parts[1]][$parts[2]];
234
                }
235
236
                return $default;
237
            case 4:
238
                if (isset($this->_info[$parts[0]][$parts[1]][$parts[2]][$parts[3]])) {
239
                    return $this->_info[$parts[0]][$parts[1]][$parts[2]][$parts[3]];
240
                }
241
242
                return $default;
243
            default:
244
                $data = $this->_info;
245
                foreach ($parts as $key) {
246
                    if (is_array($data) && isset($data[$key])) {
247
                        $data = $data[$key];
248
                    } else {
249
                        return $default;
250
                    }
251
                }
252
        }
253
254
        return $data;
255
    }
256
257
    /**
258
     * {@inheritDoc}
259
     */
260
    public function composer($full = false)
261
    {
262
        $composer = parent::composer($full);
263 View Code Duplication
        if ($this->isTheme && !isset($composer['extra']['admin'])) {
264
            $composer['extra']['admin'] = false;
265
        }
266 View Code Duplication
        if ($this->isTheme && !isset($composer['extra']['regions'])) {
267
            $composer['extra']['regions'] = [];
268
        }
269
270
        return $composer;
271
    }
272
273
    /**
274
     * Gets settings from DB for this plugin. Or reads a single settings key value.
275
     *
276
     * @param string $key Which setting to read, the entire settings will be
277
     *  returned if no key is provided
278
     * @return mixed Array of settings if $key was not provided, or the requested
279
     *  value for the given $key (null of key does not exists)
280
     */
281
    public function settings($key = null)
282
    {
283
        $plugin = $this->name();
284
        if ($cache = $this->config('settings')) {
285
            if ($key !== null) {
286
                $cache = isset($cache[$key]) ? $cache[$key] : null;
287
            }
288
289
            return $cache;
290
        }
291
292
        $pluginsTable = TableRegistry::get('System.Plugins');
293
        $settings = [];
294
        $dbInfo = $pluginsTable->find()
295
            ->cache("{$plugin}_settings", 'plugins')
296
            ->select(['name', 'settings'])
297
            ->where(['name' => $plugin])
298
            ->limit(1)
299
            ->first();
300
301
        if ($dbInfo) {
302
            $settings = (array)$dbInfo->settings;
303
        }
304
305
        if (empty($settings)) {
306
            $settings = (array)$pluginsTable->trigger("Plugin.{$plugin}.settingsDefaults")->result;
307
        }
308
309
        $this->config('settings', $settings);
310
        if ($key !== null) {
311
            $settings = isset($settings[$key]) ? $settings[$key] : null;
312
        }
313
314
        return $settings;
315
    }
316
317
    /**
318
     * Gets a collection list of plugin that depends on this plugin.
319
     *
320
     * @return \Cake\Collection\Collection List of plugins
321
     */
322
    public function requiredBy()
323
    {
324
        if ($cache = $this->config('required_by')) {
325
            return collection($cache);
326
        }
327
328
        Plugin::dropCache();
329
        $out = [];
330
        $plugins = Plugin::get()->filter(function ($v, $k) {
0 ignored issues
show
Bug introduced by
The method filter does only exist in Cake\Collection\Collection, but not in CMS\Core\Package\PluginPackage.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
331
            return $v->name() !== $this->name();
332
        });
333
334
        foreach ($plugins as $plugin) {
335
            if ($dependencies = $plugin->dependencies($plugin->name)) {
336
                $packages = array_map(function ($item) {
337
                    list(, $package) = packageSplit($item, true);
338
339
                    return strtolower($package);
340
                }, array_keys($dependencies));
341
342
                if (in_array(strtolower($this->name()), $packages)) {
343
                    $out[] = $plugin;
344
                }
345
            }
346
        }
347
348
        $this->config('required_by', $out);
349
350
        return collection($out);
351
    }
352
353
    /**
354
     * {@inheritDoc}
355
     *
356
     * It will look for plugin's version in the following places:
357
     *
358
     * - Plugin's "composer.json" file.
359
     * - Plugin's "VERSION.txt" file (or any file matching "/version?(\.\w+)/i").
360
     * - Composer's "installed.json" file.
361
     *
362
     * If not found `dev-master` is returned by default. If plugin is not registered
363
     * on QuickAppsCMS (not installed) an empty string will be returned instead.
364
     *
365
     * @return string Plugin's version, for instance `1.2.x-dev`
366
     */
367
    public function version()
368
    {
369
        if (parent::version() !== null) {
370
            return parent::version();
371
        }
372
373
        if (!Plugin::exists($this->name())) {
374
            $this->_version = '';
375
376
            return $this->_version;
377
        }
378
379
        // from composer.json
380
        if (!empty($this->composer['version'])) {
381
            $this->_version = $this->composer['version'];
382
383
            return $this->_version;
384
        }
385
386
        // from version.txt
387
        $files = glob($this->path . '/*', GLOB_NOSORT);
388
        foreach ($files as $file) {
389
            $fileName = basename(strtolower($file));
390
            if (preg_match('/version?(\.\w+)/i', $fileName)) {
391
                $versionFile = file($file);
392
                $version = trim(array_pop($versionFile));
393
                $this->_version = $version;
394
395
                return $this->_version;
396
            }
397
        }
398
399
        // from installed.json
400
        $installedJson = normalizePath(VENDOR_INCLUDE_PATH . "composer/installed.json");
401
        if (is_readable($installedJson)) {
402
            $json = (array)json_decode(file_get_contents($installedJson), true);
403
            foreach ($json as $pkg) {
404
                if (isset($pkg['version']) &&
405
                    strtolower($pkg['name']) === strtolower($this->_packageName)
406
                ) {
407
                    $this->_version = $pkg['version'];
408
409
                    return $this->_version;
410
                }
411
            }
412
        }
413
414
        $this->_version = 'dev-master';
415
416
        return $this->_version;
417
    }
418
419
    /**
420
     * Returns an array that can be used to describe the internal state of this
421
     * object.
422
     *
423
     * @return array
424
     */
425
    public function __debugInfo()
426
    {
427
        return $this->info(null);
428
    }
429
}
430