Completed
Push — 6.7 ( d8cf1b...deee41 )
by André
15:56
created

InstallPlatformCommand::executeCommand()   C

Complexity

Conditions 8
Paths 65

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 21
nc 65
nop 3
dl 0
loc 39
rs 5.3846
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of the eZ Publish Kernel package.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace EzSystems\PlatformInstallerBundle\Command;
10
11
use Doctrine\DBAL\Connection;
12
use Doctrine\DBAL\Exception\ConnectionException;
13
use Symfony\Component\Console\Command\Command;
14
use Symfony\Component\Console\Input\InputArgument;
15
use Symfony\Component\Console\Input\InputInterface;
16
use Symfony\Component\Console\Output\OutputInterface;
17
use Symfony\Component\Process\Process;
18
use Symfony\Component\Process\PhpExecutableFinder;
19
use Symfony\Component\Filesystem\Filesystem;
20
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
21
22
class InstallPlatformCommand extends Command
23
{
24
    /** @var \Doctrine\DBAL\Connection */
25
    private $db;
26
27
    /** @var \Symfony\Component\Console\Output\OutputInterface */
28
    private $output;
29
30
    /** @var \Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface */
31
    private $cacheClearer;
32
33
    /** @var \Symfony\Component\Filesystem\Filesystem */
34
    private $filesystem;
35
36
    /** @var string */
37
    private $cacheDir;
38
39
    /** @var string */
40
    private $environment;
41
42
    /** @var string */
43
    private $searchEngine;
44
45
    /** @var \EzSystems\PlatformInstallerBundle\Installer\Installer[] */
46
    private $installers = array();
47
48
    const EXIT_DATABASE_NOT_FOUND_ERROR = 3;
49
    const EXIT_GENERAL_DATABASE_ERROR = 4;
50
    const EXIT_PARAMETERS_NOT_FOUND = 5;
51
    const EXIT_UNKNOWN_INSTALL_TYPE = 6;
52
    const EXIT_MISSING_PERMISSIONS = 7;
53
54
    public function __construct(
55
        Connection $db,
56
        array $installers,
57
        CacheClearerInterface $cacheClearer,
58
        Filesystem $filesystem,
59
        $cacheDir,
60
        $environment,
61
        $searchEngine
62
    ) {
63
        $this->db = $db;
64
        $this->installers = $installers;
65
        $this->cacheClearer = $cacheClearer;
66
        $this->filesystem = $filesystem;
67
        $this->cacheDir = $cacheDir;
68
        $this->environment = $environment;
69
        $this->searchEngine = $searchEngine;
70
        parent::__construct();
71
    }
72
73
    protected function configure()
74
    {
75
        $this->setName('ezplatform:install');
76
        $this->addArgument(
77
            'type',
78
            InputArgument::REQUIRED,
79
            'The type of install. Available options: ' . implode(', ', array_keys($this->installers))
80
        );
81
    }
82
83
    protected function execute(InputInterface $input, OutputInterface $output)
84
    {
85
        $this->output = $output;
86
        $this->checkPermissions();
87
        $this->checkParameters();
88
        $this->checkDatabase();
89
90
        $type = $input->getArgument('type');
91
        $installer = $this->getInstaller($type);
92
        if ($installer === false) {
93
            $output->writeln(
94
                "Unknown install type '$type', available options in currently installed eZ Platform package: " .
95
                implode(', ', array_keys($this->installers))
96
            );
97
            exit(self::EXIT_UNKNOWN_INSTALL_TYPE);
98
        }
99
100
        $installer->setOutput($output);
101
102
        $installer->importSchema();
103
        $installer->importData();
104
        $installer->importBinaries();
105
        $this->cacheClear($output);
106
        $this->indexData($output);
107
    }
108
109
    private function checkPermissions()
110
    {
111
        if (!is_writable('web') && !is_writable('web/var')) {
112
            $this->output->writeln('[web/ | web/var] is not writable');
113
            exit(self::EXIT_MISSING_PERMISSIONS);
114
        }
115
    }
116
117
    private function checkParameters()
118
    {
119
        $parametersFile = 'app/config/parameters.yml';
120
        if (!is_file($parametersFile)) {
121
            $this->output->writeln("Required configuration file '$parametersFile' not found");
122
            exit(self::EXIT_PARAMETERS_NOT_FOUND);
123
        }
124
    }
125
126
    /**
127
     * @throws \Exception if an unexpected database error occurs
128
     */
129
    private function configuredDatabaseExists()
130
    {
131
        try {
132
            $this->db->connect();
133
        } catch (ConnectionException $e) {
134
            // @todo 1049 is MySQL's code for "database doesn't exist", refactor
135
            if ($e->getPrevious()->getCode() == 1049) {
136
                return false;
137
            }
138
            throw $e;
139
        }
140
141
        return true;
142
    }
143
144
    private function checkDatabase()
145
    {
146
        try {
147
            if (!$this->configuredDatabaseExists()) {
148
                $this->output->writeln(
149
                    sprintf(
150
                        "The configured database '%s' does not exist",
151
                        $this->db->getDatabase()
152
                    )
153
                );
154
                exit(self::EXIT_DATABASE_NOT_FOUND_ERROR);
155
            }
156
        } catch (ConnectionException $e) {
157
            $this->output->writeln('An error occurred connecting to the database:');
158
            $this->output->writeln($e->getMessage());
159
            $this->output->writeln("Please check the database configuration in 'app/config/parameters.yml'");
160
            exit(self::EXIT_GENERAL_DATABASE_ERROR);
161
        }
162
    }
163
164
    private function cacheClear(OutputInterface $output)
165
    {
166
        if (!is_writable($this->cacheDir)) {
167
            throw new \RuntimeException(
168
                sprintf(
169
                    'Unable to write in the "%s" directory, check install doc on disk permissions before you continue.',
170
                    $this->cacheDir
171
                )
172
            );
173
        }
174
175
        $output->writeln(sprintf('Clearing cache for directory <info>%s</info>', $this->cacheDir));
176
        $oldCacheDir = $this->cacheDir . '_old';
177
178
        if ($this->filesystem->exists($oldCacheDir)) {
179
            $this->filesystem->remove($oldCacheDir);
180
        }
181
182
        $this->cacheClearer->clear($this->cacheDir);
183
184
        $this->filesystem->rename($this->cacheDir, $oldCacheDir);
185
        $this->filesystem->remove($oldCacheDir);
186
    }
187
188
    /**
189
     * Calls indexing commands on search engines known to need that.
190
     *
191
     * @todo This should not be needed once/if the Installer starts using API in the future.
192
     *       So temporary measure until it is not raw SQL based for the data itself (as opposed to the schema).
193
     *       This is done after cache clearing to make sure no cached data from before sql import is used.
194
     *
195
     * IMPORTANT: This is done using a command because config has change, so container and all services are different.
196
     *
197
     * @param OutputInterface $output
198
     */
199
    private function indexData(OutputInterface $output)
200
    {
201
        if (!in_array($this->searchEngine, ['solr', 'elasticsearch'])) {
202
            return;
203
        }
204
205
        $output->writeln(
206
            sprintf('%s search engine configured, executing command ezplatform:reindex', $this->searchEngine)
207
        );
208
        $this->executeCommand($output, 'ezplatform:reindex');
209
    }
210
211
    /**
212
     * @param $type
213
     *
214
     * @return \EzSystems\PlatformInstallerBundle\Installer\Installer
215
     */
216
    private function getInstaller($type)
217
    {
218
        if (!isset($this->installers[$type])) {
219
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by EzSystems\PlatformInstal...rmCommand::getInstaller of type EzSystems\PlatformInstal...dle\Installer\Installer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
220
        }
221
222
        return $this->installers[$type];
223
    }
224
225
    /**
226
     * Executes a Symfony command in separate process.
227
     *
228
     * Typically useful when configuration has changed, or you are outside of Symfony context (Composer commands).
229
     *
230
     * Based on {@see \Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::executeCommand}.
231
     *
232
     * @param OutputInterface $output
233
     * @param string $cmd eZ Platform command to execute, like 'ezplatform:solr_create_index'
234
     *               Escape any user provided arguments, like: 'assets:install '.escapeshellarg($webDir)
235
     * @param int $timeout
236
     */
237
    private function executeCommand(OutputInterface $output, $cmd, $timeout = 300)
238
    {
239
        $phpFinder = new PhpExecutableFinder();
240
        if (!$phpPath = $phpFinder->find(false)) {
241
            throw new \RuntimeException('The php executable could not be found, add it to your PATH environment variable and try again');
242
        }
243
244
        // We don't know which php arguments where used so we gather some to be on the safe side
245
        $arguments = $phpFinder->findArguments();
246
        if (false !== ($ini = php_ini_loaded_file())) {
247
            $arguments[] = '--php-ini=' . $ini;
248
        }
249
250
        // Pass memory_limit in case this was specified as php argument, if not it will most likely be same as $ini.
251
        if ($memoryLimit = ini_get('memory_limit')) {
252
            $arguments[] = '-d memory_limit=' . $memoryLimit;
253
        }
254
255
        $phpArgs = implode(' ', array_map('escapeshellarg', $arguments));
256
        $php = escapeshellarg($phpPath) . ($phpArgs ? ' ' . $phpArgs : '');
257
258
        // Make sure to pass along relevant global Symfony options to console command
259
        $console = escapeshellarg('app/console');
260
        if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
261
            $console .= ' -' . str_repeat('v', $output->getVerbosity() - 1);
262
        }
263
264
        if ($output->isDecorated()) {
265
            $console .= ' --ansi';
266
        }
267
268
        $console .= ' --env=' . escapeshellarg($this->environment);
269
270
        $process = new Process($php . ' ' . $console . ' ' . $cmd, null, null, null, $timeout);
271
        $process->run(function ($type, $buffer) use ($output) { $output->write($buffer, false); });
272
        if (!$process->isSuccessful()) {
273
            throw new \RuntimeException(sprintf('An error occurred when executing the "%s" command.', escapeshellarg($cmd)));
274
        }
275
    }
276
}
277