Completed
Push — master ( d32ac4...f621f2 )
by Craig
06:48
created

ExtensionHelper::forceLoadExtension()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
nc 12
nop 1
dl 0
loc 25
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Zikula package.
5
 *
6
 * Copyright Zikula Foundation - http://zikula.org/
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zikula\ExtensionsModule\Helper;
13
14
use Symfony\Component\Console\Input\ArrayInput;
15
use Symfony\Component\Console\Output\NullOutput;
16
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
17
use Symfony\Component\DependencyInjection\ContainerInterface;
18
use Zikula\Bundle\CoreBundle\Bundle\Scanner;
19
use Zikula\Bundle\CoreBundle\Console\Application;
20
use Zikula\Common\Translator\TranslatorInterface;
21
use Zikula\Core\AbstractBundle;
22
use Zikula\Core\CoreEvents;
23
use Zikula\Core\Event\GenericEvent;
24
use Zikula\Core\Event\ModuleStateEvent;
25
use Zikula\Core\ExtensionInstallerInterface;
26
use Zikula\ExtensionsModule\Api\ExtensionApi;
27
use Zikula\ExtensionsModule\Entity\ExtensionEntity;
28
use Zikula\ExtensionsModule\ExtensionEvents;
29
30
class ExtensionHelper
31
{
32
    const TYPE_SYSTEM = 3;
33
    const TYPE_MODULE = 2;
34
35
    /**
36
     * @var ContainerInterface
37
     */
38
    private $container;
39
40
    /**
41
     * @var TranslatorInterface
42
     */
43
    private $translator;
44
45
    /**
46
     * @var ExtensionApi
47
     */
48
    private $extensionApi;
49
50
    /**
51
     * ExtensionHelper constructor.
52
     *
53
     * @param ContainerInterface $container
54
     */
55
    public function __construct(ContainerInterface $container)
56
    {
57
        $this->container = $container;
58
        $this->translator = $container->get('translator.default');
59
        $this->translator->setLocale('ZikulaExtensionsModule');
60
        $this->extensionApi = $container->get('zikula_extensions_module.api.extension');
61
    }
62
63
    /**
64
     * Install an extension.
65
     *
66
     * @param ExtensionEntity $extension
67
     * @return bool
68
     */
69
    public function install(ExtensionEntity $extension)
70
    {
71
        if ($extension->getState() == ExtensionApi::STATE_NOTALLOWED) {
72
            throw new \RuntimeException($this->translator->__f('Error! No permission to install %s.', ['%s' => $extension->getName()]));
73
        } elseif ($extension->getState() > 10) {
74
            throw new \RuntimeException($this->translator->__f('Error! %s is not compatible with this version of Zikula.', ['%s' => $extension->getName()]));
75
        }
76
77
        $bundle = $this->forceLoadExtension($extension);
78
79
        $installer = $this->getExtensionInstallerInstance($bundle);
0 ignored issues
show
Documentation introduced by
$bundle is of type object|null, but the function expects a object<Zikula\Core\AbstractBundle>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
80
        $result = $installer->install();
81
        if (!$result) {
82
            return false;
83
        }
84
        $this->container->get('zikula_extensions_module.extension_state_helper')->updateState($extension->getId(), ExtensionApi::STATE_ACTIVE);
85
86
        // clear the cache before calling events
87
        /** @var $cacheClearer \Zikula\Bundle\CoreBundle\CacheClearer */
88
        $cacheClearer = $this->container->get('zikula.cache_clearer');
89
        $cacheClearer->clear('symfony.config');
90
91
        $event = new ModuleStateEvent($bundle, $extension->toArray());
0 ignored issues
show
Bug introduced by
It seems like $bundle defined by $this->forceLoadExtension($extension) on line 77 can also be of type object; however, Zikula\Core\Event\ModuleStateEvent::__construct() does only seem to accept null|object<Zikula\Core\AbstractModule>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
92
        $this->container->get('event_dispatcher')->dispatch(CoreEvents::MODULE_INSTALL, $event);
93
94
        return true;
95
    }
96
97
    /**
98
     * Upgrade an extension.
99
     *
100
     * @param ExtensionEntity $extension
101
     * @return bool
102
     */
103
    public function upgrade(ExtensionEntity $extension)
104
    {
105
        switch ($extension->getState()) {
106
            case ExtensionApi::STATE_NOTALLOWED:
107
                throw new \RuntimeException($this->translator->__f('Error! Not allowed to upgrade %s.', ['%s' => $extension->getDisplayname()]));
108
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
109
            default:
110 View Code Duplication
                if ($extension->getState() > 10) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
111
                    throw new \RuntimeException($this->translator->__f('Error! %s is not compatible with this version of Zikula.', ['%s' => $extension->getDisplayname()]));
112
                }
113
        }
114
115
        if ($extension->getType() == self::TYPE_SYSTEM) {
116
            // system modules are always loaded
117
            $bundle = $this->container->get('kernel')->getModule($extension->getName());
118
        } else {
119
            $bundle = $this->forceLoadExtension($extension);
120
        }
121
122
        // @TODO: Need to check status of Dependencies here to be sure they are met for upgraded extension.
123
124
        $installer = $this->getExtensionInstallerInstance($bundle);
125
        $result = $installer->upgrade($extension->getVersion());
126
        if (is_string($result)) {
127
            if ($result != $extension->getVersion()) {
128
                // persist the last successful updated version
129
                $extension->setVersion($result);
130
                $this->container->get('doctrine')->getManager()->flush();
131
            }
132
133
            return false;
134
        } elseif (true !== $result) {
135
            return false;
136
        }
137
        // persist the updated version
138
        $newVersion = $bundle->getMetaData()->getVersion();
139
        $extension->setVersion($newVersion);
140
        $this->container->get('doctrine')->getManager()->flush();
141
142
        $this->container->get('zikula_extensions_module.extension_state_helper')->updateState($extension->getId(), ExtensionApi::STATE_ACTIVE);
143
144
        $this->container->get('zikula.cache_clearer')->clear('symfony');
145
146
        if ($this->container->getParameter('installed')) {
147
            // Upgrade succeeded, issue event.
148
            $event = new ModuleStateEvent($bundle, $extension->toArray());
149
            $this->container->get('event_dispatcher')->dispatch(CoreEvents::MODULE_UPGRADE, $event);
150
        }
151
152
        return true;
153
    }
