Completed
Push — master ( 9da3a8...079a2f )
by André
51:30 queued 31:10
created

InstallPlatformCommand::checkDatabase()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 4
nop 0
dl 0
loc 19
rs 9.4285
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 \EzSystems\PlatformInstallerBundle\Installer\Installer[] */
43
    private $installers = array();
44
45
    const EXIT_DATABASE_NOT_FOUND_ERROR = 3;
46
    const EXIT_GENERAL_DATABASE_ERROR = 4;
47
    const EXIT_PARAMETERS_NOT_FOUND = 5;
48
    const EXIT_UNKNOWN_INSTALL_TYPE = 6;
49
    const EXIT_MISSING_PERMISSIONS = 7;
50
51
    public function __construct(
52
        Connection $db,
53
        array $installers,
54
        CacheClearerInterface $cacheClearer,
55
        Filesystem $filesystem,
56
        $cacheDir,
57
        $environment
58
    ) {
59
        $this->db = $db;
60
        $this->installers = $installers;
61
        $this->cacheClearer = $cacheClearer;
62
        $this->filesystem = $filesystem;
63
        $this->cacheDir = $cacheDir;
64
        $this->environment = $environment;
65
        parent::__construct();
66
    }
67
68
    protected function configure()
69
    {
70
        $this->setName('ezplatform:install');
71
        $this->addArgument(
72
            'type',
73
            InputArgument::REQUIRED,
74
            'The type of install. Available options: ' . implode(', ', array_keys($this->installers))
75
        );
76
    }
77
78
    protected function execute(InputInterface $input, OutputInterface $output)
79
    {
80
        $this->output = $output;
81
        $this->checkPermissions();
82
        $this->checkParameters();
83
        $this->checkDatabase();
84
85
        $type = $input->getArgument('type');
86
        $installer = $this->getInstaller($type);
87
        if ($installer === false) {
88
            $output->writeln(
89
                "Unknown install type '$type', available options in currently installed eZ Platform package: " .
90
                implode(', ', array_keys($this->installers))
91
            );
92
            exit(self::EXIT_UNKNOWN_INSTALL_TYPE);
93
        }
94
95
        $installer->setOutput($output);
96
97
        $installer->importSchema();
98
        $installer->importData();
99
        $installer->importBinaries();
100
        $this->cacheClear($output);
101
        $this->indexData($output);
102
    }
103
104
    private function checkPermissions()
105
    {
106
        if (!is_writable('web') && !is_writable('web/var')) {
107
            $this->output->writeln('[web/ | web/var] is not writable');
108
            exit(self::EXIT_MISSING_PERMISSIONS);
109
        }
110
    }
111
112
    private function checkParameters()
113
    {
114
        $parametersFile = 'app/config/parameters.yml';
115
        if (!is_file($parametersFile)) {
116
            $this->output->writeln("Required configuration file '$parametersFile' not found");
117
            exit(self::EXIT_PARAMETERS_NOT_FOUND);
118
        }
119
    }
120
121
    /**
122
     * @throws \Exception if an unexpected database error occurs
123
     */
124
    private function configuredDatabaseExists()
125
    {
126
        try {
127
            $this->db->connect();
128
        } catch (ConnectionException $e) {
129
            // @todo 1049 is MySQL's code for "database doesn't exist", refactor
130
            if ($e->getPrevious()->getCode() == 1049) {
131
                return false;
132
            }
133
            throw $e;
134
        }
135
136
        return true;
137
    }
138
139
    private function checkDatabase()
140
    {
141
        try {
142
            if (!$this->configuredDatabaseExists()) {
143
                $this->output->writeln(
144
                    sprintf(
145
                        "The configured database '%s' does not exist",
146
                        $this->db->getDatabase()
147
                    )
148
                );
149
                exit(self::EXIT_DATABASE_NOT_FOUND_ERROR);
150
            }
151
        } catch (ConnectionException $e) {
152
            $this->output->writeln('An error occurred connecting to the database:');
153
            $this->output->writeln($e->getMessage());
154
            $this->output->writeln("Please check the database configuration in 'app/config/parameters.yml'");
155
            exit(self::EXIT_GENERAL_DATABASE_ERROR);
156
        }
157
    }
158
159
    private function cacheClear(OutputInterface $output)
