Completed
Push — tmp_sql_installer ( 5116d2 )
by André
26:50
created

InstallPlatformCommand::execute()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 35
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 35
rs 8.439
cc 5
eloc 22
nc 9
nop 2
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\InputOption;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use Symfony\Component\Process\Process;
19
use Symfony\Component\Process\PhpExecutableFinder;
20
use Symfony\Component\Filesystem\Filesystem;
21
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;
22
23
class InstallPlatformCommand extends Command
24
{
25
    /** @var \Doctrine\DBAL\Connection */
26
    private $db;
27
28
    /** @var \Symfony\Component\Console\Output\OutputInterface */
29
    private $output;
30
31
    /** @var \Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface */
32
    private $cacheClearer;
33
34
    /** @var \Symfony\Component\Filesystem\Filesystem */
35
    private $filesystem;
36
37
    /** @var string */
38
    private $cacheDir;
39
40
    /** @var string */
41
    private $environment;
42
43
    /** @var string */
44
    private $searchEngine;
45
46
    /** @var \EzSystems\PlatformInstallerBundle\Installer\Installer[] */
47
    private $installers = array();
48
49
    const EXIT_DATABASE_NOT_FOUND_ERROR = 3;
50
    const EXIT_GENERAL_DATABASE_ERROR = 4;
51
    const EXIT_PARAMETERS_NOT_FOUND = 5;
52
    const EXIT_UNKNOWN_INSTALL_TYPE = 6;
53
    const EXIT_MISSING_PERMISSIONS = 7;
54
55
    public function __construct(
56
        Connection $db,
57
        array $installers,
58
        CacheClearerInterface $cacheClearer,
59
        Filesystem $filesystem,
60
        $cacheDir,
61
        $environment,
62
        $searchEngine
63
    ) {
64
        $this->db = $db;
65
        $this->installers = $installers;
66
        $this->cacheClearer = $cacheClearer;
67
        $this->filesystem = $filesystem;
68
        $this->cacheDir = $cacheDir;
69
        $this->environment = $environment;
70
        $this->searchEngine = $searchEngine;
71
        parent::__construct();
72
    }
73
74
    protected function configure()
75
    {
76
        $this->setName('ezplatform:install');
77
        $this->addArgument(
78
            'type',
79
            InputArgument::REQUIRED,
80
            'The type of install. Available options: ' . implode(', ', array_keys($this->installers))
81
        );
82
        // @todo This is not the right approach, base configure step, just like base schema, should be inline here
83
        // @todo and not on installer duplicating things in every installer
84
        // @todo OR? Problem with that argument is that if installers (db) is allowed to generate config we violate 12-factor
85
        // @note Only Studio demo inserts config, and it might unneded.
86
        $this->addOption(
87
            'skip-config',
88
            null,
89
            InputOption::VALUE_NONE,
90
            'To skip dumping configuration, for use when running command first with `skip-data`'
91
        );
92
        $this->addOption(
93
            'skip-data',
94
            null,
95
            InputOption::VALUE_NONE,
96
            'To skip inserting data, for use when running command in second iteration with `skip-config`'
97
        );
98
    }
99
100
    protected function execute(InputInterface $input, OutputInterface $output)
101
    {
102
        $this->output = $output;
103
        $this->checkPermissions();
104
        $this->checkParameters();
105
        $this->checkDatabase();
106
107
        $type = $input->getArgument('type');
108
        $installer = $this->getInstaller($type);
109
        if ($installer === false) {
110
            $output->writeln("Unknown install type '$type'");
111
            exit(self::EXIT_UNKNOWN_INSTALL_TYPE);
112
        }
113
114
        $installer->setOutput($output);
115
116
        $skipConfigure = $input->getOption('skip-config');
117
        $skipData = $input->getOption('skip-data');
118
119
        if (!$skipConfigure) {
120
            $installer->createConfiguration();
121
        }
122
123
        if (!$skipData) {
124
            $installer->importSchema();
125
            $installer->importData();
126
            $installer->importBinaries();
127
        }
128
129
        $this->cacheClear($output);
130
131
        if (!$skipData) {
132
            $this->indexData($output);
133
        }
134
    }
135
136
    private function checkPermissions()
137
    {
138
        if (!is_writable('app/config')) {
139
            $this->output->writeln('app/config is not writable');
140
            exit(self::EXIT_MISSING_PERMISSIONS);
141
        }
142
    }
143
144
    private function checkParameters()
145
    {
146
        $parametersFile = 'app/config/parameters.yml';
147
        if (!is_file($parametersFile)) {
148
            $this->output->writeln("Required configuration file $parametersFile not found");
149
            exit(self::EXIT_PARAMETERS_NOT_FOUND);
150
        }
151
    }
152
153
    /**
154
     * @throws \Exception if an unexpected database error occurs
155
     */
156
    private function configuredDatabaseExists()
157
    {
158
        try {
159
            $this->db->connect();
160
        } catch (ConnectionException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\ConnectionException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
161
            // @todo 1049 is MySQL's code for "database doesn't exist", refactor
162
            if ($e->getPrevious()->getCode() == 1049) {
163
                return false;
164
            }
165
            throw $e;
166
        }
167
168
        return true;
169
    }
170
171
    private function checkDatabase()
172
    {
173
        try {
174
            if (!$this->configuredDatabaseExists()) {
175
                $this->output->writeln(
176
                    sprintf(
177
                        "The configured database '%s' does not exist",
178
                        $this->db->getDatabase()
179
                    )
180
                );
181
                exit(self::EXIT_DATABASE_NOT_FOUND_ERROR);
182
            }
183
        } catch (ConnectionException $e) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\ConnectionException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
184
            $this->output->writeln('An error occured connecting to the database:');
185
            $this->output->writeln($e->getMessage());
186
            $this->output->writeln('Please check the database configuration in parameters.yml');
187
            exit(self::EXIT_GENERAL_DATABASE_ERROR);
188
        }
189
    }
