devel_wizard-2.x-dev/src/Utils.php

src/Utils.php
<?php

declare(strict_types=1);

namespace Drupal\devel_wizard;

use Drupal\Component\Serialization\Yaml as Yaml;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Entity\ConfigEntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Psr\Log\LogLevel;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\Finder\Finder;
use Symfony\Component\String\UnicodeString;
use Symfony\Component\Yaml\Yaml as SymfonyYaml;

class Utils {

  /**
   * @todo Enum.
   */
  public const DRUPAL_PROJECT_TYPES = [
    'drush',
    'profile',
    'module',
    'theme',
    'engine',
  ];

  /**
   * Matches lowercase letters, numbers, underscores.
   */
  public const MACHINE_NAME_REGEXP = '/^[a-z][a-z0-9_]*$/';

  protected Filesystem $fs;

  public static function alwaysFalse(): false {
    return FALSE;
  }

  public function __construct(?Filesystem $fs = NULL) {
    $this->fs = $fs ?: new Filesystem();
  }

  public function selfProjectRootDir(): string {
    return dirname(__DIR__);
  }

  public function getJsonEncodeFlags(): int {
    return \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE;
  }

  public function stringVariants(string $string, string $prefix): array {
    return [
      "{$prefix}LowerSnake" => (new UnicodeString($string))
        ->snake()
        ->lower()
        ->toString(),
      "{$prefix}UpperSnake" => (new UnicodeString($string))
        ->snake()
        ->upper()
        ->toString(),
      "{$prefix}LowerDash" => (new UnicodeString($string))
        ->snake()
        ->lower()
        ->replace('_', '-')
        ->toString(),
      "{$prefix}UpperDash" => (new UnicodeString($string))
        ->snake()
        ->upper()
        ->replace('_', '-')
        ->toString(),
      "{$prefix}LowerCamel" => (new UnicodeString($string))
        ->camel()
        ->toString(),
      "{$prefix}UpperCamel" => (new UnicodeString("a_$string"))
        ->camel()
        ->trimPrefix('a')
        ->toString(),
    ];
  }

  public function logLevelByExpiryDate(
    \DateTimeInterface $expiry_date,
    int $warning_days = 180,
    int $error_day = 90,
  ): string {
    $now = new \DateTimeImmutable();
    $diff = $expiry_date->diff($now);

    if ($diff->invert === 0 || $diff->days < $error_day) {
      // Already in the past or in the near future.
      return LogLevel::ERROR;
    }

    if ($diff->invert === 1 && $diff->days < $warning_days) {
      // In the future but not far enough.
      return LogLevel::WARNING;
    }

    return LogLevel::INFO;
  }

  public function formatByExpiryDate(
    string $text,
    \DateTimeInterface $expiry_date,
    int $warning_days = 180,
    int $error_day = 90
  ): string {
    return $this->formatByLogLevel(
      $text,
      $this->logLevelByExpiryDate($expiry_date, $warning_days, $error_day),
    );
  }

  public function formatByLogLevel(
    string $text,
    string $log_level,
  ): string {
    if (php_sapi_name() !== 'cli') {
      return $text;
    }

    return match($log_level) {
      'emergency',
      'alert',
      'critical',
      'error' => "<fg=red>$text</>",
      'warning' => "<fg=yellow>$text</>",
      default => $text,
    };
  }

  /**
   * @link https://www.php.net/supported-versions.php
   */
  public function phpVersions(): array {
    return [
      '8.3' => [
        'version' => '8.3',
        'release' => new \DateTimeImmutable('2023-11-23'),
        'support' => new \DateTimeImmutable('2025-12-31'),
        'security' => new \DateTimeImmutable('2027-12-31'),
        'features' => [
          'typed class constants',
        ],
      ],
      '8.2' => [
        'version' => '8.2',
        'release' => new \DateTimeImmutable('2022-12-08'),
        'support' => new \DateTimeImmutable('2024-12-08'),
        'security' => new \DateTimeImmutable('2025-12-08'),
        'features' => [
          '"readonly" classes',
          '"true" type',
          'standalone "null" and "false"',
        ],
      ],
      '8.1' => [
        'version' => '8.1',
        'release' => new \DateTimeImmutable('2021-11-25'),
        'support' => new \DateTimeImmutable('2023-11-25'),
        'security' => new \DateTimeImmutable('2024-11-25'),
        'features' => [
          'enumerations',
          '"never" return type',
          'intersection types',
          '"readonly" properties',
        ],
      ],
      '8.0' => [
        'version' => '8.0',
        'release' => new \DateTimeImmutable('2020-11-30'),
        'support' => new \DateTimeImmutable('2022-11-30'),
        'security' => new \DateTimeImmutable('2023-11-30'),
        'features' => [
          'union types',
          'named arguments',
          'attributes',
          'match expression',
        ],
      ],
      '7.4' => [
        'version' => '7.4',
        'release' => new \DateTimeImmutable('2019-11-28'),
        'support' => new \DateTimeImmutable('2021-11-28'),
        'security' => new \DateTimeImmutable('2022-11-28'),
        'features' => [
          'typed properties',
          'short closures',
          'null coalescing operator',
        ],
      ],
      '7.3' => [
        'version' => '7.3',
        'release' => new \DateTimeImmutable('2018-12-06'),
        'support' => new \DateTimeImmutable('2020-12-06'),
        'security' => new \DateTimeImmutable('2021-12-06'),
        'features' => [
          'references in list assignments',
          'flexible heredocs',
        ],
      ],
    ];
  }

