Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
meltmedia/meltconsole / src / App / Commands / MeltCommand.php
Size: Mime:
<?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());
  }

}