diff --git a/.travis.yml b/.travis.yml index c1a70e8..3a68b3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,10 @@ matrix: env: - SYMFONY_VERSION="5.0.*" - CHECK_PHP_SYNTAX="yes" + - php: 8.2 + env: + - SYMFONY_VERSION="6.2.*" + - CHECK_PHP_SYNTAX="yes" allow_failures: - php: nightly diff --git a/composer.json b/composer.json index 68c4039..d3ffac6 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "easycorp/easy-deploy-bundle", + "name": "gangsidestep/easy-deploy-bundle", "type": "symfony-bundle", "description": "The easiest way to deploy Symfony applications", "keywords": ["deploy", "deployment", "deployer"], @@ -12,15 +12,15 @@ } ], "require": { - "php": ">=7.2.0", - "symfony/console": "~2.3|~3.0|~4.0|~5.0", - "symfony/dependency-injection": "~2.3|~3.0|~4.0|~5.0", - "symfony/expression-language": "~2.4|~3.0|~4.0|~5.0", - "symfony/filesystem": "~2.3|~3.0|~4.0|~5.0", - "symfony/http-foundation": "~2.3|~3.0|~4.0|~5.0", - "symfony/http-kernel": "~2.3|~3.0|~4.0|~5.0", + "php": ">=8.2", + "symfony/console": "~2.3|~3.0|~4.0|~5.0|~6.2|~7.2", + "symfony/dependency-injection": "~2.3|~3.0|~4.0|~5.0|~6.2|~7.2", + "symfony/expression-language": "~2.4|~3.0|~4.0|~5.0|~6.2|~7.2", + "symfony/filesystem": "~2.3|~3.0|~4.0|~5.0|~6.2|~7.2", + "symfony/http-foundation": "~2.3|~3.0|~4.0|~5.0|~6.2|~7.2", + "symfony/http-kernel": "~2.3|~3.0|~4.0|~5.0|~6.2|~7.2", "symfony/polyfill-mbstring": "^1.3", - "symfony/process": "~2.3|~3.0|~4.0|~5.0" + "symfony/process": "~2.3|~3.0|~4.0|~5.0|~6.2|~7.2" }, "require-dev": { "phpunit/phpunit": "^6.1" diff --git a/src/Command/DeployCommand.php b/src/Command/DeployCommand.php index a3c6a5c..6c24483 100644 --- a/src/Command/DeployCommand.php +++ b/src/Command/DeployCommand.php @@ -24,16 +24,12 @@ class DeployCommand extends Command { - private $fileLocator; private $projectDir; - private $logDir; private $configFilePath; - public function __construct(FileLocator $fileLocator, string $projectDir, string $logDir) + public function __construct(private readonly FileLocator $fileLocator, string $projectDir, private readonly string $logDir) { - $this->fileLocator = $fileLocator; $this->projectDir = realpath($projectDir); - $this->logDir = $logDir; parent::__construct(); } @@ -69,7 +65,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) $this->createDefaultConfigFile($input, $output, $defaultConfigPath, $input->getArgument('stage')); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $logFilePath = sprintf('%s/deploy_%s.log', $this->logDir, $input->getArgument('stage')); $context = new Context($input, $output, $this->projectDir, $logFilePath, true === $input->getOption('dry-run'), $output->isVerbose()); diff --git a/src/Command/RollbackCommand.php b/src/Command/RollbackCommand.php index 817905d..c536cc0 100644 --- a/src/Command/RollbackCommand.php +++ b/src/Command/RollbackCommand.php @@ -21,15 +21,10 @@ class RollbackCommand extends Command { - private $projectDir; - private $logDir; private $configFilePath; - public function __construct(string $projectDir, string $logDir) + public function __construct(private readonly string $projectDir, private readonly string $logDir) { - $this->projectDir = $projectDir; - $this->logDir = $logDir; - parent::__construct(); } @@ -64,7 +59,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) throw new \RuntimeException(sprintf("The default configuration file does not exist or it's not readable, and no custom configuration file was given either. Create the '%s' configuration file and run this command again.", $defaultConfigPath)); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $logFilePath = sprintf('%s/deploy_%s.log', $this->logDir, $input->getArgument('stage')); $context = new Context($input, $output, $this->projectDir, $logFilePath, true === $input->getOption('dry-run'), $output->isVerbose()); diff --git a/src/Configuration/ConfigurationAdapter.php b/src/Configuration/ConfigurationAdapter.php index c977f80..725ff63 100644 --- a/src/Configuration/ConfigurationAdapter.php +++ b/src/Configuration/ConfigurationAdapter.php @@ -19,15 +19,12 @@ * in a consistent manner, even if the configuration of each deployer is * completely different and defined using incompatible objects. */ -final class ConfigurationAdapter +final class ConfigurationAdapter implements \Stringable { - private $config; - /** @var ParameterBag */ - private $options; + private ?ParameterBag $options = null; - public function __construct(AbstractConfiguration $config) + public function __construct(private readonly AbstractConfiguration $config) { - $this->config = $config; } public function __toString(): string @@ -60,7 +57,7 @@ private function getOptions(): ParameterBag try { $property->setAccessible(true); $options->set($property->getName(), $property->getValue($this->config)); - } catch (\ReflectionException $e) { + } catch (\ReflectionException) { // ignore this error } } diff --git a/src/Configuration/DefaultConfiguration.php b/src/Configuration/DefaultConfiguration.php index 1abe5ac..a015f70 100644 --- a/src/Configuration/DefaultConfiguration.php +++ b/src/Configuration/DefaultConfiguration.php @@ -24,47 +24,45 @@ final class DefaultConfiguration extends AbstractConfiguration { // variables starting with an underscore are for internal use only - private $_symfonyEnvironmentEnvVarName; // SYMFONY_ENV or APP_ENV + private ?string $_symfonyEnvironmentEnvVarName = null; // SYMFONY_ENV or APP_ENV // properties are defined as private so the developer doesn't see them when using // their IDE autocompletion. To simplify things, the builder defines setter // methods named the same as each option. - private $symfonyEnvironment = 'prod'; - private $keepReleases = 5; - private $repositoryUrl; - private $repositoryBranch = 'master'; - private $remotePhpBinaryPath = 'php'; - private $updateRemoteComposerBinary = false; - private $remoteComposerBinaryPath = '/usr/local/bin/composer'; - private $composerInstallFlags = '--no-dev --prefer-dist --no-interaction --quiet'; - private $composerOptimizeFlags = '--optimize'; - private $installWebAssets = true; - private $dumpAsseticAssets = false; - private $warmupCache = true; - private $consoleBinaryPath; - private $localProjectDir; - private $binDir; - private $configDir; - private $cacheDir; - private $deployDir; - private $logDir; - private $srcDir; - private $templatesDir; - private $webDir; - private $controllersToRemove = []; - private $writableDirs = []; - private $permissionMethod = 'chmod'; - private $permissionMode = '0777'; - private $permissionUser; - private $permissionGroup; - private $sharedFiles = []; - private $sharedDirs = []; - private $resetOpCacheFor; - - public function __construct(string $localProjectDir) + private string $symfonyEnvironment = 'prod'; + private int $keepReleases = 5; + private ?string $repositoryUrl = null; + private string $repositoryBranch = 'master'; + private string $remotePhpBinaryPath = 'php'; + private bool $updateRemoteComposerBinary = false; + private string $remoteComposerBinaryPath = '/usr/local/bin/composer'; + private string $composerInstallFlags = '--no-dev --prefer-dist --no-interaction --quiet'; + private string $composerOptimizeFlags = '--optimize'; + private bool $installWebAssets = true; + private bool $dumpAsseticAssets = false; + private bool $warmupCache = true; + private ?string $consoleBinaryPath = null; + private ?string $binDir = null; + private ?string $configDir = null; + private ?string $cacheDir = null; + private ?string $deployDir = null; + private ?string $logDir = null; + private ?string $srcDir = null; + private ?string $templatesDir = null; + private ?string $webDir = null; + private array $controllersToRemove = []; + private array $writableDirs = []; + private string $permissionMethod = 'chmod'; + private string $permissionMode = '0777'; + private ?string $permissionUser = null; + private ?string $permissionGroup = null; + private array $sharedFiles = []; + private array $sharedDirs = []; + private ?string $resetOpCacheFor = null; + + public function __construct(private readonly string $localProjectDir) { parent::__construct(); - $this->localProjectDir = $localProjectDir; $this->setDefaultConfiguration(Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION); } @@ -264,9 +262,7 @@ public function webDir(string $path): self // the $paths can be glob() patterns, so this method needs to resolve them public function controllersToRemove(array $paths): self { - $absoluteGlobPaths = array_map(function ($globPath) { - return $this->localProjectDir.DIRECTORY_SEPARATOR.$globPath; - }, $paths); + $absoluteGlobPaths = array_map(fn($globPath) => $this->localProjectDir.DIRECTORY_SEPARATOR.$globPath, $paths); $localAbsolutePaths = []; foreach ($absoluteGlobPaths as $path) { @@ -337,7 +333,7 @@ public function sharedFilesAndDirs(array $paths = []): self foreach ($paths as $path) { $this->validatePathIsRelativeToProject($path, __METHOD__); if (is_dir($this->localProjectDir.DIRECTORY_SEPARATOR.$path)) { - $this->sharedDirs[] = rtrim($path, DIRECTORY_SEPARATOR); + $this->sharedDirs[] = rtrim((string) $path, DIRECTORY_SEPARATOR); } else { $this->sharedFiles[] = $path; } diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index ef003c8..7997d7c 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -21,37 +21,37 @@ */ final class Option { - const binDir = 'binDir'; - const cacheDir = 'cacheDir'; - const composerInstallFlags = 'composerInstallFlags'; - const composerOptimizeFlags = 'composerOptimizeFlags'; - const configDir = 'configDir'; - const consoleBinaryPath = 'consoleBinaryPath'; - const context = 'context'; - const controllersToRemove = 'controllersToRemove'; - const deployDir = 'deployDir'; - const dumpAsseticAssets = 'dumpAsseticAssets'; - const installWebAssets = 'installWebAssets'; - const keepReleases = 'keepReleases'; - const logDir = 'logDir'; - const permissionMethod = 'permissionMethod'; - const permissionMode = 'permissionMode'; - const permissionUser = 'permissionUser'; - const permissionGroup = 'permissionGroup'; - const remotePhpBinaryPath = 'remotePhpBinaryPath'; - const remoteComposerBinaryPath = 'remoteComposerBinaryPath'; - const repositoryBranch = 'repositoryBranch'; - const repositoryUrl = 'repositoryUrl'; - const resetOpCacheFor = 'resetOpCacheFor'; - const servers = 'servers'; - const sharedFiles = 'sharedFiles'; - const sharedDirs = 'sharedDirs'; - const srcDir = 'srcDir'; - const symfonyEnvironment = 'symfonyEnvironment'; - const templatesDir = 'templatesDir'; - const updateRemoteComposerBinary = 'updateRemoteComposerBinary'; - const useSshAgentForwarding = 'useSshAgentForwarding'; - const warmupCache = 'warmupCache'; - const webDir = 'webDir'; - const writableDirs = 'writableDirs'; + public const binDir = 'binDir'; + public const cacheDir = 'cacheDir'; + public const composerInstallFlags = 'composerInstallFlags'; + public const composerOptimizeFlags = 'composerOptimizeFlags'; + public const configDir = 'configDir'; + public const consoleBinaryPath = 'consoleBinaryPath'; + public const context = 'context'; + public const controllersToRemove = 'controllersToRemove'; + public const deployDir = 'deployDir'; + public const dumpAsseticAssets = 'dumpAsseticAssets'; + public const installWebAssets = 'installWebAssets'; + public const keepReleases = 'keepReleases'; + public const logDir = 'logDir'; + public const permissionMethod = 'permissionMethod'; + public const permissionMode = 'permissionMode'; + public const permissionUser = 'permissionUser'; + public const permissionGroup = 'permissionGroup'; + public const remotePhpBinaryPath = 'remotePhpBinaryPath'; + public const remoteComposerBinaryPath = 'remoteComposerBinaryPath'; + public const repositoryBranch = 'repositoryBranch'; + public const repositoryUrl = 'repositoryUrl'; + public const resetOpCacheFor = 'resetOpCacheFor'; + public const servers = 'servers'; + public const sharedFiles = 'sharedFiles'; + public const sharedDirs = 'sharedDirs'; + public const srcDir = 'srcDir'; + public const symfonyEnvironment = 'symfonyEnvironment'; + public const templatesDir = 'templatesDir'; + public const updateRemoteComposerBinary = 'updateRemoteComposerBinary'; + public const useSshAgentForwarding = 'useSshAgentForwarding'; + public const warmupCache = 'warmupCache'; + public const webDir = 'webDir'; + public const writableDirs = 'writableDirs'; } diff --git a/src/Context.php b/src/Context.php index 28cd5b4..a5e8b17 100644 --- a/src/Context.php +++ b/src/Context.php @@ -20,25 +20,12 @@ * It implements the "Context Object" pattern to encapsulate the global state of * the deployment in an immutable object. */ -class Context +class Context implements \Stringable { - private $localHost; - private $dryRun; - private $debug; - private $input; - private $output; - private $projectDir; - private $logFilePath; + private readonly Server $localHost; - public function __construct(InputInterface $input, OutputInterface $output, string $projectDir, string $logFilePath, bool $isDryRun, bool $isVerbose) + public function __construct(private readonly InputInterface $input, private readonly OutputInterface $output, private readonly string $projectDir, private readonly string $logFilePath, private readonly bool $dryRun, private readonly bool $debug) { - $this->input = $input; - $this->output = $output; - $this->projectDir = $projectDir; - $this->logFilePath = $logFilePath; - $this->dryRun = $isDryRun; - $this->debug = $isVerbose; - $this->localHost = $this->createLocalHost(); } diff --git a/src/Deployer/AbstractDeployer.php b/src/Deployer/AbstractDeployer.php index fbc7c64..e42e66a 100644 --- a/src/Deployer/AbstractDeployer.php +++ b/src/Deployer/AbstractDeployer.php @@ -26,14 +26,10 @@ abstract class AbstractDeployer { - /** @var Context */ - private $context; - /** @var TaskRunner */ - private $taskRunner; - /** @var Logger */ - private $logger; - /** @var ConfigurationAdapter */ - private $config; + private ?Context $context = null; + private ?TaskRunner $taskRunner = null; + private ?Logger $logger = null; + private ?ConfigurationAdapter $config = null; abstract public function getRequirements(): array; diff --git a/src/Deployer/DefaultDeployer.php b/src/Deployer/DefaultDeployer.php index fa28adc..a5e5954 100644 --- a/src/Deployer/DefaultDeployer.php +++ b/src/Deployer/DefaultDeployer.php @@ -22,9 +22,9 @@ abstract class DefaultDeployer extends AbstractDeployer { - private $remoteProjectDirHasBeenCreated = false; - private $remoteSymLinkHasBeenCreated = false; - + private bool $remoteProjectDirHasBeenCreated = false; + private bool $remoteSymLinkHasBeenCreated = false; + public function getConfigBuilder(): DefaultConfiguration { return new DefaultConfiguration($this->getContext()->getLocalProjectRootDir()); @@ -231,8 +231,7 @@ private function createRemoteDirectoryLayout(): void $this->log('

Creating the remote directory layout'); $this->runRemote('mkdir -p {{ deploy_dir }} && mkdir -p {{ deploy_dir }}/releases && mkdir -p {{ deploy_dir }}/shared'); - /** @var TaskCompleted[] $results */ - $results = $this->runRemote('export _release_path="{{ deploy_dir }}/releases/$(date +%Y%m%d%H%M%S)" && mkdir -p $_release_path && echo $_release_path'); + $results = $this->runRemote('export _release_path={{ deploy_dir }}/releases/'.$this->release_folder.' && mkdir -p $_release_path && echo $_release_path'); foreach ($results as $result) { $remoteProjectDir = $this->getContext()->isDryRun() ? '(the remote project_dir)' : $result->getTrimmedOutput(); $result->getServer()->set(Property::project_dir, $remoteProjectDir); @@ -290,7 +289,7 @@ private function doCreateSharedFiles(): void { $this->log('

Creating symlinks for shared files'); foreach ($this->getConfig(Option::sharedFiles) as $sharedFile) { - $sharedFileParentDir = dirname($sharedFile); + $sharedFileParentDir = dirname((string) $sharedFile); $this->runRemote(sprintf('mkdir -p {{ deploy_dir }}/shared/%s', $sharedFileParentDir)); $this->runRemote(sprintf('touch {{ deploy_dir }}/shared/%s', $sharedFile)); $this->runRemote(sprintf('ln -nfs {{ deploy_dir }}/shared/%s {{ project_dir }}/%s', $sharedFile, $sharedFile)); @@ -379,9 +378,7 @@ private function doClearControllers(): void { $this->log('

Clearing controllers'); foreach ($this->getServers()->findByRoles([Server::ROLE_APP]) as $server) { - $absolutePaths = array_map(function ($relativePath) use ($server) { - return $server->resolveProperties(sprintf('{{ project_dir }}/%s', $relativePath)); - }, $this->getConfig(Option::controllersToRemove)); + $absolutePaths = array_map(fn($relativePath) => $server->resolveProperties(sprintf('{{ project_dir }}/%s', $relativePath)), $this->getConfig(Option::controllersToRemove)); $this->safeDelete($server, $absolutePaths); } @@ -427,7 +424,7 @@ private function doKeepReleases(): void private function deleteOldReleases(Server $server, array $releaseDirs): void { foreach ($releaseDirs as $releaseDir) { - if (!preg_match('/\d{14}/', $releaseDir)) { + if (!preg_match('/\d{14}/', (string) $releaseDir)) { $this->log(sprintf('[%s] Skipping cleanup of old releases; unexpected "%s" directory found (all directory names should be timestamps)', $server, $releaseDir)); return; @@ -441,9 +438,7 @@ private function deleteOldReleases(Server $server, array $releaseDirs): void } $relativeDirsToRemove = array_slice($releaseDirs, 0, -$this->getConfig(Option::keepReleases)); - $absoluteDirsToRemove = array_map(function ($v) { - return sprintf('%s/releases/%s', $this->getConfig(Option::deployDir), $v); - }, $relativeDirsToRemove); + $absoluteDirsToRemove = array_map(fn($v) => sprintf('%s/releases/%s', $this->getConfig(Option::deployDir), $v), $relativeDirsToRemove); // the command must be run only on one server because the timestamps are // different for all servers, even when they belong to the same deploy and diff --git a/src/Helper/Str.php b/src/Helper/Str.php index 5d68cb7..9410e06 100644 --- a/src/Helper/Str.php +++ b/src/Helper/Str.php @@ -39,11 +39,9 @@ public static function lineSeparator(string $char = '-'): string public static function prefix($text, string $prefix): string { - $text = is_array($text) ? $text : explode(PHP_EOL, $text); + $text = is_array($text) ? $text : explode(PHP_EOL, (string) $text); - return implode(PHP_EOL, array_map(function ($line) use ($prefix) { - return $prefix.$line; - }, $text)); + return implode(PHP_EOL, array_map(fn($line) => $prefix.$line, $text)); } public static function stringify($value): string diff --git a/src/Logger.php b/src/Logger.php index a9bbd4e..819a249 100644 --- a/src/Logger.php +++ b/src/Logger.php @@ -14,13 +14,14 @@ use EasyCorp\Bundle\EasyDeployBundle\Helper\Str; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Filesystem\Filesystem; final class Logger { - private $isDebug; - private $output; - private $logFilePath; + private readonly bool $isDebug; + private readonly OutputInterface $output; + private readonly string $logFilePath; public function __construct(Context $context) { diff --git a/src/Requirement/AbstractRequirement.php b/src/Requirement/AbstractRequirement.php index 7f31ee7..5f8c73d 100644 --- a/src/Requirement/AbstractRequirement.php +++ b/src/Requirement/AbstractRequirement.php @@ -16,12 +16,11 @@ abstract class AbstractRequirement { - /** @var Server[] */ - private $servers; - - public function __construct(array $servers) + /** + * @param Server[] $servers + */ + public function __construct(private readonly array $servers) { - $this->servers = $servers; } public function getServers(): array diff --git a/src/Requirement/AllowsLoginViaSsh.php b/src/Requirement/AllowsLoginViaSsh.php index 814567c..b641d0b 100644 --- a/src/Requirement/AllowsLoginViaSsh.php +++ b/src/Requirement/AllowsLoginViaSsh.php @@ -12,6 +12,7 @@ namespace EasyCorp\Bundle\EasyDeployBundle\Requirement; use EasyCorp\Bundle\EasyDeployBundle\Task\Task; +use Exception; class AllowsLoginViaSsh extends AbstractRequirement { @@ -20,9 +21,12 @@ public function getMessage(): string return '[OK] The server allows to login via SSH from the local machine'; } + /** + * @throws Exception + */ public function getChecker(): Task { - $shellCommand = sprintf('echo %s', mt_rand()); + $shellCommand = sprintf('echo %s', random_int(0, mt_getrandmax())); return new Task($this->getServers(), $shellCommand); } diff --git a/src/Requirement/CommandExists.php b/src/Requirement/CommandExists.php index d985eb4..f6b58af 100644 --- a/src/Requirement/CommandExists.php +++ b/src/Requirement/CommandExists.php @@ -15,12 +15,9 @@ class CommandExists extends AbstractRequirement { - private $commandName; - - public function __construct(array $servers, string $commandName) + public function __construct(array $servers, private readonly string $commandName) { parent::__construct($servers); - $this->commandName = $commandName; } public function getMessage(): string @@ -30,7 +27,14 @@ public function getMessage(): string public function getChecker(): Task { - $shellCommand = sprintf('%s %s', $this->isWindows() ? 'where' : 'which', $this->commandName); + if(str_contains($this->commandName,"/bin/composer")) + { + $shellCommand = sprintf('%s %s', 'which', $this->commandName); + } + else + { + $shellCommand = sprintf('%s %s', $this->isWindows() ? 'where' : 'which', $this->commandName); + } return new Task($this->getServers(), $shellCommand); } diff --git a/src/Server/Property.php b/src/Server/Property.php index 8ac7580..2f46e44 100644 --- a/src/Server/Property.php +++ b/src/Server/Property.php @@ -21,15 +21,15 @@ */ final class Property { - const bin_dir = 'bin_dir'; - const config_dir = 'config_dir'; - const console_bin = 'console_bin'; - const cache_dir = 'cache_dir'; - const deploy_dir = 'deploy_dir'; - const log_dir = 'log_dir'; - const project_dir = 'project_dir'; - const src_dir = 'src_dir'; - const templates_dir = 'templates_dir'; - const use_ssh_agent_forwarding = 'use_ssh_agent_forwarding'; - const web_dir = 'web_dir'; + public const bin_dir = 'bin_dir'; + public const config_dir = 'config_dir'; + public const console_bin = 'console_bin'; + public const cache_dir = 'cache_dir'; + public const deploy_dir = 'deploy_dir'; + public const log_dir = 'log_dir'; + public const project_dir = 'project_dir'; + public const src_dir = 'src_dir'; + public const templates_dir = 'templates_dir'; + public const use_ssh_agent_forwarding = 'use_ssh_agent_forwarding'; + public const web_dir = 'web_dir'; } diff --git a/src/Server/Server.php b/src/Server/Server.php index 676a9a1..6074975 100644 --- a/src/Server/Server.php +++ b/src/Server/Server.php @@ -15,19 +15,17 @@ use EasyCorp\Bundle\EasyDeployBundle\Helper\Str; use Symfony\Component\HttpFoundation\ParameterBag; -class Server +class Server implements \Stringable { - const ROLE_APP = 'app'; + final public const ROLE_APP = 'app'; private const LOCALHOST_ADDRESSES = ['localhost', 'local', '127.0.0.1']; - private $roles; private $user; - private $host; + private readonly string $host; private $port; - private $properties; + private readonly ParameterBag $properties; - public function __construct(string $dsn, array $roles = [self::ROLE_APP], array $properties = []) + public function __construct(string $dsn, private readonly array $roles = [self::ROLE_APP], array $properties = []) { - $this->roles = $roles; $this->properties = new ParameterBag($properties); // add the 'ssh://' scheme so the URL parsing works as expected diff --git a/src/Server/ServerRepository.php b/src/Server/ServerRepository.php index d0c9b21..1b7085c 100644 --- a/src/Server/ServerRepository.php +++ b/src/Server/ServerRepository.php @@ -15,10 +15,10 @@ * It implements the "Repository" pattern to store the servers involved in the * deployment and provide some helper methods to find and filter those servers. */ -class ServerRepository +class ServerRepository implements \Stringable { /** @var Server[] $servers */ - private $servers = []; + private array $servers = []; public function __toString(): string { @@ -40,8 +40,6 @@ public function findAll(): array */ public function findByRoles(array $roles): array { - return array_filter($this->servers, function (Server $server) use ($roles) { - return !empty(array_intersect($roles, $server->getRoles())); - }); + return array_filter($this->servers, fn(Server $server) => !empty(array_intersect($roles, $server->getRoles()))); } } diff --git a/src/Task/Task.php b/src/Task/Task.php index 007da0a..8b07ab7 100644 --- a/src/Task/Task.php +++ b/src/Task/Task.php @@ -16,19 +16,15 @@ class Task { /** @var Server[] $servers */ - private $servers; - private $shellCommand; - private $envVars; + private readonly array $servers; - public function __construct(array $servers, string $shellCommand, array $envVars = []) + public function __construct(array $servers, private readonly string $shellCommand, private readonly array $envVars = []) { if (empty($servers)) { throw new \InvalidArgumentException('The "servers" argument of a Task cannot be an empty array. Add at least one server.'); } $this->servers = $servers; - $this->shellCommand = $shellCommand; - $this->envVars = $envVars; } /** diff --git a/src/Task/TaskCompleted.php b/src/Task/TaskCompleted.php index 6af3c44..77d521b 100644 --- a/src/Task/TaskCompleted.php +++ b/src/Task/TaskCompleted.php @@ -19,15 +19,8 @@ */ class TaskCompleted { - private $server; - private $output; - private $exitCode; - - public function __construct(Server $server, string $output, int $exitCode) + public function __construct(private readonly Server $server, private readonly string $output, private readonly int $exitCode) { - $this->server = $server; - $this->output = $output; - $this->exitCode = $exitCode; } public function isSuccessful(): bool diff --git a/src/Task/TaskRunner.php b/src/Task/TaskRunner.php index ce53693..e2c5961 100644 --- a/src/Task/TaskRunner.php +++ b/src/Task/TaskRunner.php @@ -19,13 +19,8 @@ class TaskRunner { - private $isDryRun; - private $logger; - - public function __construct(bool $isDryRun, Logger $logger) + public function __construct(private readonly bool $isDryRun, private readonly Logger $logger) { - $this->isDryRun = $isDryRun; - $this->logger = $logger; } /** @@ -47,9 +42,14 @@ private function createProcess(string $shellCommand): Process return Process::fromShellCommandline($shellCommand); } - return new Process($shellCommand); + return new Process((array)$shellCommand); } + private function isWindows(): bool + { + return DIRECTORY_SEPARATOR === '\\'; + } + private function doRun(Server $server, string $shellCommand, array $envVars): TaskCompleted { if ($server->has(Property::project_dir)) { @@ -62,7 +62,9 @@ private function doRun(Server $server, string $shellCommand, array $envVars): Ta $envVarsAsString = http_build_query($envVars, '', ' '); // the ';' after the env vars makes them available to all commands, not only the first one // parenthesis create a sub-shell so the env vars don't affect to the parent shell - $shellCommand = sprintf('(export %s; %s)', $envVarsAsString, $shellCommand); + $winOrnot= $this->isWindows() ? 'set' : 'export'; + // 'export' command is valid only for unix shells. In Windows - use 'set' instead + $shellCommand = sprintf('(%s %s; %s)',$winOrnot, $envVarsAsString, $shellCommand); } $this->logger->log(sprintf('[%s] Executing command: %s', $server, $shellCommand)); diff --git a/tests/Configuration/ConfigurationAdapterTest.php b/tests/Configuration/ConfigurationAdapterTest.php index e6bedbf..c94636c 100644 --- a/tests/Configuration/ConfigurationAdapterTest.php +++ b/tests/Configuration/ConfigurationAdapterTest.php @@ -18,10 +18,9 @@ class ConfigurationAdapterTest extends TestCase { - /** @var DefaultConfiguration */ - private $config; + private \EasyCorp\Bundle\EasyDeployBundle\Configuration\DefaultConfiguration $config; - protected function setUp() + protected function setUp(): void { $this->config = (new DefaultConfiguration(__DIR__)) ->sharedFilesAndDirs([]) diff --git a/tests/Helper/StrTest.php b/tests/Helper/StrTest.php index c19c70e..17c43f4 100644 --- a/tests/Helper/StrTest.php +++ b/tests/Helper/StrTest.php @@ -58,7 +58,7 @@ public function test_format_as_table() $this->assertSame($result, Str::formatAsTable($values)); } - public function startsWithProvider() + public function startsWithProvider(): \Generator { yield ['', '', false]; yield ['abc', '', false]; @@ -71,7 +71,7 @@ public function startsWithProvider() yield ['

a bc', 'a', false]; } - public function endsWithProvider() + public function endsWithProvider(): \Generator { yield ['', '', true]; yield ['abc', '', false]; @@ -84,7 +84,7 @@ public function endsWithProvider() yield ['ab

c', 'c', false]; } - public function containsProvider() + public function containsProvider(): \Generator { yield ['', '', false]; yield ['abc', '', false]; @@ -102,7 +102,7 @@ public function containsProvider() yield ['ab

c', 'ab c', false]; } - public function prefixProvider() + public function prefixProvider(): \Generator { yield ['', '', '']; yield ['aaa', 'xxx', 'xxxaaa']; @@ -110,7 +110,7 @@ public function prefixProvider() yield [['aaa', 'bbb', 'ccc'], 'xxx', "xxxaaa\nxxxbbb\nxxxccc"]; } - public function stringifyProvider() + public function stringifyProvider(): \Generator { yield ['', '']; yield [fopen('php://memory', 'r+'), 'PHP Resource']; @@ -121,7 +121,7 @@ public function stringifyProvider() yield [['a' => 'aaa', 'b' => '3.14', 'c' => ['a', 'b']], '{"a":"aaa","b":"3.14","c":["a","b"]}']; yield [new class() { public $a = 'aaa'; - private $b = 'bbb'; + private string $b = 'bbb'; }, '{"a":"aaa"}']; yield [new class() { public function __toString() diff --git a/tests/Server/ServerRepositoryTest.php b/tests/Server/ServerRepositoryTest.php index a32d1ed..87ea981 100644 --- a/tests/Server/ServerRepositoryTest.php +++ b/tests/Server/ServerRepositoryTest.php @@ -17,10 +17,9 @@ class ServerRepositoryTest extends TestCase { - /** @var ServerRepository */ - private $servers; + private ServerRepository $servers; - protected function setUp() + protected function setUp(): void { $repository = new ServerRepository(); $repository->add(new Server('host0')); @@ -36,7 +35,7 @@ protected function setUp() public function test_find_all() { $servers = $this->servers->findAll(); - $serverNames = array_values(array_map(function ($v) { return (string) $v; }, $servers)); + $serverNames = array_values(array_map(fn($v) => (string) $v, $servers)); $this->assertSame(['host0', 'host1', 'host2', 'host3', 'host4', 'host5'], $serverNames); } @@ -46,7 +45,7 @@ public function test_find_by_roles(array $roles, array $expectedResult) { $servers = $this->servers->findByRoles($roles); // array_values() is needed to reset the values of the keys - $serverNames = array_values(array_map(function ($v) { return (string) $v; }, $servers)); + $serverNames = array_values(array_map(fn($v) => (string) $v, $servers)); $this->assertSame($expectedResult, $serverNames); } diff --git a/tests/Server/ServerTest.php b/tests/Server/ServerTest.php index c3960f9..05f0561 100644 --- a/tests/Server/ServerTest.php +++ b/tests/Server/ServerTest.php @@ -11,6 +11,7 @@ namespace EasyCorp\Bundle\EasyDeployBundle\EasyDeployBundle\Tests; +use EasyCorp\Bundle\EasyDeployBundle\Exception\ServerConfigurationException; use EasyCorp\Bundle\EasyDeployBundle\Server\Property; use EasyCorp\Bundle\EasyDeployBundle\Server\Server; use PHPUnit\Framework\TestCase; @@ -27,12 +28,10 @@ public function test_dsn_parsing(string $dsn, string $expectedHost, ?string $exp $this->assertSame($expectedPort, $server->getPort()); } - /** - * @expectedException \EasyCorp\Bundle\EasyDeployBundle\Exception\ServerConfigurationException - * @expectedExceptionMessage The host is missing (define it as an IP address or a host name) - */ public function test_dsn_parsing_error() { + $this->expectExceptionMessage("The host is missing (define it as an IP address or a host name)"); + $this->expectException(ServerConfigurationException::class); new Server('deployer@'); } @@ -115,16 +114,16 @@ public function test_resolve_properties(array $properties, string $expression, s /** * @dataProvider wrongExpressionProvider - * @expectedException \InvalidArgumentException - * @expectedExceptionMessageRegExp /The ".*" property in ".*" expression is not a valid server property./ */ public function test_resolve_unknown_properties(array $properties, string $expression) { + $this->expectExceptionMessageMatches("/The \".*\" property in \".*\" expression is not a valid server property./"); + $this->expectException(\InvalidArgumentException::class); $server = new Server('host', [], $properties); $server->resolveProperties($expression); } - public function dsnProvider() + public function dsnProvider(): \Generator { yield ['123.123.123.123', '123.123.123.123', null, null]; yield ['deployer@123.123.123.123', '123.123.123.123', 'deployer', null]; @@ -143,7 +142,7 @@ public function dsnProvider() yield ['ssh://deployer@host:22001', 'host', 'deployer', 22001]; } - public function localDsnProvider() + public function localDsnProvider(): \Generator { yield ['local']; yield ['deployer@local']; @@ -158,7 +157,7 @@ public function localDsnProvider() yield ['deployer@127.0.0.1:22001']; } - public function serverRolesProvider() + public function serverRolesProvider(): \Generator { yield [[], []]; yield [[Server::ROLE_APP], [Server::ROLE_APP]]; @@ -166,7 +165,7 @@ public function serverRolesProvider() yield [['custom_role_1', 'custom_role_2'], ['custom_role_1', 'custom_role_2']]; } - public function sshConnectionStringProvider() + public function sshConnectionStringProvider(): \Generator { yield ['localhost', '']; yield ['123.123.123.123', 'ssh 123.123.123.123']; @@ -174,7 +173,7 @@ public function sshConnectionStringProvider() yield ['deployer@123.123.123.123:22001', 'ssh deployer@123.123.123.123 -p 22001']; } - public function expressionProvider() + public function expressionProvider(): \Generator { yield [['prop1' => 'aaa'], '{{ prop1 }}', 'aaa']; yield [['prop.1' => 'aaa'], '{{ prop.1 }}', 'aaa']; @@ -188,7 +187,7 @@ public function expressionProvider() yield [['prop1' => 'aaa', 'prop2' => 'bbb'], 'cd {{ prop1 }}{{ prop2 }}', 'cd aaabbb']; } - public function wrongExpressionProvider() + public function wrongExpressionProvider(): \Generator { yield [[], '{{ prop1 }}']; yield [['prop1' => 'aaa'], '{{ prop 1 }}'];