fox-1.1.2/src/Drush/Commands/FoxCommands.php
src/Drush/Commands/FoxCommands.php
<?php
namespace Drupal\fox\Drush\Commands;
use Consolidation\SiteAlias\SiteAliasManagerAwareInterface;
use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\fox\FoxCommandsHelper as Helper;
use Drupal\fox\FoxCommandsHelperTrait;
use Drupal\fox\FoxEntityQuery;
use Drupal\fox\Plugin\FoxCommandsManager;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* A Drush commandfile.
*
* In addition to this file, you need a drush.services.yml
* in root of your module, and a composer.json file that provides the name
* of the services file to use.
*/
class FoxCommands extends DrushCommands implements SiteAliasManagerAwareInterface {
use FoxCommandsHelperTrait;
use SiteAliasManagerAwareTrait;
/**
* Options list.
*
* @var array
*/
private $options;
/**
* Variables list.
*
* @var array
*/
private array $variables = [];
/**
* Commands history.
*
* @var array
*/
private array $history = [];
/**
* Constructs a FoxCommands object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly FoxCommandsManager $foxCommandsManager,
private readonly FoxEntityQuery $foxEntityQuery,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('plugin.manager.fox_commands'),
$container->get('fox.query.entity'),
);
}
#[CLI\Command(name: 'fox:console', aliases: ['fox'])]
#[CLI\Option(name: 'input', description: 'Input commands: "command1;command2;..." or FILE')]
#[CLI\Option(name: 'output', description: 'Output command history [FILE]')]
#[CLI\Option(name: 'mode', description: 'Command mode: [default]|debug')]
#[CLI\Option(name: 'quit', description: 'Non-interactive mode')]
/**
* Console function.
*/
public function console(
array $options = [
'input' => '',
'output' => '',
'mode' => 'default',
],
) {
$this->options = $options;
$quit_mode = !empty($options['quit']);
if (!$quit_mode) {
$this->io()->info(dt("Welcome to the Fox console!\nType \"exit\" or \"quit\" to quit."));
}
// Variables for executing external drush commands.
$this->variables['self'] = $this->siteAliasManager()->getSelf();
$this->variables['process_manager'] = $this->processManager();
// Enter the interactive loop.
while (TRUE) {
$rows = $this->foxInput();
$need_exit = FALSE;
foreach ($rows as $row) {
if ($need_exit = $this->runCommands($row)) {
break;
}
}
if ($need_exit || $quit_mode) {
break;
}
}
// Exiting.
if (!$quit_mode) {
$this->io()->info(dt('Exiting the Fox console.'));
$this->foxOutput();
}
}
/**
* Input by user, commands or file.
*
* @return array
* Commands rows.
*/
protected function foxInput(): array {
$rows = [];
if (is_string($this->options['input'])) {
$file = Helper::getFilePath($this->options['input']);
if (file_exists($file)) {
$rows = Helper::loadFile($file);
}
else {
$rows[] = htmlspecialchars_decode(trim($this->options['input']));
}
$this->options['input'] = NULL;
}
else {
$input = $this->readlineWithHistory('Enter a command:');
$params = $this->getFoxCommandParameters($input);
$command = strtolower($params[0]);
// Load command.
if ($command === 'load') {
$result = $this->parseFoxCommands($input);
if (!empty($result)) {
if (isset($result['commands'])) {
$rows = $result['commands'];
}
$success = $this->printResult($result);
if ($success) {
$this->history[] = $input;
}
}
}
else {
$rows[] = $input;
}
}
return $rows;
}
/**
* Output commands.
*/
protected function foxOutput() {
if (!empty($this->options['output'])) {
if (is_string($this->options['output'])) {
// Output to file.
$file = Helper::getFilePath($this->options['output']);
if (file_exists($file)) {
$confirmed = $this->io()->confirm(dt('File @file is exist, can overwrite it?', ['@file' => $file]));
if (!$confirmed) {
return;
}
}
$output = implode("\n", $this->history);
file_put_contents($file, $output);
$this->io()->info(dt('Command history was saved to @file', ['@file' => $file]));
}
else {
$header = [
'#',
dt('Command'),
];
$output = [];
foreach ($this->history as $number => $data) {
$output[] = [$number + 1, $data];
}
$this->io()->table($header, $output);
}
}
}
/**
* Run readline.
*
* @param string $prompt
* Readline prompt.
*
* @return string
* Readline input.
*/
protected function readlineWithHistory(string $prompt): string {
readline_completion_function([$this, 'autocompleteHistory']);
$this->io()->writeln("\033[1;32m" . $prompt . "\033[0m");
$input = readline('> ');
if ($input !== FALSE && $input !== '') {
readline_add_history($input);
}
return trim($input);
}
/**
* History autocomplete.
*
* @param string $input
* Readline input.
* @param int $index
* History index.
*
* @return array
* Autocomplete result.
*/
protected function autocompleteHistory(string $input, int $index): array {
if ($input === '') {
return $this->history;
}
$matches = array_values(array_filter($this->history, fn($cmd) => str_starts_with($cmd, $input)));
return $matches;
}
/**
* Run Fox commands.
*
* @param string $row
* Input string with commands.
*
* @return bool
* Need to exit.
*/
protected function runCommands($row) {
// All commands.
$commands = array_filter(explode(';', $row));
foreach ($commands as $command) {
$command = trim($command);
// Evaluate condition.
$this->parseCondition($command);
if (empty($command)) {
continue;
}
// Check for exit condition.
if (in_array(strtolower($command), ['exit', 'quit'])) {
return TRUE;
}
$completed = FALSE;
// Fox commands.
if (!$completed) {
$result = $this->parseFoxCommands($command);
if (!empty($result)) {
$this->setVariables($result);
$success = $this->printResult($result);
if ($success) {
$this->history[] = $command;
}
$completed = TRUE;
}
}
// Run Entity Query.
if (!$completed) {
$result = $this->foxEntityQuery->doEntityQuery($command, $this->variables, $this->options);
if (!empty($result)) {
$this->setVariables($result);
$success = $this->printResult($result);
if ($success) {
$this->history[] = $command;
}
$completed = TRUE;
}
}
// Print variables.
if (empty($result['hide_variables'])) {
$this->printVariables();
}
}
}
/**
* Fox commands parse.
*
* @param string $input
* User input.
*
* @return mixed
* Result of parsing.
*/
protected function parseFoxCommands(string $input) {
$foxCommands = $this->foxCommandsManager->getDefinitions();
$params = $this->getFoxCommandParameters($input);
$command = strtolower($params[0]);
if (!empty($foxCommands[$command])) {
array_shift($params);
$instance = $this->foxCommandsManager->createInstance($command);
$result = $instance->execute($params, $this->variables, $this->options);
return $result;
}
return NULL;
}
/**
* Get Fox command parameters.
*
* @param string $input
* Input string.
*
* @return array
* Parameters array.
*/
protected function getFoxCommandParameters($input): array {
$params = array_filter(explode(' ', $input), function ($value) {
if (is_numeric($value)) {
return TRUE;
}
return !empty($value);
});
return $params;
}
/**
* Print variables.
*/
protected function printVariables(): void {
$message = '';
foreach ($this->variables as $name => $value) {
if (!is_object($value) && !is_array($value) && !is_null($value)) {
$message .= "$name=" . Helper::getVariable($value) . ' ';
}
}
$this->output->writeln($message);
}
/**
* Print result.
*
* @param array $result
* Command result.
*
* @return bool
* Success result output.
*/
protected function printResult(array $result): bool {
if (empty($result)) {
return TRUE;
}
$this->variables['last_error'] = NULL;
if (isset($result['error'])) {
$this->variables['last_error'] = TRUE;
$this->io()->error($result['error']);
return FALSE;
}
if (isset($result['warning'])) {
$this->io()->warning($result['warning']);
}
if (isset($result['debug'])) {
$this->io()->info($result['debug']);
}
$message = $result['message'] ?? NULL;
if (!empty($message)) {
if (is_array($message)) {
$keys = array_keys($message);
if (count($keys) >= 2 && in_array('header', $keys) && in_array('data', $keys)) {
$message = [$message];
}
$this->printTable($message);
}
else {
$this->output()->writeln($message);
}
return TRUE;
}
return TRUE;
}
/**
* Print table.
*
* @param array $arr
* Data for table output.
*/
protected function printTable(array $arr): void {
foreach ($arr as $index => $data) {
if (is_string($index)) {
$this->output()->writeln($index);
}
if (is_object($data)) {
$data = (array) $data;
}
if (!isset($data['data'])) {
if (is_string($data)) {
$this->output()->writeln($data);
}
}
else {
if (!empty($data['horizontal'])) {
$this->io()->horizontalTable($data['header'], $data['data']);
}
else {
$this->io()->table($data['header'], $data['data']);
}
}
}
}
/**
* Set local variables.
*
* @param array $result
* Command result.
*/
protected function setVariables(array $result): void {
$variables = $result['variables'] ?? NULL;
if (!$variables) {
return;
}
foreach ($variables as $name => $value) {
if (is_array($value)) {
foreach ($value as $name2 => $value2) {
$this->variables[$name2] = $value2;
}
}
elseif (!is_object($value)) {
$this->variables[$name] = $value;
}
}
}
/**
* Parse condition expression.
*
* @param string $input
* Input value.
*/
protected function parseCondition(&$input) {
// Condition IF THEN ELSE.
$pattern = '/\bIF\s+(.*?)\s+THEN\s+(.*?)(?:\s+ELSE\s+(.*))?$/i';
if (preg_match($pattern, $input, $matches)) {
$condition = trim($matches[1]);
$then = trim($matches[2]);
$else = isset($matches[3]) ? trim($matches[3]) : NULL;
$eval_condition = $this->evalCondition($condition);
if (is_bool($eval_condition)) {
if ($eval_condition === TRUE) {
$input = $then;
}
elseif ($eval_condition === FALSE) {
$input = $else ? $else : '';
}
}
else {
$this->io()->error(dt('Error condition parsing'));
}
}
}
/**
* Evaluate condition.
*
* @param string $condition
* Condition value.
*
* @return bool|int
* Boolean expression or error status.
*/
protected function evalCondition($condition) {
preg_match_all('/@([a-zA-Z0-9_.]+)/', $condition, $matches);
$vars = $matches[0];
if (empty($vars)) {
return 0;
}
$params = [];
foreach ($vars as $var) {
$new_var = $this->foxCommandsHelper()->stringRender($var, $this->variables);
if ($new_var != $var) {
$key = ':' . substr($var, 1);
$key = str_replace('.', '_', $key);
$params[$key] = $new_var;
}
}
if (empty($params)) {
return 0;
}
$condition = str_replace('@', ':', $condition);
$condition = str_replace('.', '_', $condition);
$database = \Drupal::database();
try {
$query = $database->query("SELECT uid FROM {users} WHERE uid=0 AND ($condition)", $params);
$result = $query->fetchAll();
}
catch (\Exception $e) {
$this->io()->error($e->getMessage());
return 0;
}
return count($result) > 0;
}
}