  public function phpVersionChoices(
    ?array $php_versions = NULL,
    string $pattern = '{version} - active: {support}; security: {security}'
  ): array {
    if ($php_versions === NULL) {
      $php_versions = $this->phpVersions();
    }

    $choices = [];
    $date_formatter = \Drupal::getContainer()->get('date.formatter');
    foreach ($php_versions as $php) {
      $args = [];
      foreach ($php as $key => $value) {
        switch (gettype($value)) {
          case 'array':
            $args["{{$key}}"] = implode(', ', $value);
            break;

          case 'object':
            if ($value instanceof \DateTimeInterface) {
              $date = $date_formatter->format($value->getTimestamp(), 'html_date');
              $args["{{$key}}"] = $key === 'release' ? $date : Utils::formatByExpiryDate($date, $value);
            }
            break;

          default:
            $args["{{$key}}"] = $value;
            break;
        }
      }
      $choices[$php['version']] = strtr($pattern, $args);
    }

    return $choices;
  }

  /**
   * @return \Symfony\Component\Console\Question\Question[]
   */
  public function questionsPhpMinimum(): array {
    $questions = [];

    $questions['php_minimum'] = (new ChoiceQuestion('Minimum PHP version', Utils::phpVersionChoices(), '7.4'))
      ->setValidator([$this, 'validateRequired']);

    return $questions;
  }

  /**
   * @return string[]
   */
  public function projectDestinationChoices(
    string $type,
    string $rootProjectDir,
    string $drupalRootDir
  ): array {
    assert(
      in_array($type, static::DRUPAL_PROJECT_TYPES),
      "invalid project type: '$type'",
    );

    $choices = [];

    $vendorDrupalDir = "$rootProjectDir/../../drupal";
    $fs = new Filesystem();
    if ($fs->exists($vendorDrupalDir)) {
      $choices[] = $fs->makePathRelative($vendorDrupalDir, $drupalRootDir);
    }

    switch ($type) {
      case 'profile':
      case 'module':
      case 'theme':
        $choices[] = $fs->makePathRelative("$drupalRootDir/{$type}s/custom", $drupalRootDir);
        break;

      case 'theme_engine':
        $choices[] = $fs->makePathRelative("$drupalRootDir/themes/custom", $drupalRootDir);
        break;
    }

    return $choices;
  }

  /**
   * @return array<string, string>
   */
  public function gitTemplateChoices(string $home): array {
    return ['' => '- Default - '] + $this->dirsToOptions($this->getAvailableGitTemplateDirs($home));
  }

  /**
   * @return iterable<string, \Symfony\Component\Finder\SplFileInfo>
   */
  public function getAvailableGitTemplateDirs(string $home): iterable {
    return (new Finder())
      ->in("$home/Templates/Git/")
      ->directories()
      ->depth(0);
  }

  /**
   * @param iterable<string, \Symfony\Component\Finder\SplFileInfo> $dirs
   *
   * @return array<string, string>
   */
  public function dirsToOptions(iterable $dirs): array {
    $options = [];
    foreach ($dirs as $dir) {
      $options[$dir->getPathname()] = $dir->getBasename();
    }

    return $options;
  }

  public function getStackedValidator(callable ...$validators): callable {
    return function ($input) use ($validators) {
      foreach ($validators as $validator) {
        $input = $validator($input);
      }

      return $input;
    };
  }