154
155
    /**
156
     * Uninstall an extension.
157
     *
158
     * @param ExtensionEntity $extension
159
     * @return bool
160
     */
161
    public function uninstall(ExtensionEntity $extension)
162
    {
163
        if ($extension->getState() == ExtensionApi::STATE_NOTALLOWED
164
            || ($extension->getType() == self::TYPE_SYSTEM && $extension->getName() != 'ZikulaPageLockModule')) {
165
            throw new \RuntimeException($this->translator->__f('Error! No permission to uninstall %s.', ['%s' => $extension->getDisplayname()]));
166
        }
167 View Code Duplication
        if ($extension->getState() == ExtensionApi::STATE_UNINITIALISED) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
168
            throw new \RuntimeException($this->translator->__f('Error! %s is not yet installed, therefore it cannot be uninstalled.', ['%s' => $extension->getDisplayname()]));
169
        }
170
171
        // allow event to prevent extension removal
172
        $vetoEvent = new GenericEvent($extension);
173
        $this->container->get('event_dispatcher')->dispatch(ExtensionEvents::REMOVE_VETO, $vetoEvent);
174
        if ($vetoEvent->isPropagationStopped()) {
175
            return false;
176
        }
177
178
        $bundle = $this->forceLoadExtension($extension);
179
180
        // remove hooks
181
        $this->container->get('zikula_hook_bundle.api.hook')->uninstallProviderHooks($bundle->getMetaData());
182
        $this->container->get('zikula_hook_bundle.api.hook')->uninstallSubscriberHooks($bundle->getMetaData());
183
184
        $installer = $this->getExtensionInstallerInstance($bundle);
0 ignored issues
show
Documentation introduced by
$bundle is of type object|null, but the function expects a object<Zikula\Core\AbstractBundle>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
185
        $result = $installer->uninstall();
186
        if (!$result) {
187
            return false;
188
        }
189
190
        // remove remaining extension variables
191
        $this->container->get('zikula_extensions_module.api.variable')->delAll($extension->getName());
192
193
        // remove the entry from the modules table
194
        $this->container->get('doctrine')->getManager()->getRepository('ZikulaExtensionsModule:ExtensionEntity')->removeAndFlush($extension);
195
196
        // clear the cache before calling events
197
        /** @var $cacheClearer \Zikula\Bundle\CoreBundle\CacheClearer */
198
        $cacheClearer = $this->container->get('zikula.cache_clearer');
199
        $cacheClearer->clear('symfony.config');
200
201
        $event = new ModuleStateEvent($bundle, $extension->toArray());
