Completed
Pull Request — master (#220)
by Alejandro
07:25
created

InstallCommand::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 5
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Shlinkio\Shlink\Installer\Command;
5
6
use Psr\Container\ContainerExceptionInterface;
7
use Psr\Container\NotFoundExceptionInterface;
8
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManagerInterface;
9
use Shlinkio\Shlink\Installer\Config\Plugin;
10
use Shlinkio\Shlink\Installer\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
    public 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 \Shlinkio\Shlink\Installer\Config\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
    public function __construct(
71
        WriterInterface $configWriter,
72
        Filesystem $filesystem,
73
        ConfigCustomizerManagerInterface $configCustomizers,
74
        bool $isUpdate = false,
75
        PhpExecutableFinder $phpFinder = null
76
    ) {
77
        parent::__construct();
78
        $this->configWriter = $configWriter;
79
        $this->isUpdate = $isUpdate;
80
        $this->filesystem = $filesystem;
81
        $this->configCustomizers = $configCustomizers;
82
        $this->phpFinder = $phpFinder ?: new PhpExecutableFinder();
83
    }
84
85
    protected function configure(): void
86
    {
87
        $this
88
            ->setName('shlink:install')
89
            ->setDescription('Installs or updates Shlink');
90
    }
91
92
    /**
93
     * @param InputInterface $input
94
     * @param OutputInterface $output
95
     * @return void
96
     * @throws ContainerExceptionInterface
97
     * @throws NotFoundExceptionInterface
98
     */
99
    protected function execute(InputInterface $input, OutputInterface $output): void
100
    {
101
        $this->io = new SymfonyStyle($input, $output);
102
103
        $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
            '<info>Welcome to Shlink!!</info>',
105
            'This tool will guide you through the installation process.',
106
        ]);
107
108
        // Check if a cached config file exists and drop it if so
109
        if ($this->filesystem->exists('data/cache/app_config.php')) {
110
            $this->io->write('Deleting old cached config...');
111
            try {
112
                $this->filesystem->remove('data/cache/app_config.php');
113
                $this->io->writeln(' <info>Success</info>');
114
            } catch (IOException $e) {
115
                $this->io->error(
116
                    'Failed! You will have to manually delete the data/cache/app_config.php file to'
117
                    . ' get new config applied.'
118
                );
119
                if ($this->io->isVerbose()) {
120
                    $this->getApplication()->renderException($e, $output);
121
                }
122
                return;
123
            }
124
        }
125
126
        // If running update command, ask the user to import previous config
127
        $config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
128
129
        // Ask for custom config params
130
        foreach ([
131
            Plugin\DatabaseConfigCustomizer::class,
132
            Plugin\UrlShortenerConfigCustomizer::class,
133
            Plugin\LanguageConfigCustomizer::class,
134
            Plugin\ApplicationConfigCustomizer::class,
135
        ] as $pluginName) {
136
            /** @var Plugin\ConfigCustomizerInterface $configCustomizer */
137
            $configCustomizer = $this->configCustomizers->get($pluginName);
138
            $configCustomizer->process($this->io, $config);
139
        }
140
141
        // Generate config params files
142
        $this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
143
        $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
        if (!  $this->isUpdate) {
147
            $this->io->write('Initializing database...');
148
            if (! $this->runPhpCommand(
149
                'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
150
                'Error generating database.',
151
                $output
152
            )) {
153
                return;
154
            }
155
        }
156
157
        // Run database migrations
158
        $this->io->write('Updating database...');
159
        if (! $this->runPhpCommand(
160
            'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
161
            'Error updating database.',
162
            $output
163
        )) {
164
            return;
165
        }
166
167
        // Generate proxies
168
        $this->io->write('Generating proxies...');
169
        if (! $this->runPhpCommand(
170
            'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
171
            'Error generating proxies.',
172
            $output
173
        )) {
174
            return;
175
        }
176
177
        $this->io->success('Installation complete!');
178
    }
179
180
    /**
181
     * @return CustomizableAppConfig
182
     * @throws RuntimeException
183
     */
184
    private function importConfig(): CustomizableAppConfig
185
    {
186
        $config = new CustomizableAppConfig();
187
188
        // Ask the user if he/she wants to import an older configuration
189
        $importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
190
        if (! $importConfig) {
191
            return $config;
192
        }
193
194
        // Ask the user for the older shlink path
195
        $keepAsking = true;
196
        do {
197
            $config->setImportedInstallationPath($this->io->ask(
198
                'Previous shlink installation path from which to import config'
199
            ));
200
            $configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
201
            $configExists = $this->filesystem->exists($configFile);
202
203
            if (! $configExists) {
204
                $keepAsking = $this->io->confirm(
205
                    'Provided path does not seem to be a valid shlink root path. Do you want to try another path?'
206
                );
207
            }
208
        } while (! $configExists && $keepAsking);
209
210
        // If after some retries the user has chosen not to test another path, return
211
        if (! $configExists) {
212
            return $config;
213
        }
214
215
        // Read the config file
216
        $config->exchangeArray(include $configFile);
217
        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
    private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
229
    {
230
        if ($this->processHelper === null) {
231
            $this->processHelper = $this->getHelper('process');
232
        }
233
234
        if ($this->phpBinary === null) {
235
            $this->phpBinary = $this->phpFinder->find(false) ?: 'php';
236
        }
237
238
        $this->io->write(
239
            ' <options=bold>[Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"]</> ',
240
            false,
241
            OutputInterface::VERBOSITY_VERBOSE
242
        );
243
        $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...
244
        if ($process->isSuccessful()) {
245
            $this->io->writeln(' <info>Success!</info>');
246
            return true;
247
        }
248
249
        if (! $this->io->isVerbose()) {
250
            $this->io->error($errorMessage . '  Run this command with -vvv to see specific error info.');
251
        }
252
253
        return false;
254
    }
255
}
256