Completed
Push — master ( b36645...a84650 )
by Alejandro
10:40
created

InstallCommand   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 89.29%

Importance

Changes 0
Metric Value
dl 0
loc 228
ccs 75
cts 84
cp 0.8929
rs 10
c 0
b 0
f 0
wmc 25
lcom 1
cbo 8

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 2
A configure() 0 6 1
C execute() 0 80 10
B importConfig() 0 35 6
B runPhpCommand() 0 23 6
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 Symfony\Component\Process\PhpExecutableFinder;
22
use Zend\Config\Writer\WriterInterface;
23
24
class InstallCommand extends Command
25
{
26
    const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
27
28
    /**
29
     * @var SymfonyStyle
30
     */
31
    private $io;
32
    /**
33
     * @var ProcessHelper
34
     */
35
    private $processHelper;
36
    /**
37
     * @var WriterInterface
38
     */
39
    private $configWriter;
40
    /**
41
     * @var Filesystem
42
     */
43
    private $filesystem;
44
    /**
45
     * @var ConfigCustomizerManagerInterface
46
     */
47
    private $configCustomizers;
48
    /**
49
     * @var bool
50
     */
51
    private $isUpdate;
52
    /**
53
     * @var PhpExecutableFinder
54
     */
55
    private $phpFinder;
56
    /**
57
     * @var string|bool
58
     */
59
    private $phpBinary;
60
61
    /**
62
     * InstallCommand constructor.
63
     * @param WriterInterface $configWriter
64
     * @param Filesystem $filesystem
65
     * @param ConfigCustomizerManagerInterface $configCustomizers
66
     * @param bool $isUpdate
67
     * @param PhpExecutableFinder|null $phpFinder
68
     * @throws LogicException
69
     */
70 5
    public function __construct(
71
        WriterInterface $configWriter,
72
        Filesystem $filesystem,
73
        ConfigCustomizerManagerInterface $configCustomizers,
74
        bool $isUpdate = false,
75
        PhpExecutableFinder $phpFinder = null
76
    ) {
77 5
        parent::__construct();
78 5
        $this->configWriter = $configWriter;
79 5
        $this->isUpdate = $isUpdate;
80 5
        $this->filesystem = $filesystem;
81 5
        $this->configCustomizers = $configCustomizers;
82 5
        $this->phpFinder = $phpFinder ?: new PhpExecutableFinder();
83 5
    }
84
85 5
    protected function configure(): void
86
    {
87
        $this
88 5
            ->setName('shlink:install')
89 5
            ->setDescription('Installs or updates Shlink');
90 5
    }
91
92
    /**
93
     * @param InputInterface $input
94
     * @param OutputInterface $output
95
     * @return void
96
     * @throws ContainerExceptionInterface
97
     * @throws NotFoundExceptionInterface
98
     */
99 4
    protected function execute(InputInterface $input, OutputInterface $output): void
100
    {
101 4
        $this->io = new SymfonyStyle($input, $output);
102
103 4
        $this->io->writeln([
0 ignored issues
show
Documentation introduced by
array('<info>Welcome to ...installation process.') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string|object<Symfony\Co...onsole\Output\iterable>.

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...
104 4
            '<info>Welcome to Shlink!!</info>',
105
            'This will guide you through the installation process.',
106
        ]);
107
108
        // Check if a cached config file exists and drop it if so
109 4
        if ($this->filesystem->exists('data/cache/app_config.php')) {
110 2
            $this->io->write('Deleting old cached config...');
111
            try {
112 2
                $this->filesystem->remove('data/cache/app_config.php');
113 1
                $this->io->writeln(' <info>Success</info>');
114 1
            } catch (IOException $e) {
115 1
                $this->io->error(
116
                    'Failed! You will have to manually delete the data/cache/app_config.php file to'
117 1
                    . ' get new config applied.'
118
                );
119 1
                if ($this->io->isVerbose()) {
120
                    $this->getApplication()->renderException($e, $output);
121
                }
122 1
                return;
123
            }
124
        }
125
126
        // If running update command, ask the user to import previous config
127 3
        $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
128
129
        // Ask for custom config params
130
        foreach ([
131 3
            Plugin\DatabaseConfigCustomizer::class,
132
            Plugin\UrlShortenerConfigCustomizer::class,
133
            Plugin\LanguageConfigCustomizer::class,
134
            Plugin\ApplicationConfigCustomizer::class,
135
        ] as $pluginName) {
136
            /** @var Plugin\ConfigCustomizerInterface $configCustomizer */
137 3
            $configCustomizer = $this->configCustomizers->get($pluginName);
138 3
            $configCustomizer->process($this->io, $config);
139
        }
140
141
        // Generate config params files
142 3
        $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
143 3
        $this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
0 ignored issues
show
Documentation introduced by
array('<info>Custom conf...generated!</info>', '') is of type array<integer,string,{"0":"string","1":"string"}>, but the function expects a string|object<Symfony\Co...onsole\Output\iterable>.

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...
144
145
        // If current command is not update, generate database
146 3
        if (!  $this->isUpdate) {
147 2
            $this->io->write('Initializing database...');
148 2
            if (! $this->runPhpCommand(
149 2
                'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
150 2
                'Error generating database.',
151 2
                $output
152
            )) {
153
                return;
154
            }
155
        }
156
157
        // Run database migrations
158 3
        $this->io->write('Updating database...');
159 3
        if (! $this->runPhpCommand(
160 3
            'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
161 3
            'Error updating database.',
162 3
            $output
163
        )) {
164
            return;
165
        }
166
167
        // Generate proxies
168 3
        $this->io->write('Generating proxies...');
169 3
        if (! $this->runPhpCommand(
170 3
            'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
171 3
            'Error generating proxies.',
172 3
            $output
173
        )) {
174
            return;
175
        }
176
177 3
        $this->io->success('Installation complete!');
178 3
    }
179
180
    /**
181
     * @return CustomizableAppConfig
182
     * @throws RuntimeException
183
     */
184 1
    private function importConfig(): CustomizableAppConfig
185
    {
186 1
        $config = new CustomizableAppConfig();
187
188
        // Ask the user if he/she wants to import an older configuration
189 1
        $importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
190 1
        if (! $importConfig) {
191
            return $config;
192
        }
193
194
        // Ask the user for the older shlink path
195 1
        $keepAsking = true;
196
        do {
197 1
            $config->setImportedInstallationPath($this->io->ask(
198 1
                'Previous shlink installation path from which to import config'
199
            ));
200 1
            $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
201 1
            $configExists = $this->filesystem->exists($configFile);
202
203 1
            if (! $configExists) {
204 1
                $keepAsking = $this->io->confirm(
205 1
                    'Provided path does not seem to be a valid shlink root path. Do you want to try another path?'
206
                );
207
            }
208 1
        } while (! $configExists && $keepAsking);
209
210
        // If after some retries the user has chosen not to test another path, return
211 1
        if (! $configExists) {
212
            return $config;
213
        }
214
215
        // Read the config file
216 1
        $config->exchangeArray(include $configFile);
217 1
        return $config;
218
    }
219
220
    /**
221
     * @param string $command
222
     * @param string $errorMessage
223
     * @param OutputInterface $output
224
     * @return bool
225
     * @throws LogicException
226
     * @throws InvalidArgumentException
227
     */
228 3
    private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
229
    {
230 3
        if ($this->processHelper === null) {
231 3
            $this->processHelper = $this->getHelper('process');
232
        }
233
234 3
        if ($this->phpBinary === null) {
235 3
            $this->phpBinary = $this->phpFinder->find(false) ?: 'php';
236
        }
237
238 3
        $this->io->writeln('Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"');
239 3
        $process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $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...
240 3
        if ($process->isSuccessful()) {
241 3
            $this->io->writeln(' <info>Success!</info>');
242 3
            return true;
243
        }
244
245
        if (! $this->io->isVerbose()) {
246
            $this->io->error($errorMessage . '  Run this command with -vvv to see specific error info.');
247
        }
248
249
        return false;
250
    }
251
}
252