0 ignored issues
show
Bug introduced by
It seems like $bundle defined by $this->forceLoadExtension($extension) on line 178 can also be of type object; however, Zikula\Core\Event\ModuleStateEvent::__construct() does only seem to accept null|object<Zikula\Core\AbstractModule>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
202
        $this->container->get('event_dispatcher')->dispatch(CoreEvents::MODULE_REMOVE, $event);
203
204
        return true;
205
    }
206
207
    /**
208
     * Uninstall an array of extensions.
209
     *
210
     * @param ExtensionEntity[] $extensions
211
     * @return bool
212
     */
213
    public function uninstallArray(array $extensions)
214
    {
215
        foreach ($extensions as $extension) {
216
            if (!$extension instanceof ExtensionEntity) {
217
                throw new \InvalidArgumentException();
218
            }
219
            $result = $this->uninstall($extension);
220
            if (!$result) {
221
                return false;
222
            }
223
        }
224
225
        return true;
226
    }
227
228
    /**
229
     * Based on the state of the extension, either install, upgrade or activate the extension.
230
     *
231
     * @param ExtensionEntity $extension
232
     * @return bool
233
     */
234
    public function enableExtension(ExtensionEntity $extension)
235
    {
236
        switch ($extension->getState()) {
237
            case ExtensionApi::STATE_UNINITIALISED:
238
                return $this->install($extension);
239
            case ExtensionApi::STATE_UPGRADED:
240
                return $this->upgrade($extension);
241
            case ExtensionApi::STATE_INACTIVE:
242
                return $this->container->get('zikula_extensions_module.extension_state_helper')->updateState($extension->getId(), ExtensionApi::STATE_ACTIVE);
243
            default:
244
                return false;
245
        }
246
    }
247
248
    /**
249
     * Get an instance of a bundle class that is not currently loaded into the kernel.
250
     * Extensions that are deactivated or uninstalled are NOT loaded into the kernel.
251
     * Note: All System modules are always loaded into the kernel.
252
     *
253
     * @param ExtensionEntity $extension
254
     * @return null|AbstractBundle
255
     */
256
    private function forceLoadExtension(ExtensionEntity $extension)
257
    {
258
        $osDir = $extension->getDirectory();
259
        $scanner = new Scanner();
260
        $directory = \ZikulaKernel::isCoreModule($extension->getName()) ? 'system' : 'modules';
261
        $scanner->scan(["$directory/$osDir"], 1);
262
        $modules = $scanner->getModulesMetaData(true);
263
        /** @var $moduleMetaData \Zikula\Bundle\CoreBundle\Bundle\MetaData */
264
        $moduleMetaData = !empty($modules[$extension->getName()]) ? $modules[$extension->getName()] : null;
265
        if (null !== $moduleMetaData) {
266
            // moduleMetaData only exists for bundle-type modules
267
            $boot = new \Zikula\Bundle\CoreBundle\Bundle\Bootstrap();
268
            $boot->addAutoloaders($this->container->get('kernel'), $moduleMetaData->getAutoload());
269
            $moduleClass = $moduleMetaData->getClass();
270
            $bundle = new $moduleClass();
271
            $bootstrap = $bundle->getPath() . "/bootstrap.php";
272
            if (file_exists($bootstrap)) {
273
                include_once $bootstrap;
274
            }
275
276
            return $bundle;
277
        }
278
279
        return null;
280
    }
281
282
    /**
283
     * Run the console command app/console assets:install
284
     *
285
     * @throws \Exception
286
     */
287
    public function installAssets()
288
    {
289
        $kernel = $this->container->get('kernel');
290
        $application = new Application($kernel);
291
        $application->setAutoExit(false);
292
        $input = new ArrayInput([
293
            'command' => 'assets:install'
294
        ]);
295
        $output = new NullOutput();
296
        $application->run($input, $output);
297
    }
298
299
    /**
300
     * Get an instance of an extension Installer.
301
     *
302
     * @param AbstractBundle $bundle
303
     * @return ExtensionInstallerInterface
304
     */
305
    private function getExtensionInstallerInstance(AbstractBundle $bundle)
306
    {
307
        $className = $bundle->getInstallerClass();
308
        $reflectionInstaller = new \ReflectionClass($className);
309
        if (!$reflectionInstaller->isSubclassOf('\Zikula\Core\ExtensionInstallerInterface')) {
310
            throw new \RuntimeException($this->translator->__f("%s must implement ExtensionInstallerInterface", ['%s' => $className]));
311
        }
312
        $installer = $reflectionInstaller->newInstance();
313
        $installer->setBundle($bundle);
314
        if ($installer instanceof ContainerAwareInterface) {
315
            $installer->setContainer($this->container);
316
        }
317
318
        return $installer;
319
    }
320
}
321