Issues (281)

Branch: master

InstallerBundle/Console/InstallModuleCommand.php (2 issues)

1
<?php
2
3
namespace ForkCMS\Bundle\InstallerBundle\Console;
4
5
use Backend\Core\Engine\Model as BackendModel;
6
use Backend\Modules\Extensions\Engine\Model;
7
use Backend\Modules\Extensions\Engine\Model as BackendExtensionsModel;
8
use Doctrine\DBAL\Connection;
9
use Doctrine\ORM\EntityManager;
10
use Exception;
11
use ForkCMS\App\BaseModel;
12
use PDO;
13
use RuntimeException;
14
use Symfony\Component\Console\Command\Command;
15
use Symfony\Component\Console\Input\ArrayInput;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Question\Question;
20
use Symfony\Component\Console\Style\SymfonyStyle;
21
use Symfony\Component\Finder\Finder;
22
use Symfony\Component\HttpKernel\KernelInterface;
23
24
/**
25
 * This command allows you to install a module via the CLI
26
 */
27
class InstallModuleCommand extends Command
28
{
29
    /** @var SymfonyStyle */
30
    private $formatter;
31
32
    /** @var Connection */
33
    private $dbConnection;
34
35
    /** @var KernelInterface */
36
    private $kernel;
37
38
    public function __construct(EntityManager $em, KernelInterface $kernel)
39
    {
40
        parent::__construct();
41
        $this->dbConnection = $em->getConnection();
42
        $this->kernel = $kernel;
43
    }
44
45
    protected function configure(): void
46
    {
47
        $this
48
            ->setName('forkcms:install:module')
49
            ->setDescription('Command to install a module in Fork CMS')
50
            ->addArgument('module', InputArgument::OPTIONAL, 'Name of the module to install');
51
    }
52
53
    protected function execute(InputInterface $input, OutputInterface $output): void
54
    {
55
        $this->formatter = new SymfonyStyle($input, $output);
56
        $module = $this->getModuleToInstall($input, $output);
57
58
        if (BackendExtensionsModel::existsModule($module)) {
59
            // Make sure this module can be installed
60
            $output->writeln("<comment>Validating if module can be installed...</comment>");
61
            $this->validateIfModuleCanBeInstalled($module);
62
63
            // Reboot the kernel to trigger a kernel initialize which registers the Dependency Injection Extension of the
64
            // module we would like to install. Also, make sure to replace the static cached container in our BaseModel!
65
            $_SERVER['INSTALLING_MODULE'] = $module;
66
            $this->kernel->shutdown();
67
            $this->kernel->boot();
68
            BaseModel::setContainer($this->kernel->getContainer());
69
70
            // Do the actual module install
71
            $output->writeln("<comment>Installing module $module...</comment>");
72
            BackendExtensionsModel::installModule($module);
73
74
            // Remove our container cache after this installation
75
            $output->writeln("<comment>Triggering cache clear...</comment>");
76
            $symfonyCacheClearCommand = $this->getApplication()->find('cache:clear');
77
            $symfonyCacheClearCommand->run(new ArrayInput(['--no-warmup' => true]), $output);
78
79
            $output->writeln("<info>Module $module is installed succesfully 🎉!");
80
        }
81
    }
82
83
    /**
84
     * Get the module name from the input arguments, or by creating an interactive selection menu.
85
     */
86
    private function getModuleToInstall(InputInterface $input, OutputInterface $output): string
87
    {
88
        $moduleName = $input->getArgument('module');
89
        $options = $this->getModulesToInstall();
90
        if ($moduleName !== null && array_key_exists($moduleName, $options)) {
91
            return $moduleName;
92
        }
93
94
        // Ask question
95
        $output->writeln('<question>Select the module to install:</question>');
96
97
        // Calculate max width to align the descriptions
98
        $width = $this->getColumnWidth($options);
99
100
        // Write the modules with name & description
101
        foreach ($options as $option) {
102
            $name = $option['name'];
103
            $description = $option['description'];
104
            $spacingWidth = $width - strlen($name);
105
106
            $output->write(
107
                sprintf('  <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $description),
108
                $options
0 ignored issues
show
$options of type array is incompatible with the type boolean expected by parameter $newline of Symfony\Component\Consol...utputInterface::write(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

108
                /** @scrutinizer ignore-type */ $options
Loading history...
109
            );
110
        }
111
112
        // Write the module selection question w/ autocomplete
113
        $helper = $this->getHelper('question');
114
        $question = new Question('<question>Your selection:</question> ');
115
        $question->setAutocompleterValues(array_keys($options));
116
        $question->setMaxAttempts(3);
117
        $question->setValidator(function ($answer) use ($options) {
118
            if (!array_key_exists($answer, $options)) {
119
                throw new RunTimeException("Incorrect option: {$answer}");
120
            }
121
            return $answer;
122
        });
123
124
        return $helper->ask($input, $output, $question);
125
    }
126
127
    private function getAlreadyInstalledModules(): array
128
    {
129
        return $this->dbConnection
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCom...lity\Result::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

129
        return /** @scrutinizer ignore-deprecated */ $this->dbConnection

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
130
            ->executeQuery('SELECT name FROM modules')
131
            ->fetchAll(PDO::FETCH_COLUMN);
132
    }
133
134
    private function getModulesToInstall(): array
135
    {
136
        $modules = [];
137
138
        $finder = new Finder();
139
        $directories = $finder->directories()->in(__DIR__ . '/../../../../Backend/Modules')->depth(0);
140
        $installedModules = $this->getAlreadyInstalledModules();
141
142
        foreach ($directories->getIterator() as $directory) {
143
            $name = $directory->getFilename();
144
145
            // Skip module if already installed
146
            if (in_array($name, $installedModules, true)) {
147
                continue;
148
            }
149
150
            // Build array with module information
151
            $moduleInformation = Model::getModuleInformation($name);
152
            $description = preg_replace(['/\s{2,}/', '/[\t\n]/'], '', strip_tags($moduleInformation['data']['description']) ?? "");
153
            $modules[$name] = [
154
                'name' => $name,
155
                'description' => strlen($description) > 100 ? substr($description, 0, 100)."..." : $description,
156
            ];
157
        }
158
159
        ksort($modules);
160
        return $modules;
161
    }
162
163
    /**
164
     * Calculate the optimal column width for our interactive selection menu.
165
     */
166
    private function getColumnWidth(array $modules): int
167
    {
168
        $width = 0;
169
        foreach ($modules as $module) {
170
            $width = strlen($module['name']) > $width ? strlen($module['name']) : $width;
171
        }
172
173
        return $width + 2;
174
    }
175
176
177
    private function validateIfModuleCanBeInstalled(string $module): void
178
    {
179
        // Check if module is already installed
180
        if (BackendModel::isModuleInstalled($module)) {
181
            throw new Exception("Module is already installed");
182
        }
183
184
        // Check if installer class is present
185
        if (!is_file(BACKEND_MODULES_PATH . '/' . $module . '/Installer/Installer.php')) {
186
            throw new Exception("Module does not have an installer class");
187
        }
188
    }
189
}
190