diff --git a/src/Build/BuildContainerImageStep.php b/src/Build/BuildContainerImageStep.php
index a790a93..4555378 100644
--- a/src/Build/BuildContainerImageStep.php
+++ b/src/Build/BuildContainerImageStep.php
@@ -15,9 +15,9 @@
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Exception\RuntimeException;
+use Ymir\Cli\Executable\DockerExecutable;
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
use Ymir\Cli\Support\Arr;
-use Ymir\Cli\Tool\Docker;
class BuildContainerImageStep implements BuildStepInterface
{
@@ -28,6 +28,13 @@ class BuildContainerImageStep implements BuildStepInterface
*/
private $buildDirectory;
+ /**
+ * The Docker executable.
+ *
+ * @var DockerExecutable
+ */
+ private $dockerExecutable;
+
/**
* The file system.
*
@@ -38,9 +45,10 @@ class BuildContainerImageStep implements BuildStepInterface
/**
* Constructor.
*/
- public function __construct(string $buildDirectory, Filesystem $filesystem)
+ public function __construct(string $buildDirectory, DockerExecutable $dockerExecutable, Filesystem $filesystem)
{
$this->buildDirectory = rtrim($buildDirectory, '/');
+ $this->dockerExecutable = $dockerExecutable;
$this->filesystem = $filesystem;
}
@@ -75,6 +83,6 @@ public function perform(string $environment, ProjectConfiguration $projectConfig
throw new RuntimeException('Unable to find a "Dockerfile" to build the container image');
}
- Docker::build($dockerfileName, sprintf('%s:%s', $projectConfiguration->getProjectName(), $environment), $this->buildDirectory);
+ $this->dockerExecutable->build($dockerfileName, sprintf('%s:%s', $projectConfiguration->getProjectName(), $environment), $this->buildDirectory);
}
}
diff --git a/src/Command/Cache/CacheTunnelCommand.php b/src/Command/Cache/CacheTunnelCommand.php
index 8eb0d30..09045b2 100644
--- a/src/Command/Cache/CacheTunnelCommand.php
+++ b/src/Command/Cache/CacheTunnelCommand.php
@@ -16,9 +16,12 @@
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
+use Ymir\Cli\ApiClient;
+use Ymir\Cli\CliConfiguration;
use Ymir\Cli\Command\Network\AddBastionHostCommand;
+use Ymir\Cli\Executable\SshExecutable;
+use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
use Ymir\Cli\Support\Arr;
-use Ymir\Cli\Tool\Ssh;
class CacheTunnelCommand extends AbstractCacheCommand
{
@@ -29,6 +32,23 @@ class CacheTunnelCommand extends AbstractCacheCommand
*/
public const NAME = 'cache:tunnel';
+ /**
+ * The SSH executable.
+ *
+ * @var SshExecutable
+ */
+ protected $sshExecutable;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, SshExecutable $sshExecutable)
+ {
+ parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
+
+ $this->sshExecutable = $sshExecutable;
+ }
+
/**
* {@inheritdoc}
*/
@@ -66,7 +86,7 @@ protected function perform()
$this->output->info(sprintf('Creating SSH tunnel to the "%s" cache cluster. You can connect using: localhost:%s', $cache['name'], $localPort));
- $tunnel = Ssh::tunnelBastionHost($network->get('bastion_host'), $localPort, $cache['endpoint'], 6379);
+ $tunnel = $this->sshExecutable->openTunnelToBastionHost($network->get('bastion_host'), $localPort, $cache['endpoint'], 6379);
$this->output->info('Once finished, press "Ctrl+C" to close the tunnel');
diff --git a/src/Command/Database/AbstractDatabaseCommand.php b/src/Command/Database/AbstractDatabaseCommand.php
index ea3e83c..bc65923 100644
--- a/src/Command/Database/AbstractDatabaseCommand.php
+++ b/src/Command/Database/AbstractDatabaseCommand.php
@@ -15,15 +15,35 @@
use Carbon\Carbon;
use Symfony\Component\Console\Exception\RuntimeException;
+use Ymir\Cli\ApiClient;
+use Ymir\Cli\CliConfiguration;
use Ymir\Cli\Command\AbstractCommand;
use Ymir\Cli\Command\Network\AddBastionHostCommand;
use Ymir\Cli\Exception\InvalidInputException;
+use Ymir\Cli\Executable\SshExecutable;
use Ymir\Cli\Process\Process;
+use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
use Ymir\Cli\Support\Arr;
-use Ymir\Cli\Tool\Ssh;
abstract class AbstractDatabaseCommand extends AbstractCommand
{
+ /**
+ * The SSH executable.
+ *
+ * @var SshExecutable
+ */
+ protected $sshExecutable;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, SshExecutable $sshExecutable)
+ {
+ parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
+
+ $this->sshExecutable = $sshExecutable;
+ }
+
/**
* Determine the database server that the command is interacting with.
*/
@@ -81,9 +101,9 @@ protected function determineUser(): string
}
/**
- * Start a SSH tunnel to a private database server.
+ * Open a SSH tunnel to a private database server.
*/
- protected function startSshTunnel(array $databaseServer): Process
+ protected function openSshTunnel(array $databaseServer): Process
{
$this->output->info(sprintf('Opening SSH tunnel to "%s" private database server', $databaseServer['name']));
@@ -94,7 +114,7 @@ protected function startSshTunnel(array $databaseServer): Process
}
$bastionHost = $network->get('bastion_host');
- $tunnel = Ssh::tunnelBastionHost($bastionHost, 3305, $databaseServer['endpoint'], 3306);
+ $tunnel = $this->sshExecutable->openTunnelToBastionHost($bastionHost, 3305, $databaseServer['endpoint'], 3306);
$timeout = Carbon::now()->addSeconds(10);
diff --git a/src/Command/Database/DatabaseServerTunnelCommand.php b/src/Command/Database/DatabaseServerTunnelCommand.php
index 14f853a..be444cd 100644
--- a/src/Command/Database/DatabaseServerTunnelCommand.php
+++ b/src/Command/Database/DatabaseServerTunnelCommand.php
@@ -16,9 +16,12 @@
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
+use Ymir\Cli\ApiClient;
+use Ymir\Cli\CliConfiguration;
use Ymir\Cli\Command\Network\AddBastionHostCommand;
+use Ymir\Cli\Executable\SshExecutable;
+use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
use Ymir\Cli\Support\Arr;
-use Ymir\Cli\Tool\Ssh;
class DatabaseServerTunnelCommand extends AbstractDatabaseServerCommand
{
@@ -29,6 +32,23 @@ class DatabaseServerTunnelCommand extends AbstractDatabaseServerCommand
*/
public const NAME = 'database:server:tunnel';
+ /**
+ * The SSH executable.
+ *
+ * @var SshExecutable
+ */
+ protected $sshExecutable;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, SshExecutable $sshExecutable)
+ {
+ parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
+
+ $this->sshExecutable = $sshExecutable;
+ }
+
/**
* {@inheritdoc}
*/
@@ -68,7 +88,7 @@ protected function perform()
$this->output->info(sprintf('Opening SSH tunnel to the "%s" database server. You can connect using: localhost:%s', $databaseServer['name'], $localPort));
- $tunnel = Ssh::tunnelBastionHost($network->get('bastion_host'), $localPort, $databaseServer['endpoint'], 3306);
+ $tunnel = $this->sshExecutable->openTunnelToBastionHost($network->get('bastion_host'), $localPort, $databaseServer['endpoint'], 3306);
$this->output->info('Once finished, press "Ctrl+C" to close the tunnel');
diff --git a/src/Command/Database/ExportDatabaseCommand.php b/src/Command/Database/ExportDatabaseCommand.php
index 71386ff..780b5be 100644
--- a/src/Command/Database/ExportDatabaseCommand.php
+++ b/src/Command/Database/ExportDatabaseCommand.php
@@ -23,6 +23,7 @@
use Ymir\Cli\Database\Connection;
use Ymir\Cli\Database\Mysqldump;
use Ymir\Cli\Exception\InvalidInputException;
+use Ymir\Cli\Executable\SshExecutable;
use Ymir\Cli\Process\Process;
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
@@ -45,9 +46,9 @@ class ExportDatabaseCommand extends AbstractDatabaseCommand
/**
* Constructor.
*/
- public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration)
+ public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration, SshExecutable $sshExecutable)
{
- parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
+ parent::__construct($apiClient, $cliConfiguration, $projectConfiguration, $sshExecutable);
$this->filesystem = $filesystem;
}
@@ -88,7 +89,7 @@ protected function perform()
$tunnel = null;
if (!$connection->needsSshTunnel()) {
- $tunnel = $this->startSshTunnel($connection->getDatabaseServer());
+ $tunnel = $this->openSshTunnel($connection->getDatabaseServer());
}
$this->output->infoWithDelayWarning(sprintf('Exporting "%s" database', $connection->getDatabase()));
diff --git a/src/Command/Database/ImportDatabaseCommand.php b/src/Command/Database/ImportDatabaseCommand.php
index f896851..5840416 100644
--- a/src/Command/Database/ImportDatabaseCommand.php
+++ b/src/Command/Database/ImportDatabaseCommand.php
@@ -23,6 +23,7 @@
use Ymir\Cli\Database\Connection;
use Ymir\Cli\Database\PDO;
use Ymir\Cli\Exception\InvalidInputException;
+use Ymir\Cli\Executable\SshExecutable;
use Ymir\Cli\Process\Process;
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
@@ -45,9 +46,9 @@ class ImportDatabaseCommand extends AbstractDatabaseCommand
/**
* Constructor.
*/
- public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration)
+ public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration, SshExecutable $sshExecutable)
{
- parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
+ parent::__construct($apiClient, $cliConfiguration, $projectConfiguration, $sshExecutable);
$this->filesystem = $filesystem;
}
@@ -77,7 +78,7 @@ protected function perform()
$tunnel = null;
if ($connection->needsSshTunnel()) {
- $tunnel = $this->startSshTunnel($connection->getDatabaseServer());
+ $tunnel = $this->openSshTunnel($connection->getDatabaseServer());
}
$this->output->infoWithDelayWarning(sprintf('Importing "%s" to the "%s" database', $filename, $connection->getDatabase()));
diff --git a/src/Command/Docker/DeleteDockerImagesCommand.php b/src/Command/Docker/DeleteDockerImagesCommand.php
index cb2cc50..e8ac1f4 100644
--- a/src/Command/Docker/DeleteDockerImagesCommand.php
+++ b/src/Command/Docker/DeleteDockerImagesCommand.php
@@ -15,8 +15,11 @@
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputOption;
+use Ymir\Cli\ApiClient;
+use Ymir\Cli\CliConfiguration;
use Ymir\Cli\Command\AbstractCommand;
-use Ymir\Cli\Tool\Docker;
+use Ymir\Cli\Executable\DockerExecutable;
+use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
class DeleteDockerImagesCommand extends AbstractCommand
{
@@ -34,6 +37,23 @@ class DeleteDockerImagesCommand extends AbstractCommand
*/
private const ALL_PATTERN = 'dkr.ecr';
+ /**
+ * The Docker executable.
+ *
+ * @var DockerExecutable
+ */
+ private $dockerExecutable;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(ApiClient $apiClient, DockerExecutable $dockerExecutable, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration)
+ {
+ parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
+
+ $this->dockerExecutable = $dockerExecutable;
+ }
+
/**
* {@inheritdoc}
*/
@@ -60,7 +80,7 @@ protected function perform()
return;
}
- Docker::rmigrep($pattern);
+ $this->dockerExecutable->removeImagesMatchingPattern($pattern);
$this->output->info('Deployment docker images deleted successfully');
}
diff --git a/src/Command/Project/ConfigureProjectCommand.php b/src/Command/Project/ConfigureProjectCommand.php
index 54b2d80..b1c192a 100644
--- a/src/Command/Project/ConfigureProjectCommand.php
+++ b/src/Command/Project/ConfigureProjectCommand.php
@@ -20,10 +20,10 @@
use Ymir\Cli\CliConfiguration;
use Ymir\Cli\Command\AbstractProjectCommand;
use Ymir\Cli\Exception\InvalidInputException;
+use Ymir\Cli\Executable\WpCliExecutable;
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
use Ymir\Cli\ProjectConfiguration\WordPress\WordPressConfigurationChangeInterface;
use Ymir\Cli\Support\Arr;
-use Ymir\Cli\Tool\WpCli;
class ConfigureProjectCommand extends AbstractProjectCommand
{
@@ -48,14 +48,22 @@ class ConfigureProjectCommand extends AbstractProjectCommand
*/
private $configurationChanges;
+ /**
+ * The WP-CLI executable.
+ *
+ * @var WpCliExecutable
+ */
+ private $wpCliExecutable;
+
/**
* Constructor.
*/
- public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, iterable $configurationChanges = [])
+ public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, ProjectConfiguration $projectConfiguration, WpCliExecutable $wpCliExecutable, iterable $configurationChanges = [])
{
parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
$this->configurationChanges = new Collection();
+ $this->wpCliExecutable = $wpCliExecutable;
foreach ($configurationChanges as $configurationChange) {
$this->addConfigurationChange($configurationChange);
@@ -79,9 +87,9 @@ protected function configure()
*/
protected function perform()
{
- if (!WpCli::isInstalledGlobally()) {
+ if ($this->wpCliExecutable->isInstalled()) {
throw new RuntimeException('WP-CLI needs to be available globally to scan your project');
- } elseif (!WpCli::isWordPressInstalled()) {
+ } elseif (!$this->wpCliExecutable->isWordPressInstalled()) {
throw new RuntimeException('WordPress needs to be installed and connected to a database to scan your project');
}
@@ -93,7 +101,7 @@ protected function perform()
$this->output->info('Scanning your project');
- $plugins = WpCli::listPlugins()->groupBy('status');
+ $plugins = $this->wpCliExecutable->listPlugins()->groupBy('status');
$activePlugins = $plugins->only(['active', 'must-use'])->flatten(1);
$inactivePlugins = $plugins->only(['inactive'])->flatten(1);
diff --git a/src/Command/Project/InitializeProjectCommand.php b/src/Command/Project/InitializeProjectCommand.php
index df078a9..312313e 100644
--- a/src/Command/Project/InitializeProjectCommand.php
+++ b/src/Command/Project/InitializeProjectCommand.php
@@ -24,11 +24,11 @@
use Ymir\Cli\Command\Docker\CreateDockerfileCommand;
use Ymir\Cli\Command\InstallPluginCommand;
use Ymir\Cli\Command\Provider\ConnectProviderCommand;
+use Ymir\Cli\Executable\DockerExecutable;
+use Ymir\Cli\Executable\WpCliExecutable;
use Ymir\Cli\Process\Process;
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
use Ymir\Cli\Support\Arr;
-use Ymir\Cli\Tool\Docker;
-use Ymir\Cli\Tool\WpCli;
class InitializeProjectCommand extends AbstractCommand
{
@@ -46,6 +46,13 @@ class InitializeProjectCommand extends AbstractCommand
*/
public const NAME = 'project:init';
+ /**
+ * Docker executable.
+ *
+ * @var DockerExecutable
+ */
+ private $dockerExecutable;
+
/**
* The file system.
*
@@ -60,15 +67,24 @@ class InitializeProjectCommand extends AbstractCommand
*/
private $projectDirectory;
+ /**
+ * The WP-CLI executable.
+ *
+ * @var WpCliExecutable
+ */
+ private $wpCliExecutable;
+
/**
* Constructor.
*/
- public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, Filesystem $filesystem, ProjectConfiguration $projectConfiguration, string $projectDirectory)
+ public function __construct(ApiClient $apiClient, CliConfiguration $cliConfiguration, DockerExecutable $dockerExecutable, Filesystem $filesystem, ProjectConfiguration $projectConfiguration, string $projectDirectory, WpCliExecutable $wpCliExecutable)
{
parent::__construct($apiClient, $cliConfiguration, $projectConfiguration);
+ $this->dockerExecutable = $dockerExecutable;
$this->filesystem = $filesystem;
$this->projectDirectory = rtrim($projectDirectory, '/');
+ $this->wpCliExecutable = $wpCliExecutable;
}
/**
@@ -142,11 +158,11 @@ protected function perform()
$this->invoke(InstallPluginCommand::NAME);
}
- if ($this->output->confirm('Do you want to deploy this project using a container image?', Docker::isInstalledGlobally())) {
+ if ($this->output->confirm('Do you want to deploy this project using a container image?', $this->dockerExecutable->isInstalled())) {
$this->invoke(CreateDockerfileCommand::NAME, ['--configure-project' => null]);
}
- if (WpCli::isInstalledGlobally() && WpCli::isWordPressInstalled() && $this->output->confirm('Do you want to have Ymir scan your plugins and themes and configure your project?')) {
+ if ($this->wpCliExecutable->isInstalled() && $this->wpCliExecutable->isWordPressInstalled() && $this->output->confirm('Do you want to have Ymir scan your plugins and themes and configure your project?')) {
$this->invoke(ConfigureProjectCommand::NAME);
}
}, 'Do you want to try creating a project again?');
@@ -194,7 +210,7 @@ private function checkForWordPress(string $projectType)
Process::runShellCommandline('composer create-project roots/bedrock .');
} elseif ('wordpress' === $projectType) {
$this->output->info('Downloading WordPress using WP-CLI');
- WpCli::downloadWordPress();
+ $this->wpCliExecutable->downloadWordPress();
}
$this->output->info('WordPress downloaded successfully');
@@ -276,7 +292,7 @@ private function getBaseEnvironmentsConfiguration(string $projectType): Collecti
*/
private function isPluginInstalled(string $projectType): bool
{
- return ('wordpress' === $projectType && WpCli::isYmirPluginInstalled())
+ return ('wordpress' === $projectType && $this->wpCliExecutable->isYmirPluginInstalled())
|| ('bedrock' === $projectType && str_contains((string) file_get_contents('./composer.json'), 'ymirapp/wordpress-plugin'));
}
@@ -287,7 +303,7 @@ private function isWordPressDownloadable(string $projectType): bool
{
try {
return in_array($projectType, ['bedrock', 'wordpress'])
- && !WpCli::isWordPressInstalled()
+ && !$this->wpCliExecutable->isWordPressInstalled()
&& ('bedrock' !== $projectType || !(new \FilesystemIterator($this->projectDirectory))->valid());
} catch (\Throwable $exception) {
return false;
diff --git a/src/Deployment/UploadFunctionCodeStep.php b/src/Deployment/UploadFunctionCodeStep.php
index 3929105..d946401 100644
--- a/src/Deployment/UploadFunctionCodeStep.php
+++ b/src/Deployment/UploadFunctionCodeStep.php
@@ -18,10 +18,10 @@
use Ymir\Cli\ApiClient;
use Ymir\Cli\Console\Input;
use Ymir\Cli\Console\Output;
+use Ymir\Cli\Executable\DockerExecutable;
use Ymir\Cli\FileUploader;
use Ymir\Cli\ProjectConfiguration\ProjectConfiguration;
use Ymir\Cli\Support\Arr;
-use Ymir\Cli\Tool\Docker;
class UploadFunctionCodeStep implements DeploymentStepInterface
{
@@ -46,6 +46,13 @@ class UploadFunctionCodeStep implements DeploymentStepInterface
*/
private $buildDirectory;
+ /**
+ * The Docker executable.
+ *
+ * @var DockerExecutable
+ */
+ private $dockerExecutable;
+
/**
* The Ymir project configuration.
*
@@ -63,11 +70,12 @@ class UploadFunctionCodeStep implements DeploymentStepInterface
/**
* Constructor.
*/
- public function __construct(ApiClient $apiClient, string $buildArtifactPath, string $buildDirectory, ProjectConfiguration $projectConfiguration, FileUploader $uploader)
+ public function __construct(ApiClient $apiClient, string $buildArtifactPath, string $buildDirectory, DockerExecutable $dockerExecutable, ProjectConfiguration $projectConfiguration, FileUploader $uploader)
{
$this->apiClient = $apiClient;
$this->buildArtifactPath = $buildArtifactPath;
$this->buildDirectory = rtrim($buildDirectory, '/');
+ $this->dockerExecutable = $dockerExecutable;
$this->projectConfiguration = $projectConfiguration;
$this->uploader = $uploader;
}
@@ -99,9 +107,9 @@ private function pushImage(Collection $deployment, string $environment, Output $
$output->infoWithDelayWarning('Pushing container image');
- Docker::login($user, $password, Arr::get((array) explode('/', $imageUri), 0), $this->buildDirectory);
- Docker::tag(sprintf('%s:%s', $this->projectConfiguration->getProjectName(), $environment), $imageUri, $this->buildDirectory);
- Docker::push($imageUri, $this->buildDirectory);
+ $this->dockerExecutable->login($user, $password, Arr::get((array) explode('/', $imageUri), 0), $this->buildDirectory);
+ $this->dockerExecutable->tag(sprintf('%s:%s', $this->projectConfiguration->getProjectName(), $environment), $imageUri, $this->buildDirectory);
+ $this->dockerExecutable->push($imageUri, $this->buildDirectory);
}
/**
diff --git a/src/Exception/CommandLineToolNotDetectedException.php b/src/Exception/Executable/ExecutableNotDetectedException.php
similarity index 66%
rename from src/Exception/CommandLineToolNotDetectedException.php
rename to src/Exception/Executable/ExecutableNotDetectedException.php
index eb7bfa4..31f0b68 100644
--- a/src/Exception/CommandLineToolNotDetectedException.php
+++ b/src/Exception/Executable/ExecutableNotDetectedException.php
@@ -11,17 +11,18 @@
* file that was distributed with this source code.
*/
-namespace Ymir\Cli\Exception;
+namespace Ymir\Cli\Exception\Executable;
use Symfony\Component\Console\Exception\RuntimeException;
+use Ymir\Cli\Executable\ExecutableInterface;
-class CommandLineToolNotDetectedException extends RuntimeException
+class ExecutableNotDetectedException extends RuntimeException
{
/**
* Constructor.
*/
- public function __construct($name)
+ public function __construct(ExecutableInterface $executable)
{
- parent::__construct(sprintf('Cannot detect %1$s on this computer. Please ensure %1$s is installed and properly configured.', $name));
+ parent::__construct(sprintf('Cannot detect %1$s on this computer. Please ensure %1$s is installed and properly configured.', $executable->getDisplayName()));
}
}
diff --git a/src/Exception/Executable/SshPortInUseException.php b/src/Exception/Executable/SshPortInUseException.php
new file mode 100644
index 0000000..6f9e80f
--- /dev/null
+++ b/src/Exception/Executable/SshPortInUseException.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ymir\Cli\Exception\Executable;
+
+use Symfony\Component\Console\Exception\RuntimeException;
+
+class SshPortInUseException extends RuntimeException
+{
+ /**
+ * Constructor.
+ */
+ public function __construct(int $port)
+ {
+ parent::__construct(sprintf('Unable to open SSH tunnel. Local port "%s" is already in use.', $port));
+ }
+}
diff --git a/src/Exception/WpCliException.php b/src/Exception/Executable/WpCliException.php
similarity index 90%
rename from src/Exception/WpCliException.php
rename to src/Exception/Executable/WpCliException.php
index 0ecb29b..84a5b4e 100644
--- a/src/Exception/WpCliException.php
+++ b/src/Exception/Executable/WpCliException.php
@@ -11,7 +11,7 @@
* file that was distributed with this source code.
*/
-namespace Ymir\Cli\Exception;
+namespace Ymir\Cli\Exception\Executable;
use Symfony\Component\Console\Exception\RuntimeException;
diff --git a/src/Executable/AbstractExecutable.php b/src/Executable/AbstractExecutable.php
new file mode 100644
index 0000000..5b6bcf3
--- /dev/null
+++ b/src/Executable/AbstractExecutable.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ymir\Cli\Executable;
+
+use Ymir\Cli\Exception\Executable\ExecutableNotDetectedException;
+use Ymir\Cli\Process\Process;
+
+abstract class AbstractExecutable implements ExecutableInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isInstalled(): bool
+ {
+ return $this->isExecutableInstalled($this->getExecutable());
+ }
+
+ /**
+ * Get an unstarted Process object to run the executable with the given command.
+ */
+ protected function getProcess(string $command, ?string $cwd = null, ?float $timeout = 60): Process
+ {
+ if (!$this->isInstalled()) {
+ throw new ExecutableNotDetectedException($this);
+ }
+
+ return Process::fromShellCommandline(sprintf('%s %s', $this->getExecutable(), $command), $cwd, null, null, $timeout);
+ }
+
+ /**
+ * Check if the given executable is installed.
+ */
+ protected function isExecutableInstalled(string $executable): bool
+ {
+ return 0 === Process::fromShellCommandline(sprintf('which %s', $executable))->run();
+ }
+
+ /**
+ * Run the executable with the given command and return the Process object used to run it.
+ */
+ protected function run(string $command, ?string $cwd = null, ?float $timeout = 60): Process
+ {
+ if (!$this->isInstalled()) {
+ throw new ExecutableNotDetectedException($this);
+ }
+
+ return Process::runShellCommandline(sprintf('%s %s', $this->getExecutable(), $command), $cwd, null, null, $timeout);
+ }
+}
diff --git a/src/Tool/Docker.php b/src/Executable/DockerExecutable.php
similarity index 55%
rename from src/Tool/Docker.php
rename to src/Executable/DockerExecutable.php
index d915d20..959d230 100644
--- a/src/Tool/Docker.php
+++ b/src/Executable/DockerExecutable.php
@@ -11,43 +11,59 @@
* file that was distributed with this source code.
*/
-namespace Ymir\Cli\Tool;
+namespace Ymir\Cli\Executable;
use Symfony\Component\Console\Exception\RuntimeException;
-class Docker extends CommandLineTool
+class DockerExecutable extends AbstractExecutable
{
/**
* Build a docker image.
*/
- public static function build(string $file, string $tag, ?string $cwd = null)
+ public function build(string $file, string $tag, ?string $cwd = null)
{
- self::runCommand(sprintf('build --pull --file=%s --tag=%s .', $file, $tag), $cwd);
+ $this->run(sprintf('build --pull --file=%s --tag=%s .', $file, $tag), $cwd);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDisplayName(): string
+ {
+ return 'Docker';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExecutable(): string
+ {
+ return 'docker';
}
/**
* Login to a Docker registry.
*/
- public static function login(string $username, string $password, string $server, ?string $cwd = null)
+ public function login(string $username, string $password, string $server, ?string $cwd = null)
{
- self::runCommand(sprintf('login --username %s --password %s %s', $username, $password, $server), $cwd);
+ $this->run(sprintf('login --username %s --password %s %s', $username, $password, $server), $cwd);
}
/**
* Push a docker image.
*/
- public static function push(string $image, ?string $cwd = null)
+ public function push(string $image, ?string $cwd = null)
{
- self::runCommand(sprintf('push %s', $image), $cwd);
+ $this->run(sprintf('push %s', $image), $cwd);
}
/**
* Remove all images matching grep pattern.
*/
- public static function rmigrep(string $pattern, ?string $cwd = null)
+ public function removeImagesMatchingPattern(string $pattern, ?string $cwd = null)
{
try {
- self::runCommand(sprintf('rmi -f $(docker images | grep \'%s\')', $pattern), $cwd);
+ $this->run(sprintf('rmi -f $(docker images | grep \'%s\')', $pattern), $cwd);
} catch (RuntimeException $exception) {
$throwException = collect([
'"docker rmi" requires at least 1 argument',
@@ -65,24 +81,8 @@ public static function rmigrep(string $pattern, ?string $cwd = null)
/**
* Create a docker image tag.
*/
- public static function tag(string $sourceImage, string $targetImage, ?string $cwd = null)
+ public function tag(string $sourceImage, string $targetImage, ?string $cwd = null)
{
- self::runCommand(sprintf('tag %s %s', $sourceImage, $targetImage), $cwd);
- }
-
- /**
- * {@inheritdoc}
- */
- protected static function getCommand(): string
- {
- return 'docker';
- }
-
- /**
- * {@inheritdoc}
- */
- protected static function getName(): string
- {
- return 'Docker';
+ $this->run(sprintf('tag %s %s', $sourceImage, $targetImage), $cwd);
}
}
diff --git a/src/Executable/ExecutableInterface.php b/src/Executable/ExecutableInterface.php
new file mode 100644
index 0000000..a3c7982
--- /dev/null
+++ b/src/Executable/ExecutableInterface.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ymir\Cli\Executable;
+
+interface ExecutableInterface
+{
+ /**
+ * Get the human-readable name for this executable.
+ */
+ public function getDisplayName(): string;
+
+ /**
+ * Get the actual binary name or command string used to invoke this executable.
+ */
+ public function getExecutable(): string;
+
+ /**
+ * Determines if this command-line executable is installed and accessible globally.
+ */
+ public function isInstalled(): bool;
+}
diff --git a/src/Executable/SshExecutable.php b/src/Executable/SshExecutable.php
new file mode 100644
index 0000000..a3597a9
--- /dev/null
+++ b/src/Executable/SshExecutable.php
@@ -0,0 +1,86 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ymir\Cli\Executable;
+
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Filesystem\Filesystem;
+use Ymir\Cli\Exception\Executable\SshPortInUseException;
+use Ymir\Cli\Process\Process;
+
+class SshExecutable extends AbstractExecutable
+{
+ /**
+ * The file system.
+ *
+ * @var Filesystem
+ */
+ private $filesystem;
+
+ /**
+ * The SSH directory.
+ *
+ * @var string
+ */
+ private $sshDirectory;
+
+ /**
+ * Constructor.
+ */
+ public function __construct(Filesystem $filesystem, ?string $sshDirectory = null)
+ {
+ $this->filesystem = $filesystem;
+ $this->sshDirectory = $sshDirectory ?? rtrim((string) getenv('HOME'), '/').'/.ssh';
+ }
+
+ public function getDisplayName(): string
+ {
+ return 'SSH';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExecutable(): string
+ {
+ return 'ssh';
+ }
+
+ /**
+ * Opens an SSH tunnel to a bastion host and returns the running tunnel process.
+ */
+ public function openTunnelToBastionHost(array $bastionHost, int $localPort, string $remoteHost, int $remotePort, ?string $cwd = null): Process
+ {
+ if (!isset($bastionHost['endpoint'], $bastionHost['private_key'])) {
+ throw new InvalidArgumentException('Bastion host configuration must contain an "endpoint" and a "private_key"');
+ }
+
+ if (!is_dir($this->sshDirectory)) {
+ $this->filesystem->mkdir($this->sshDirectory, 0700);
+ }
+
+ $identityFilePath = $this->sshDirectory.'/ymir-tunnel';
+
+ $this->filesystem->dumpFile($identityFilePath, $bastionHost['private_key']);
+ $this->filesystem->chmod($identityFilePath, 0600);
+
+ $process = $this->getProcess(sprintf('ec2-user@%s -i %s -o LogLevel=debug -L %s:%s:%s -N', $bastionHost['endpoint'], $identityFilePath, $localPort, $remoteHost, $remotePort), $cwd, null);
+ $process->start(function ($type, $buffer) use ($localPort) {
+ if (Process::ERR === $type && false !== stripos($buffer, sprintf('%s: address already in use', $localPort))) {
+ throw new SshPortInUseException($localPort);
+ }
+ });
+
+ return $process;
+ }
+}
diff --git a/src/Tool/WpCli.php b/src/Executable/WpCliExecutable.php
similarity index 57%
rename from src/Tool/WpCli.php
rename to src/Executable/WpCliExecutable.php
index e9f842a..04eac0d 100644
--- a/src/Tool/WpCli.php
+++ b/src/Executable/WpCliExecutable.php
@@ -11,30 +11,46 @@
* file that was distributed with this source code.
*/
-namespace Ymir\Cli\Tool;
+namespace Ymir\Cli\Executable;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Exception\RuntimeException;
-use Ymir\Cli\Exception\WpCliException;
+use Ymir\Cli\Exception\Executable\WpCliException;
use Ymir\Cli\Process\Process;
-class WpCli extends CommandLineTool
+class WpCliExecutable extends AbstractExecutable
{
/**
* Download WordPress.
*/
- public static function downloadWordPress(?string $cwd = null)
+ public function downloadWordPress(?string $cwd = null)
{
- self::runCommand('core download', $cwd);
+ $this->run('core download', $cwd);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDisplayName(): string
+ {
+ return 'WP-CLI';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExecutable(): string
+ {
+ return 'wp';
}
/**
* Checks if WordPress is installed.
*/
- public static function isWordPressInstalled(?string $cwd = null): bool
+ public function isWordPressInstalled(?string $cwd = null): bool
{
try {
- self::runCommand('core is-installed', $cwd);
+ $this->run('core is-installed', $cwd);
return true;
} catch (WpCliException $exception) {
@@ -45,10 +61,10 @@ public static function isWordPressInstalled(?string $cwd = null): bool
/**
* Checks if the Ymir plugin is installed.
*/
- public static function isYmirPluginInstalled(?string $cwd = null): bool
+ public function isYmirPluginInstalled(?string $cwd = null): bool
{
try {
- return self::listPlugins($cwd)->contains(function (array $plugin) {
+ return $this->listPlugins($cwd)->contains(function (array $plugin) {
return !empty($plugin['file']) && 1 === preg_match('/\/ymir\.php$/', $plugin['file']);
});
} catch (\Throwable $exception) {
@@ -59,46 +75,30 @@ public static function isYmirPluginInstalled(?string $cwd = null): bool
/**
* List all the installed plugins.
*/
- public static function listPlugins(?string $cwd = null): Collection
+ public function listPlugins(?string $cwd = null): Collection
{
- $process = self::runCommand('plugin list --fields=file,name,status,title,version --format=json', $cwd);
+ $process = $this->run('plugin list --fields=file,name,status,title,version --format=json', $cwd);
- $plugins = collect(json_decode($process->getOutput(), true));
+ $plugins = json_decode($process->getOutput(), true);
- if ($plugins->isEmpty()) {
+ if (JSON_ERROR_NONE !== json_last_error()) {
throw new RuntimeException('Unable to get the list of installed plugins');
}
- return $plugins;
- }
-
- /**
- * {@inheritdoc}
- */
- protected static function getCommand(): string
- {
- return 'wp';
- }
-
- /**
- * {@inheritdoc}
- */
- protected static function getName(): string
- {
- return 'WP-CLI';
+ return collect($plugins);
}
/**
* {@inheritdoc}
*/
- protected static function runCommand(string $command, ?string $cwd = null): Process
+ protected function run(string $command, ?string $cwd = null, ?float $timeout = 60): Process
{
if (function_exists('posix_geteuid') && 0 === posix_geteuid()) {
- throw new RuntimeException('WP-CLI commands can only be run as a non-root user');
+ throw new WpCliException('WP-CLI commands can only be run as a non-root user');
}
try {
- return parent::runCommand($command, $cwd);
+ return parent::run($command, $cwd, $timeout);
} catch (RuntimeException $exception) {
throw new WpCliException($exception->getMessage(), $exception->getCode(), $exception);
}
diff --git a/src/Tool/CommandLineTool.php b/src/Tool/CommandLineTool.php
deleted file mode 100644
index cb23102..0000000
--- a/src/Tool/CommandLineTool.php
+++ /dev/null
@@ -1,69 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Ymir\Cli\Tool;
-
-use Symfony\Component\Console\Exception\RuntimeException;
-use Ymir\Cli\Exception\CommandLineToolNotDetectedException;
-use Ymir\Cli\Process\Process;
-
-class CommandLineTool
-{
- /**
- * Checks if command-line tool is installed globally.
- */
- public static function isInstalledGlobally(): bool
- {
- return 0 === Process::fromShellCommandline(sprintf('which %s', static::getCommand()))->run();
- }
-
- /**
- * Get the command to interact with the command-line tool.
- */
- protected static function getCommand(): string
- {
- throw new RuntimeException('Must override "getCommand" method');
- }
-
- /**
- * Get the name of the command-line tool.
- */
- protected static function getName(): string
- {
- throw new RuntimeException('Must override "getName" method');
- }
-
- /**
- * Get a Process object for the given command.
- */
- protected static function getProcess(string $command, ?string $cwd = null, ?float $timeout = 60): Process
- {
- if (!static::isInstalledGlobally()) {
- throw new CommandLineToolNotDetectedException(static::getName());
- }
-
- return Process::fromShellCommandline(sprintf('%s %s', static::getCommand(), $command), $cwd, null, null, $timeout);
- }
-
- /**
- * Run command.
- */
- protected static function runCommand(string $command, ?string $cwd = null): Process
- {
- if (!static::isInstalledGlobally()) {
- throw new CommandLineToolNotDetectedException(static::getName());
- }
-
- return Process::runShellCommandline(sprintf('%s %s', static::getCommand(), $command), $cwd, null, null, null);
- }
-}
diff --git a/src/Tool/Ssh.php b/src/Tool/Ssh.php
deleted file mode 100644
index 6c90930..0000000
--- a/src/Tool/Ssh.php
+++ /dev/null
@@ -1,75 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Ymir\Cli\Tool;
-
-use Symfony\Component\Console\Exception\InvalidArgumentException;
-use Symfony\Component\Console\Exception\RuntimeException;
-use Symfony\Component\Filesystem\Filesystem;
-use Ymir\Cli\Process\Process;
-
-class Ssh extends CommandLineTool
-{
- /**
- * Creates an SSH tunnel to a bastion host and returns the running process.
- */
- public static function tunnelBastionHost(array $bastionHost, int $localPort, string $remoteHost, int $remotePort, ?string $cwd = null): Process
- {
- if (!isset($bastionHost['endpoint'], $bastionHost['private_key'])) {
- throw new InvalidArgumentException('Invalid bastion host given');
- }
-
- $filesystem = new Filesystem();
- $sshDirectory = rtrim((string) getenv('HOME'), '/').'/.ssh';
-
- if (!is_dir($sshDirectory)) {
- $filesystem->mkdir($sshDirectory, 0700);
- }
-
- $identityFilePath = $sshDirectory.'/ymir-tunnel';
-
- $filesystem->dumpFile($identityFilePath, $bastionHost['private_key']);
- $filesystem->chmod($identityFilePath, 0600);
-
- $command = sprintf('ec2-user@%s -i %s -o LogLevel=debug -L %s:%s:%s -N', $bastionHost['endpoint'], $identityFilePath, $localPort, $remoteHost, $remotePort);
-
- $process = self::getProcess($command, $cwd, null);
- $process->start(function ($type, $buffer) use ($localPort) {
- if (Process::ERR !== $type) {
- return;
- }
-
- if (false !== stripos($buffer, sprintf('%s: address already in use', $localPort))) {
- throw new RuntimeException(sprintf('Unable to create SSH tunnel. Local port "%s" is already in use.', $localPort));
- }
- });
-
- return $process;
- }
-
- /**
- * {@inheritdoc}
- */
- protected static function getCommand(): string
- {
- return 'ssh';
- }
-
- /**
- * {@inheritdoc}
- */
- protected static function getName(): string
- {
- return 'SSH';
- }
-}