Skip to content
Open
191 changes: 191 additions & 0 deletions Command/AppSynchronizeCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php

declare(strict_types=1);

namespace Flagbit\Shopware\ShopwareMaintenance\Command;

use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Path;

#[AsCommand(
name: 'app:sync',
description: 'Install/uninstall apps as defined in file config/apps.php',
)]
class AppSynchronizeCommand extends Command
{
public const GROUP_CORE = 'core';
public const GROUP_THIRD_PARTY = 'third_party';
public const GROUP_AGENCY = 'agency';
public const GROUP_PROJECT = 'project';
private const SEQUENTIAL_GROUPS = [
self::GROUP_CORE,
self::GROUP_THIRD_PARTY,
self::GROUP_AGENCY,
self::GROUP_PROJECT,
];

private const CONFIG_FILE_PATH = 'config/apps.php';

private string $projectDir;
private LoggerInterface $logger;

public function __construct(
string $projectDir,
LoggerInterface $logger
) {
parent::__construct();
$this->projectDir = $projectDir;
$this->logger = $logger;
}

/**
* @throws ExceptionInterface
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$configPath = Path::join($this->projectDir, self::CONFIG_FILE_PATH);
if (!file_exists($configPath)) {
$output->writeln(sprintf('%s not found', $configPath));

return self::FAILURE;
}

$appGroups = require $configPath;
if (!is_array($appGroups)) {
throw new \RuntimeException('Invalid apps config: expected array');
}

$errorSum = 0;
foreach (self::SEQUENTIAL_GROUPS as $group) {
$errorSum += $this->installUninstallAppGroup($appGroups, $group, $output);
}

if ($errorSum > 0) {
return self::FAILURE;
}

return self::SUCCESS;
}

/**
* @param array<string, array<string, bool>> $appsGroups
* @param string $groupName
* @param OutputInterface $output
*
* @return int
*/
private function installUninstallAppGroup(array $appsGroups, string $groupName, OutputInterface $output): int
{
if (!array_key_exists($groupName, $appsGroups)) {
return 0;
}

$apps = $appsGroups[$groupName];
if (!is_array($apps)) {
throw new \RuntimeException(sprintf(
'Invalid apps config for group "%s": expected array',
$groupName
));
}

$enabledApps = [];
$disabledApps = [];
foreach ($apps as $app => $isEnabled) {
if (!is_bool($isEnabled)) {
throw new \RuntimeException(sprintf(
'Invalid value for app "%s" in group "%s": expected boolean',
$app,
$groupName
));
}

if ($isEnabled) {
$enabledApps[] = $app;
continue;
}

$disabledApps[] = $app;
}

$uninstallFailed = 0;
foreach ($disabledApps as $disabledApp) {
$uninstallFailed += $this->executeAppUninstall($disabledApp, $output);
}

$installFailed = 0;
foreach ($enabledApps as $enabledPlugin) {
$installFailed += $this->executeAppInstall($enabledPlugin, $output);
}

return $uninstallFailed + $installFailed;
}

private function executeAppUninstall(string $disabledApp, OutputInterface $output): int
{
try {
$this->logger->info(sprintf('Uninstalling app: %s', $disabledApp));
$this->runCommand([
'command' => 'app:uninstall',
'name' => $disabledApp,
], $output);
$this->logger->info(sprintf('Successfully uninstalled app: %s', $disabledApp));
} catch (Exception|ExceptionInterface $e) {
$this->logger->error('Error while uninstalling app: ' . $e->getMessage());
return self::FAILURE;
}

return self::SUCCESS;
}

private function executeAppInstall(string $enabledApp, OutputInterface $output): int
{
try {
$this->logger->info(sprintf('Installing app: %s', $enabledApp));
$this->runCommand([
'command' => 'app:install',
'name' => $enabledApp,
'--activate' => true,
'--force' => true,
], $output);
$this->logger->info(sprintf('Successfully installed app: %s', $enabledApp));
} catch (Exception|ExceptionInterface $e) {
$this->logger->error('Error while installing app: ' . $e->getMessage());

return self::FAILURE;
}

return self::SUCCESS;
}

/**
* @param array $parameters
* @param OutputInterface $output
*
* @return int
* @throws ExceptionInterface
*/
private function runCommand(array $parameters, OutputInterface $output): int
{
$application = $this->getApplication();
if ($application === null) {
throw new \RuntimeException('No application initialised');
}

$output->writeln('');

$command = $application->find($parameters['command']);
unset($parameters['command']);

$input = new ArrayInput($parameters);
$input->setInteractive(false);

return $command->run($input, $output);
}
}
15 changes: 7 additions & 8 deletions Command/PluginSynchronizeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Path;