160
    {
161
        if (!is_writable($this->cacheDir)) {
162
            throw new \RuntimeException(
163
                sprintf(
164
                    'Unable to write in the "%s" directory, check install doc on disk permissions before you continue.',
165
                    $this->cacheDir
166
                )
167
            );
168
        }
169
170
        $output->writeln(sprintf('Clearing cache for directory <info>%s</info>', $this->cacheDir));
171
        $oldCacheDir = $this->cacheDir . '_old';
172
173
        if ($this->filesystem->exists($oldCacheDir)) {
174
            $this->filesystem->remove($oldCacheDir);
175
        }
176
177
        $this->cacheClearer->clear($this->cacheDir);
178
179
        $this->filesystem->rename($this->cacheDir, $oldCacheDir);
180
        $this->filesystem->remove($oldCacheDir);
181
    }
182
183
    /**
184
     * Calls indexing commands.
185
     *
186
     * @todo This should not be needed once/if the Installer starts using API in the future.
187
     *       So temporary measure until it is not raw SQL based for the data itself (as opposed to the schema).
188
     *       This is done after cache clearing to make sure no cached data from before sql import is used.
189
     *
190
     * IMPORTANT: This is done using a command because config has change, so container and all services are different.
191
     *
192
     * @param OutputInterface $output
193
     */
194
    private function indexData(OutputInterface $output)
195
    {
196
        $output->writeln(
197
            sprintf('Search engine re-indexing, executing command ezplatform:reindex')
198
        );
199
        $this->executeCommand($output, 'ezplatform:reindex');
200
    }
201
202
    /**
203
     * @param $type
204
     *
205
     * @return \EzSystems\PlatformInstallerBundle\Installer\Installer
206
     */
207
    private function getInstaller($type)
208
    {
209
        if (!isset($this->installers[$type])) {
210
            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...
211
        }
212
213
        return $this->installers[$type];
214
    }
215
216
    /**
217
     * Executes a Symfony command in separate process.
218
     *
219
     * Typically useful when configuration has changed, or you are outside of Symfony context (Composer commands).
220
     *
221
     * Based on {@see \Sensio\Bundle\DistributionBundle\Composer\ScriptHandler::executeCommand}.
222
     *
223
     * @param OutputInterface $output
224
     * @param string $cmd eZ Platform command to execute, like 'ezplatform:solr_create_index'
225
     *               Escape any user provided arguments, like: 'assets:install '.escapeshellarg($webDir)
226
     * @param int $timeout
227
     */
228
    private function executeCommand(OutputInterface $output, $cmd, $timeout = 300)
229
    {
230
        $phpFinder = new PhpExecutableFinder();
231
        if (!$phpPath = $phpFinder->find(false)) {
232
            throw new \RuntimeException('The php executable could not be found, add it to your PATH environment variable and try again');
233
        }
234
235
        // We don't know which php arguments where used so we gather some to be on the safe side
236
        $arguments = $phpFinder->findArguments();
237
        if (false !== ($ini = php_ini_loaded_file())) {
238
            $arguments[] = '--php-ini=' . $ini;
239
        }
240
241
        // Pass memory_limit in case this was specified as php argument, if not it will most likely be same as $ini.
242
        if ($memoryLimit = ini_get('memory_limit')) {
243
            $arguments[] = '-d memory_limit=' . $memoryLimit;
244
        }
245
246
        $phpArgs = implode(' ', array_map('escapeshellarg', $arguments));
247
        $php = escapeshellarg($phpPath) . ($phpArgs ? ' ' . $phpArgs : '');
248
249
        // Make sure to pass along relevant global Symfony options to console command
250
        $console = escapeshellarg('app/console');
251
        if ($output->getVerbosity() > OutputInterface::VERBOSITY_NORMAL) {
252
            $console .= ' -' . str_repeat('v', $output->getVerbosity() - 1);
253
        }
254
255
        if ($output->isDecorated()) {
256
            $console .= ' --ansi';
257
        }
258
259
        $console .= ' --env=' . escapeshellarg($this->environment);
260
261
        $process = new Process($php . ' ' . $console . ' ' . $cmd, null, null, null, $timeout);
262
        $process->run(function ($type, $buffer) use ($output) { $output->write($buffer, false); });
263
        if (!$process->isSuccessful()) {
264
            throw new \RuntimeException(sprintf('An error occurred when executing the "%s" command.', escapeshellarg($cmd)));
265
        }
266
    }
267
}
268