  public function getConfigEntityIdExistsValidator(string $message, ConfigEntityStorageInterface $storage, bool $hasToBeExists = FALSE): callable {
    return function ($value) use ($message, $storage, $hasToBeExists) {
      if ($value === NULL || $value === '') {
        return $value;
      }

      $msgText = '';
      $entity = $storage->load($value);
      if ($entity && !$hasToBeExists) {
        $msgText = '@message: @entity_type_label %machine_name already exists';
      }
      elseif (!$entity && $hasToBeExists) {
        $msgText = '@message: @entity_type_label %machine_name not exists';
      }

      if ($msgText) {
        $msgArgs = [
          '@message' => $message,
          '@entity_type_label' => $storage->getEntityType()->getLabel(),
          '%machine_name' => $value,
        ];

        throw new \InvalidArgumentException(strtr($msgText, $msgArgs), 1);
      }

      return $value;
    };
  }

  public function getRequiredValidator(string $message): callable {
    return function ($input) use ($message) {
      // FALSE is not considered as empty value because question helper uses
      // it as negative answer on confirmation questions.
      if ($input === NULL || $input === '') {
        throw new \UnexpectedValueException($message);
      }

      return $input;
    };
  }

  public function getRegexpValidator(string $message, string $pattern, bool $invert = FALSE): callable {
    return function ($input) use ($message, $pattern, $invert) {
      if ($input === NULL || $input === '') {
        return $input;
      }

      $valid = (bool) preg_match($pattern, $input);
      $valid = $invert ? !$valid : $valid;
      if ($valid) {
        return $input;
      }

      throw new \InvalidArgumentException(
        strtr(
          '@message; pattern @pattern does not match to input: @input',
          [
            '@message' => $message,
            '@pattern' => $pattern,
            '@input' => $input,
          ],
        ),
        1,
      );
    };
  }

  /**
   * @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $entities
   *
   * @return string[]
   */
  public function configEntityChoices(iterable $entities): array {
    $choices = [];
    foreach ($entities as $entity) {
      $choices[$entity->id()] = $entity->label();
    }

    return $choices;
  }

  /**
   * @param \Drupal\views\Entity\View[] $views
   * @param string $path
   */
  public function findViewDisplayByPath(iterable $views, string $path): ?array {
    foreach ($views as $view) {
      foreach ($view->get('display') as $displayId => $display) {
        $actual = $display['display_options']['path'] ?? NULL;
        if ($actual === $path) {
          return [
            'view' => $view,
            'display_id' => $displayId,
          ];
        }
      }
    }

    return NULL;
  }

  public function openFileUrl(string $fileName, int $line = 0, int $column = 0, string $basePath = '') {
    if ($basePath === '') {
      $basePath = getcwd();
    }

    return strtr(
      ini_get('xdebug.file_link_format') ?: 'phpstorm://open?file=%f&line=%l&column=%c',
      [
        '%f' => urlencode(Path::makeAbsolute($fileName, $basePath)),
        '%l' => $line,
        '%c' => $column,
      ],
    );
  }

  public function className(string $fqn): string {
    return preg_replace('@^.*\\\\@', '', $fqn);
  }

  /**
   * @param string $namespace
   *   Fully qualified name.
   *
   * @return string
   *   Relative path from the MODULE/src directory.
   */
  public function relativeNamespaceDir(string $namespace): string {
    $namespaceParts = array_slice(explode('\\', $namespace), 2);

    return Path::join(...$namespaceParts) ?: '.';
  }

  /**
   * @param string $classFqn
   *   Class FQN "Drupal\my_module\Foo\Bar\Something".
   *
   * @return string
   *   File path "src/Foo/Bar/Something.php".
   *
   * @code
   * \Symfony\Component\Filesystem\Path::join(
   *   $moduleDir,
   *   $utils->classFqnToFilePath($classFqn),
   * )
   * @endcode
   */
  public function classFqnToFilePath(string $classFqn): string {
    return Path::join(
      'src',
      $this->relativeNamespaceDir($classFqn) . '.php',
    );
  }

