Completed
Push — master ( aca90e...8ef0e7 )
by Alejandro
10s
created

InstallCommand::runCommand()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.7691

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 6
nop 3
dl 0
loc 19
rs 9.2
c 0
b 0
f 0
ccs 7
cts 11
cp 0.6364
crap 4.7691
1
<?php
2
declare(strict_types=1);
3
4
namespace Shlinkio\Shlink\CLI\Command\Install;
5
6
use Psr\Container\ContainerExceptionInterface;
7
use Psr\Container\NotFoundExceptionInterface;
8
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManagerInterface;
9
use Shlinkio\Shlink\CLI\Install\Plugin;
10
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
11
use Symfony\Component\Console\Command\Command;
12
use Symfony\Component\Console\Exception\InvalidArgumentException;
13
use Symfony\Component\Console\Exception\LogicException;
14
use Symfony\Component\Console\Exception\RuntimeException;
15
use Symfony\Component\Console\Helper\ProcessHelper;
16
use Symfony\Component\Console\Input\InputInterface;
17
use Symfony\Component\Console\Output\OutputInterface;
18
use Symfony\Component\Console\Style\SymfonyStyle;
19
use Symfony\Component\Filesystem\Exception\IOException;
20
use Symfony\Component\Filesystem\Filesystem;
21
use Zend\Config\Writer\WriterInterface;
22
23
class InstallCommand extends Command
24
{
25
    const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
26
27
    /**
28
     * @var SymfonyStyle
29
     */
30
    private $io;
31
    /**
32
     * @var ProcessHelper
33
     */
34
    private $processHelper;
35
    /**
36
     * @var WriterInterface
37
     */
38
    private $configWriter;
39
    /**
40
     * @var Filesystem
41
     */
42
    private $filesystem;
43
    /**
44
     * @var ConfigCustomizerManagerInterface
45
     */
46
    private $configCustomizers;
47
    /**
48
     * @var bool
49
     */
50
    private $isUpdate;
51
52
    /**
53
     * InstallCommand constructor.
54
     * @param WriterInterface $configWriter
55
     * @param Filesystem $filesystem
56
     * @param ConfigCustomizerManagerInterface $configCustomizers
57
     * @param bool $isUpdate
58
     * @throws LogicException
59
     */
60 5
    public function __construct(
61
        WriterInterface $configWriter,
62
        Filesystem $filesystem,
63
        ConfigCustomizerManagerInterface $configCustomizers,
64
        $isUpdate = false
65
    ) {
66 5
        parent::__construct();
67 5
        $this->configWriter = $configWriter;
68 5
        $this->isUpdate = $isUpdate;
69 5
        $this->filesystem = $filesystem;
70 5
        $this->configCustomizers = $configCustomizers;
71 5
    }
72
73 5
    public function configure()
74
    {
75
        $this
76 5
            ->setName('shlink:install')
77 5
            ->setDescription('Installs or updates Shlink');
78 5
    }
79
80
    /**
81
     * @param InputInterface $input
82
     * @param OutputInterface $output
83
     * @return void
84
     * @throws ContainerExceptionInterface
85
     * @throws NotFoundExceptionInterface
86
     */
87 4
    public function execute(InputInterface $input, OutputInterface $output)
88
    {
89 4
        $this->io = new SymfonyStyle($input, $output);
90
91 4
        $this->io->writeln([
92 4
            '<info>Welcome to Shlink!!</info>',
93
            'This will guide you through the installation process.',
94
        ]);
95
96
        // Check if a cached config file exists and drop it if so
97 4
        if ($this->filesystem->exists('data/cache/app_config.php')) {
98 2
            $this->io->write('Deleting old cached config...');
99
            try {
100 2
                $this->filesystem->remove('data/cache/app_config.php');
101 1
                $this->io->writeln(' <info>Success</info>');
102 1
            } catch (IOException $e) {
103 1
                $this->io->error(
104
                    'Failed! You will have to manually delete the data/cache/app_config.php file to'
105 1
                    . ' get new config applied.'
106
                );
107 1
                if ($this->io->isVerbose()) {
108
                    $this->getApplication()->renderException($e, $output);
109
                }
110 1
                return;
111
            }
112
        }
113
114
        // If running update command, ask the user to import previous config
115 3
        $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
116
117
        // Ask for custom config params
118
        foreach ([
119 3
            Plugin\DatabaseConfigCustomizer::class,
120
            Plugin\UrlShortenerConfigCustomizer::class,
121
            Plugin\LanguageConfigCustomizer::class,
122
            Plugin\ApplicationConfigCustomizer::class,
123
        ] as $pluginName) {
124
            /** @var Plugin\ConfigCustomizerInterface $configCustomizer */
125 3
            $configCustomizer = $this->configCustomizers->get($pluginName);
126 3
            $configCustomizer->process($this->io, $config);
127
        }
128
129
        // Generate config params files
130 3
        $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
131 3
        $this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
132
133
        // If current command is not update, generate database
134 3
        if (!  $this->isUpdate) {
135 2
            $this->io->write('Initializing database...');
136 2
            if (! $this->runCommand(
137 2
                'php vendor/bin/doctrine.php orm:schema-tool:create',
138 2
                'Error generating database.',
139 2
                $output
140
            )) {
141
                return;
142
            }
143
        }
144
145
        // Run database migrations
146 3
        $this->io->write('Updating database...');
147 3
        if (! $this->runCommand(
148 3
            'php vendor/bin/doctrine-migrations migrations:migrate',
149 3
            'Error updating database.',
150 3
            $output
151
        )) {
152
            return;
153
        }
154
155
        // Generate proxies
156 3
        $this->io->write('Generating proxies...');
157 3
        if (! $this->runCommand(
158 3
            'php vendor/bin/doctrine.php orm:generate-proxies',
159 3
            'Error generating proxies.',
160 3
            $output
161
        )) {
162
            return;
163
        }
164
165 3
        $this->io->success('Installation complete!');
166 3
    }
167
168
    /**
169
     * @return CustomizableAppConfig
170
     * @throws RuntimeException
171
     */
172 1
    private function importConfig(): CustomizableAppConfig
173
    {
174 1
        $config = new CustomizableAppConfig();
175
176
        // Ask the user if he/she wants to import an older configuration
177 1
        $importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
178 1
        if (! $importConfig) {
179
            return $config;
180
        }
181
182
        // Ask the user for the older shlink path
183 1
        $keepAsking = true;
184
        do {
185 1
            $config->setImportedInstallationPath($this->io->ask(
186 1
                'Previous shlink installation path from which to import config'
187
            ));
188 1
            $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
189 1
            $configExists = $this->filesystem->exists($configFile);
190
191 1
            if (! $configExists) {
192 1
                $keepAsking = $this->io->confirm(
193 1
                    'Provided path does not seem to be a valid shlink root path. Do you want to try another path?'
194
                );
195
            }
196 1
        } while (! $configExists && $keepAsking);
197
198
        // If after some retries the user has chosen not to test another path, return
199 1
        if (! $configExists) {
200
            return $config;
201
        }
202
203
        // Read the config file
204 1
        $config->exchangeArray(include $configFile);
205 1
        return $config;
206
    }
207
208
    /**
209
     * @param string $command
210
     * @param string $errorMessage
211
     * @param OutputInterface $output
212
     * @return bool
213
     * @throws LogicException
214
     * @throws InvalidArgumentException
215
     */
216 3
    private function runCommand($command, $errorMessage, OutputInterface $output): bool
217
    {
218 3
        if ($this->processHelper === null) {
219 3
            $this->processHelper = $this->getHelper('process');
220
        }
221
222 3
        $process = $this->processHelper->run($output, $command);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Console\Helper\HelperInterface as the method run() does only exist in the following implementations of said interface: Symfony\Component\Console\Helper\ProcessHelper.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
223 3
        if ($process->isSuccessful()) {
224 3
            $this->io->writeln(' <info>Success!</info>');
225 3
            return true;
226
        }
227
228
        if ($this->io->isVerbose()) {
229
            return false;
230
        }
231
232
        $this->io->error($errorMessage . '  Run this command with -vvv to see specific error info.');
233
        return false;
234
    }
235
}
236