GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Pull Request — master (#2835)
by
unknown
06:01
created

ExtensionManager   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1156
Duplicated Lines 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
c 7
b 1
f 0
dl 0
loc 1156
rs 0.6314
wmc 145

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __getClassName() 0 3 1
A __buildExtensionList() 0 7 3
A fetchInstalledVersion() 0 5 2
A getCacheProvider() 0 18 4
A fetchExtensionID() 0 5 1
A __getHandleFromFilename() 0 3 1
A __requiresInstallation() 0 6 2
B __construct() 0 7 5
C getProvidersOf() 0 34 7
A getInstance() 0 3 2
A __getDriverPath() 0 3 1
A __getClassPath() 0 3 1
B fetchStatus() 0 20 6
C notifyMembers() 0 50 12
B sortByAuthor() 0 19 8
A __requiresUpdate() 0 11 3
B getDelegateSubscriptions() 0 32 3
B listAll() 0 16 5
C fetch() 0 56 17
B uninstall() 0 25 3
A listInstalledHandles() 0 10 3
A isInstalled() 0 3 1
B registerDelegates() 0 35 6
A removeDelegates() 0 22 2
C enable() 0 51 7
B disable() 0 25 1
C __canUninstallOrDisable() 0 56 13
A select() 0 3 1
B cleanupDatabase() 0 33 5
B create() 0 36 5
D about() 0 104 14

How to fix   Complexity   

Complex Class

Complex classes like ExtensionManager 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.

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 ExtensionManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @package toolkit
5
 */
6
/**
7
 * The ExtensionManager class is responsible for managing all extensions
8
 * in Symphony. Extensions are stored on the file system in the `EXTENSIONS`
9
 * folder. They are auto-discovered where the Extension class name is the same
10
 * as it's folder name (excluding the extension prefix).
11
 */
12
13
class ExtensionManager implements FileResource
14
{
15
    /**
16
     * An array of all the objects that the Manager is responsible for.
17
     * Defaults to an empty array.
18
     * @var array
19
     */
20
    protected static $_pool = array();
21
22
    /**
23
     * An array of all extensions whose status is enabled
24
     * @var array
25
     */
26
    private static $_enabled_extensions = array();
27
28
    /**
29
     * An array of all the subscriptions to Symphony delegates made by extensions.
30
     * @var array
31
     */
32
    private static $_subscriptions = array();
33
34
    /**
35
     * An associative array of all the extensions in `tbl_extensions` where
36
     * the key is the extension name and the value is an array
37
     * representation of it's accompanying database row.
38
     * @var array
39
     */
40
    private static $_extensions = array();
41
42
    /**
43
     * An associative array of all the providers from the enabled extensions.
44
     * The key is the type of object, with the value being an associative array
45
     * with the name, classname and path to the object
46
     *
47
     * @since Symphony 2.3
48
     * @var array
49
     */
50
    private static $_providers = array();
51
52
    /**
53
     * The constructor will populate the `$_subscriptions` variable from
54
     * the `tbl_extension` and `tbl_extensions_delegates` tables.
55
     */
56
    public function __construct()
57
    {
58
        if (empty(self::$_subscriptions) && Symphony::Database() && Symphony::Database()->isConnected()) {
59
            $subscriptions = $this->getDelegateSubscriptions();
60
61
            while ($subscription = $subscriptions->next()) {
62
                self::$_subscriptions[$subscription['delegate']][] = $subscription;
63
            }
64
        }
65
    }
66
67
    public static function __getHandleFromFilename($filename)
68
    {
69
        return false;
70
    }
71
72
    /**
73
     * Given a name, returns the full class name of an Extension.
74
     * Extension use an 'extension' prefix.
75
     *
76
     * @param string $name
77
     *  The extension handle
78
     * @return string
79
     */
80
    public static function __getClassName($name)
81
    {
82
        return 'extension_' . $name;
83
    }
84
85
    /**
86
     * Finds an Extension by name by searching the `EXTENSIONS` folder and
87
     * returns the path to the folder.
88
     *
89
     * @param string $name
90
     *  The extension folder
91
     * @return string
92
     */
93
    public static function __getClassPath($name)
94
    {
95
        return EXTENSIONS . strtolower("/$name");
0 ignored issues
show
Bug introduced by
The constant EXTENSIONS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
96
    }
97
98
    /**
99
     * Given a name, return the path to the driver of the Extension.
100
     *
101
     * @see toolkit.ExtensionManager#__getClassPath()
102
     * @param string $name
103
     *  The extension folder
104
     * @return string
105
     */
106
    public static function __getDriverPath($name)
107
    {
108
        return self::__getClassPath($name) . '/extension.driver.php';
109
    }
110
111
    /**
112
     * This function returns an instance of an extension from it's name
113
     *
114
     * @param string $name
115
     *  The name of the Extension Class minus the extension prefix.
116
     * @throws SymphonyErrorPage
117
     * @throws Exception
118
     * @return Extension
119
     */
120
    public static function getInstance($name)
121
    {
122
        return (isset(self::$_pool[$name]) ? self::$_pool[$name] : self::create($name));
123
    }
124
125
    /**
126
     * Populates the `ExtensionManager::$_extensions` array with all the
127
     * extensions stored in `tbl_extensions`. If `ExtensionManager::$_extensions`
128
     * isn't empty, passing true as a parameter will force the array to update
129
     *
130
     * @param boolean $update
131
     *  Updates the `ExtensionManager::$_extensions` array even if it was
132
     *  populated, defaults to false.
133
     * @throws DatabaseException
134
     */
135
    private static function __buildExtensionList($update = false)
136
    {
137
        if (empty(self::$_extensions) || $update) {
138
            self::$_extensions = (new ExtensionManager)
139
                ->select()
140
                ->execute()
141
                ->rowsIndexedByColumn('name');
142
        }
143
    }
144
145
    /**
146
     * Returns the status of an Extension given an associative array containing
147
     * the Extension `handle` and `version` where the `version` is the file
148
     * version, not the installed version. This function returns an array
149
     * which may include a maximum of two statuses.
150
     *
151
     * @param array $about
152
     *  An associative array of the extension meta data, typically returned
153
     *  by `ExtensionManager::about()`. At the very least this array needs
154
     *  `handle` and `version` keys.
155
     * @return array
156
     *  An array of extension statuses, with the possible values being
157
     * `EXTENSION_ENABLED`, `EXTENSION_DISABLED`, `EXTENSION_REQUIRES_UPDATE`
158
     *  or `EXTENSION_NOT_INSTALLED`. If an extension doesn't exist,
159
     *  `EXTENSION_NOT_INSTALLED` will be returned.
160
     */
161
    public static function fetchStatus($about)
162
    {
163
        $return = array();
164
        self::__buildExtensionList();
165
166
        if (isset($about['handle']) && array_key_exists($about['handle'], self::$_extensions)) {
167
            if (self::$_extensions[$about['handle']]['status'] == 'enabled') {
168
                $return[] = Extension::EXTENSION_ENABLED;
169
            } else {
170
                $return[] = Extension::EXTENSION_DISABLED;
171
            }
172
        } else {
173
            $return[] = Extension::EXTENSION_NOT_INSTALLED;
174
        }
175
176
        if (isset($about['handle'], $about['version']) && self::__requiresUpdate($about['handle'], $about['version'])) {
177
            $return[] = Extension::EXTENSION_REQUIRES_UPDATE;
178
        }
179
180
        return $return;
181
    }
182
183
    /**
184
     * A convenience method that returns an extension version from it's name.
185
     *
186
     * @param string $name
187
     *  The name of the Extension Class minus the extension prefix.
188
     * @return string
189
     */
190
    public static function fetchInstalledVersion($name)
191
    {
192
        self::__buildExtensionList();
193
194
        return (isset(self::$_extensions[$name]) ? self::$_extensions[$name]['version'] : null);
195
    }
196
197
    /**
198
     * A convenience method that returns an extension ID from it's name.
199
     *
200
     * @param string $name
201
     *  The name of the Extension Class minus the extension prefix.
202
     * @return integer
203
     */
204
    public static function fetchExtensionID($name)
205
    {
206
        self::__buildExtensionList();
207
208
        return self::$_extensions[$name]['id'];
209
    }
210
211
    /**
212
     * Return an array all the Provider objects supplied by extensions,
213
     * optionally filtered by a given `$type`.
214
     *
215
     * @since Symphony 2.3
216
     * @todo Add information about the possible types
217
     * @param string $type
218
     *  This will only return Providers of this type. If null, which is
219
     *  default, all providers will be returned.
220
     * @throws Exception
221
     * @throws SymphonyErrorPage
222
     * @return array
223
     *  An array of objects
224
     */
225
    public static function getProvidersOf($type = null)
226
    {
227
        // Loop over all extensions and build an array of providable objects
228
        if (empty(self::$_providers)) {
229
            self::$_providers = array();
230
231
            foreach (self::listInstalledHandles() as $handle) {
232
                $obj = self::getInstance($handle);
233
234
                if (!method_exists($obj, 'providerOf')) {
235
                    continue;
236
                }
237
238
                $providers = $obj->providerOf();
239
240
                if (empty($providers)) {
241
                    continue;
242
                }
243
244
                // For each of the matching objects (by $type), resolve the object path
245
                self::$_providers = array_merge_recursive(self::$_providers, $obj->providerOf());
246
            }
247
        }
248
249
        // Return an array of objects
250
        if (is_null($type)) {
251
            return self::$_providers;
252
        }
253
254
        if (!isset(self::$_providers[$type])) {
255
            return array();
256
        }
257
258
        return self::$_providers[$type];
259
    }
260
261
    /**
262
     * This function will return the `Cacheable` object with the appropriate
263
     * caching layer for the given `$key`. This `$key` should be stored in
264
     * the Symphony configuration in the caching group with a reference
265
     * to the class of the caching object. If the key is not found, this
266
     * will return a default `Cacheable` object created with the Database driver.
267
     *
268
     * @since Symphony 2.4
269
     * @param string $key
270
     *  Should be a reference in the Configuration file to the Caching class
271
     * @param boolean $reuse
272
     *  By default true, which will reuse an existing Cacheable object of `$key`
273
     *  if it exists. If false, a new instance will be generated.
274
     * @return Cacheable
275
     */
276
    public static function getCacheProvider($key = null, $reuse = true)
277
    {
278
        $cacheDriver = Symphony::Configuration()->get($key, 'caching');
279
280
        if (in_array($cacheDriver, array_keys(Symphony::ExtensionManager()->getProvidersOf('cache')))) {
281
            $cacheable = new $cacheDriver;
282
        } else {
283
            $cacheable = Symphony::Database();
284
            $cacheDriver = 'CacheDatabase';
285
        }
286
287
        if ($reuse === false) {
288
            return new Cacheable($cacheable);
0 ignored issues
show
Bug introduced by
It seems like $cacheable can also be of type Database; however, parameter $cacheProvider of Cacheable::__construct() does only seem to accept iCache, maybe add an additional type check? ( Ignorable by Annotation )

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

288
            return new Cacheable(/** @scrutinizer ignore-type */ $cacheable);
Loading history...
289
        } elseif (!isset(self::$_pool[$cacheDriver])) {
290
            self::$_pool[$cacheDriver] = new Cacheable($cacheable);
291
        }
292
293
        return self::$_pool[$cacheDriver];
294
    }
295
296
    /**
297
     * Determines whether the current extension is installed or not by checking
298
     * for an id in `tbl_extensions`
299
     *
300
     * @param string $name
301
     *  The name of the Extension Class minus the extension prefix.
302
     * @return boolean
303
     */
304
    private static function __requiresInstallation($name)
305
    {
306
        self::__buildExtensionList();
307
        $id = self::$_extensions[$name]['id'];
308
309
        return (is_numeric($id) ? false : true);
310
    }
311
312
    /**
313
     * Determines whether an extension needs to be updated or not.
314
     * This function will return the
315
     * installed version if the extension requires an update, or
316
     * false otherwise.
317
     *
318
     * @param string $name
319
     *  The name of the Extension Class minus the extension prefix.
320
     * @param string $file_version
321
     *  The version of the extension from the **file**, not the Database.
322
     * @return string|boolean
323
     *  If the given extension (by $name) requires updating, the installed
324
     *  version is returned, otherwise, if the extension doesn't require
325
     *  updating, false.
326
     */
327
    private static function __requiresUpdate($name, $file_version)
328
    {
329
        $installed_version = self::fetchInstalledVersion($name);
330
331
        if (!$installed_version) {
332
            return false;
333
        } elseif (\Composer\Semver\Comparator::lessThan($installed_version, $file_version)) {
334
            return $installed_version;
335
        }
336
337
        return false;
338
    }
339
340
    /**
341
     * Enabling an extension will re-register all it's delegates with Symphony.
342
     * It will also install or update the extension if needs be by calling the
343
     * extensions respective install and update methods. The enable method is
344
     * of the extension object is finally called.
345
     *
346
     * @see toolkit.ExtensionManager#registerDelegates()
347
     * @see toolkit.ExtensionManager#__canUninstallOrDisable()
348
     * @param string $name
349
     *  The name of the Extension Class minus the extension prefix.
350
     * @throws SymphonyErrorPage
351
     * @throws Exception
352
     * @return boolean
353
     */
354
    public static function enable($name)
355
    {
356
        $obj = self::getInstance($name);
357
358
        // If not installed, install it
359
        if (self::__requiresInstallation($name) && $obj->install() === false) {
0 ignored issues
show
introduced by
The condition $obj->install() === false is always false.
Loading history...
360
            // If the installation failed, run the uninstall method which
361
            // should rollback the install method. #1326
362
            $obj->uninstall();
363
            return false;
364
365
            // If the extension requires updating before enabling, then update it
366
        } elseif (($about = self::about($name)) && ($previousVersion = self::__requiresUpdate($name, $about['version'])) !== false) {
367
            $obj->update($previousVersion);
368
        }
369
370
        if (!isset($about)) {
371
            $about = self::about($name);
372
        }
373
374
        $id = self::fetchExtensionID($name);
375
376
        $fields = array(
377
            'name' => $name,
378
            'status' => 'enabled',
379
            'version' => $about['version']
380
        );
381
382
        // If there's no $id, the extension needs to be installed
383
        if (is_null($id)) {
0 ignored issues
show
introduced by
The condition is_null($id) is always false.
Loading history...
384
            Symphony::Database()
385
                ->insert('tbl_extensions')
386
                ->values($fields)
387
                ->execute();
388
            self::__buildExtensionList(true);
389
390
        // Extension is installed, so update!
391
        } else {
392
            Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::update() has been deprecated: Symphony 3.0.0 This parameter is deprecated and will be removed. Use DatabaseUpdate::where() ( Ignorable by Annotation )

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

392
            /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
393
                ->update('tbl_extensions')
394
                ->set($fields)
395
                ->where(['id' => $id])
396
                ->execute();
397
        }
398
399
        self::registerDelegates($name);
400
401
        // Now enable the extension
402
        $obj->enable();
403
404
        return true;
405
    }
406
407
    /**
408
     * Disabling an extension will prevent it from executing but retain all it's
409
     * settings in the relevant tables. Symphony checks that an extension can
410
     * be disabled using the `canUninstallorDisable()` before removing
411
     * all delegate subscriptions from the database and calling the extension's
412
     * `disable()` function.
413
     *
414
     * @see toolkit.ExtensionManager#removeDelegates()
415
     * @see toolkit.ExtensionManager#__canUninstallOrDisable()
416
     * @param string $name
417
     *  The name of the Extension Class minus the extension prefix.
418
     * @throws DatabaseException
419
     * @throws SymphonyErrorPage
420
     * @throws Exception
421
     * @return boolean
422
     */
423
    public static function disable($name)
424
    {
425
        $obj = self::getInstance($name);
426
427
        self::__canUninstallOrDisable($obj);
428
429
        $info = self::about($name);
430
        $id = self::fetchExtensionID($name);
431
432
        $disabled = Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::update() has been deprecated: Symphony 3.0.0 This parameter is deprecated and will be removed. Use DatabaseUpdate::where() ( Ignorable by Annotation )

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

432
        $disabled = /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
433
            ->update('tbl_extensions')
434
            ->set([
435
                'name' => $name,
436
                'status' => 'disabled',
437
                'version' => $info['version']
438
            ])
439
            ->where(['id' => $id])
440
            ->execute()
441
            ->success();
442
443
        $obj->disable();
444
445
        self::removeDelegates($name);
446
447
        return $disabled;
448
    }
449
450
    /**
451
     * Uninstalling an extension will unregister all delegate subscriptions and
452
     * remove all extension settings. Symphony checks that an extension can
453
     * be uninstalled using the `canUninstallorDisable()` before calling
454
     * the extension's `uninstall()` function. Alternatively, if this function
455
     * is called because the extension described by `$name` cannot be found
456
     * it's delegates and extension meta information will just be removed from the
457
     * database.
458
     *
459
     * @see toolkit.ExtensionManager#removeDelegates()
460
     * @see toolkit.ExtensionManager#__canUninstallOrDisable()
461
     * @param string $name
462
     *  The name of the Extension Class minus the extension prefix.
463
     * @throws Exception
464
     * @throws SymphonyErrorPage
465
     * @throws DatabaseException
466
     * @throws Exception
467
     * @return boolean
468
     */
469
    public static function uninstall($name)
470
    {
471
        // If this function is called because the extension doesn't exist,
472
        // then catch the error and just remove from the database. This
473
        // means that the uninstall() function will not run on the extension,
474
        // which may be a blessing in disguise as no entry data will be removed
475
        try {
476
            $obj = self::getInstance($name);
477
            self::__canUninstallOrDisable($obj);
478
            $obj->uninstall();
479
        } catch (SymphonyErrorPage $ex) {
480
            // Create a consistant key
481
            $key = str_replace('-', '_', $ex->getTemplateName());
482
483
            if ($key !== 'missing_extension') {
484
                throw $ex;
485
            }
486
        }
487
488
        self::removeDelegates($name);
489
        return Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::delete() has been deprecated: Symphony 3.0.0 This parameter is deprecated and will be removed. Use DatabaseDelete::where() ( Ignorable by Annotation )

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

489
        return /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
490
            ->delete('tbl_extensions')
491
            ->where(['name' => $name])
492
            ->execute()
493
            ->success();
494
    }
495
496
    /**
497
     * Retrieves all subscribed delegates from the database.
498
     *
499
     * @return DatabaseQueryResult
500
     */
501
    public function getDelegateSubscriptions()
502
    {
503
        $projection = ['t1.name', 't2.page', 't2.delegate', 't2.callback', 't2.order'];
504
        $orderBy = ['t2.delegate', 't2.order', 't1.name'];
505
        $removeOrder = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $removeOrder is dead and can be removed.
Loading history...
506
        try {
507
            $removeOrder = !(Symphony::Database()
508
                ->showColumns()
509
                ->from('tbl_extensions_delegates')
510
                ->like('order')
511
                ->execute()
512
                ->next());
513
        } catch (DatabaseException $ex) {
514
            $removeOrder = true;
515
            // Ignore for now
516
            // This catch and check will be removed in Symphony 5.0.0
517
        }
518
519
        // Remove order col from projection and order by
520
        if ($removeOrder) {
521
            array_pop($projection);
522
            $orderBy = ['t2.delegate', 't1.name'];
523
        }
524
525
        return Symphony::Database()
526
            ->select($projection)
527
            ->from('tbl_extensions', 't1')
528
            ->innerJoin('tbl_extensions_delegates', 't2')
529
            ->on(['t1.id' => '$t2.extension_id'])
530
            ->where(['t1.status' => 'enabled'])
531
            ->orderBy($orderBy, 'ASC')
532
            ->execute();
533
    }
534
535
    /**
536
     * This functions registers an extensions delegates in `tbl_extensions_delegates`.
537
     *
538
     * @param string $name
539
     *  The name of the Extension Class minus the extension prefix.
540
     * @throws Exception
541
     * @throws SymphonyErrorPage
542
     * @return integer
543
     *  The Extension ID
544
     */
545
    public static function registerDelegates($name)
546
    {
547
        $obj = self::getInstance($name);
548
        $id = self::fetchExtensionID($name);
549
550
        if (!$id) {
551
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
552
        }
553
554
        Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::delete() has been deprecated: Symphony 3.0.0 This parameter is deprecated and will be removed. Use DatabaseDelete::where() ( Ignorable by Annotation )

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

554
        /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
555
            ->delete('tbl_extensions_delegates')
556
            ->where(['extension_id' => $id])
557
            ->execute();
558
559
        $delegates = $obj->getSubscribedDelegates();
560
561
        if (is_array($delegates) && !empty($delegates)) {
562
            foreach ($delegates as $delegate) {
563
                Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::insert() has been deprecated: Symphony 3.0.0 If $table is an array, it is treated as the fields values Use DatabaseInsert::values() ( Ignorable by Annotation )

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

563
                /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
564
                    ->insert('tbl_extensions_delegates')
565
                    ->values([
566
                        'extension_id' => $id,
567
                        'page' => $delegate['page'],
568
                        'delegate' => $delegate['delegate'],
569
                        'callback' => $delegate['callback'],
570
                        'order' => isset($delegate['order']) ? (int)$delegate['order'] : 0
571
                    ])
572
                    ->execute();
573
            }
574
        }
575
576
        // Remove the unused DB records
577
        self::cleanupDatabase();
578
579
        return $id;
580
    }
581
582
    /**
583
     * This function will remove all delegate subscriptions for an extension
584
     * given an extension's name. This triggers `cleanupDatabase()`
585
     *
586
     * @see toolkit.ExtensionManager#cleanupDatabase()
587
     * @param string $name
588
     *  The name of the Extension Class minus the extension prefix.
589
     * @return boolean
590
     */
591
    public static function removeDelegates($name)
592
    {
593
        $delegates = Symphony::Database()
594
            ->select(['ted.id'])
595
            ->from('tbl_extensions_delegates', 'ted')
596
            ->leftJoin('tbl_extensions')
597
            ->on(['tbl_extensions.id' => '$ted.extension_id'])
598
            ->where(['tbl_extensions.name' => $name])
599
            ->execute()
600
            ->column('id');
601
602
        if (!empty($delegates)) {
603
            Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::delete() has been deprecated: Symphony 3.0.0 This parameter is deprecated and will be removed. Use DatabaseDelete::where() ( Ignorable by Annotation )

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

603
            /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
604
                ->delete('tbl_extensions_delegates')
605
                ->where(['id' => ['in' => $delegates]])
606
                ->execute();
607
        }
608
609
        // Remove the unused DB records
610
        self::cleanupDatabase();
611
612
        return true;
613
    }
614
615
    /**
616
     * This function checks that if the given extension has provided Fields,
617
     * Data Sources or Events, that they aren't in use before the extension
618
     * is uninstalled or disabled. This prevents exceptions from occurring when
619
     * accessing an object that was using something provided by this Extension
620
     * can't anymore because it has been removed.
621
     *
622
     * @param Extension $obj
623
     *  An extension object
624
     * @throws SymphonyErrorPage
625
     * @throws Exception
626
     */
627
    private static function __canUninstallOrDisable(Extension $obj)
628
    {
629
        $extension_handle = strtolower(preg_replace('/^extension_/i', null, get_class($obj)));
630
        $about = self::about($extension_handle);
631
632
        // Fields:
633
        if (is_dir(EXTENSIONS . "/{$extension_handle}/fields")) {
0 ignored issues
show
Bug introduced by
The constant EXTENSIONS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
634
            foreach (glob(EXTENSIONS . "/{$extension_handle}/fields/field.*.php") as $file) {
635
                $type = preg_replace(array('/^field\./i', '/\.php$/i'), null, basename($file));
636
637
                if (FieldManager::isFieldUsed($type)) {
638
                    throw new Exception(
639
                        __('The field ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
640
                        . ' ' . __("Please remove it from your sections prior to uninstalling or disabling.")
641
                    );
642
                }
643
            }
644
        }
645
646
        // Data Sources:
647
        if (is_dir(EXTENSIONS . "/{$extension_handle}/data-sources")) {
648
            foreach (glob(EXTENSIONS . "/{$extension_handle}/data-sources/data.*.php") as $file) {
649
                $handle = preg_replace(array('/^data\./i', '/\.php$/i'), null, basename($file));
650
651
                if (PageManager::isDataSourceUsed($handle)) {
652
                    throw new Exception(
653
                        __('The Data Source ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
654
                        . ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
655
                    );
656
                }
657
            }
658
        }
659
660
        // Events
661
        if (is_dir(EXTENSIONS . "/{$extension_handle}/events")) {
662
            foreach (glob(EXTENSIONS . "/{$extension_handle}/events/event.*.php") as $file) {
663
                $handle = preg_replace(array('/^event\./i', '/\.php$/i'), null, basename($file));
664
665
                if (PageManager::isEventUsed($handle)) {
666
                    throw new Exception(
667
                        __('The Event ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
668
                        . ' ' . __("Please remove it from your pages prior to uninstalling or disabling.")
669
                    );
670
                }
671
            }
672
        }
673
674
        // Text Formatters
675
        if (is_dir(EXTENSIONS . "/{$extension_handle}/text-formatters")) {
676
            foreach (glob(EXTENSIONS . "/{$extension_handle}/text-formatters/formatter.*.php") as $file) {
677
                $handle = preg_replace(array('/^formatter\./i', '/\.php$/i'), null, basename($file));
678
679
                if (FieldManager::isTextFormatterUsed($handle)) {
680
                    throw new Exception(
681
                        __('The Text Formatter ‘%s’, provided by the Extension ‘%s’, is currently in use.', array(basename($file), $about['name']))
682
                        . ' ' . __("Please remove it from your fields prior to uninstalling or disabling.")
683
                    );
684
                }
685
            }
686
        }
687
    }
688
689
    /**
690
     * Given a delegate name, notify all extensions that have registered to that
691
     * delegate to executing their callbacks with a `$context` array parameter
692
     * that contains information about the current Symphony state.
693
     *
694
     * @param string $delegate
695
     *  The delegate name
696
     * @param string $page
697
     *  The current page namespace that this delegate operates in
698
     * @param array $context
699
     *  The `$context` param is an associative array that at minimum will contain
700
     *  the current Administration class, the current page object and the delegate
701
     *  name. Other context information may be passed to this function when it is
702
     *  called. eg.
703
     *
704
     * array(
705
     *        'parent' =>& $this->Parent,
706
     *        'page' => $page,
707
     *        'delegate' => $delegate
708
     *    );
709
     * @throws Exception
710
     * @throws SymphonyErrorPage
711
     * @return null|void
712
     */
713
    public static function notifyMembers($delegate, $page, array $context = array())
714
    {
715
        // Make sure $page is an array
716
        if (!is_array($page)) {
0 ignored issues
show
introduced by
The condition is_array($page) is always false.
Loading history...
717
            $page = array($page);
718
        }
719
720
        // Support for global delegate subscription
721
        if (!in_array('*', $page)) {
722
            $page[] = '*';
723
        }
724
725
        $services = array();
726
727
        if (isset(self::$_subscriptions[$delegate])) {
728
            foreach (self::$_subscriptions[$delegate] as $subscription) {
729
                if (!in_array($subscription['page'], $page)) {
730
                    continue;
731
                }
732
733
                $services[] = $subscription;
734
            }
735
        }
736
737
        if (empty($services)) {
738
            return null;
739
        }
740
741
        $context += array('page' => $page, 'delegate' => $delegate);
742
        $profiling = Symphony::Profiler() instanceof Profiler;
743
744
        foreach ($services as $s) {
745
            if ($profiling) {
746
                // Initial seeding and query count
747
                Symphony::Profiler()->seed();
748
                $queries = Symphony::Database()->queryCount();
749
            }
750
751
            // Get instance of extension and execute the callback passing
752
            // the `$context` along
753
            $obj = self::getInstance($s['name']);
754
755
            if (is_object($obj) && method_exists($obj, $s['callback'])) {
756
                $obj->{$s['callback']}($context);
757
            }
758
759
            // Complete the Profiling sample
760
            if ($profiling) {
761
                $queries = Symphony::Database()->queryCount() - $queries;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $queries does not seem to be defined for all execution paths leading up to this point.
Loading history...
762
                Symphony::Profiler()->sample($delegate . '|' . $s['name'], PROFILE_LAP, 'Delegate', $queries);
0 ignored issues
show
Bug introduced by
The constant PROFILE_LAP was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
763
            }
764
        }
765
    }
766
767
    /**
768
     * Returns an array of all the enabled extensions available
769
     *
770
     * @return array
771
     */
772
    public static function listInstalledHandles()
773
    {
774
        if (empty(self::$_enabled_extensions) && Symphony::Database()->isConnected()) {
775
            self::$_enabled_extensions = (new ExtensionManager)
776
                ->select(['name'])
777
                ->enabled()
778
                ->execute()
779
                ->column('name');
780
        }
781
        return self::$_enabled_extensions;
782
    }
783
784
    /**
785
     * Returns true if the extension is installed.
786
     *
787
     * @uses listInstalledHandles()
788
     * @since Symphony 3.0.0
789
     * @param string $handle
790
     *  The name of the extension
791
     * @return boolean
792
     */
793
    public static function isInstalled($handle)
794
    {
795
        return in_array($handle, self::listInstalledHandles());
796
    }
797
798
    /**
799
     * Will return an associative array of all extensions and their about information
800
     *
801
     * @param string $filter
802
     *  Allows a regular expression to be passed to return only extensions whose
803
     *  folders match the filter.
804
     * @throws SymphonyErrorPage
805
     * @throws Exception
806
     * @return array
807
     *  An associative array with the key being the extension folder and the value
808
     *  being the extension's about information
809
     */
810
    public static function listAll($filter = '/^((?![-^?%:*|"<>]).)*$/')
811
    {
812
        $result = array();
813
        $extensions = General::listDirStructure(EXTENSIONS, $filter, false, EXTENSIONS);
0 ignored issues
show
Bug introduced by
The constant EXTENSIONS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
814
815
        if (is_array($extensions) && !empty($extensions)) {
816
            foreach ($extensions as $extension) {
817
                $e = trim($extension, '/');
818
819
                if ($about = self::about($e)) {
820
                    $result[$e] = $about;
821
                }
822
            }
823
        }
824
825
        return $result;
826
    }
827
828
    /**
829
     * Custom user sorting function used inside `fetch` to recursively sort authors
830
     * by their names.
831
     *
832
     * @param array $a
833
     * @param array $b
834
     * @param integer $i
835
     * @return integer
836
     */
837
    private static function sortByAuthor($a, $b, $i = 0)
838
    {
839
        $first = $a;
840
        $second = $b;
841
842
        if (isset($a[$i])) {
843
            $first = $a[$i];
844
        }
845
846
        if (isset($b[$i])) {
847
            $second = $b[$i];
848
        }
849
850
        if ($first == $a && $second == $b && $first['name'] == $second['name']) {
851
            return 1;
852
        } elseif ($first['name'] == $second['name']) {
853
            return self::sortByAuthor($a, $b, $i + 1);
854
        } else {
855
            return ($first['name'] < $second['name']) ? -1 : 1;
856
        }
857
    }
858
859
    /**
860
     * This function will return an associative array of Extension information. The
861
     * information returned is defined by the `$select` parameter, which will allow
862
     * a developer to restrict what information is returned about the Extension.
863
     * Optionally, `$where` (not implemented) and `$order_by` parameters allow a developer to
864
     * further refine their query.
865
     *
866
     * @see listAll()
867
     * @param array $select (optional)
868
     *  Accepts an array of keys to return from the listAll() method. If omitted, all keys
869
     *  will be returned.
870
     * @param array $where (optional)
871
     *  Not implemented.
872
     * @param string $order_by (optional)
873
     *  Allows a developer to return the extensions in a particular order. The syntax is the
874
     *  same as other `fetch` methods. If omitted this will return resources ordered by `name`.
875
     * @throws Exception
876
     * @throws SymphonyErrorPage
877
     * @return array
878
     *  An associative array of Extension information, formatted in the same way as the
879
     *  listAll() method.
880
     */
881
    public static function fetch(array $select = array(), array $where = array(), $order_by = null)
882
    {
883
        $extensions = self::listAll();
884
        $data = array();
885
886
        if (empty($select) && empty($where) && is_null($order_by)) {
887
            return $extensions;
888
        }
889
890
        if (empty($extensions)) {
891
            return array();
892
        }
893
894
        if (!is_null($order_by)) {
895
            $author = $name = $label = array();
896
            $order_by = array_map('strtolower', explode(' ', $order_by));
897
            $order = ($order_by[1] == 'desc') ? SORT_DESC : SORT_ASC;
898
            $sort = $order_by[0];
899
900
            if ($sort == 'author') {
901
                foreach ($extensions as $key => $about) {
902
                    $author[$key] = $about['author'];
903
                }
904
905
                uasort($author, array('self', 'sortByAuthor'));
906
907
                if ($order == SORT_DESC) {
908
                    $author = array_reverse($author);
909
                }
910
911
                foreach ($author as $key => $value) {
912
                    $data[$key] = $extensions[$key];
913
                }
914
915
                $extensions = $data;
916
            } elseif ($sort == 'name') {
917
                foreach ($extensions as $key => $about) {
918
                    $name[$key] = strtolower($about['name']);
919
                    $label[$key] = $key;
920
                }
921
922
                array_multisort($name, $order, $label, $order, $extensions);
923
            }
924
        }
925
926
        foreach ($extensions as $i => $e) {
927
            $data[$i] = array();
928
            foreach ($e as $key => $value) {
929
                // If $select is empty, we assume every field is requested
930
                if (in_array($key, $select) || empty($select)) {
931
                    $data[$i][$key] = $value;
932
                }
933
            }
934
        }
935
936
        return $data;
937
    }
938
939
    /**
940
     * This function will load an extension's meta information given the extension
941
     * `$name`. Since Symphony 2.3, this function will look for an `extension.meta.xml`
942
     * file inside the extension's folder. If this is not found, it will initialise
943
     * the extension and invoke the `about()` function. By default this extension will
944
     * return an associative array display the basic meta data about the given extension.
945
     * If the `$rawXML` parameter is passed true, and the extension has a `extension.meta.xml`
946
     * file, this function will return `DOMDocument` of the file.
947
     *
948
     * @param string $name
949
     *  The name of the Extension Class minus the extension prefix.
950
     * @param boolean $rawXML
951
     *  If passed as true, and is available, this function will return the
952
     *  DOMDocument of representation of the given extension's `extension.meta.xml`
953
     *  file. If the file is not available, the extension will return the normal
954
     *  `about()` results. By default this is false.
955
     * @throws Exception
956
     * @throws SymphonyErrorPage
957
     * @return array
958
     *  An associative array describing this extension
959
     */
960
    public static function about($name, $rawXML = false)
961
    {
962
        // See if the extension has the new meta format
963
        if (file_exists(self::__getClassPath($name) . '/extension.meta.xml')) {
964
            try {
965
                $meta = new DOMDocument;
966
                $meta->load(self::__getClassPath($name) . '/extension.meta.xml');
967
                $xpath = new DOMXPath($meta);
968
                $rootNamespace = $meta->lookupNamespaceUri($meta->namespaceURI);
969
970
                if (is_null($rootNamespace)) {
0 ignored issues
show
introduced by
The condition is_null($rootNamespace) is always false.
Loading history...
971
                    throw new Exception(__('Missing default namespace definition.'));
972
                } else {
973
                    $xpath->registerNamespace('ext', $rootNamespace);
974
                }
975
            } catch (Exception $ex) {
976
                Symphony::Engine()->throwCustomError(
977
                    __('The %1$s file for the %2$s extension is not valid XML: %3$s', array(
978
                        '<code>extension.meta.xml</code>',
979
                        '<code>' . $name . '</code>',
980
                        '<br /><code>' . $ex->getMessage() . '</code>'
981
                    ))
982
                );
983
            }
984
985
            // Load <extension>
986
            $extension = $xpath->query('/ext:extension')->item(0);
987
988
            // Check to see that the extension is named correctly, if it is
989
            // not, then return nothing
990
            if (self::__getClassName($name) !== self::__getClassName($xpath->evaluate('string(@id)', $extension))) {
991
                return array();
992
            }
993
994
            // If `$rawXML` is set, just return our DOMDocument instance
995
            if ($rawXML) {
996
                return $meta;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $meta returns the type DOMDocument which is incompatible with the documented return type array.
Loading history...
997
            }
998
999
            $about = array(
1000
                'name' => $xpath->evaluate('string(ext:name)', $extension),
1001
                'handle' => $name,
1002
                'github' => $xpath->evaluate('string(ext:repo)', $extension),
1003
                'discuss' => $xpath->evaluate('string(ext:url[@type="discuss"])', $extension),
1004
                'homepage' => $xpath->evaluate('string(ext:url[@type="homepage"])', $extension),
1005
                'wiki' => $xpath->evaluate('string(ext:url[@type="wiki"])', $extension),
1006
                'issues' => $xpath->evaluate('string(ext:url[@type="issues"])', $extension),
1007
                'status' => array()
1008
            );
1009
1010
            // find the latest <release> (largest version number)
1011
            $latest_release_version = '0.0.0';
1012
            foreach ($xpath->query('//ext:release', $extension) as $release) {
1013
                $version = $xpath->evaluate('string(@version)', $release);
1014
1015
                if (\Composer\Semver\Comparator::greaterThan($version, $latest_release_version)) {
1016
                    $latest_release_version = $version;
1017
                }
1018
            }
1019
1020
            // Load the latest <release> information
1021
            if ($release = $xpath->query("//ext:release[@version='$latest_release_version']", $extension)->item(0)) {
1022
                $about += array(
1023
                    'version' => $xpath->evaluate('string(@version)', $release),
1024
                    'release-date' => $xpath->evaluate('string(@date)', $release)
1025
                );
1026
1027
                // If it exists, load in the 'min/max' version data for this release
1028
                $required_min_version = $xpath->evaluate('string(@min)', $release);
1029
                $required_max_version = $xpath->evaluate('string(@max)', $release);
1030
                $current_symphony_version = Symphony::Configuration()->get('version', 'symphony');
1031
1032
                // Min version
1033
                if (!empty($required_min_version) &&
1034
                    \Composer\Semver\Comparator::lessThan($current_symphony_version, $required_min_version)) {
0 ignored issues
show
Bug introduced by
It seems like $current_symphony_version can also be of type array; however, parameter $version1 of Composer\Semver\Comparator::lessThan() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1034
                    \Composer\Semver\Comparator::lessThan(/** @scrutinizer ignore-type */ $current_symphony_version, $required_min_version)) {
Loading history...
1035
                    $about['status'][] = Extension::EXTENSION_NOT_COMPATIBLE;
1036
                    $about['required_version'] = $required_min_version;
1037
1038
                // Max version
1039
                } elseif (!empty($required_max_version) &&
1040
                    \Composer\Semver\Comparator::greaterThan($current_symphony_version, $required_max_version)) {
0 ignored issues
show
Bug introduced by
It seems like $current_symphony_version can also be of type array; however, parameter $version1 of Composer\Semver\Comparator::greaterThan() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1040
                    \Composer\Semver\Comparator::greaterThan(/** @scrutinizer ignore-type */ $current_symphony_version, $required_max_version)) {
Loading history...
1041
                    $about['status'][] = Extension::EXTENSION_NOT_COMPATIBLE;
1042
                    $about['required_version'] = $required_max_version;
1043
                }
1044
            }
1045
1046
            // Add the <author> information
1047
            foreach ($xpath->query('//ext:author', $extension) as $author) {
1048
                $a = array(
1049
                    'name' => $xpath->evaluate('string(ext:name)', $author),
1050
                    'website' => $xpath->evaluate('string(ext:website)', $author),
1051
                    'github' => $xpath->evaluate('string(ext:name/@github)', $author),
1052
                    'email' => $xpath->evaluate('string(ext:email)', $author)
1053
                );
1054
1055
                $about['author'][] = array_filter($a);
1056
            }
1057
1058
            $about['status'] = array_merge($about['status'], self::fetchStatus($about));
1059
            return $about;
1060
        } else {
1061
            Symphony::Log()->pushToLog(sprintf('%s does not have an extension.meta.xml file', $name), E_DEPRECATED, true);
1062
1063
            return array();
1064
        }
1065
    }
1066
1067
    /**
1068
     * Creates an instance of a given class and returns it
1069
     *
1070
     * @param string $name
1071
     *  The name of the Extension Class minus the extension prefix.
1072
     * @throws Exception
1073
     * @throws SymphonyErrorPage
1074
     * @return Extension
1075
     */
1076
    public static function create($name)
1077
    {
1078
        if (!isset(self::$_pool[$name])) {
1079
            $classname = self::__getClassName($name);
1080
            $path = self::__getDriverPath($name);
1081
1082
            if (!is_file($path)) {
1083
                $errMsg = __('Could not find extension %s at location %s.', array(
1084
                    '<code>' . $name . '</code>',
1085
                    '<code>' . str_replace(DOCROOT . '/', '', $path) . '</code>'
0 ignored issues
show
Bug introduced by
The constant DOCROOT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1086
                ));
1087
                try {
1088
                    Symphony::Engine()->throwCustomError(
1089
                        $errMsg,
1090
                        __('Symphony Extension Missing Error'),
1091
                        Page::HTTP_STATUS_ERROR,
1092
                        'missing_extension',
1093
                        array(
1094
                            'name' => $name,
1095
                            'path' => $path
1096
                        )
1097
                    );
1098
                } catch (Exception $ex) {
1099
                    throw new Exception($errMsg, 0, $ex);
1100
                }
1101
            }
1102
1103
            if (!class_exists($classname)) {
1104
                require_once($path);
1105
            }
1106
1107
            // Create the extension object
1108
            self::$_pool[$name] = new $classname(array());
1109
        }
1110
1111
        return self::$_pool[$name];
1112
    }
1113
1114
    /**
1115
     * A utility function that is used by the ExtensionManager to ensure
1116
     * stray delegates are not in `tbl_extensions_delegates`. It is called when
1117
     * a new Delegate is added or removed.
1118
     */
1119
    public static function cleanupDatabase()
1120
    {
1121
        // Grab any extensions sitting in the database
1122
        $rows = Symphony::Database()
1123
            ->select(['name', 'status'])
1124
            ->from('tbl_extensions')
1125
            ->execute()
1126
            ->rows();
1127
1128
        // Iterate over each row
1129
        if (!empty($rows)) {
1130
            foreach ($rows as $r) {
1131
                $name = $r['name'];
1132
                $status = $r['status'];
1133
1134
                // Grab the install location
1135
                $path = self::__getClassPath($name);
1136
                $existing_id = self::fetchExtensionID($name);
1137
1138
                $removeDelegatesSQL = Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::delete() has been deprecated: Symphony 3.0.0 This parameter is deprecated and will be removed. Use DatabaseDelete::where() ( Ignorable by Annotation )

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

1138
                $removeDelegatesSQL = /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1139
                    ->delete('tbl_extensions_delegates')
1140
                    ->where(['extension_id' => $existing_id]);
1141
1142
                // If it doesn't exist, remove the DB rows
1143
                if (!@is_dir($path)) {
1144
                    $removeDelegatesSQL->execute();
1145
                    Symphony::Database()
0 ignored issues
show
Deprecated Code introduced by
The function Database::delete() has been deprecated: Symphony 3.0.0 This parameter is deprecated and will be removed. Use DatabaseDelete::where() ( Ignorable by Annotation )

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

1145
                    /** @scrutinizer ignore-deprecated */ Symphony::Database()

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1146
                        ->delete('tbl_extensions')
1147
                        ->wehere(['id' => $existing_id])
0 ignored issues
show
Bug introduced by
The method wehere() does not exist on DatabaseDelete. ( Ignorable by Annotation )

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

1147
                        ->/** @scrutinizer ignore-call */ wehere(['id' => $existing_id])

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1148
                        ->limit(1)
1149
                        ->execute();
1150
                } elseif ($status == 'disabled') {
1151
                    $removeDelegatesSQL->execute();
1152
                }
1153
            }
1154
        }
1155
    }
1156
1157
    /**
1158
     * Factory method that creates a new ExtensionQuery.
1159
     *
1160
     * @since Symphony 3.0.0
1161
     * @param array $projection
1162
     *  The projection to select.
1163
     *  If no projection gets added, it defaults to `DatabaseQuery::getDefaultProjection()`.
1164
     * @return ExtensionQuery
1165
     */
1166
    public function select(array $projection = [])
1167
    {
1168
        return new ExtensionQuery(Symphony::Database(), $projection);
1169
    }
1170
}
1171