190
191
    private function cacheClear(OutputInterface $output)
192
    {
193
        if (!is_writable($this->cacheDir)) {
194
            throw new \RuntimeException(sprintf('Unable to write in the "%s" directory', $this->cacheDir));
195
        }
196
197
        $output->writeln(sprintf('Clearing cache for directory <info>%s</info>', $this->cacheDir));
198
        $oldCacheDir = $this->cacheDir . '_old';
199
200
        if ($this->filesystem->exists($oldCacheDir)) {
201
            $this->filesystem->remove($oldCacheDir);
202
        }
203
204
        $this->cacheClearer->clear($this->cacheDir);
205
206
        $this->filesystem->rename($this->cacheDir, $oldCacheDir);
207
        $this->filesystem->remove($oldCacheDir);
208
    }
209
210
    /**
211
     * Calls indexing commands on search engines known to need that.
212
     *
213
     * @todo This should not be needed once/if the Installer starts using API in the future.
214
     *       So temporary measure until it is not raw SQL based for the data itself (as opposed to the schema).
215
     *       This is done after cache clearing to make sure no cached data from before sql import is used.
216
     *
217
     * IMPORTANT: This is done using a command because config has change, so container and all services are different.
218
     *
219
     * @param OutputInterface $output
220
     */
221
    private function indexData(OutputInterface $output)
222
    {
223
        if ($this->searchEngine === 'solr') {
224
            $output->writeln('Solr search engine configured, executing command ezplatform:solr_create_index');
225
            $this->executeCommand($output, 'ezplatform:solr_create_index');
226
        }
227
228
        if ($this->searchEngine === 'elasticsearch') {
229
            $output->writeln('Elasticsearch search engine configured, executing command ezplatform:elasticsearch_create_index');
230
            $this->executeCommand($output, 'ezplatform:elasticsearch_create_index');
231
        }
232
    }
233
234
    /**
235
     * @param $type
236
     *
237
     * @return \EzSystems\PlatformInstallerBundle\Installer\Installer
238
     */
239
    private function getInstaller($type)
240
    {
241
        if (!isset($this->installers[$type])) {
242
            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...
243
        }
244
245
        return $this->installers[$type];
246
    }
247
248
    /**
249
     * Executes a Symfony command in separate process.
250
     *
251
     * Typically usefull when configuration has changed, our you are outside of Symfony context (Composer commands).
252
     *
253
     * Based on {@see \Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::executeCommand}.
254
     *
255
     * @param OutputInterface $output
256
     * @param string $cmd eZ Platform command to execute, like 'ezplatform:solr_create_index'
257
     *               Escape any user provided arguments, like: 'assets:install '.escapeshellarg($webDir)
258
     * @param int $timeout
259
     */
260
    private function executeCommand(OutputInterface $output, $cmd, $timeout = 300)
261
    {
262
        $phpFinder = new PhpExecutableFinder();
263
        if (!$phpPath = $phpFinder->find(false)) {
264
            throw new \RuntimeException('The php executable could not be found, add it to your PATH environment variable and try again');
265
        }
266
267
        // We don't know which php arguments where used so we gather some to be on the safe side
268
        $arguments = $phpFinder->findArguments();
269
        if (false !== ($ini = php_ini_loaded_file())) {
270
            $arguments[] = '--php-ini=' . $ini;
271
        }
272
273
        // Pass memory_limit in case this was specified as php argument, if not it will most likely be same as $ini.
274
        if ($memoryLimit = ini_get('memory_limit')) {
275
            $arguments[] = '-d memory_limit=' . $memoryLimit;
276
        }
277
278
        $phpArgs = implode(' ', array_map('escapeshellarg', $arguments));
279
        $php = escapeshellarg($phpPath) . ($phpArgs ? ' ' . $phpArgs : '');
280
281
        // Make sure to pass along relevant global Symfony options to console command
282
        $console = escapeshellarg('app/console');
283
        if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
284
            $console .= ' -' . str_repeat('v', $output->getVerbosity() - 1);
285
        }
286
287
        if ($output->isDecorated()) {
288
            $console .= ' --ansi';
289
        }
290
291
        $console .= ' --env=' . escapeshellarg($this->environment);
292
293
        $process = new Process($php . ' ' . $console . ' ' . $cmd, null, null, null, $timeout);
294
        $process->run(function ($type, $buffer) use ($output) { $output->write($buffer, false); });
295
        if (!$process->isSuccessful()) {
296
            throw new \RuntimeException(sprintf('An error occurred when executing the "%s" command.', escapeshellarg($cmd)));
297
        }
298
    }
299
}
300