diff --git a/composer.json b/composer.json index 5a18d4a..ed4b5d2 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ }, "require-dev": { "phpunit/phpunit": "^8.0|^9.3", - "symfony/var-dumper": "^6.0" + "symfony/var-dumper": "^6.0", + "phpstan/phpstan": "^1.9" } } diff --git a/composer.lock b/composer.lock index 2896615..c1f9a7d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aa4d3a4300a1d69e9645c8e1a111bf52", + "content-hash": "ae3f8b07892d777d1abfbab47b58e953", "packages": [ { "name": "aws/aws-crt-php", @@ -11678,6 +11678,65 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/phpstan", + "version": "1.9.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "d6fdf01c53978b6429f1393ba4afeca39cc68afa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d6fdf01c53978b6429f1393ba4afeca39cc68afa", + "reference": "d6fdf01c53978b6429f1393ba4afeca39cc68afa", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.9.2" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2022-11-10T09:56:11+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.17", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b1ee26d --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 8 + paths: + - ./src + excludePaths: + - ./src/Resources/* diff --git a/src/Command/Common/RunPhpStanCommand.php b/src/Command/Common/RunPhpStanCommand.php new file mode 100644 index 0000000..fe6c0f3 --- /dev/null +++ b/src/Command/Common/RunPhpStanCommand.php @@ -0,0 +1,57 @@ +setName(self::$defaultName) + ->setDescription('Run PHP stan for src and tests') + ->addOption('options', null, InputOption::VALUE_OPTIONAL, 'The options that are added to the phpstan command as string i.e. --options="-c phpstan.neo"', '-c phpstan.neon'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $command = sprintf('vendor/bin/phpstan analyse src Tests -c phpstan.neon %s', $input->getOption('options')); + + $cli = Process::fromShellCommandline($command, null, null, null, 240); + $cli->setTty(true); + + $cli->run(function ($type, $line) use ($output) { + $output->write($line); + }); + + return Command::SUCCESS; + } + +} diff --git a/src/Command/Common/RunPhpUnitCommand.php b/src/Command/Common/RunPhpUnitCommand.php new file mode 100644 index 0000000..ce199bc --- /dev/null +++ b/src/Command/Common/RunPhpUnitCommand.php @@ -0,0 +1,68 @@ +setName(self::$defaultName) + ->setDescription('Run PHP Unit inside the docker container') + ->addArgument('pluginName', InputArgument::OPTIONAL, 'The name of the plugin you want to test. (optional, default will be the current plugin)', null) + ->addOption('container', 'c', InputOption::VALUE_OPTIONAL , 'The docker container name', 'shop_plugin') + ->addOption('options', null, InputOption::VALUE_OPTIONAL , 'The options that are added to the phpunit command as string i.e. --options="--testdox"', '--colors=always --testdox'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + + $pluginName = $input->getArgument('pluginName'); + if (!$input->getArgument('pluginName')) { + $resolver = $this->getNamespaceResolver(); + $pluginName = $resolver->getPluginName(); + } + + $command = sprintf('docker exec %s vendor/bin/phpunit --configuration="%s" %s', $input->getOption('container'), 'custom/plugins/' . $pluginName, $input->getOption('options')); + + $cli = Process::fromShellCommandline($command, null, null, null, 240); + $cli->setTty(true); + + $cli->run(function ($type, $line) use ($output) { + $output->write($line); + }); + + + + return Command::SUCCESS; + } + +} diff --git a/src/Command/New/NewPluginCommand.php b/src/Command/New/NewPluginCommand.php new file mode 100644 index 0000000..8873359 --- /dev/null +++ b/src/Command/New/NewPluginCommand.php @@ -0,0 +1,45 @@ +setName('new:plugin') + ->addArgument('pluginName', InputArgument::OPTIONAL, 'The name of a plugin', null) + ->addOption('shopwareVersion', 's', InputArgument::OPTIONAL, sprintf('Shopware version i.e. %s. If not set the latest Shopware tag will be used', PluginGenerator::DEFAULT_VERSION), null) + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Override files. Be careful when using!') + ->addOption('workingDir', null, InputOption::VALUE_OPTIONAL, 'The path were you want to create the new plugin', null) + ->addOption('git', null, InputOption::VALUE_NONE, 'Initialize a git repo and first commit with a branch. Set a remote url afterwards.') + ->addOption('branch', null, InputOption::VALUE_OPTIONAL, 'Init branch name for git', 'main') + ->setDescription('Create a new standalone plugin with a ready to use boilerplate.'); + parent::configure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln(PHP_EOL." + _____ _ _ _____ ______ +| __ \| | | | __ \| ____| +| |__) | | | | |__) | |__ +| ___/| | | | _ /| __| +| | | |__| | | \ \| |____ +|_| \____/|_| \_\______| ".PHP_EOL.PHP_EOL); + + $output->writeln('Pure installer'); + + return (new PluginGenerator())->generate($input, $output); + + return self::SUCCESS; + } +} diff --git a/src/Generator/GeneratorInterface.php b/src/Generator/GeneratorInterface.php new file mode 100644 index 0000000..7e0fee7 --- /dev/null +++ b/src/Generator/GeneratorInterface.php @@ -0,0 +1,11 @@ +pluginName = $input->getArgument('pluginName'); + + $io = new SymfonyStyle($input, $output); + $io->progressStart(4); + if (!$this->pluginName) { + $this->pluginName = $io->ask('Name of the plugin'); + } + + $this->shopwareVersion = $input->getOption('shopwareVersion') ?? $this->getLatestShopwareVersion(); + $dockwareVersion = $this->getDockwareVersion($input->getOption('shopwareVersion')); + if (version_compare($this->shopwareVersion, $dockwareVersion, '==') === false) { + $io->warning(sprintf('Could not match the dockware version for shopware v%s. Latest dockware is %s', $this->shopwareVersion, $dockwareVersion)); + if ($io->askQuestion(new ConfirmationQuestion('Do you want to choose a dockware and shopware version manually'))) { + $dockwareVersion = $io->ask('Input a dockware and shopware version', $dockwareVersion); + $this->shopwareVersion = $dockwareVersion; + } + } + + if (version_compare($this->shopwareVersion, '6', '>=') === false) { + throw new \RuntimeException('The Plugin Generator only works for shopware 6'); + } + + $this->resolveNamespace(); + + $this->namespace = $io->ask('Base namespace', $this->namespace); /** @todo validate input */ + + $workingDir = $input->getOption('workingDir') ?? getcwd(); + $this->workingDir = rtrim($workingDir, DIRECTORY_SEPARATOR); + $pluginPath = $workingDir . DIRECTORY_SEPARATOR . $this->pluginName; + + $parser = new TwigParser(); + $parser->setTemplateData( + [ + 'pluginName' => $this->pluginName, + 'namespace' => $this->namespace, + 'composerName' => $this->composerName, + 'copyright' => '', + 'composerDescriptionEn' => $this->pluginName, + 'composerDescriptionDe' => $this->pluginName, + 'phpVersion' => version_compare('6.5', $this->shopwareVersion) > 0 ? '^7.4.3 || ^8.0' : '^8.0', + 'dockwarePhpVersion' => version_compare('6.5', $this->shopwareVersion) > 0 ? '7.4' : '8.0', + 'shopwareVersion' => '~' . pathinfo( $this->shopwareVersion, PATHINFO_FILENAME), + 'dockwareVersion' => $dockwareVersion, + 'containerName' => 'shop_plugin' + ] + ); + $io->progressAdvance(1); + + $generator = new DirectoryGenerator($pluginPath, $parser); + if ($input->getOption('force')) { + $generator->setForce(true); + } + + $directory = (new TreeBuilder())->buildTree(__DIR__ . '/../../Resources/skeleton/Plugin', $this->pluginName); + $io->progressAdvance(1); + + $generator->generate($directory); + + $commands = [ + $this->findComposer() . ' install --working-dir=' . $pluginPath, + $this->findComposer() . ' require --dev phpstan/phpstan phpunit/phpunit --working-dir=' . $pluginPath, + sprintf('echo "%s"' + , 'PURE installed composer dependencies'), + sprintf('ls -la %s', $pluginPath) + ]; + + $this->executeCommands($commands, $output); + $io->progressAdvance(1); + $output->writeln(''); + + $messages = [ + '', + sprintf(' ✓ %s %s: %s', $this->pluginName, 'Plugin created. Change directory', str_replace($_SERVER['HOME'], '~', $pluginPath)), + '✓ Installed composer dependencies' + ]; + + if ($input->getOption('git')) { + $this->initGit($output, $input->getOption('branch')); + $messages[] = '✓ init git. Dont forget to set remote url.'; + } + + $io->success($messages); + + return Command::SUCCESS; + + } + + public function resolveNamespace(): void { + + $snakeCase = (new UnicodeString($this->pluginName))->camel()->title()->snake(); + $strings = explode('_', $snakeCase); + if (count($strings) < 2) { + throw new \RuntimeException('Could not resolve a namespace for this plugin name. Provide a name with a prefix i.e. SwagPlugin'); + } + + $prefix = \ucfirst(\array_shift($strings)); + $class = (new UnicodeString(implode("_", $strings)))->camel()->title(); + + $this->namespace = $prefix . "\\" . $class; + + $this->composerName = \strtolower($prefix) . "/" . (new AsciiSlugger())->slug($class->snake()); + } + + protected function findComposer(): string + { + $composerPath = getcwd() . '/composer.phar'; + + if (file_exists($composerPath)) { + return '"'.PHP_BINARY.'" '.$composerPath; + } + + return 'composer'; + } + + private function initGit(OutputInterface $output, string $branch): void { + if (file_exists($this->workingDir . DIRECTORY_SEPARATOR . $this->pluginName . DIRECTORY_SEPARATOR . '.git')) { + $output->write('Git already exists. Skipping.'); + return; + } + + $commands = [ + 'cd ' . $this->workingDir . DIRECTORY_SEPARATOR . $this->pluginName, + 'git init', + 'git add .', + 'git commit -m "PURE shopware plugin"', + "git branch -M {$branch}", + ]; + + $this->executeCommands($commands, $output); + } + + protected function executeCommands($commands, OutputInterface $output): Process + { + $cli = Process::fromShellCommandline(implode(' && ', $commands)); + $cli->setTty(true); + + $cli->run(function ($type, $line) use ($output) { + $output->write($line); + }); + + return $cli; + } + + protected function getLatestShopwareVersion(): string { + + try { + $client = new \GuzzleHttp\Client(); + $get = $client->get('https://api.github.com/repos/shopware/platform/releases/latest', [ + 'content-type' => 'application/json' + ]); + $content = $get->getBody()->getContents(); + $json = json_decode($content, true); + return str_replace('v', '', $json['tag_name']); + + } catch (\Exception $exception) { + return self::DEFAULT_VERSION; + } + } + + /** + * @param string|null $inputShopwareVersion | if version is not set or not found the latest version tag is returned + * @return string + * @throws \GuzzleHttp\Exception\GuzzleException + */ + private function getDockwareVersion(?string $inputShopwareVersion = null): string + { + + $client = new \GuzzleHttp\Client(); + $url = 'https://hub.docker.com/v2/repositories/dockware/dev/tags/?page_size=1&page=1'; + + if (!is_null($inputShopwareVersion)) { + $url .= '&name=' . $inputShopwareVersion; + } + + $get = $client->get($url, [ + 'content-type' => 'application/json' + ]); + $content = $get->getBody()->getContents(); + $json = json_decode($content, true); + + if (empty($json['results'])) { + return $this->getDockwareVersion(); + } + + return $json['results'][0]['name']; + } +} diff --git a/src/Resolver/NamespaceResolverInterface.php b/src/Resolver/NamespaceResolverInterface.php index 0ea99cf..ba01cc7 100644 --- a/src/Resolver/NamespaceResolverInterface.php +++ b/src/Resolver/NamespaceResolverInterface.php @@ -5,11 +5,14 @@ interface NamespaceResolverInterface { + public function resolvePluginNamespace(string $composerJson); public function getFullNamespace(?string $additional = null): string; public function getWorkingDir(?string $additional = null): string; + public function getPluginName(): string; + public function isNamespace(string $path): bool; } diff --git a/src/Resolver/PluginNamespaceResolver.php b/src/Resolver/PluginNamespaceResolver.php index 7046df5..754e26e 100644 --- a/src/Resolver/PluginNamespaceResolver.php +++ b/src/Resolver/PluginNamespaceResolver.php @@ -9,6 +9,8 @@ class PluginNamespaceResolver implements NamespaceResolverInterface protected string $pluginSrcPath; + protected string $pluginName; + public function resolvePluginNamespace(string $composerJson) { $separator = '_NAMESPACE_SEPARATOR_'; @@ -28,6 +30,7 @@ public function resolvePluginNamespace(string $composerJson) $namespace = str_replace($separator, '\\', key($path)); $namespace = str_replace('\\\\', '\\', $namespace); $this->pluginBaseNamespace = rtrim($namespace, '\\'); + $this->pluginName = str_replace('\\', '', $this->pluginBaseNamespace); $this->pluginSrcPath = getcwd() . DIRECTORY_SEPARATOR . rtrim(current($path), '/'); } @@ -74,4 +77,9 @@ protected function getPluginSrcPath(): string { return $this->pluginSrcPath; } + + public function getPluginName(): string + { + return $this->pluginName; + } } diff --git a/src/Resources/skeleton/Plugin/.gitignore b/src/Resources/skeleton/Plugin/.gitignore new file mode 100644 index 0000000..8015ed0 --- /dev/null +++ b/src/Resources/skeleton/Plugin/.gitignore @@ -0,0 +1,12 @@ +vendor/ +node_modules +var/ +/src/Resources/public/administration/ +/src/Resources/app/storefront/dist/ +.DS_Store +.idea/ +.vscode/ +coverage/ +.phpunit.result.cache +phpstan.neon +docker-compose.override.yml diff --git a/src/Resources/skeleton/Plugin/README.md b/src/Resources/skeleton/Plugin/README.md new file mode 100644 index 0000000..15bcd24 --- /dev/null +++ b/src/Resources/skeleton/Plugin/README.md @@ -0,0 +1,23 @@ +# {{pluginName}} Shopware 6 Plugin + +## Getting Started +``` +composer install + +docker-compose up +``` + +## Run PHPUnit inside the dockware container +``` +pure test:phpunit +``` + +## Run PHPStan +``` +pure phpstan +``` +or + +``` +vendor/bin/phpstan analyse src Tests -c phpstan.neon +``` diff --git a/src/Resources/skeleton/Plugin/Tests/PHPUnit/{{pluginName}}TestCase.php b/src/Resources/skeleton/Plugin/Tests/PHPUnit/{{pluginName}}TestCase.php new file mode 100644 index 0000000..54a5533 --- /dev/null +++ b/src/Resources/skeleton/Plugin/Tests/PHPUnit/{{pluginName}}TestCase.php @@ -0,0 +1,21 @@ + $originalClassName + * @return MockObject + */ + protected function getSimpleMock(string $originalClassName): MockObject + { + return $this->getMockBuilder($originalClassName) + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/src/Resources/skeleton/Plugin/Tests/{{'Bootstrap.php'}} b/src/Resources/skeleton/Plugin/Tests/{{'Bootstrap.php'}} new file mode 100644 index 0000000..2a2e630 --- /dev/null +++ b/src/Resources/skeleton/Plugin/Tests/{{'Bootstrap.php'}} @@ -0,0 +1,17 @@ +setProjectDir($_SERVER['PROJECT_ROOT'] ?? $projectDir) + ->setLoadEnvFile(true) + ->setForceInstallPlugins(true) + ->addActivePlugins('{{pluginName}}') + ->addCallingPlugin() + ->bootstrap() + ->setClassLoader(require $projectDir . '/vendor/autoload.php') + ->getClassLoader(); diff --git a/src/Resources/skeleton/Plugin/Tests/{{'TestBootstrapper.php'}} b/src/Resources/skeleton/Plugin/Tests/{{'TestBootstrapper.php'}} new file mode 100644 index 0000000..2e1840f --- /dev/null +++ b/src/Resources/skeleton/Plugin/Tests/{{'TestBootstrapper.php'}} @@ -0,0 +1,348 @@ +getProjectDir(); + if (!\defined('TEST_PROJECT_DIR')) { + \define('TEST_PROJECT_DIR', $_SERVER['PROJECT_ROOT']); + } + + $classLoader = $this->getClassLoader(); + + if ($this->loadEnvFile) { + $this->loadEnvFile(); + } + + $_SERVER['DATABASE_URL'] = $_ENV['DATABASE_URL'] = $this->getDatabaseUrl(); + + $_SERVER['KERNEL_CLASS'] = $_SERVER['KERNEL_CLASS'] ?? $_ENV['KERNEL_CLASS'] ?? Kernel::class; + KernelLifecycleManager::prepare($classLoader); + + if ($this->isForceInstall() || !$this->dbExists()) { + $this->install(); + + if (!empty($this->activePlugins)) { + $this->installPlugins(); + } + } elseif ($this->forceInstallPlugins) { + $this->installPlugins(); + } + + return $this; + } + + public function getClassLoader(): ClassLoader + { + if ($this->classLoader !== null) { + return $this->classLoader; + } + + return $this->classLoader = require $this->getProjectDir() . '/vendor/autoload.php'; + } + + public function getProjectDir(): string + { + if ($this->projectDir !== null) { + return $this->projectDir; + } + + if (isset($_SERVER['PROJECT_ROOT']) && \file_exists($_SERVER['PROJECT_ROOT'])) { + return $this->projectDir = $_SERVER['PROJECT_ROOT']; + } + + if (isset($_ENV['PROJECT_ROOT']) && \file_exists($_ENV['PROJECT_ROOT'])) { + return $this->projectDir = $_ENV['PROJECT_ROOT']; + } + + // only test cwd if it's not platform embedded (custom/plugins) + if (!$this->platformEmbedded && \file_exists('vendor')) { + return $this->projectDir = (string) \getcwd(); + } + + $dir = $rootDir = __DIR__; + while (!\file_exists($dir . '/vendor')) { + if ($dir === \dirname($dir)) { + return $rootDir; + } + $dir = \dirname($dir); + } + + return $this->projectDir = $dir; + } + + public function getDatabaseUrl(): string + { + if ($this->databaseUrl !== null) { + return $this->databaseUrl; + } + + $dbUrlParts = \parse_url($_SERVER['DATABASE_URL'] ?? '') ?: []; + + $testToken = \getenv('TEST_TOKEN'); + $dbUrlParts['path'] = ($dbUrlParts['path'] ?? 'root') . '_' . ($testToken ?: 'test'); + + $auth = isset($dbUrlParts['user']) ? ($dbUrlParts['user'] . (isset($dbUrlParts['pass']) ? (':' . $dbUrlParts['pass']) : '') . '@') : ''; + + return $this->databaseUrl = \sprintf( + '%s://%s%s%s%s%s', + $dbUrlParts['scheme'] ?? 'mysql', + $auth, + $dbUrlParts['host'] ?? 'localhost', + isset($dbUrlParts['port']) ? (':' . $dbUrlParts['port']) : '', + $dbUrlParts['path'], + isset($dbUrlParts['query']) ? ('?' . $dbUrlParts['query']) : '' + ); + } + + public function setProjectDir(?string $projectDir): TestBootstrapper + { + $this->projectDir = $projectDir; + + return $this; + } + + public function setClassLoader(ClassLoader $classLoader): TestBootstrapper + { + $this->classLoader = $classLoader; + + return $this; + } + + public function setForceInstall(bool $forceInstall): TestBootstrapper + { + $this->forceInstall = $forceInstall; + + return $this; + } + + public function addActivePlugins(string ...$activePlugins): TestBootstrapper + { + $this->activePlugins = \array_unique(\array_merge($this->activePlugins, $activePlugins)); + + return $this; + } + + /** + * @param string|null $pathToComposerJson The composer.json to determine the plugin name. In most cases it's possible to find it automatically. + * + * Adds the calling plugin to the plugin list that is installed and activated + */ + public function addCallingPlugin(?string $pathToComposerJson = null): TestBootstrapper + { + if (!$pathToComposerJson) { + $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); + $callerFile = $trace[0]['file'] ?? ''; + + $dir = \dirname($callerFile); + $max = 10; + while ($max-- > 0 && !\file_exists($dir . '/composer.json')) { + $dir = \dirname($dir); + } + + if ($max <= 0) { + throw new \RuntimeException('Failed to find plugin composer.json. Starting point ' . $callerFile); + } + + $pathToComposerJson = $dir . '/composer.json'; + } + + if (!\file_exists($pathToComposerJson)) { + throw new \RuntimeException('Could not auto detect plugin name via composer.json. Path: ' . $pathToComposerJson); + } + + $composer = \json_decode((string) \file_get_contents($pathToComposerJson), true); + $baseClass = $composer['extra']['shopware-plugin-class'] ?? ''; + if ($baseClass === '') { + throw new \RuntimeException('composer.json does not contain `extra.shopware-plugin-class`. Path: ' . $pathToComposerJson); + } + + $parts = \explode('\\', $baseClass); + $pluginName = \end($parts); + + $this->addActivePlugins($pluginName); + + return $this; + } + + public function setPlatformEmbedded(bool $platformEmbedded): TestBootstrapper + { + $this->platformEmbedded = $platformEmbedded; + + return $this; + } + + public function setLoadEnvFile(bool $loadEnvFile): TestBootstrapper + { + $this->loadEnvFile = $loadEnvFile; + + return $this; + } + + public function setDatabaseUrl(?string $databaseUrl): TestBootstrapper + { + $this->databaseUrl = $databaseUrl; + + return $this; + } + + public function getOutput(): OutputInterface + { + if ($this->output !== null) { + return $this->output; + } + + return $this->output = new ConsoleOutput(); + } + + public function setOutput(?OutputInterface $output): TestBootstrapper + { + $this->output = $output; + + return $this; + } + + public function setForceInstallPlugins(bool $forceInstallPlugins): TestBootstrapper + { + $this->forceInstallPlugins = $forceInstallPlugins; + + return $this; + } + + public function isForceInstall(): bool + { + if ($this->forceInstall !== null) { + return $this->forceInstall; + } + + return $this->forceInstall = (bool) ($_SERVER['FORCE_INSTALL'] ?? false); + } + + private function getKernel(): KernelInterface + { + return KernelLifecycleManager::getKernel(); + } + + private function getContainer(): ContainerInterface + { + return $this->getKernel()->getContainer(); + } + + private function dbExists(): bool + { + try { + /** @var Connection $connection */ + $connection = $this->getContainer()->get(Connection::class); + $connection->executeQuery('SELECT 1 FROM `plugin`')->fetchAll(); + + return true; + } catch (\Throwable $exists) { + return false; + } + } + + private function loadEnvFile(): void + { + if (!\class_exists(Dotenv::class)) { + throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.'); + } + + $envFilePath = $this->getProjectDir() . '/.env'; + if (\file_exists($envFilePath) || \file_exists($envFilePath . '.dist')) { + (new Dotenv())->usePutenv()->loadEnv($envFilePath); + } + } + + private function install(): void + { + $application = new Application($this->getKernel()); + $installCommand = $application->find('system:install'); + + $returnCode = $installCommand->run( + new ArrayInput( + [ + '--create-database' => true, + '--force' => true, + '--drop-database' => true, + '--basic-setup' => true, + '--no-assign-theme' => true, + ], + $installCommand->getDefinition() + ), + $this->getOutput() + ); + if ($returnCode !== 0) { + throw new \RuntimeException('system:install failed'); + } + + // create new kernel after install + KernelLifecycleManager::bootKernel(false); + } + + private function installPlugins(): void + { + $application = new Application($this->getKernel()); + $refreshCommand = $application->find('plugin:refresh'); + $refreshCommand->run(new ArrayInput([], $refreshCommand->getDefinition()), $this->getOutput()); + + foreach ($this->activePlugins as $plugin) { + $kernel = KernelLifecycleManager::bootKernel(); + + $application = new Application($kernel); + + $installCommand = $application->find('plugin:install'); + $args = [ + '--activate' => true, + '--reinstall' => true, + 'plugins' => [$plugin], + ]; + + $returnCode = $installCommand->run( + new ArrayInput($args, $installCommand->getDefinition()), + $this->getOutput() + ); + + if ($returnCode !== 0) { + throw new \RuntimeException('system:install failed'); + } + } + + KernelLifecycleManager::bootKernel(); + } +} diff --git a/src/Resources/skeleton/Plugin/boot_end.sh b/src/Resources/skeleton/Plugin/boot_end.sh new file mode 100644 index 0000000..f80fd25 --- /dev/null +++ b/src/Resources/skeleton/Plugin/boot_end.sh @@ -0,0 +1,7 @@ +#!/usr/bin/php + +cd /var/www/html || exit; + +/usr/bin/php bin/console plugin:refresh; + +/usr/bin/php bin/console plugin:install -a -c {{pluginName}}; diff --git a/src/Resources/skeleton/Plugin/composer.json b/src/Resources/skeleton/Plugin/composer.json new file mode 100644 index 0000000..27ea0c2 --- /dev/null +++ b/src/Resources/skeleton/Plugin/composer.json @@ -0,0 +1,30 @@ +{ + "name": "{{composerName}}", + "description": "{{composerDescriptionEn}}", + "type": "shopware-platform-plugin", + "license": "proprietary", + "authors": [], + "require": { + "php": "{{phpVersion}}", + "shopware/core": "{{shopwareVersion}}", + "shopware/administration": "{{shopwareVersion}}", + "shopware/storefront": "{{shopwareVersion}}" + }, + "autoload": { + "psr-4": { + "{{namespace|replace({'\\': '\\\\'})}}\\": "src/" + } + }, + "extra": { + "shopware-plugin-class": "{{namespace|replace({'\\': '\\\\'})}}\\{{pluginName}}", + "copyright": "{{copyright}}", + "description": { + "de-DE": "{{pluginName}}", + "en-GB": "{{pluginName}}" + }, + "label": { + "de-DE": "{{composerDescriptionDe}}", + "en-GB": "{{composerDescriptionEn}}" + } + } +} diff --git a/src/Resources/skeleton/Plugin/docker-compose.yml b/src/Resources/skeleton/Plugin/docker-compose.yml new file mode 100644 index 0000000..f160991 --- /dev/null +++ b/src/Resources/skeleton/Plugin/docker-compose.yml @@ -0,0 +1,38 @@ +version: "3" + +services: + + shop: + image: dockware/dev:{{dockwareVersion}} + container_name: {{containerName}} + volumes: + - "db_volume:/var/lib/mysql" + - "./boot_end.sh:/var/www/boot_end.sh" + - "./:/var/www/html/custom/plugins/{{pluginName}}" + # exclude by using anonymous volumes + - "/var/www/html/custom/plugins/{{pluginName}}/.git/" + - "/var/www/html/custom/plugins/{{pluginName}}/vendor/" + - "/var/www/html/custom/plugins/{{pluginName}}/src/Resources/app/administration/node_modules/" + - "/var/www/html/custom/plugins/{{pluginName}}/src/Resources/app/storefront/node_modules/" + ports: + - "80:80" + - "3306:3306" + - "22:22" + - "8888:8888" + - "8080:8080" + - "9999:9999" + - "9998:9998" + + networks: + - web + environment: + - XDEBUG_ENABLED=1 + - PHP_VERSION={{dockwarePhpVersion}} + +volumes: + db_volume: + driver: local + +networks: + web: + external: false diff --git a/src/Resources/skeleton/Plugin/phpunit.xml b/src/Resources/skeleton/Plugin/phpunit.xml new file mode 100644 index 0000000..0e2b57a --- /dev/null +++ b/src/Resources/skeleton/Plugin/phpunit.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + ./Tests/PHPUnit + + + + + + ./src + + + + diff --git a/src/Resources/skeleton/Plugin/src/Resources/config/services.xml b/src/Resources/skeleton/Plugin/src/Resources/config/services.xml new file mode 100644 index 0000000..eddde08 --- /dev/null +++ b/src/Resources/skeleton/Plugin/src/Resources/config/services.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/src/Resources/skeleton/Plugin/src/{{pluginName|u.camel.title}}.php b/src/Resources/skeleton/Plugin/src/{{pluginName|u.camel.title}}.php new file mode 100644 index 0000000..c8017a7 --- /dev/null +++ b/src/Resources/skeleton/Plugin/src/{{pluginName|u.camel.title}}.php @@ -0,0 +1,11 @@ +getNamespaceResolver(); $input = $this->getInputInterface(); $maker->make($namespaceResolver, $input, ['timestamp' => 1667133679]); diff --git a/tests/Plugin/PluginGeneratorTest.php b/tests/Plugin/PluginGeneratorTest.php new file mode 100644 index 0000000..6dc76e4 --- /dev/null +++ b/tests/Plugin/PluginGeneratorTest.php @@ -0,0 +1,74 @@ +assertDirectoryDoesNotExist($fullPath, 'Plugin still exist'); + + + $application = new Application(); + $application->add(new NewPluginCommand()); + $command = $application->find('new:plugin'); + $command = new CommandTester($command); + $execute = $command->execute( + [ + 'pluginName' => $testPluginName, + '--workingDir' => $executeDirectory, + '--git' => true, + '--quiet' => true + ] + ); + + $this->assertDirectoryExists($fullPath, 'Plugin dir does not exists'); + $this->assertDirectoryExists($fullPath . DIRECTORY_SEPARATOR . '.git', 'Git dir does not exists'); + + } + + public function test_command_creates_new_plugin_with_shopware_version() + { + $testPluginName = 'TestFancyPluginNameWithVersion'; + $executeDirectory = __DIR__ . '/../../test_output'; + $fullPath = $executeDirectory . DIRECTORY_SEPARATOR . $testPluginName; + + if (file_exists($fullPath)) { + exec('rm -rf ' . $fullPath); + } + + $this->assertDirectoryDoesNotExist($fullPath, 'Plugin still exist'); + + + $application = new Application(); + $application->add(new NewPluginCommand()); + $command = $application->find('new:plugin'); + $command = new CommandTester($command); + $execute = $command->execute( + [ + 'pluginName' => $testPluginName, + '--workingDir' => $executeDirectory, + '--git' => true, + '--quiet' => true, + '--shopwareVersion' => '6.4.15.1' + ] + ); + + $this->assertDirectoryExists($fullPath, 'Plugin dir does not exists'); + $this->assertDirectoryExists($fullPath . DIRECTORY_SEPARATOR . '.git', 'Git dir does not exists'); + + } +}