Repository URL to install this package:
|
Version:
2.2.4 ▾
|
<?php
namespace MeltConsole\App\Commands;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Filesystem\Exception\ExceptionInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Logger\ConsoleLogger;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Twig\Loader\FilesystemLoader;
use Twig\Environment;
/**
* Master class for all other commands.
*
* All Commands will extend off of this command. This class does nothing other
* than provide a ton of helper functions other Commands can use.
*
* @TODO may need to separate all of these helper functions into their own Trait?
*
* @package MeltConsole\App\Commands
*/
class MeltCommand extends Command {
protected $appName = 'meltconsole';
protected $templatesDir = __DIR__ . '/../../../templates';
protected $projectDir = __DIR__ . '/../../..';
protected $pathToDrushAlias = 'drush/sites/melt.site.yml';
protected $projectName;
protected $serverRoot;
protected $isLandoEnv;
/**
* Logger.
*
* @var \Symfony\Component\Console\Logger\ConsoleLogger
*/
protected $logger;
/**
* Config file.
*
* File used to store configuration we use for things like
* git url and ssh credentials.
*
* @var string
*/
protected $configFile = '.lando.local.yml';
/**
* Config from @var $configFile.
*
* @var config
*/
protected $config;
/**
* {@inheritdoc}
*
* Initializes variables to be re-used in other classes that extend
* this as a base class.
*/
protected function initialize(InputInterface $input, OutputInterface $output) {
$this->logger = new ConsoleLogger($output);
$this->projectName = $this->getProjectName();
$this->serverRoot = $this->getServerRoot();
// Look for Lando related environment variable.
$this->isLandoEnv = getenv('LANDO_INFO');
}
/**
* Sets the @var $serverRoot.
*
* @param string $path
* Path to root.
*/
protected function getServerRoot(string $path = '/var/www/apps') {
$fs = new Filesystem();
if ($fs->exists($this->configFile)) {
$config = $this->parseYamlFile($this->configFile);
// Remove the beginning "/".
return ltrim($config['melt']['ssh']['root'], '/');
}
else {
return $path;
}
}
/**
* Helper function to wrap the Yaml::parsefile method.
*
* @param string $path
* Path to file.
*
* @return array|null
* What Yaml::parseFile returns;
*/
protected function parseYamlFile(string $path) {
try {
return Yaml::parseFile($path);
}
catch (ExceptionInterface $err) {
$this->logger->error($err->getMessage());
}
}
/**
* Get's the project name.
*
* Get's the project name from `name` property from
* the `.lando.yml` file.
*/
protected function getProjectName() {
$data = $this->parseYamlFile('.lando.yml');
return $data['name'];
}
/**
* Get's configuration from the @var $configFile file.
*
* @return false|array
* Parsed yaml data.
*/
protected function getConfig() {
$fs = new Filesystem();
if (!$fs->exists($this->configFile)) {
$this->logger->warning("Trying to get config and it looks like {$this->configFile} doesn't exist. You should run `melt:setup-local`.");
return FALSE;
}
$config = $this->parseYamlFile($this->configFile);
if (!isset($config['melt'])) {
throw new LogicException("Missing `melt` property in the {$this->configFile} file");
}
if (!isset($config['melt']['ssh']) && !isset($config['melt']['git'])) {
throw new LogicException("Values missing for `melt.ssh` and `melt.git` properties in the {$this->configFile} file");
}
return $config;
}
/**
* Checks if environment has a drush alias.
*
* @param string $env
* Environment.
*
* @return bool
* Returns a boolean.
*/
protected function environmentHasDrushAlias(string $env) {
$aliases = $this->getDrushAliases();
return isset($aliases[$env]);
}
/**
* Get's current aliases from @var $pathToDrushAlias.
*
* @return array
* Array of aliases.
*/
protected function getDrushAliases() {
$fs = new Filesystem();
// If the @var $pathToDrushAlias does not exists, use the template file.
if ($fs->exists($this->pathToDrushAlias)) {
$aliases = $this->parseYamlFile($this->pathToDrushAlias);
}
else {
// Create en example alias using the settings from
// the $configFile file.
$config = $this->parseYamlFile($this->configFile);
$ssh = $config['melt']['ssh'];
$aliases = [
'example' => [
'uri' => $ssh['uri'],
'host' => $ssh['host'],
'root' => $ssh['root'],
'user' => $ssh['user'],
'ssh' => [
'options' => '-p 22',
],
],
];
}
return $aliases;
}
/**
* Helper method to write associative arrays to YAML files.
*
* @param array $arr
* Associative array of YAML data.
* @param string $path
* Path to write file to.
*/
protected function writeToYamlFile(array $arr = [], string $path = "") {
$yaml = Yaml::dump($arr, 2, 2);
try {
$filesystem = new Filesystem();
$filesystem->dumpFile($path, $yaml);
}
catch (ExceptionInterface $err) {
$this->logger->log('error', $err->getMessage());
}
}
/**
* Writes to a file leveraging Twig templates on the server.
*
* @param string $src
* Source path.
* @param string $dest
* Destination path.
* @param array $data
* Associative array to pass to Twig renderer.
*/
protected function renderFile($src = '', $dest = '', $data = [], $env = null) {
if($env == 'server') {
$loaderPath = getenv('HOME') . '/.meltconsole/templates';
} else {
$loaderPath = __DIR__ . '/../../../templates';
}
$loader = new FilesystemLoader($loaderPath);
$twig = new Environment($loader);
$content = $twig->render($src, $data);
try {
$filesystem = new Filesystem();
$filesystem->dumpFile($dest, $content);
}
catch (ExceptionInterface $err) {
$this->logger->log('error', $err->getMessage());
}
}
/**
* Helper function for Synfony Process component.
*
* This uses Process::fromShellCommandline so we can chain commands together
* when running commands locally. When trying to use the `new Process()`
* class, we were getting errors:
* - Pseudo-terminal will not be allocated because stdin is not a terminal.
*
* @param array $commands
* Array of command(s).
* @param bool $show_stdout
* Show's the STDOUT.
*
* @example
* $process = $this->runLocalCommands([
* 'lando start'
* 'lando drush cr'
* ]);
*
* @return \Symfony\Component\Process\Process
* Returns the process.
*
* @see https://symfony.com/doc/current/components/process.html
*/
protected function runLocalCommands(array $commands = [], bool $show_stdout = TRUE) {
// Set delimiter to `&&` meaning commands don't
// run until the previous finished.
// See: https://stackoverflow.com/questions/6152659/bash-sh-difference-between-and
$delimiter = '&&';
// Add all of the chained commands together using $delimiter.
$chainedCommands = implode(" {$delimiter} ", $commands);
$process = Process::fromShellCommandline($chainedCommands);
// Set timeout for 10 mins.
$process->setTimeout(900);
if ($show_stdout) {
$process->run(function ($type, $buffer) {
echo $buffer;
});
}
else {
$process->run();
}
// Executes after the command finishes.
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process;
}
/**
* Helper function to run commands on the server using ssh.
*
* @param array $commands
* Array of command(s).
* @param bool $show_stdout
* Show's the STDOUT.
*
* @example
* $process = $this->runServerCommands([
* 'cd apps',
* 'git clone someproject@git.com my-project
* 'lando start'
* ]);
*
* @return \Symfony\Component\Process\Process
* Returns the process.
*
* @see https://symfony.com/doc/current/components/process.html
*/
protected function runServerCommands(array $commands = [], bool $show_stdout = TRUE) {
$config = $this->getConfig();
$ssh = $config['melt']['ssh'];
$user = $ssh['user'];
$host = $ssh['host'];
// Set delimiter to `&&` meaning commands don't
// run until the previous finished.
// See: https://stackoverflow.com/questions/6152659/bash-sh-difference-between-and
$delimiter = '&&';
$command = ['ssh', "{$user}@{$host}"];
// Add all of the chained commands together using $delimiter.
$chainedCommands = implode(" {$delimiter} ", $commands);
$command[] = $chainedCommands;
$process = new Process($command);
// Set timeout for 10 mins.
$process->setTimeout(900);
if ($show_stdout) {
$process->run(function ($type, $buffer) {
echo $buffer;
});
}
else {
$process->run();
}
// Executes after the command finishes.
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process;
}
/**
* Checks if Lando is running for our a particular environment.
*
* Uses `lando list` piped to `grep` to check if the environment we're looking
* for is in the list of running lando containers.
*
* @param string $env
* Environment name.
*
* @return bool
* Returns true/false.
*/
protected function isLandoRunning(string $env) {
$process = $this->runServerCommands([
"lando list | grep -w {$this->projectName}.{$env}",
], FALSE);
return !empty($process->getOutput());
}
/**
* Gets the directories from server.
*
* @return array
* Array of directories.
*/
protected function getDirectoryListingFromServer() {
// Connect to server and dump all of the environments.
$process = $this->runServerCommands([
"cd /{$this->serverRoot}",
"ls",
], FALSE);
// Turn string listing into an array of directories and
// filter out falsy values.
$dirs = array_filter(explode("\n", $process->getOutput()));
return $dirs;
}
/**
* Get's environment from server.
*
* SSH's into server, loops through all of the directories looking for ones
* that match the `$projectName` assuming they follow a <PROJECT>.<ENV>
* naming convention. e.g. meltmedia.dev.
*
* @return array
* Array of environments.
*/
protected function getEnvironmentsFromServer() {
$envs = [];
$dirs = $this->getDirectoryListingFromServer();
if (!empty($dirs)) {
// Loop through each directory and search for the naming convention:
// <PROJECT_NAME>-<PROJECT_ENV>.
foreach ($dirs as $dir) {
$split_dir = explode('-', $dir);
if (count($split_dir) > 1) {
$project_env = array_pop($split_dir);
$projectName = implode('-', $split_dir);
// Add the environment if we find a directory that
// matches our project name.
if ($projectName && $project_env && ($projectName == $this->getProjectName())) {
array_push($envs, $project_env);
}
}
}
}
return $envs;
}
/**
* Checks to see if the envrionment being passed already exists on the server.
*
* @param string $env
* Environment.
*
* @return bool
* True or false.
*/
protected function validateEnvironmentExistsOnServer(string $env) {
return in_array($env, $this->getEnvironmentsFromServer());
}
/**
* Gets list of remote branches.
*
* @return array
* Array of branch names
*/
protected function getRemoteBranches() {
$remoteHeads = $this->runLocalCommands([
'git ls-remote --heads',
], FALSE)->getOutput();
// @example output of ls-remote:
// 1df0924dc319efb84d96045f9382d1620f945d4a refs/heads/dev-build
// d4d653b9e9b5d6c437deca9575310648cb66394a refs/heads/master
//
// Use $matches to get array of all occurrences.
preg_match_all('/refs.*/', $remoteHeads, $matches);
$remote_branches = array_map(function ($branch) {
$split = explode('/', $branch);
return trim(end($split));
}, reset($matches));
return $remote_branches;
}
/**
* Get environment variable from server.
*
* @param string $var
* @return string
*/
protected function getServerEnvVar(string $var) {
$output = $this->runServerCommands([
'source ~/.meltconsole.env; echo $' . $var
], FALSE);
return trim($output->getOutput());
}
/**
* Get environment variable from local environment.
*
* @param string $var
* @return string
*/
protected function getLocalEnvVar(string $var) {
$output = $this->runLocalCommands([
'source ~/.meltconsole.env; echo $' . $var
], FALSE);
return trim($output->getOutput());
}
}