#[AsCommand(
name: 'plugin:sync',
Expand All @@ -23,6 +24,8 @@ class PluginSynchronizeCommand extends Command
public const GROUP_AGENCY = 'agency';
public const GROUP_PROJECT = 'project';

private const CONFIG_FILE_PATH = 'config/plugins.php';

private string $projectDir;
private LoggerInterface $logger;

Expand All @@ -35,20 +38,16 @@ public function __construct(
$this->logger = $logger;
}

protected function configure(): void
{
parent::configure();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!file_exists($this->projectDir . '/config/plugins.php')) {
$output->writeln(sprintf('%s not found', $this->projectDir . '/config/plugins.php'));
$configPath = Path::join($this->projectDir, self::CONFIG_FILE_PATH);
if (!file_exists($configPath)) {
$output->writeln(sprintf('%s not found', $configPath));

return 1;
}

$pluginGroups = require $this->projectDir . '/config/plugins.php';
$pluginGroups = require $configPath;

$errorSum = $this->installUninstallPluginGroup($pluginGroups, self::GROUP_CORE, $output);
$errorSum += $this->installUninstallPluginGroup($pluginGroups, self::GROUP_THIRD_PARTY, $output);
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,29 @@ Plugins which are from flagbit but aren't for specific for this project and can
##### Project
The group `project` is for plugins which are specifically developed for this project.

### Install/Uninstall apps

The file `config/apps.php` defines which Shopware apps should be enabled or disabled.

> [!CAUTION]
> While installing the plugins the command will accept all permissions and hosts.


**Example**

```php
# config/apps.php

<?php declare(strict_types=1);

return [
'SwagAnalytics' => true, # enabled
'InstoImmersiveElements' => false, # disbled
'DmitsPaymentCostApp' => true, # enabled
];

```

### Define Config

The file `config/config.yaml` defines Shopware plugin configuration values to be set.
Expand Down Expand Up @@ -116,6 +139,7 @@ We do not use the **uuid** from the SalesChannel because this can be different f
bin/console config:sync # synchronizes configuration values as defined in config/config.yaml
bin/console plugin:refresh # ensure plugins classes are loaded before plugin:sync execution
bin/console plugin:sync # synchronizes plugin enable/disable status as defined in config/plugins.php
bin/console app:sync # synchronizes app enable/disable status as defined in config/apps.php
```

## Troubleshooting
Expand Down
6 changes: 6 additions & 0 deletions Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="Flagbit\Shopware\ShopwareMaintenance\Command\AppSynchronizeCommand">
<argument type="string">%kernel.project_dir%</argument>
<argument type="service" id="logger"/>
<tag name="console.command"/>
</service>

<service id="Flagbit\Shopware\ShopwareMaintenance\Command\PluginSynchronizeCommand">
<argument type="string">%kernel.project_dir%</argument>
<argument type="service" id="logger"/>
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
]
},
"require": {
"shopware/core": "^6.5|^6.6",
"shopware/core": "^6.5|^6.6|^6.7",
"symfony/console": "^6.3|^7.0",
"symfony/framework-bundle": "^6.3|^7.0"
},
Expand Down