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
Bug
introduced
by
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
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
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 |