Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,31 @@
<?php
namespace Consolidation\SiteProcess\Factory;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Transport\DockerComposeTransport;
use Consolidation\Config\ConfigInterface;
/**
* DockerComposeTransportFactory will create an DockerComposeTransport for
* applicable site aliases.
*/
class DockerComposeTransportFactory implements TransportFactoryInterface
{
/**
* @inheritdoc
*/
public function check(SiteAliasInterface $siteAlias)
{
// TODO: deprecate and eventually remove 'isContainer()', and move the logic here.
return $siteAlias->isContainer();
}
/**
* @inheritdoc
*/
public function create(SiteAliasInterface $siteAlias)
{
return new DockerComposeTransport($siteAlias);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Consolidation\SiteProcess\Factory;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Transport\KubectlTransport;
/**
* KubectlTransportFactory will create an KubectlTransport for applicable site aliases.
*/
class KubectlTransportFactory implements TransportFactoryInterface
{
/**
* @inheritdoc
*/
public function check(SiteAliasInterface $siteAlias)
{
return $siteAlias->has('kubectl');
}
/**
* @inheritdoc
*/
public function create(SiteAliasInterface $siteAlias)
{
return new KubectlTransport($siteAlias);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Consolidation\SiteProcess\Factory;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Transport\SkprTransport;
/**
* SkprTransportFactory will create an SkprTransport for applicable site aliases.
*/
class SkprTransportFactory implements TransportFactoryInterface
{
/**
* @inheritdoc
*/
public function check(SiteAliasInterface $siteAlias)
{
return $siteAlias->has('skpr');
}
/**
* @inheritdoc
*/
public function create(SiteAliasInterface $siteAlias)
{
return new SkprTransport($siteAlias);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Consolidation\SiteProcess\Factory;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Transport\SshTransport;
use Consolidation\Config\ConfigInterface;
/**
* SshTransportFactory will create an SshTransport for applicable site aliases.
*/
class SshTransportFactory implements TransportFactoryInterface
{
/**
* @inheritdoc
*/
public function check(SiteAliasInterface $siteAlias)
{
// TODO: deprecate and eventually remove 'isRemote()', and move the logic here.
return $siteAlias->isRemote();
}
/**
* @inheritdoc
*/
public function create(SiteAliasInterface $siteAlias)
{
return new SshTransport($siteAlias);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Consolidation\SiteProcess\Factory;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Transport\TransportInterface;
use Consolidation\Config\ConfigInterface;
/**
* TransportFactoryInterface defines a transport factory that is responsible
* for:
*
* - Determining whether a provided site alias is applicable to this transport
* - Creating an instance of a transport for an applicable site alias.
*
* There is always a transport for every factory, and visa-versa.
* @see Consolidation\SiteProcess\Transport\TransportInterface
*/
interface TransportFactoryInterface
{
/**
* Check to see if a provided site alias is applicable to this transport type.
* @param SiteAliasInterface $siteAlias
* @return bool
*/
public function check(SiteAliasInterface $siteAlias);
/**
* Create a transport instance for an applicable site alias.
* @param SiteAliasInterface $siteAlias
* @return TransportInterface
*/
public function create(SiteAliasInterface $siteAlias);
}

View File

@ -0,0 +1,28 @@
<?php
namespace Consolidation\SiteProcess\Factory;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Transport\VagrantTransport;
/**
* VagrantTransportFactory will create a VagrantTransport for applicable site aliases.
*/
class VagrantTransportFactory implements TransportFactoryInterface
{
/**
* @inheritdoc
*/
public function check(SiteAliasInterface $siteAlias)
{
return $siteAlias->has('vagrant');
}
/**
* @inheritdoc
*/
public function create(SiteAliasInterface $siteAlias)
{
return new VagrantTransport($siteAlias);
}
}

View File

@ -0,0 +1,235 @@
<?php
namespace Consolidation\SiteProcess;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Style\OutputStyle;
use Symfony\Component\Process\Process;
use Consolidation\SiteProcess\Util\RealtimeOutputHandler;
use Consolidation\SiteProcess\Util\Escape;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
/**
* A wrapper around Symfony Process.
*
* - Supports simulated mode. Typically enabled via a --simulate option.
* - Supports verbose mode - logs all runs.
* - Can convert output json data into php array (convenience method)
* - Provides a "realtime output" helper
*/
class ProcessBase extends Process
{
/**
* @var OutputStyle
*/
protected $output;
/**
* @var OutputInterface
*/
protected $stderr;
private $simulated = false;
private $verbose = false;
/**
* @var LoggerInterface
*/
private $logger;
/**
* realtimeStdout returns the output stream that realtime output
* should be sent to (if applicable)
*
* @return OutputStyle $output
*/
public function realtimeStdout()
{
return $this->output;
}
protected function realtimeStderr()
{
if ($this->stderr) {
return $this->stderr;
}
if (method_exists($this->output, 'getErrorStyle')) {
return $this->output->getErrorStyle();
}
return $this->realtimeStdout();
}
/**
* setRealtimeOutput allows the caller to inject an OutputStyle object
* that will be used to stream realtime output if applicable.
*
* @param OutputStyle $output
*/
public function setRealtimeOutput(OutputInterface $output, $stderr = null)
{
$this->output = $output;
$this->stderr = $stderr instanceof ConsoleOutputInterface ? $stderr->getErrorOutput() : $stderr;
}
/**
* @return bool
*/
public function isVerbose()
{
return $this->verbose;
}
/**
* @param bool $verbose
*/
public function setVerbose($verbose)
{
$this->verbose = $verbose;
}
/**
* @return bool
*/
public function isSimulated()
{
return $this->simulated;
}
/**
* @param bool $simulated
*/
public function setSimulated($simulated)
{
$this->simulated = $simulated;
}
/**
* @return LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* @param LoggerInterface $logger
*/
public function setLogger($logger)
{
$this->logger = $logger;
}
/**
* @inheritDoc
*/
public function start(?callable $callback = null, array $env = []): void
{
$cmd = $this->getCommandLine();
if ($this->isSimulated()) {
$this->getLogger()->notice('Simulating: ' . $cmd);
// Run a command that always succeeds (on Linux and Windows).
$this->overrideCommandLine('true');
} elseif ($this->isVerbose()) {
$this->getLogger()->info('Executing: ' . $cmd);
}
parent::start($callback, $env);
// Set command back to original value in case anyone asks.
if ($this->isSimulated()) {
$this->overrideCommandLine($cmd);
}
}
/**
* Get Process output and decode its JSON.
*
* @return array
* An associative array.
*/
public function getOutputAsJson()
{
$output = trim($this->getOutput());
if (empty($output)) {
throw new \InvalidArgumentException('Output is empty.');
}
if (Escape::isWindows()) {
// Doubled double quotes were converted to \\".
// Revert to double quote.
$output = str_replace('\\"', '"', $output);
// Revert of doubled backslashes.
$output = preg_replace('#\\\\{2}#', '\\', $output);
}
$sanitizedOutput = $this->removeNonJsonJunk($output);
$json = json_decode($sanitizedOutput, true);
if (!isset($json)) {
$msg = 'Unable to decode output into JSON: ' . json_last_error_msg();
if (json_last_error() == JSON_ERROR_SYNTAX) {
$msg .= "\n\n$output";
}
throw new \InvalidArgumentException($msg);
}
return $json;
}
/**
* Allow for a certain amount of resiliancy in the output received when
* json is expected.
*
* @param string $data
* @return string
*/
protected function removeNonJsonJunk($data)
{
// Exit early if we have no output.
$data = trim($data);
if (empty($data)) {
return $data;
}
// If the data is a simple quoted string, or an array, then exit.
if ((($data[0] == '"') && ($data[strlen($data) - 1] == '"')) ||
(($data[0] == "[") && ($data[strlen($data) - 1] == "]"))
) {
return $data;
}
// If the json is not a simple string or a simple array, then is must
// be an associative array. We will remove non-json garbage characters
// before and after the enclosing curley-braces.
$start = strpos($data, '{');
$end = strrpos($data, '}') + 1;
$data = substr($data, $start, $end - $start);
return $data;
}
/**
* Return a realTime output object.
*
* @return callable
*/
public function showRealtime()
{
$realTimeOutput = new RealtimeOutputHandler($this->realtimeStdout(), $this->realtimeStderr());
$realTimeOutput->configure($this);
return $realTimeOutput;
}
/**
* Overrides the command line to be executed.
*
* @param string|array $commandline The command to execute
*
* @return $this
*
* @todo refactor library so this hack to get around changes in
* symfony/process 5 is unnecessary.
*/
private function overrideCommandLine($commandline)
{
$commandlineSetter = function ($commandline) {
$this->commandline = $commandline;
};
$commandlineSetter->bindTo($this, Process::class)($commandline);
return $this;
}
}

View File

@ -0,0 +1,180 @@
<?php
namespace Consolidation\SiteProcess;
use Consolidation\SiteProcess\Factory\KubectlTransportFactory;
use Consolidation\SiteProcess\Factory\SkprTransportFactory;
use Consolidation\SiteProcess\Factory\VagrantTransportFactory;
use Psr\Log\LoggerInterface;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Factory\SshTransportFactory;
use Consolidation\SiteProcess\Factory\DockerComposeTransportFactory;
use Consolidation\SiteProcess\Factory\TransportFactoryInterface;
use Consolidation\SiteProcess\Transport\LocalTransport;
use Symfony\Component\Process\Process;
use Consolidation\Config\Config;
use Consolidation\Config\ConfigInterface;
use Consolidation\Config\ConfigAwareInterface;
use Consolidation\Config\ConfigAwareTrait;
use Consolidation\SiteAlias\SiteAliasWithConfig;
/**
* ProcessManager will create a SiteProcess to run a command on a given
* site as indicated by a SiteAlias.
*
* ProcessManager also manages a collection of transport factories, and
* will produce transport instances as needed for provided site aliases.
*/
class ProcessManager implements ConfigAwareInterface
{
use ConfigAwareTrait;
/** @var ConfigInterface */
protected $configRuntime;
protected $transportFactories = [];
public function __construct()
{
$this->config = new Config();
$this->configRuntime = new Config();
}
/**
* setConfigRuntime sets the config object that holds runtime config
* items, i.e. config set from the commandline rather than a config file.
* Configuration priority (highest to lowest) is:
* - config runtime
* - site alias
* - config files
*/
public function setConfigRuntime(ConfigInterface $configRuntime)
{
$this->configRuntime = $configRuntime;
return $this;
}
/**
* createDefault creates a Transport manager and add the default transports to it.
*/
public static function createDefault()
{
$processManager = new self();
return static::addTransports($processManager);
}
/**
* addTransports adds the avaiable transports to the
* provided process manager.
*/
public static function addTransports(ProcessManager $processManager)
{
$processManager->add(new SshTransportFactory());
$processManager->add(new KubectlTransportFactory());
$processManager->add(new SkprTransportFactory());
$processManager->add(new DockerComposeTransportFactory());
$processManager->add(new VagrantTransportFactory());
return $processManager;
}
/**
* Return a site process configured with an appropriate transport
*
* @param SiteAliasInterface $siteAlias Target for command
* @param array $args Command arguments
* @param array $options Associative array of command options
* @param array $optionsPassedAsArgs Associtive array of options to be passed as arguments (after double-dash)
* @return Process
*/
public function siteProcess(SiteAliasInterface $siteAlias, $args = [], $options = [], $optionsPassedAsArgs = [])
{
$transport = $this->getTransport($siteAlias);
$process = new SiteProcess($siteAlias, $transport, $args, $options, $optionsPassedAsArgs);
return $process;
}
/**
* Create a Process instance from a commandline string.
* @param array $command The command to run and its arguments listed as separate entries
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param array|null $env The environment variables or null to use the same environment as the current PHP process
* @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
* @param int|float|null $timeout The timeout in seconds or null to disable
* @return Process
*/
public function process($command, $cwd = null, ?array $env = null, $input = null, $timeout = 60)
{
return new ProcessBase($command, $cwd, $env, $input, $timeout);
}
/**
* Create a Process instance from a commandline string.
* @param string $command The commandline string to run
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param array|null $env The environment variables or null to use the same environment as the current PHP process
* @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
* @param int|float|null $timeout The timeout in seconds or null to disable
* @return Process
*/
public function shell($command, $cwd = null, ?array $env = null, $input = null, $timeout = 60)
{
return ProcessBase::fromShellCommandline($command, $cwd, $env, $input, $timeout);
}
/**
* add a transport factory to our factory list
* @param TransportFactoryInterface $factory
*/
public function add(TransportFactoryInterface $factory)
{
$this->transportFactories[] = $factory;
return $this;
}
/**
* hasTransport determines if there is a transport that handles the
* provided site alias.
*
* @param SiteAliasInterface $siteAlias
* @return boolean
*/
public function hasTransport(SiteAliasInterface $siteAlias)
{
return $this->getTransportFactory($siteAlias) !== null;
}
/**
* getTransport returns a transport that is applicable to the provided site alias.
*
* @param SiteAliasInterface $siteAlias
* @return TransportInterface
*/
public function getTransport(SiteAliasInterface $siteAlias)
{
$factory = $this->getTransportFactory($siteAlias);
$siteAliasWithConfig = SiteAliasWithConfig::create($siteAlias, $this->config, $this->configRuntime);
if ($factory) {
return $factory->create($siteAliasWithConfig);
}
return new LocalTransport();
}
/**
* getTransportFactory returns a factory for the provided site alias.
*
* @param SiteAliasInterface $siteAlias
* @return TransportFactoryInterface
*/
protected function getTransportFactory(SiteAliasInterface $siteAlias)
{
foreach ($this->transportFactories as $factory) {
if ($factory->check($siteAlias)) {
return $factory;
}
}
return null;
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Consolidation\SiteProcess;
/**
* Inflection interface for the site alias manager.
*/
interface ProcessManagerAwareInterface
{
/**
* @param ProcessManager $processManager
*/
public function setProcessManager(ProcessManager $processManager);
/**
* @return ProcessManager
*/
public function processManager();
/**
* @return bool
*/
public function hasProcessManager();
}

View File

@ -0,0 +1,34 @@
<?php
namespace Consolidation\SiteProcess;
/**
* Inflection trait for the site alias manager.
*/
trait ProcessManagerAwareTrait
{
protected $processManager;
/**
* @inheritdoc
*/
public function setProcessManager(ProcessManager $processManager)
{
$this->processManager = $processManager;
}
/**
* @return ProcessManager
*/
public function processManager()
{
return $this->processManager;
}
/**
* @inheritdoc
*/
public function hasProcessManager()
{
return isset($this->processManager);
}
}

View File

@ -0,0 +1,278 @@
<?php
namespace Consolidation\SiteProcess;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Transport\DockerComposeTransport;
use Consolidation\SiteProcess\Util\ArgumentProcessor;
use Consolidation\SiteProcess\Transport\LocalTransport;
use Consolidation\SiteProcess\Transport\SshTransport;
use Consolidation\SiteProcess\Transport\TransportInterface;
use Consolidation\Config\Util\Interpolator;
use Consolidation\SiteProcess\Util\Shell;
use Consolidation\SiteProcess\Util\ShellOperatorInterface;
use Consolidation\SiteProcess\Util\Escape;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
/**
* A wrapper around Symfony Process that uses site aliases
* (https://github.com/consolidation/site-alias)
*
* - Interpolate arguments using values from the alias
* e.g. `$process = new SiteProcess($alias, ['git', '-C', '{{root}}']);`
* - Make remote calls via ssh as if they were local.
*/
class SiteProcess extends ProcessBase
{
/** @var SiteAliasInterface */
protected $siteAlias;
/** @var string[] */
protected $args;
/** @var string[] */
protected $options;
/** @var string[] */
protected $optionsPassedAsArgs;
/** @var string */
protected $cd_remote;
/** @var TransportInterface */
protected $transport;
/**
* Process arguments and options per the site alias and build the
* actual command to run.
*/
public function __construct(SiteAliasInterface $siteAlias, TransportInterface $transport, $args, $options = [], $optionsPassedAsArgs = [])
{
$this->siteAlias = $siteAlias;
$this->transport = $transport;
$this->args = $args;
$this->options = $options;
$this->optionsPassedAsArgs = $optionsPassedAsArgs;
parent::__construct([]);
}
/**
* Get a starting directory for the remote process.
*
* @return string|null
*/
public function getWorkingDirectory(): ?string
{
return $this->cd_remote;
}
/**
* Set a starting directory for the remote process.
*
* @param string $cd_remote
*
* @return \Consolidation\SiteProcess\SiteProcess
*/
public function setWorkingDirectory($cd_remote): static
{
$this->cd_remote = $cd_remote;
return $this;
}
/**
* Set a starting directory for the initial/local process.
*
* @param string $cd
*
* @return \Consolidation\SiteProcess\SiteProcess
*/
public function setWorkingDirectoryLocal($cd)
{
// Symfony 4 REQUIRES that there be a directory set, and defaults
// it to the cwd if it is not set. We will maintain that pattern here.
if (!$cd) {
$cd = getcwd();
}
return parent::setWorkingDirectory($cd);
}
/**
* Get the starting directory for the initial/local process.
*
* @return string|null;
*/
public function getWorkingDirectoryLocal()
{
return parent::getWorkingDirectory();
}
/**
*
* @param bool $shouldUseSiteRoot
* @return $this|\Symfony\Component\Process\Process
* @throws \Exception
*/
public function chdirToSiteRoot($shouldUseSiteRoot = true)
{
if (!$shouldUseSiteRoot || !$this->siteAlias->hasRoot()) {
return $this;
}
return $this->setWorkingDirectory($this->siteAlias->root());
}
/**
* Take all of our individual arguments and process them for use.
*/
protected function processArgs()
{
$transport = $this->getTransport($this->siteAlias);
$transport->configure($this);
$processor = new ArgumentProcessor();
$selectedArgs = $processor->selectArgs(
$this->siteAlias,
$this->args,
$this->options,
$this->optionsPassedAsArgs
);
// Set environment variables if needed.
if ($this->siteAlias->has('env-vars')) {
$selectedArgs = $this->addEnvVars($this->siteAlias->get('env-vars'), $selectedArgs);
}
// Ask the transport to drop in a 'cd' if needed.
if ($this->getWorkingDirectory()) {
$selectedArgs = $transport->addChdir($this->getWorkingDirectory(), $selectedArgs);
}
// Do any necessary interpolation on the selected arguments.
$processedArgs = $this->interpolate($selectedArgs);
// Wrap the command with 'ssh' or some other transport if this is
// a remote command; otherwise, leave it as-is.
return $transport->wrap($processedArgs);
}
/**
* Wrap the command/args in an env call.
* @todo Check if this needs to depend on linux/win.
* @todo Check if this needs to be delegated to transport.
*/
public function addEnvVars($envVars, $args)
{
$envArgs = ['env'];
foreach ($envVars as $key => $value) {
$envArgs[] = Escape::forSite($this->siteAlias, $key) . '='
. Escape::forSite($this->siteAlias, $value);
}
return array_merge($envArgs, $args);
}
public function setTransport($transport)
{
$this->transport = $transport;
}
/**
* Ask the transport manager for the correct transport for the
* provided alias.
*/
protected function getTransport(SiteAliasInterface $siteAlias)
{
return $this->transport;
}
/**
* @inheritDoc
*/
public function getCommandLine(): string
{
$commandLine = parent::getCommandLine();
if (empty($commandLine)) {
$processedArgs = $this->processArgs();
$commandLine = Escape::argsForSite($this->siteAlias, $processedArgs);
$commandLine = implode(' ', $commandLine);
$this->overrideCommandLine($commandLine);
}
return $commandLine;
}
/**
* @inheritDoc
*/
public function start(?callable $callback = null, array $env = []): void
{
$cmd = $this->getCommandLine();
parent::start($callback, $env);
}
public function mustRun(?callable $callback = null, array $env = []): static
{
if (0 !== $this->run($callback, $env)) {
// Be less verbose when there is nothing in stdout or stderr.
if (empty($this->getOutput()) && empty($this->getErrorOutput())) {
$this->disableOutput();
}
throw new ProcessFailedException($this);
}
return $this;
}
/**
* @inheritDoc
*/
public function wait(?callable $callback = null): int
{
$return = parent::wait($callback);
return $return;
}
/**
* interpolate examines each of the arguments in the provided argument list
* and replaces any token found therein with the value for that key as
* pulled from the given site alias.
*
* Example: "git -C {{root}} status"
*
* The token "{{root}}" will be converted to a value via $siteAlias->get('root').
* The result will replace the token.
*
* It is possible to use dot notation in the keys to access nested elements
* within the site alias record.
*
* @param SiteAliasInterface $siteAlias
* @param type $args
* @return type
*/
protected function interpolate($args)
{
$interpolator = new Interpolator();
return array_map(
function ($arg) use ($interpolator) {
if ($arg instanceof ShellOperatorInterface) {
return $arg;
}
return $interpolator->interpolate($this->siteAlias, $arg, false);
},
$args
);
}
/**
* Overrides the command line to be executed.
*
* @param string|array $commandline The command to execute
*
* @return $this
*
* @todo refactor library so this hack to get around changes in
* symfony/process 5 is unnecessary.
*/
private function overrideCommandLine($commandline)
{
$commandlineSetter = function ($commandline) {
$this->commandline = $commandline;
};
$commandlineSetter->bindTo($this, Process::class)($commandline);
return $this;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace Consolidation\SiteProcess\Transport;
use Consolidation\SiteProcess\SiteProcess;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Util\Shell;
use Consolidation\Config\ConfigInterface;
/**
* DockerComposeTransport knows how to wrap a command such that it executes
* on a Docker Compose service.
*/
class DockerComposeTransport implements TransportInterface
{
protected $tty;
protected $siteAlias;
protected $cd_remote;
public function __construct(SiteAliasInterface $siteAlias)
{
$this->siteAlias = $siteAlias;
}
/**
* @inheritdoc
*/
public function configure(SiteProcess $process)
{
$this->tty = $process->isTty();
}
/**
* @inheritdoc
*/
public function wrap($args)
{
$transport = $this->getTransport();
$transportOptions = $this->getTransportOptions();
$commandToExecute = $this->getCommandToExecute($args);
return array_merge(
$transport,
$transportOptions,
$commandToExecute
);
}
/**
* @inheritdoc
*/
public function addChdir($cd, $args)
{
$this->cd_remote = $cd;
return $args;
}
/**
* getTransport returns the transport along with the docker-compose
* project in case it is defined.
*/
protected function getTransport()
{
$version = $this->siteAlias->get('docker.compose.version', '1');
if ($version == 2) {
$transport = ['docker', 'compose'];
} else {
$transport = ['docker-compose'];
}
$project = $this->siteAlias->get('docker.project', '');
$options = $this->siteAlias->get('docker.compose.options', '');
$command = $this->siteAlias->get('docker.compose.command', 'exec');
if ($project && (strpos($options, '-p') === false || strpos($options, '--project') === false)) {
$transport = array_merge($transport, ['-p', $project]);
}
if ($options) {
$transport[] = Shell::preEscaped($options);
}
return array_merge($transport, [$command]);
}
/**
* getTransportOptions returns the transport options for the tranport
* mechanism itself
*/
protected function getTransportOptions()
{
$transportOptions = [
$this->siteAlias->get('docker.service', ''),
];
if ($options = $this->siteAlias->get('docker.exec.options', '')) {
array_unshift($transportOptions, Shell::preEscaped($options));
}
if (!$this->tty) {
array_unshift($transportOptions, '-T');
}
if ($this->cd_remote) {
$transportOptions = array_merge(['--workdir', $this->cd_remote], $transportOptions);
}
return array_filter($transportOptions);
}
/**
* getCommandToExecute processes the arguments for the command to
* be executed such that they are appropriate for the transport mechanism.
*
* Nothing to do for this transport.
*/
protected function getCommandToExecute($args)
{
return $args;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Consolidation\SiteProcess\Transport;
use Consolidation\SiteProcess\SiteProcess;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Util\Shell;
/**
* KubectlTransport knows how to wrap a command such that it runs in a container
* on Kubernetes via kubectl.
*/
class KubectlTransport implements TransportInterface
{
/** @var bool */
protected $tty;
/** @var \Consolidation\SiteAlias\SiteAliasInterface */
protected $siteAlias;
public function __construct(SiteAliasInterface $siteAlias)
{
$this->siteAlias = $siteAlias;
}
/**
* @inheritdoc
*/
public function configure(SiteProcess $process)
{
$this->tty = $process->isTty();
}
/**
* inheritdoc
*/
public function wrap($args)
{
# TODO: How/where do we complain if a required argument is not available?
$namespace = $this->siteAlias->get('kubectl.namespace');
$tty = $this->tty && $this->siteAlias->get('kubectl.tty', false) ? "true" : "false";
$interactive = $this->tty && $this->siteAlias->get('kubectl.interactive', false) ? "true" : "false";
$resource = $this->siteAlias->get('kubectl.resource');
$container = $this->siteAlias->get('kubectl.container');
$kubeconfig = $this->siteAlias->get('kubectl.kubeconfig');
$entrypoint = $this->siteAlias->get('kubectl.entrypoint');
$transport = [
'kubectl',
"--namespace=$namespace",
'exec',
"--tty=$tty",
"--stdin=$interactive",
$resource,
];
if ($container) {
$transport[] = "--container=$container";
}
if ($kubeconfig) {
$transport[] = "--kubeconfig=$kubeconfig";
}
$transport[] = "--";
if ($entrypoint) {
$transport = is_array($entrypoint) ? [...$transport, ...$entrypoint] : [...$transport, $entrypoint];
}
return array_merge($transport, $args);
}
/**
* @inheritdoc
*/
public function addChdir($cd_remote, $args)
{
return array_merge(
[
'cd',
$cd_remote,
Shell::op('&&'),
],
$args
);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Consolidation\SiteProcess\Transport;
use Consolidation\SiteProcess\SiteProcess;
/**
* LocalTransport just runs the command on the local system.
*/
class LocalTransport implements TransportInterface
{
/**
* @inheritdoc
*/
public function configure(SiteProcess $process)
{
$process->setWorkingDirectoryLocal($process->getWorkingDirectory());
}
/**
* @inheritdoc
*/
public function wrap($args)
{
return $args;
}
/**
* @inheritdoc
*/
public function addChdir($cd, $args)
{
return $args;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Consolidation\SiteProcess\Transport;
use Consolidation\SiteProcess\SiteProcess;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Util\Shell;
/**
* SkprTransport knows how to wrap a command to run on a site hosted
* on the Skpr platform.
*/
class SkprTransport implements TransportInterface
{
/** @var \Consolidation\SiteAlias\SiteAliasInterface */
protected $siteAlias;
public function __construct(SiteAliasInterface $siteAlias)
{
$this->siteAlias = $siteAlias;
}
/**
* @inheritdoc
*/
public function configure(SiteProcess $process)
{
$path = $this->siteAlias->getDefault('skpr.path', getcwd());
if ($path) {
$process->chdirToSiteRoot($path);
}
}
/**
* inheritdoc
*/
public function wrap($args)
{
$environment = $this->siteAlias->get('skpr.env');
$transport = [
'skpr',
'exec',
"$environment",
];
$transport[] = "--";
return array_merge($transport, $args);
}
/**
* @inheritdoc
*/
public function addChdir($cd_remote, $args)
{
return array_merge(
[
'cd',
$cd_remote,
Shell::op('&&'),
],
$args
);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace Consolidation\SiteProcess\Transport;
use Consolidation\SiteProcess\SiteProcess;
use Consolidation\SiteProcess\Util\Escape;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Util\Shell;
use Consolidation\Config\ConfigInterface;
/**
* SshTransport knows how to wrap a command such that it runs on a remote
* system via the ssh cli.
*/
class SshTransport implements TransportInterface
{
protected $tty;
protected $siteAlias;
public function __construct(SiteAliasInterface $siteAlias)
{
$this->siteAlias = $siteAlias;
}
/**
* @inheritdoc
*/
public function configure(SiteProcess $process)
{
$this->tty = $process->isTty();
}
/**
* inheritdoc
*/
public function wrap($args)
{
$transport = ['ssh'];
$transportOptions = $this->getTransportOptions();
$commandToExecute = $this->getCommandToExecute($args);
return array_merge(
$transport,
$transportOptions,
$commandToExecute
);
}
/**
* @inheritdoc
*/
public function addChdir($cd_remote, $args)
{
return array_merge(
[
'cd',
$cd_remote,
Shell::op('&&'),
],
$args
);
}
/**
* getTransportOptions returns the transport options for the tranport
* mechanism itself
*/
protected function getTransportOptions()
{
$transportOptions = [
Shell::preEscaped($this->siteAlias->get('ssh.options', '-o PasswordAuthentication=no')),
$this->siteAlias->remoteHostWithUser(),
];
if ($this->tty) {
array_unshift($transportOptions, '-t');
}
return $transportOptions;
}
/**
* getCommandToExecute processes the arguments for the command to
* be executed such that they are appropriate for the transport mechanism.
*/
protected function getCommandToExecute($args)
{
// Escape each argument for the target system and then join
$args = Escape::argsForSite($this->siteAlias, $args);
$commandToExecute = implode(' ', $args);
return [$commandToExecute];
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Consolidation\SiteProcess\Transport;
use Consolidation\SiteProcess\SiteProcess;
/**
* Transports know how to wrap a command such that it runs on a remote system
* via some other command.
*
* There is always a transport for every factory, and visa-versa.
*
* @see Consolidation\SiteProcess\Factory\TransportFactoryInterface
*/
interface TransportInterface
{
/**
* Configure ourselves based on the settings of the process object
* (e.g. isTty()).
*
* @param \Consolidation\SiteProcess\SiteProcess $process
*/
public function configure(SiteProcess $process);
/**
* wrapWithTransport examines the provided site alias; if it is a local
* alias, then the provided arguments are returned unmodified. If the
* alias points at a remote system, though, then the arguments are
* escaped and wrapped in an appropriate ssh command.
*
* @param array $args arguments provided by caller.
* @return array command and arguments to execute.
*/
public function wrap($args);
/**
* addChdir adds an appropriate 'chdir' / 'cd' command for the transport.
*/
public function addChdir($cd, $args);
}

View File

@ -0,0 +1,85 @@
<?php
namespace Consolidation\SiteProcess\Transport;
use Consolidation\SiteProcess\SiteProcess;
use Consolidation\SiteProcess\Util\Escape;
use Consolidation\SiteAlias\SiteAliasInterface;
use Consolidation\SiteProcess\Util\Shell;
/**
* VagrantTransport knows how to wrap a command such that it runs on a remote
* system via the vagrant cli.
*/
class VagrantTransport implements TransportInterface
{
protected $tty;
protected $siteAlias;
public function __construct(SiteAliasInterface $siteAlias)
{
$this->siteAlias = $siteAlias;
}
/**
* @inheritdoc
*/
public function configure(SiteProcess $process)
{
$this->tty = $process->isTty();
}
/**
* inheritdoc
*/
public function wrap($args)
{
$transport = ['vagrant', 'ssh'];
$transportOptions = $this->getTransportOptions();
$commandToExecute = $this->getCommandToExecute($args);
return array_merge(
$transport,
$transportOptions,
['-c'],
$commandToExecute
);
}
/**
* @inheritdoc
*/
public function addChdir($cd_remote, $args)
{
return array_merge(
[
'cd',
$cd_remote,
Shell::op('&&'),
],
$args
);
}
/**
* getTransportOptions returns the transport options for the tranport
* mechanism itself
*/
protected function getTransportOptions()
{
return $this->tty ? ['-t'] : [];
}
/**
* getCommandToExecute processes the arguments for the command to
* be executed such that they are appropriate for the transport mechanism.
*/
protected function getCommandToExecute($args)
{
// Escape each argument for the target system and then join
$args = Escape::argsForSite($this->siteAlias, $args);
$commandToExecute = implode(' ', $args);
return [$commandToExecute];
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace Consolidation\SiteProcess\Util;
use Consolidation\SiteAlias\SiteAliasInterface;
use Symfony\Component\Process\Process;
use Consolidation\SiteProcess\Transport\TransportInterface;
/**
* ArgumentProcessor takes a set of arguments and options from the caller
* and processes them with the provided site alias to produce a final
* executable command that will run either locally or on a remote system,
* as applicable.
*/
class ArgumentProcessor
{
private $short_options = ['vv', 'vvv'];
public function getShortOptions(): array
{
return $this->short_options;
}
public function setShortOptions(array $short_options): void
{
$this->short_options = $short_options;
}
/**
* selectArgs selects the appropriate set of arguments for the command
* to be executed and orders them as needed.
*
* @param SiteAliasInterface $siteAlias Description of
* @param array $args Command and arguments to execute (source)
* @param array $options key / value pair of option and value in include
* in final arguments
* @param array $optionsPassedAsArgs key / value pair of option and value
* to include in final arguments after the '--' argument.
* @return array Command and arguments to execute
*/
public function selectArgs(SiteAliasInterface $siteAlias, $args, $options = [], $optionsPassedAsArgs = [])
{
// Split args into three arrays separated by the `--`
list($leadingArgs, $dashDash, $remaingingArgs) = $this->findArgSeparator($args);
$convertedOptions = $this->convertOptions($options);
$convertedOptionsPassedAsArgs = $this->convertOptions($optionsPassedAsArgs);
// If the caller provided options that should be passed as args, then we
// always need a `--`, whether or not one existed to begin with in $args
if (!empty($convertedOptionsPassedAsArgs)) {
$dashDash = ['--'];
}
// Combine our separated args in the correct order. $dashDash will
// always be `['--']` if $optionsPassedAsArgs or $remaingingArgs are
// not empty, and otherwise will usually be empty.
return array_merge(
$leadingArgs,
$convertedOptions,
$dashDash,
$convertedOptionsPassedAsArgs,
$remaingingArgs
);
}
/**
* findArgSeparator finds the "--" argument in the provided arguments list,
* if present, and returns the arguments in three sets.
*
* @return array of three arrays, leading, "--" and trailing
*/
protected function findArgSeparator($args)
{
$pos = array_search('--', $args);
if ($pos === false) {
return [$args, [], []];
}
return [
array_slice($args, 0, $pos),
['--'],
array_slice($args, $pos + 1),
];
}
/**
* convertOptions takes an associative array of options (key / value) and
* converts it to an array of strings in the form --key=value.
*
* @param array $options in key => value form
* @return array options in --option=value form
*/
protected function convertOptions($options)
{
$result = [];
foreach ($options as $option => $value) {
$dashes = str_repeat('-', $this->dashCount($option));
if ($value === true || $value === null) {
$result[] = $dashes . $option;
} elseif ($value === false) {
// Ignore this option.
} else {
$result[] = "{$dashes}{$option}={$value}";
}
}
return $result;
}
protected function dashCount($name): int
{
return in_array($name, $this->getShortOptions()) ? 1 : 2;
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace Consolidation\SiteProcess\Util;
use Consolidation\SiteAlias\SiteAliasInterface;
use Symfony\Component\Process\Process;
use Consolidation\Config\Util\Interpolator;
use Symfony\Component\Console\Output\OutputInterface;
use Consolidation\SiteProcess\Util\ShellOperatorInterface;
/**
* Escape will shell-escape commandline arguments for different platforms.
*/
class Escape
{
/**
* argsForSite escapes each argument in an array for the given site.
*/
public static function argsForSite(SiteAliasInterface $siteAlias, $args)
{
return array_map(
function ($arg) use ($siteAlias) {
return Escape::forSite($siteAlias, $arg);
},
$args
);
}
/**
* forSite escapes the provided argument for the specified alias record.
*/
public static function forSite(SiteAliasInterface $siteAlias, $arg)
{
return static::shellArg($arg, $siteAlias->os());
}
/**
* shellArg escapes the provided argument for the specified OS
*
* @param string|ShellOperatorInterface $arg The argument to escape
* @param string|null $os The OS to escape for. Optional; defaults to LINUX
*
* @return string The escaped string
*/
public static function shellArg($arg, $os = null)
{
// Short-circuit escaping for simple params (keep stuff readable);
// also skip escaping for shell operators (e.g. &&), which must not
// be escaped.
if (($arg instanceof ShellOperatorInterface) || preg_match('|^[a-zA-Z0-9@=.:/_-]*$|', $arg)) {
return (string) $arg;
}
if (static::isWindows($os)) {
return static::windowsArg($arg);
}
return static::linuxArg($arg);
}
/**
* isWindows determines whether the provided OS is Windows.
*
* @param string|null $os The OS to escape for.
*
* @return boolean
*/
public static function isWindows($os = null)
{
// In most cases, $os will be NULL and PHP_OS will be returned. However,
// if an OS is specified in $os, return that instead.
$os = $os ?: PHP_OS;
return strtoupper(substr($os, 0, 3)) === 'WIN';
}
/**
* linuxArg is the Linux version of escapeshellarg().
*
* This is intended to work the same way that escapeshellarg() does on
* Linux. If we need to escape a string that will be used remotely on
* a Linux system, then we need our own implementation of escapeshellarg,
* because the Windows version behaves differently.
*
* Note that we behave somewhat differently than the built-in escapeshellarg()
* with respect to whitespace replacement in order
*
* @param string $arg The argument to escape
*
* @return string The escaped string
*/
public static function linuxArg($arg)
{
// For single quotes existing in the string, we will "exit"
// single-quote mode, add a \' and then "re-enter"
// single-quote mode. The result of this is that
// 'quote' becomes '\''quote'\''
$arg = preg_replace('/\'/', '\'\\\'\'', $arg);
// Replace "\t", "\n", "\r", "\0", "\x0B" with a whitespace.
// Note that this replacement makes Drush's escapeshellarg work differently
// than the built-in escapeshellarg in PHP on Linux, as these characters
// usually are NOT replaced. However, this was done deliberately to be more
// conservative when running _drush_escapeshellarg_linux on Windows
// (this can happen when generating a command to run on a remote Linux server.)
//
// TODO: Perhaps we should only do this if the local system is Windows?
// n.b. that would be a little more complicated to test.
$arg = str_replace(["\t", "\n", "\r", "\0", "\x0B"], ' ', $arg);
// Add surrounding quotes.
$arg = "'" . $arg . "'";
return $arg;
}
/**
* windowsArg is the Windows version of escapeshellarg().
*
* @param string $arg The argument to escape
*
* @return string The escaped string
*/
public static function windowsArg($arg)
{
if ('' === $arg || null === $arg) {
return '""';
}
if (false !== strpos($arg, "\0")) {
$arg = str_replace("\0", '?', $arg);
}
if (!preg_match('/[\/()%!^"<>&|\s]/', $arg)) {
return $arg;
}
// Double up existing backslashes
$arg = preg_replace('/(\\\\+)$/', '$1$1', $arg);
// Replacing whitespace for good measure (see comment above).
$arg = str_replace(["\t", "\n", "\r", "\0", "\x0B"], ' ', $arg);
$arg = str_replace(['"', '^', '%', '!'], ['""', '"^^"', '"^%"', '"^!"'], $arg);
// Add surrounding quotes.
$arg = '"' . $arg . '"';
return $arg;
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace Consolidation\SiteProcess\Util;
use Symfony\Component\Process\Process;
use Consolidation\Config\Util\Interpolator;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\NullOutput;
/**
* RealtimeOutput can be provided to a process object when you want
* to display the output of the running command as it is being produced.
*/
class RealtimeOutputHandler
{
protected $stdout;
protected $stderr;
protected $stdoutMarker = '';
protected $stderrMarker = '';
/**
* Provide the output streams to use for stdout and stderr
*/
const MARKER_ERR = '> ';
public function __construct(OutputInterface $stdout, OutputInterface $stderr)
{
$this->stdout = $stdout;
$this->stderr = $stderr;
$this->stdoutMarker = '';
$this->stderrMarker = self::MARKER_ERR;
}
/**
* This gives us an opportunity to adapt to the settings of the
* process object (e.g. do we need to do anything differently if
* it is in tty mode, etc.)
*/
public function configure(Process $process)
{
return $this;
}
/**
* setStderrMarker defines the string that should be added at
* the beginning of every line of stderr that is printed.
*/
public function setStderrMarker($marker)
{
$this->stderrMarker = $marker;
return $this;
}
/**
* setStdoutMarker defines the string that should be added at
* the beginning of every line of stdout that is printed.
*/
public function setStdoutMarker($marker)
{
$this->stdoutMarker = $marker;
return $this;
}
/**
* hideStdout overrides whatever was formerly stored in $this->stdout
* with a null output buffer so that none of the standard output data
* is visible.
*/
public function hideStdout()
{
$this->stdout = new NullOutput();
$this->stdoutMarker = '';
return $this;
}
/**
* hideStderr serves the same function as hideStdout, but for the
* standard error stream. Note that it is not useful to unconditionally
* call both hideStdout and hideStderr; if no output is desired, then
* the RealtimeOutputHandler should not be used.
*/
public function hideStderr()
{
$this->stderr = new NullOutput();
$this->stderrMarker = '';
return $this;
}
/**
* If this object is used as a callable, then run 'handleOutput'.
*/
public function __invoke($type, $buffer)
{
$this->handleOutput($type, $buffer);
}
/**
* Helper method when you want real-time output from a Process call.
* @param string $type
* @param string $buffer
*/
public function handleOutput($type, $buffer)
{
if (Process::ERR === $type) {
$this->stderr->write($this->addMarker($buffer, $this->stderrMarker), false, OutputInterface::OUTPUT_RAW);
} else {
$this->stdout->write($this->addMarker($buffer, $this->stdoutMarker), false, OutputInterface::OUTPUT_RAW);
}
}
/**
* Make sure that every line in $buffer begins with a MARKER_ERR.
*/
protected function addMarker($buffer, $marker)
{
// Exit early if there is no marker to add
if (empty($marker)) {
return $buffer;
}
// Add a marker on the beginning of every line.
return $marker . rtrim(implode("\n" . $marker, explode("\n", $buffer)), $marker);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Consolidation\SiteProcess\Util;
/**
* Shell::op is a static factory that will create shell operators for use
* in command line arguments list. Shell operators are characters that have
* special meaning to the shell, such as "output redirection". When a shell
* operator object is used, it indicates that this element is intended to
* be used as an operator, and is not simply some other parameter to be escaped.
*/
class Shell implements ShellOperatorInterface
{
protected $value;
public static function op($operator)
{
static::validateOp($operator);
return new self($operator);
}
public static function preEscaped($value)
{
return new self($value);
}
public function __construct($value)
{
$this->value = $value;
}
public function __toString()
{
return $this->value;
}
protected static function validateOp($operator)
{
$valid = [
'&&',
'||',
'|',
'<',
'>',
'>>',
';',
];
if (!in_array($operator, $valid)) {
throw new \Exception($operator . ' is not a valid shell operator.');
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Consolidation\SiteProcess\Util;
/**
* ShellOperatorInterface is a marker interface indicating that the object
* represents a shell operator.
*/
interface ShellOperatorInterface
{
}

View File

@ -0,0 +1,27 @@
<?php
namespace Consolidation\SiteProcess\Util;
use Symfony\Component\Process\Process;
/**
* Wrapper for universal support of TTY-related functionality across versions of
* Symfony Process.
*/
class Tty
{
/**
* In Symfony Process 4+, this is simply a wrapper for Process::isTtySupported().
* In lower versions, it mimics the same functionality.
*/
public static function isTtySupported()
{
// Start off by checking STDIN with `posix_isatty`, as that appears to be more reliable
if (function_exists('posix_isatty')) {
return posix_isatty(STDIN);
}
if (method_exists('\Symfony\Component\Process\Process', 'isTtySupported')) {
return Process::isTtySupported();
}
return (bool) @proc_open('echo 1 >/dev/null', array(array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w')), $pipes);
}
}