  public function addUseStatements(array $useStatements, string $fileContent): string {
    $afterThis = "declare(strict_types=1);\n";
    $pos = strpos($fileContent, $afterThis);
    if ($pos !== FALSE) {
      $pos += mb_strlen($afterThis);
    }
    else {
      $pos = strpos($fileContent, "\nuse ");
      if ($pos !== FALSE) {
        $pos -= 5;
      }
    }

    if ($pos === FALSE) {
      // @todo Better detection.
      return $fileContent;
    }

    // @todo Sort.
    foreach (array_reverse($useStatements) as $useStatement) {
      if (strpos($fileContent, $useStatement) !== FALSE) {
        continue;
      }

      $fileContent = substr_replace(
        $fileContent,
        "$useStatement\n",
        $pos,
        0,
      );
    }

    return $fileContent;
  }

  public function inputName(string|array ...$parts): string {
    $parents = [];
    foreach ($parts as $part) {
      if (is_string($part)) {
        $parents[] = $part;

        continue;
      }

      $parents = array_merge($parents, $part);
    }

    $name = array_shift($parents);
    if ($parents) {
      $name .= '[' . implode('][', $parents) . ']';
    }

    return $name;
  }

  public function detectExtensionMachineName(string $dir): ?string {
    $fs = new Filesystem();
    $filename = Path::join($dir, 'composer.json');
    $project_type = 'module';
    if ($fs->exists($filename)) {
      $composer = \json_decode(
        file_get_contents($filename) ?: '{}',
        TRUE,
      );
      if (isset($composer['type'])) {
        $project_type = preg_replace('/^drupal-/', '', $composer['type']);
      }

      $project_name = $composer['name'] ?? '';
      if (preg_match('@^drupal/@', $project_name) === 1) {
        return str_replace(
          '-',
          '_',
          preg_replace('@^drupal/@', '', $project_name),
        );
      }

      if ($project_type === 'project') {
        return 'app';
      }
    }

    $info_files = (new Finder())
      ->in($dir)
      ->files()
      ->name('*.info.yml')
      ->depth(0)
      ->getIterator();

    $info_files->rewind();
    /** @var \Symfony\Component\Finder\SplFileInfo $info_file */
    $info_file = $info_files->current();

    return $info_file?->getBasename('.info.yml');
  }

  public function initDrushInProject(string $projectRoot) {
    $composerJsonFileName = Path::join($projectRoot, 'composer.json');
    $composerJson = json_decode(
      file_get_contents($composerJsonFileName) ?: '{}',
      TRUE,
    );
    if (!empty($composerJson['extra']['drush'])) {
      return;
    }

    $composerJson['extra']['drush'] = [
      'services' => [
        'drush.services.yml' => '^11 || ^12',
      ],
    ];

    $this->fs->dumpFile(
      $composerJsonFileName,
      json_encode($composerJson, $this->getJsonEncodeFlags()),
    );
  }

  /**
   * Replaces elements in the $fileName YAML file.
   */
  public function ymlFileReplace(string $filePath, array $entries, array $parents = []): array {
    $report = [
      '@num_of_added' => 0,
      '@num_of_updated' => 0,
    ];

    if (!$entries) {
      return $report;
    }

    // @todo Comment friendly way.
    $content = $this->fs->exists($filePath) ? file_get_contents($filePath) : '{}';
    $root = Yaml::decode($content);
    if (!NestedArray::keyExists($root, $parents)) {
      NestedArray::setValue($root, $parents, []);
    }
    $data =& NestedArray::getValue($root, $parents);
    $countBefore = count($data);
    foreach ($entries as $entryId => $entry) {
      $data[$entryId] = $entry;
    }

    $this->fs->mkdir(Path::getDirectory($filePath));
    $this->fs->dumpFile($filePath, SymfonyYaml::dump($root, 999, 2));

    $report['@num_of_added'] = count($data) - $countBefore;
    $report['@num_of_updated'] = count($entries) - $report['@num_of_added'];

    return $report;
  }

  public function getEntityTypeInfo(EntityTypeInterface $entityType): array {
    $idUpperCamel = (new UnicodeString('a_' . $entityType->id()))
      ->camel()
      ->trimPrefix('a')
      ->toString();

    $info = [
      'id' => $entityType->id(),
      'idUpperCamel' => $idUpperCamel,
      'definition' => $entityType,
      'interface' => NULL,
      'interfaceFqn' => NULL,
    ];

    $class = $entityType->getClass();
    $implements = class_implements($class);
    foreach ($implements as $interfaceFqn) {
      if (str_ends_with($interfaceFqn, "\\{$idUpperCamel}Interface")) {
        $info['interface'] = "{$idUpperCamel}Interface";
        $info['interfaceFqn'] = $interfaceFqn;
      }
    }

    return $info;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc