first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator;
use Composer\InstalledVersions;
use Drupal\Core\DependencyInjection\ContainerNotInitializedException;
use DrupalCodeGenerator\Command\Navigation;
use DrupalCodeGenerator\Event\GeneratorInfo;
use DrupalCodeGenerator\Event\GeneratorInfoAlter;
use DrupalCodeGenerator\Helper\Drupal\ConfigInfo;
use DrupalCodeGenerator\Helper\Drupal\HookInfo;
use DrupalCodeGenerator\Helper\Drupal\ModuleInfo;
use DrupalCodeGenerator\Helper\Drupal\PermissionInfo;
use DrupalCodeGenerator\Helper\Drupal\RouteInfo;
use DrupalCodeGenerator\Helper\Drupal\ServiceInfo;
use DrupalCodeGenerator\Helper\Drupal\ThemeInfo;
use DrupalCodeGenerator\Helper\Dumper\DryDumper;
use DrupalCodeGenerator\Helper\Dumper\FileSystemDumper;
use DrupalCodeGenerator\Helper\Printer\ListPrinter;
use DrupalCodeGenerator\Helper\Printer\TablePrinter;
use DrupalCodeGenerator\Helper\QuestionHelper;
use DrupalCodeGenerator\Helper\Renderer\TwigRenderer;
use DrupalCodeGenerator\Twig\TwigEnvironment;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem as SymfonyFileSystem;
use Twig\Loader\FilesystemLoader as TemplateLoader;
/**
* DCG console application.
*
* @psalm-suppress DeprecatedInterface
* @psalm-suppress DeprecatedTrait
*
* @todo Use Drupal replacement for ContainerAwareInterface when it's available.
* @see https://www.drupal.org/project/drupal/issues/3397522
*/
final class Application extends BaseApplication implements ContainerAwareInterface, EventDispatcherInterface {
use ContainerAwareTrait;
/**
* Path to DCG root directory.
*/
public const ROOT = __DIR__ . '/..';
/**
* DCG version.
*
* @deprecated Use \DrupalCodeGenerator\Application->getVersion() instead.
*/
public const VERSION = 'unknown';
/**
* DCG API version.
*/
public const API = 3;
/**
* Path to templates directory.
*/
public const TEMPLATE_PATH = self::ROOT . '/templates';
/**
* Creates the application.
*
* @psalm-suppress ArgumentTypeCoercion
*/
public static function create(ContainerInterface $container): self {
$application = new self(
'Drupal Code Generator',
InstalledVersions::getPrettyVersion('chi-teck/drupal-code-generator'),
);
$application->setContainer($container);
$file_system = new SymfonyFileSystem();
$template_loader = new TemplateLoader();
$template_loader->addPath(self::TEMPLATE_PATH . '/_lib', 'lib');
$application->setHelperSet(
new HelperSet([
new QuestionHelper(),
new DryDumper($file_system),
new FileSystemDumper($file_system),
new TwigRenderer(new TwigEnvironment($template_loader)),
new ListPrinter(),
new TablePrinter(),
new ModuleInfo($container->get('module_handler'), $container->get('extension.list.module')),
new ThemeInfo($container->get('theme_handler')),
new ServiceInfo($container),
new HookInfo($container->get('module_handler')),
new RouteInfo($container->get('router.route_provider')),
new ConfigInfo($container->get('config.factory')),
new PermissionInfo($container->get('user.permissions')),
]),
);
$generator_factory = new GeneratorFactory(
$application->getContainer()->get('class_resolver'),
);
$core_generators = $generator_factory->getGenerators();
$user_generators = [];
$application->dispatch(new GeneratorInfo($user_generators));
$all_generators = \array_merge($core_generators, $user_generators);
$application->addCommands(
$application->dispatch(new GeneratorInfoAlter($all_generators))->generators,
);
$application->add(new Navigation());
$application->setDefaultCommand('navigation');
/** @var \DrupalCodeGenerator\Application $application */
$application = $application->dispatch($application);
return $application;
}
/**
* Returns Drupal container.
*/
public function getContainer(): ContainerInterface {
if (!isset($this->container)) {
throw new ContainerNotInitializedException('Application::$container is not initialized yet.');
}
return $this->container;
}
/**
* {@inheritdoc}
*
* @template T as object
* @psalm-param T $event
* @psalm-return T
*
* @todo Remove this once Symfony drops support for event-dispatcher-contracts v2.
* @see \Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch()
* @psalm-suppress UnusedPsalmSuppress
* @psalm-suppress InvalidReturnType
* @psalm-suppress InvalidReturnStatement
*/
public function dispatch(object $event): object {
return $this->getContainer()->get('event_dispatcher')->dispatch($event);
}
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Asset\Resolver\PreserveResolver;
use DrupalCodeGenerator\Asset\Resolver\ReplaceResolver;
use DrupalCodeGenerator\Asset\Resolver\ResolverDefinition;
use DrupalCodeGenerator\Asset\Resolver\ResolverInterface;
use DrupalCodeGenerator\InputOutput\IO;
use DrupalCodeGenerator\Utils;
/**
* Base class for assets.
*/
abstract class Asset implements \Stringable {
/**
* Indicates that the asset can be updated but never created.
*/
private bool $virtual = FALSE;
/**
* Asset mode.
*
* @psalm-var int<0, 511>
*/
private int $mode = 0444;
/**
* Template variables.
*
* @psalm-var array<string, mixed>
*/
private array $vars = [];
/**
* Content resolver.
*/
protected ?ResolverInterface $resolver = NULL;
/**
* Resolver definition.
*/
protected ResolverDefinition $resolverDefinition;
/**
* Asset constructor.
*/
public function __construct(protected readonly string $path) {
// @todo Test this.
match (TRUE) {
$this instanceof Directory,
$this instanceof File,
$this instanceof Symlink => NULL,
default => throw new \LogicException(\sprintf('%s class is internal for extension.', self::class)),
};
$this->resolverDefinition = new ResolverDefinition(ReplaceResolver::class);
}
/**
* Getter for the asset path.
*/
final public function getPath(): string {
return $this->replaceTokens($this->path);
}
/**
* Getter for the asset mode.
*
* @psalm-return int<0, 511>
*/
final public function getMode(): int {
return $this->mode;
}
/**
* Getter for the asset vars.
*
* @psalm-return array<string, mixed>
*/
final public function getVars(): array {
return $this->vars;
}
/**
* Checks if the asset is virtual.
*
* Virtual assets should not cause creating new directories, files or symlinks
* on file system. They meant to be used by resolvers to update existing
* objects.
*/
final public function isVirtual(): bool {
return $this->virtual;
}
/**
* Returns the asset resolver.
*/
public function getResolver(IO $io): ResolverInterface {
return $this->resolver ?? $this->resolverDefinition->createResolver($io);
}
/**
* Setter for asset mode.
*
* @psalm-param int<0, 511> $mode
*/
final public function mode(int $mode): static {
/** @psalm-suppress DocblockTypeContradiction */
if ($mode < 0000 || $mode > 0777) {
throw new \InvalidArgumentException('Incorrect mode value.');
}
$this->mode = $mode;
return $this;
}
/**
* Setter for the asset vars.
*
* @psalm-param array<string, mixed> $vars
*/
final public function vars(array $vars): static {
$this->vars = $vars;
return $this;
}
/**
* Makes the asset "virtual".
*/
final public function setVirtual(bool $virtual): static {
$this->virtual = $virtual;
return $this;
}
/**
* Indicates that existing asset should be replaced.
*/
final public function replaceIfExists(): static {
$this->resolverDefinition = new ResolverDefinition(ReplaceResolver::class);
return $this;
}
/**
* Indicates that existing asset should be preserved.
*/
final public function preserveIfExists(): static {
$this->resolverDefinition = new ResolverDefinition(PreserveResolver::class);
return $this;
}
/**
* Setter for asset resolver.
*/
final public function resolver(ResolverInterface $resolver): static {
$this->resolver = $resolver;
return $this;
}
/**
* Implements the magic __toString() method.
*/
final public function __toString(): string {
return $this->getPath();
}
/**
* Replaces all tokens in a given string with appropriate values.
*/
final protected function replaceTokens(string $input): string {
return Utils::replaceTokens($input, $this->vars);
}
}

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
/**
* Asset collection.
*
* @template-implements \ArrayAccess<string,\DrupalCodeGenerator\Asset\Asset>
* @template-implements \IteratorAggregate<string,\DrupalCodeGenerator\Asset\Asset>
*/
final class AssetCollection implements \ArrayAccess, \IteratorAggregate, \Countable, \Stringable {
/**
* AssetCollection constructor.
*
* @param \DrupalCodeGenerator\Asset\Asset[] $assets
* Assets.
*/
public function __construct(private array $assets = []) {}
/**
* Creates a directory asset.
*/
public function addDirectory(string $path): Directory {
$directory = new Directory($path);
$this->assets[] = $directory;
return $directory;
}
/**
* Creates a file asset.
*/
public function addFile(string $path, ?string $template = NULL): File {
$file = new File($path);
if ($template) {
$file->template($template);
}
$this->assets[] = $file;
return $file;
}
/**
* Creates a symlink asset.
*
* @noinspection PhpUnused
*/
public function addSymlink(string $path, string $target): Symlink {
$symlink = new Symlink($path, $target);
$this->assets[] = $symlink;
return $symlink;
}
/**
* Adds an asset for configuration schema file.
*/
public function addSchemaFile(string $path = 'config/schema/{machine_name}.schema.yml'): File {
return $this->addFile($path)
->appendIfExists();
}
/**
* Adds an asset for service file.
*/
public function addServicesFile(string $path = '{machine_name}.services.yml'): File {
return $this->addFile($path)
->appendIfExists(1);
}
/**
* {@inheritdoc}
*
* @psalm-param \DrupalCodeGenerator\Asset\Asset $value
*/
public function offsetSet(mixed $offset, mixed $value): void {
match (TRUE) {
$value instanceof Directory,
$value instanceof File,
$value instanceof Symlink => NULL,
default => throw new \InvalidArgumentException('Unsupported asset type.'),
};
if ($offset === NULL) {
$this->assets[] = $value;
}
else {
$this->assets[$offset] = $value;
}
}
/**
* {@inheritdoc}
*/
public function offsetGet(mixed $offset): ?Asset {
return $this->assets[$offset] ?? NULL;
}
/**
* {@inheritdoc}
*/
public function offsetUnset(mixed $offset): void {
unset($this->assets[$offset]);
}
/**
* {@inheritdoc}
*/
public function offsetExists(mixed $offset): bool {
return isset($this->assets[$offset]);
}
/**
* {@inheritdoc}
*/
public function getIterator(): \ArrayIterator {
return new \ArrayIterator($this->assets);
}
/**
* {@inheritdoc}
*
* @psalm-return int<0, max>
*/
public function count(): int {
return \count($this->assets);
}
/**
* Returns a collection of directory assets.
*/
public function getDirectories(): self {
return $this->getFiltered(
static fn (Asset $asset): bool => $asset instanceof Directory,
);
}
/**
* Returns a collection of file assets.
*/
public function getFiles(): self {
return $this->getFiltered(
static fn (Asset $asset): bool => $asset instanceof File,
);
}
/**
* Returns a collection of symlink assets.
*/
public function getSymlinks(): self {
return $this->getFiltered(
static fn (Asset $asset): bool => $asset instanceof Symlink,
);
}
/**
* Returns a collection of sorted assets.
*/
public function getSorted(): self {
$sorter = static function (Asset $a, Asset $b): int {
$name_a = (string) $a;
$name_b = (string) $b;
// Top level assets should go first.
$result = \strcasecmp(\dirname($name_a), \dirname($name_b));
if ($result === 0) {
$result = \strcasecmp($name_a, $name_b);
}
return $result;
};
$assets = $this->assets;
\usort($assets, $sorter);
return new self($assets);
}
/**
* Filters the asset collection.
*/
public function getFiltered(callable $filter): self {
$iterator = new \CallbackFilterIterator($this->getIterator(), $filter);
$assets = \iterator_to_array($iterator);
$str_keys = \array_filter(\array_keys($assets), 'is_string');
// Reindex if it's not an associative array.
return new self(\count($str_keys) > 0 ? $assets : \array_values($assets));
}
/**
* {@inheritdoc}
*/
public function __toString(): string {
$output = '';
foreach ($this->getSorted() as $asset) {
$output .= '• ' . $asset . \PHP_EOL;
}
return $output;
}
}

View File

@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
// @todo Is it still needed?
\class_alias(AssetCollection::class, '\DrupalCodeGenerator\Asset\Assets');

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Asset\Resolver\PreserveResolver;
use DrupalCodeGenerator\Asset\Resolver\ResolverDefinition;
/**
* Simple data structure to represent a directory being created.
*/
final class Directory extends Asset {
/**
* {@inheritdoc}
*/
public function __construct(string $path) {
parent::__construct($path);
$this->mode(0755);
// Recreating existing directories makes no sense.
$this->resolverDefinition = new ResolverDefinition(PreserveResolver::class);
}
/**
* Named constructor.
*/
public static function create(string $path): self {
return new self($path);
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Asset\Resolver\AppendResolver;
use DrupalCodeGenerator\Asset\Resolver\PrependResolver;
use DrupalCodeGenerator\Asset\Resolver\ResolverDefinition;
use DrupalCodeGenerator\Helper\Renderer\RendererInterface;
/**
* A data structure to represent a file being generated.
*/
final class File extends Asset implements RenderableInterface {
/**
* Asset content.
*/
private string $content = '';
/**
* Template to render main content.
*/
private ?string $template = NULL;
/**
* The template string to render.
*/
private ?string $inlineTemplate = NULL;
/**
* {@inheritdoc}
*/
public function __construct(string $path) {
parent::__construct($path);
$this->mode(0644);
}
/**
* Named constructor.
*/
public static function create(string $path): self {
return new self($path);
}
/**
* Returns the asset content.
*/
public function getContent(): string {
return $this->content;
}
/**
* Sets the asset content.
*/
public function content(string $content): self {
$this->content = $content;
return $this;
}
/**
* Sets the asset template.
*
* Templates with 'twig' extension are processed with Twig template engine.
*/
public function template(string $template): self {
if ($this->inlineTemplate) {
throw new \LogicException('A file cannot have both inline and regular templates.');
}
$this->template = $template;
return $this;
}
/**
* Returns the asset inline template.
*/
public function inlineTemplate(string $inline_template): self {
if ($this->template) {
throw new \LogicException('A file cannot have both inline and regular templates.');
}
$this->inlineTemplate = $inline_template;
return $this;
}
/**
* Sets the "prepend" resolver.
*/
public function prependIfExists(): self {
$this->resolverDefinition = new ResolverDefinition(PrependResolver::class);
return $this;
}
/**
* Sets the "append" resolver.
*
* @psalm-param int<0, max> $header_size
*/
public function appendIfExists(int $header_size = 0): self {
$this->resolverDefinition = new ResolverDefinition(AppendResolver::class, $header_size);
return $this;
}
/**
* {@inheritdoc}
*/
public function render(RendererInterface $renderer): void {
if ($this->inlineTemplate) {
$content = $renderer->renderInline($this->inlineTemplate, $this->getVars());
$this->content($content);
}
elseif ($this->template) {
$template = $this->replaceTokens($this->template);
$content = $renderer->render($template, $this->getVars());
$this->content($content);
}
// It's OK that the file has no templates as consumers may set rendered
// content directly through `content()` method.
}
/**
* Checks if the asset is a PHP script.
*/
public function isPhp(): bool {
return \in_array(
\pathinfo($this->getPath(), \PATHINFO_EXTENSION),
['php', 'module', 'install', 'inc', 'theme'],
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
use DrupalCodeGenerator\Helper\Renderer\RendererInterface;
/**
* An interface for renderable assets.
*/
interface RenderableInterface {
/**
* Renders the asset.
*/
public function render(RendererInterface $renderer): void;
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\InputOutput\IO;
final class AppendResolver implements ResolverInterface, ResolverFactoryInterface {
/**
* Constructs the object.
*
* @psalm-param int<0, max> $headerSize
*/
public function __construct(private readonly int $headerSize = 0) {
/** @psalm-suppress DocblockTypeContradiction */
if ($headerSize < 0) {
throw new \InvalidArgumentException('Header size must be greater than or equal to 0.');
}
}
/**
* {@inheritdoc}
*/
public static function createResolver(IO $io, mixed $options): self {
return new self($options);
}
/**
* {@inheritdoc}
*/
public function resolve(Asset $asset, string $path): File {
if (!$asset instanceof File) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$new_content = $asset->getContent();
// Remove header from existing content.
if ($this->headerSize > 0) {
$new_content = \implode("\n", \array_slice(\explode("\n", $new_content), $this->headerSize));
}
$existing_content = \file_get_contents($path);
return clone $asset->content($existing_content . "\n" . $new_content);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\File;
final class PrependResolver implements ResolverInterface {
/**
* {@inheritdoc}
*/
public function resolve(Asset $asset, string $path): File {
if (!$asset instanceof File) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$new_content = $asset->getContent();
$existing_content = \file_get_contents($path);
return clone $asset->content($new_content . "\n" . $existing_content);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
final class PreserveResolver implements ResolverInterface {
/**
* {@inheritdoc}
*
* @psalm-return null
*/
public function resolve(Asset $asset, string $path): ?Asset {
return NULL;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Asset\Symlink;
use DrupalCodeGenerator\InputOutput\IO;
final class ReplaceResolver implements ResolverInterface, ResolverFactoryInterface {
/**
* Constructs the object.
*/
public function __construct(private readonly IO $io) {}
/**
* {@inheritdoc}
*/
public static function createResolver(IO $io, mixed $options): self {
return new self($io);
}
/**
* {@inheritdoc}
*/
public function resolve(Asset $asset, string $path): NULL|File|Symlink {
if (!$asset instanceof File && !$asset instanceof Symlink) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$replace = $this->io->getInput()->getOption('replace') ||
$this->io->getInput()->getOption('dry-run') ||
$this->io->confirm("The file <comment>$path</comment> already exists. Would you like to replace it?");
return $replace ? clone $asset : NULL;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\InputOutput\IO;
final class ResolverDefinition {
/**
* Constructs the object.
*
* @psalm-param class-string<\DrupalCodeGenerator\Asset\Resolver\ResolverInterface> $className
*/
public function __construct(
public readonly string $className,
public readonly mixed $options = NULL,
) {}
/**
* Creates asset resolver.
*/
public function createResolver(IO $io): ResolverInterface {
if (\is_subclass_of($this->className, ResolverFactoryInterface::class)) {
$resolver = $this->className::createResolver($io, $this->options);
}
else {
$resolver = new $this->className();
}
return $resolver;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\InputOutput\IO;
/**
* Interface for classes capable of creating resolvers.
*/
interface ResolverFactoryInterface {
/**
* Creates a resolver.
*/
public static function createResolver(IO $io, mixed $options): ResolverInterface;
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset\Resolver;
use DrupalCodeGenerator\Asset\Asset;
/**
* Interface resolver.
*
* A resolver is called when the asset with the same path already exists in the
* file system. The purpose of the resolver is to merge the existing asset with
* the one provided by a generator.
*/
interface ResolverInterface {
/**
* Resolves an asset.
*
* Returns the resolved asset or NULL if existing asset is up-to-date.
*
* @throw \InvalidArgumentException
*/
public function resolve(Asset $asset, string $path): ?Asset;
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Asset;
/**
* Simple data structure to represent a symlink being generated.
*/
final class Symlink extends Asset {
/**
* Symlink target.
*/
private readonly string $target;
/**
* {@inheritdoc}
*/
public function __construct(string $path, string $target) {
parent::__construct($path);
$this->target = $target;
$this->mode(0644);
}
/**
* Named constructor.
*/
public static function create(string $path, string $target): self {
return new self($path, $target);
}
/**
* Getter for symlink target.
*/
public function getTarget(): string {
return $this->replaceTokens($this->target);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Attribute;
use DrupalCodeGenerator\GeneratorType;
/**
* Generator definition.
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Generator {
public function __construct(
public readonly string $name,
public readonly string $description = '',
public readonly array $aliases = [],
public readonly bool $hidden = FALSE,
public readonly ?string $templatePath = NULL,
public readonly GeneratorType $type = GeneratorType::OTHER,
public readonly ?string $label = NULL,
) {}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator;
use Composer\Autoload\ClassLoader;
use Composer\InstalledVersions;
use Drupal\Core\DrupalKernel;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a handler to bootstrap Drupal.
*/
final class BootstrapHandler {
/**
* Constructs the object.
*/
public function __construct(private readonly ClassLoader $classLoader) {}
/**
* Bootstraps Drupal.
*/
public function bootstrap(): ContainerInterface {
self::assertInstallation();
$root_package = InstalledVersions::getRootPackage();
\chdir($root_package['install_path']);
$request = Request::createFromGlobals();
$kernel = DrupalKernel::createFromRequest($request, $this->classLoader, 'prod');
$kernel->boot();
$kernel->preHandle($request);
// Cancel Drupal error handler to get all errors in STDOUT.
\restore_error_handler();
\error_reporting(\E_ALL);
return $kernel->getContainer();
}
/**
* Asserts Drupal instance.
*
* @throws \RuntimeException
*/
private static function assertInstallation(): void {
$preflight = \defined('Drupal::VERSION') &&
\version_compare(\Drupal::VERSION, '10.0.0-dev', '>=') &&
\class_exists(InstalledVersions::class) &&
\class_exists(Request::class) &&
\class_exists(DrupalKernel::class);
if (!$preflight) {
throw new \RuntimeException('Could not load Drupal.');
}
}
}

View File

@@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator as GeneratorDefinition;
use DrupalCodeGenerator\Event\AssetPostProcess;
use DrupalCodeGenerator\Event\AssetPreProcess;
use DrupalCodeGenerator\Exception\ExceptionInterface;
use DrupalCodeGenerator\Exception\SilentException;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Helper\Drupal\NullExtensionInfo;
use DrupalCodeGenerator\InputOutput\DefaultOptions;
use DrupalCodeGenerator\InputOutput\Interviewer;
use DrupalCodeGenerator\InputOutput\IO;
use DrupalCodeGenerator\InputOutput\IOAwareInterface;
use DrupalCodeGenerator\InputOutput\IOAwareTrait;
use DrupalCodeGenerator\Logger\ConsoleLogger;
use DrupalCodeGenerator\Utils;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Base class for code generators.
*
* @method \DrupalCodeGenerator\Application getApplication()
* @method string getName()
* @method \Symfony\Component\Console\Helper\HelperSet getHelperSet()
*/
abstract class BaseGenerator extends Command implements LabelInterface, IOAwareInterface, LoggerAwareInterface {
use IOAwareTrait;
use LoggerAwareTrait;
/**
* {@inheritdoc}
*/
public function __construct() {
parent::__construct();
$this->logger = new NullLogger();
}
/**
* {@inheritdoc}
*/
protected function configure(): void {
parent::configure();
$definition = $this->getGeneratorDefinition();
$this->setName($definition->name)
->setDescription($definition->description)
->setAliases($definition->aliases)
->setHidden($definition->hidden);
DefaultOptions::apply($this);
}
/**
* {@inheritdoc}
*
* @psalm-suppress PossiblyNullReference
*/
protected function initialize(InputInterface $input, OutputInterface $output): void {
parent::initialize($input, $output);
$logger = new ConsoleLogger($output);
$question_helper = $this->getHelper('question');
$io = new IO($input, $output, $question_helper);
$items = \iterator_to_array($this->getHelperSet());
$items[] = $this;
foreach ($items as $item) {
if ($item instanceof IOAwareInterface) {
$item->io($io);
}
if ($item instanceof LoggerAwareInterface) {
$item->setLogger($logger);
}
}
$template_path = $this->getTemplatePath();
if ($template_path !== NULL) {
$this->getHelper('renderer')->registerTemplatePath($template_path);
}
$this->logger->debug('PHP binary: {binary}', ['binary' => \PHP_BINARY]);
/** @psalm-var array{PHP_SELF: string} $_SERVER */
// @phpcs:ignore SlevomatCodingStandard.Variables.DisallowSuperGlobalVariable.DisallowedSuperGlobalVariable
$this->logger->debug('DCG executable: {dcg}', ['dcg' => \realpath($_SERVER['PHP_SELF'])]);
$this->logger->debug('Working directory: {directory}', ['directory' => $io->getWorkingDirectory()]);
}
/**
* {@inheritdoc}
*
* @noinspection PhpMissingParentCallCommonInspection
* @psalm-suppress PossiblyNullReference
*
* @psalm-return int<0, 1>
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$this->logger->debug('Command: {command}', ['command' => static::class]);
try {
$this->printHeader();
$vars = [];
$assets = new AssetCollection();
$this->generate($vars, $assets);
$this->generateInfoFile($vars, $assets);
$vars = Utils::processVars($vars);
$collected_vars = \preg_replace('/^Array/', '', \print_r($vars, TRUE));
$this->logger->debug('Collected variables: {vars}', ['vars' => $collected_vars]);
foreach ($assets as $asset) {
// Local asset variables take precedence over global ones.
$asset->vars(\array_merge($vars, Utils::processVars($asset->getVars())));
}
$this->render($assets);
// Destination passed through command line option takes precedence over
// destination defined in a generator.
$destination = $input->getOption('destination') ?: $this->getDestination($vars);
$this->logger->debug('Destination directory: {directory}', ['directory' => $destination]);
$dumped_assets = $this->dump($assets, $destination);
$full_path = $input->getOption('full-path');
$this->printSummary($dumped_assets, $full_path ? $destination . '/' : '');
}
catch (ExceptionInterface $exception) {
if (!$exception instanceof SilentException) {
$this->io()->getErrorStyle()->error($exception->getMessage());
}
return self::FAILURE;
}
$this->logger->debug('Memory usage: {memory}', ['memory' => Helper::formatMemory(\memory_get_peak_usage())]);
return self::SUCCESS;
}
/**
* Generates assets.
*/
abstract protected function generate(array &$vars, AssetCollection $assets): void;
/**
* Gets generator definition.
*/
protected function getGeneratorDefinition(): GeneratorDefinition {
$attributes = (new \ReflectionClass(static::class))->getAttributes(GeneratorDefinition::class);
if (\count($attributes) === 0) {
throw new \LogicException(\sprintf('Command %s does not have generator annotation.', static::class));
}
return $attributes[0]->newInstance();
}
/**
* Creates interviewer.
*/
protected function createInterviewer(array &$vars): Interviewer {
$extension_info = match ($this->getGeneratorDefinition()->type) {
GeneratorType::MODULE, GeneratorType::MODULE_COMPONENT => $this->getHelper('module_info'),
GeneratorType::THEME, GeneratorType::THEME_COMPONENT => $this->getHelper('theme_info'),
default => new NullExtensionInfo(),
};
return new Interviewer(
io: $this->io,
vars: $vars,
generatorDefinition: $this->getGeneratorDefinition(),
serviceInfo: $this->getHelper('service_info'),
extensionInfo: $extension_info,
permissionInfo: $this->getHelper('permission_info'),
);
}
/**
* Render assets.
*/
protected function render(AssetCollection $assets): void {
$renderer = $this->getHelper('renderer');
foreach ($assets->getFiles() as $file) {
/** @var \DrupalCodeGenerator\Asset\File $file */
$renderer->renderAsset($file);
}
}
/**
* Dumps assets.
*/
protected function dump(AssetCollection $assets, string $destination): AssetCollection {
$is_dry = $this->io()->getInput()->getOption('dry-run');
$pre_process_event = $this->getApplication()->dispatch(
new AssetPreProcess($assets, $destination, $this->getName(), $is_dry),
);
$assets = $pre_process_event->assets;
$destination = $pre_process_event->destination;
/** @var \DrupalCodeGenerator\Helper\Dumper\DumperInterface $dumper */
$dumper = $this->getHelper($is_dry ? 'dry_dumper' : 'filesystem_dumper');
$dumped_assets = $dumper->dump($assets, $destination);
$post_process_event = $this->getApplication()->dispatch(
new AssetPostProcess($dumped_assets, $destination, $this->getName(), $is_dry),
);
return $post_process_event->assets;
}
/**
* Prints header.
*/
protected function printHeader(): void {
$this->io()->title(\sprintf('Welcome to %s generator!', $this->getAliases()[0] ?? $this->getName()));
}
/**
* Prints summary.
*/
protected function printSummary(AssetCollection $dumped_assets, string $base_path): void {
$printer_name = $this->io()->isVerbose() ? 'assets_table_printer' : 'assets_list_printer';
/** @psalm-suppress UndefinedInterfaceMethod */
$this->getHelper($printer_name)->printAssets($dumped_assets, $base_path);
}
/**
* {@inheritdoc}
*/
public function getLabel(): ?string {
return $this->getGeneratorDefinition()->label;
}
/**
* Returns template path.
*/
final protected function getTemplatePath(): ?string {
return $this->getGeneratorDefinition()->templatePath;
}
/**
* Returns destination for generated files.
*
* @todo Test this.
*/
protected function getDestination(array $vars): string {
if (!isset($vars['machine_name'])) {
return $this->io()->getWorkingDirectory();
}
$definition = $this->getGeneratorDefinition();
$is_new = $definition->type->isNewExtension();
return match ($definition->type) {
GeneratorType::MODULE, GeneratorType::MODULE_COMPONENT =>
$this->getHelper('module_info')->getDestination($vars['machine_name'], $is_new),
GeneratorType::THEME, GeneratorType::THEME_COMPONENT =>
$this->getHelper('theme_info')->getDestination($vars['machine_name'], $is_new),
default => $this->io()->getWorkingDirectory(),
};
}
/**
* Generates info file.
*
* @todo Test this.
* @todo Generate info file for theme components.
*/
protected function generateInfoFile(array &$vars, AssetCollection $assets): void {
if (\count($assets) === 0) {
return;
}
if ($this->getGeneratorDefinition()->type !== GeneratorType::MODULE_COMPONENT) {
return;
}
// @todo Throw an exception if machine name was not provided.
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
$vars['name'] ??= Utils::machine2human($vars['machine_name']);
$info_template = <<< 'TWIG'
name: '{{ name }}'
type: module
description: '@todo Add description.'
package: '@todo Add package'
core_version_requirement: ^10
TWIG;
$assets->addFile('{machine_name}.info.yml')
->inlineTemplate($info_template)
->preserveIfExists();
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
/**
* A generator for composer.json file.
*
* @todo Clean-up
* @todo Define destination automatically based on project type.
*/
#[Generator(
name: 'composer',
description: 'Generates a composer.json file',
aliases: ['composer.json'],
templatePath: Application::TEMPLATE_PATH . '/_composer',
type: GeneratorType::OTHER,
label: 'composer.json',
)]
final class Composer extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
// @see https://getcomposer.org/doc/04-schema.md#name
// @todo Test this.
$validator = static function (string $input): string {
if (!\preg_match('#^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$#', $input)) {
throw new \UnexpectedValueException("The package name \"$input\" is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name.");
}
return $input;
};
$vars['project_name'] = $ir->ask('Project name', 'drupal/example', $validator);
[, $vars['machine_name']] = \explode('/', $vars['project_name']);
$vars['description'] = $ir->ask('Description');
$type_choices = [
'drupal-module',
'drupal-custom-module',
'drupal-theme',
'drupal-custom-theme',
'drupal-library',
'drupal-profile',
'drupal-custom-profile',
'drupal-drush',
];
$vars['type'] = $ir->choice('Project type', \array_combine($type_choices, $type_choices));
$vars['drupal_org'] = match($vars['type']) {
'drupal-custom-module', 'drupal-custom-theme', 'drupal-custom-profile' => FALSE,
default => $ir->confirm('Will this project be hosted on drupal.org?'),
};
$assets->addFile('composer.json', 'composer.twig');
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'controller',
description: 'Generates a controller',
templatePath: Application::TEMPLATE_PATH . '/_controller',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Controller extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}Controller');
$vars['services'] = $ir->askServices(FALSE);
if ($ir->confirm('Would you like to create a route for this controller?')) {
$unprefixed_class = Utils::camel2machine(Utils::removeSuffix($vars['class'], 'Controller'));
// Route name like 'foo.foo' would look weird.
if ($unprefixed_class === $vars['machine_name']) {
$unprefixed_class = 'example';
}
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $unprefixed_class);
$vars['unprefixed_route_name'] = \str_replace(
'.', '_', Utils::removePrefix($vars['route_name'], $vars['machine_name'] . '.'),
);
$vars['route_path'] = $ir->ask('Route path', '/{machine_name|u2h}/{unprefixed_route_name|u2h}');
$vars['route_title'] = $ir->ask('Route title', '{unprefixed_route_name|m2t}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'access content');
$assets->addFile('{machine_name}.routing.yml', 'route.twig')->appendIfExists();
}
$assets->addFile('src/Controller/{class}.php', 'controller.twig');
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Drush;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\RegExp;
#[Generator(
name: 'drush:symfony-command',
description: 'Generates Symfony console command',
aliases: ['symfony-command'],
templatePath: Application::TEMPLATE_PATH . '/Drush/_symfony-command',
type: GeneratorType::MODULE_COMPONENT,
)]
final class SymfonyCommand extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$command_name_validator = new RegExp('/^[a-z][a-z0-9-_:]*[a-z0-9]$/', 'The value is not correct command name.');
$vars['command']['name'] = $ir->ask('Command name', '{machine_name}:example', $command_name_validator);
$vars['command']['description'] = $ir->ask('Command description');
$sub_names = \explode(':', $vars['command']['name']);
$short_name = \array_pop($sub_names);
$alias_validator = new RegExp('/^[a-z0-9_-]+$/', 'The value is not correct alias name.');
$vars['command']['alias'] = $ir->ask('Command alias', $short_name, $alias_validator);
$vars['class'] = $ir->askClass('Class', Utils::camelize($short_name) . 'Command');
if ($ir->confirm('Would you like to run the command with Drush')) {
// Make service name using the following guides.
// `foo:example` -> `foo.example` (not `foo:foo_example`)
// `foo` -> `foo.foo` (not `foo`)
$service_name = Utils::removePrefix($vars['command']['name'], $vars['machine_name'] . ':');
if (!$service_name) {
$service_name = $vars['command']['name'];
}
$vars['service_name'] = $vars['machine_name'] . '.' . \str_replace(':', '_', $service_name);
$vars['services'] = $ir->askServices(FALSE);
$assets->addServicesFile('drush.services.yml')->template('services.twig');
}
$assets->addFile('src/Command/{class}.php', 'command.twig');
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Entity;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Asset;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Asset\Resolver\ResolverInterface;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'entity:configuration',
description: 'Generates configuration entity',
aliases: ['config-entity'],
templatePath: Application::TEMPLATE_PATH . '/Entity/_configuration-entity',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ConfigurationEntity extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['entity_type_label'] = $ir->ask('Entity type label', '{name}');
$vars['entity_type_id'] = $ir->ask('Entity type ID', '{entity_type_label|h2m}');
$vars['class_prefix'] = '{entity_type_id|camelize}';
$assets->addFile('src/{class_prefix}ListBuilder.php', 'src/ExampleListBuilder.php.twig');
$assets->addFile('src/Form/{class_prefix}Form.php', 'src/Form/ExampleForm.php.twig');
$assets->addFile('src/{class_prefix}Interface.php', 'src/ExampleInterface.php.twig');
$assets->addFile('src/Entity/{class_prefix}.php', 'src/Entity/Example.php.twig');
$assets->addFile('{machine_name}.routing.yml', 'model.routing.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.action.yml', 'model.links.action.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.menu.yml', 'model.links.menu.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.permissions.yml', 'model.permissions.yml.twig')
->appendIfExists();
$assets->addFile('config/schema/{machine_name}.schema.yml', 'config/schema/model.schema.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.info.yml.twig')
->setVirtual(TRUE)
->resolver($this->getInfoResolver($vars));
}
/**
* Returns resolver for the module info file.
*/
private function getInfoResolver(array $vars): ResolverInterface {
// Add 'configure' link to the info file if it exists.
return new class ($vars) implements ResolverInterface {
public function __construct(private readonly array $vars) {}
public function resolve(Asset $asset, string $path): Asset {
if (!$asset instanceof File) {
throw new \InvalidArgumentException('Wrong asset type.');
}
$resolved = clone $asset;
$existing_content = \file_get_contents($path);
if (!\preg_match('/^configure: /m', $existing_content)) {
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
$content = "{$existing_content}configure: entity.{$this->vars['entity_type_id']}.collection\n";
return $resolved->content($content);
}
return $resolved;
}
};
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Entity;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'entity:content',
description: 'Generates content entity',
aliases: ['content-entity'],
templatePath: Application::TEMPLATE_PATH . '/Entity/_content-entity',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ContentEntity extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['entity_type_label'] = $ir->ask('Entity type label', '{name}');
// Make sure the default entity type ID is not like 'example_example'.
// @todo Create a test for this.
$default_entity_type_id = Utils::human2machine($vars['entity_type_label']) === $vars['machine_name'] ?
$vars['machine_name'] : $vars['machine_name'] . '_' . Utils::human2machine($vars['entity_type_label']);
$vars['entity_type_id'] = $ir->ask('Entity type ID', $default_entity_type_id);
$vars['entity_type_id_short'] = $vars['machine_name'] === $vars['entity_type_id'] ?
$vars['entity_type_id'] : Utils::removePrefix($vars['entity_type_id'], $vars['machine_name'] . '_');
$vars['class'] = $ir->ask('Entity class', '{entity_type_label|camelize}');
$vars['entity_base_path'] = $ir->ask('Entity base path', '/{entity_type_id_short|u2h}');
$vars['fieldable'] = $ir->confirm('Make the entity type fieldable?');
$vars['revisionable'] = $ir->confirm('Make the entity type revisionable?', FALSE);
$vars['translatable'] = $ir->confirm('Make the entity type translatable?', FALSE);
$vars['bundle'] = $ir->confirm('The entity type has bundle?', FALSE);
$vars['canonical'] = $ir->confirm('Create canonical page?');
$vars['template'] = $vars['canonical'] && $ir->confirm('Create entity template?');
$vars['access_controller'] = $ir->confirm('Create CRUD permissions?', FALSE);
$vars['label_base_field'] = $ir->confirm('Add "label" base field?');
$vars['status_base_field'] = $ir->confirm('Add "status" base field?');
$vars['created_base_field'] = $ir->confirm('Add "created" base field?');
$vars['changed_base_field'] = $ir->confirm('Add "changed" base field?');
$vars['author_base_field'] = $ir->confirm('Add "author" base field?');
$vars['description_base_field'] = $ir->confirm('Add "description" base field?');
$vars['has_base_fields'] = $vars['label_base_field'] ||
$vars['status_base_field'] ||
$vars['created_base_field'] ||
$vars['changed_base_field'] ||
$vars['author_base_field'] ||
$vars['description_base_field'];
$vars['permissions']['administer'] = $vars['bundle']
? 'administer {entity_type_id} types' : 'administer {entity_type_id}';
if ($vars['access_controller']) {
$vars['permissions']['view'] = 'view {entity_type_id}';
$vars['permissions']['edit'] = 'edit {entity_type_id}';
$vars['permissions']['delete'] = 'delete {entity_type_id}';
$vars['permissions']['create'] = 'create {entity_type_id}';
}
if ($vars['access_controller'] && $vars['revisionable']) {
$vars['permissions']['view_revision'] = 'view {entity_type_id} revision';
$vars['permissions']['revert_revision'] = 'revert {entity_type_id} revision';
$vars['permissions']['delete_revision'] = 'delete {entity_type_id} revision';
}
$vars['rest_configuration'] = $ir->confirm('Create REST configuration for the entity?', FALSE);
if (!\str_starts_with($vars['entity_base_path'], '/')) {
$vars['entity_base_path'] = '/' . $vars['entity_base_path'];
}
if (($vars['fieldable_no_bundle'] = $vars['fieldable'] && !$vars['bundle'])) {
$vars['configure'] = 'entity.{entity_type_id}.settings';
}
elseif ($vars['bundle']) {
$vars['configure'] = 'entity.{entity_type_id}_type.collection';
}
$vars['template_name'] = '{entity_type_id|u2h}.html.twig';
// Contextual links need title suffix to be added to entity template.
if ($vars['template']) {
$assets->addFile('{machine_name}.links.contextual.yml', 'model.links.contextual.yml.twig')
->appendIfExists();
}
$assets->addFile('{machine_name}.links.action.yml', 'model.links.action.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.menu.yml', 'model.links.menu.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.links.task.yml', 'model.links.task.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.permissions.yml', 'model.permissions.yml.twig')
->appendIfExists();
// Delete action plugins only registered for entity types that have
// 'delete-multiple-confirm' form handler and 'delete-multiple-form' link
// template.
// @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getDeleteMultipleFormRoute
// @see \Drupal\Core\Action\Plugin\Action\Derivative\EntityDeleteActionDeriver
$assets->addFile(
'config/install/system.action.{entity_type_id}_delete_action.yml',
'config/install/system.action.example_delete_action.yml.twig',
);
// Save action plugins only registered for entity types that implement
// Drupal\Core\Entity\EntityChangedInterface.
// @see \Drupal\Core\Action\Plugin\Action\Derivative\EntityChangedActionDeriver
if ($vars['changed_base_field']) {
$assets->addFile(
'config/install/system.action.{entity_type_id}_save_action.yml',
'config/install/system.action.example_save_action.yml.twig',
);
}
$assets->addFile('src/Entity/{class}.php', 'src/Entity/Example.php.twig');
$assets->addFile('src/{class}Interface.php', 'src/ExampleInterface.php.twig');
if (!$vars['canonical']) {
$assets->addFile('src/Routing/{class}HtmlRouteProvider.php', 'src/Routing/ExampleHtmlRouteProvider.php.twig');
}
$assets->addFile('src/{class}ListBuilder.php', 'src/ExampleListBuilder.php.twig');
$assets->addFile('src/Form/{class}Form.php', 'src/Form/ExampleForm.php.twig');
if ($vars['fieldable_no_bundle']) {
$assets->addFile('{machine_name}.routing.yml', 'model.routing.yml.twig')
->appendIfExists();
$assets->addFile('src/Form/{class}SettingsForm.php', 'src/Form/ExampleSettingsForm.php.twig');
}
if ($vars['template']) {
$assets->addFile('templates/{entity_type_id|u2h}.html.twig', 'templates/model-example.html.twig.twig');
$assets->addFile('{machine_name}.module', 'model.module.twig')
->appendIfExists(9);
}
if ($vars['access_controller']) {
$assets->addFile('src/{class}AccessControlHandler.php', 'src/ExampleAccessControlHandler.php.twig');
}
if ($vars['rest_configuration']) {
$assets->addFile('config/optional/rest.resource.entity.{entity_type_id}.yml', 'config/optional/rest.resource.entity.example.yml.twig');
}
if ($vars['bundle']) {
$assets->addFile('config/schema/{machine_name}.entity_type.schema.yml', 'config/schema/model.entity_type.schema.yml.twig')
->appendIfExists();
$assets->addFile('src/{class}TypeListBuilder.php', 'src/ExampleTypeListBuilder.php.twig');
$assets->addFile('src/Entity/{class}Type.php', 'src/Entity/ExampleType.php.twig');
$assets->addFile('src/Form/{class}TypeForm.php', 'src/Form/ExampleTypeForm.php.twig');
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Entity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\Exception\RuntimeException;
use DrupalCodeGenerator\GeneratorType;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'entity:bundle-class',
description: 'Generate a bundle class for a content entity.',
aliases: ['bundle-class'],
templatePath: Application::TEMPLATE_PATH . '/Entity/_entity-bundle-class',
type: GeneratorType::MODULE_COMPONENT,
)]
final class EntityBundleClass extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly EntityTypeBundleInfoInterface $bundleInfo,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self(
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
);
}
/**
* {@inheritdoc}
*
* @psalm-suppress PossiblyInvalidArgument
* @psalm-suppress PossiblyUndefinedStringArrayOffset
* @psalm-suppress PossiblyInvalidArrayOffset
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
/** @psalm-var array<string, \Drupal\Core\Entity\ContentEntityTypeInterface> $definitions */
$definitions = \array_filter(
$this->entityTypeManager->getDefinitions(),
static fn (EntityTypeInterface $definition): bool => $definition->getGroup() === 'content',
);
$entity_types = \array_map(
static fn (ContentEntityTypeInterface $definition): string => (string) $definition->get('label'),
$definitions,
);
$vars['entity_type_id'] = $ir->choice('Entity type', $entity_types);
// @todo Should this use 'original_class' instead?
$vars['entity_class_fqn'] = $definitions[$vars['entity_type_id']]->get('class');
$vars['entity_class'] = \array_slice(\explode('\\', $vars['entity_class_fqn']), -1)[0];
$vars['namespace'] = 'Drupal\\\{machine_name}\Entity\\\{entity_class}';
$bundles = \array_map(
static fn (array $bundle): string => (string) $bundle['label'],
$this->bundleInfo->getBundleInfo($vars['entity_type_id']),
);
if (\count($bundles) === 0) {
throw new RuntimeException(
\sprintf('The "%s" entity type has no bundles.', $entity_types[$vars['entity_type_id']]),
);
}
// Skip the question if only 1 bundle exists.
$bundle_ids = \count($bundles) === 1 ?
\array_keys($bundles) : $ir->choice('Bundles, comma separated', $bundles, NULL, TRUE);
$vars['classes'] = [];
$vars['classes_fqn'] = [];
/** @psalm-var list<string> $bundle_ids */
foreach ($bundle_ids as $bundle_id) {
$vars['bundle_id'] = $bundle_id;
$vars['class'] = $ir->ask(
\sprintf('Class for "%s" bundle', $bundles[$bundle_id]),
'{bundle_id|camelize}',
);
$assets->addFile('src/Entity/{entity_class}/{class}.php', 'bundle-class.twig')->vars($vars);
// Track all bundle classes to generate hook_entity_bundle_info_alter().
$vars['classes'][$bundle_id] = $vars['class'];
$vars['classes_fqn'][$bundle_id] = '\\' . $vars['namespace'] . '\\' . $vars['class'];
}
$vars['base_class'] = NULL;
if ($ir->confirm('Use a base class?', FALSE)) {
$vars['base_class'] = $ir->ask('Base class', '{entity_type_id|camelize}Base');
$assets->addFile('src/Entity/{entity_class}/{base_class}.php', 'bundle-base-class.twig');
}
// @todo Handle duplicated hooks.
$assets->addFile('{machine_name}.module', 'module.twig')
->appendIfExists(9);
}
}

View File

@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\Required;
use DrupalCodeGenerator\Validator\RequiredMachineName;
#[Generator(
name: 'field',
description: 'Generates a field',
templatePath: Application::TEMPLATE_PATH . '/_field',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Field extends BaseGenerator {
/**
* Field sub-types.
*/
private const SUB_TYPES = [
'boolean' => [
'label' => 'Boolean',
'list' => FALSE,
'random' => FALSE,
'inline' => FALSE,
'link' => FALSE,
'data_type' => 'boolean',
],
'string' => [
'label' => 'Text',
'list' => TRUE,
'random' => TRUE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'string',
],
'text' => [
'label' => 'Text (long)',
'list' => FALSE,
'random' => TRUE,
'inline' => FALSE,
'link' => FALSE,
'data_type' => 'string',
],
'integer' => [
'label' => 'Integer',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'integer',
],
'float' => [
'label' => 'Float',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'float',
],
'numeric' => [
'label' => 'Numeric',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => FALSE,
'data_type' => 'float',
],
'email' => [
'label' => 'Email',
'list' => TRUE,
'random' => TRUE,
'inline' => TRUE,
'link' => TRUE,
'data_type' => 'email',
],
'telephone' => [
'label' => 'Telephone',
'list' => TRUE,
'random' => FALSE,
'inline' => TRUE,
'link' => TRUE,
'data_type' => 'string',
],
'uri' => [
'label' => 'Url',
'list' => TRUE,
'random' => TRUE,
'inline' => TRUE,
'link' => TRUE,
'data_type' => 'uri',
],
'datetime' => [
'label' => 'Date',
'list' => TRUE,
'random' => FALSE,
'inline' => FALSE,
'link' => FALSE,
'data_type' => 'datetime_iso8601',
],
];
/**
* Date types.
*/
private const DATE_TYPES = [
'date' => 'Date only',
'datetime' => 'Date and time',
];
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['field_label'] = $ir->ask('Field label', 'Example', new Required());
$vars['field_id'] = $ir->ask('Field ID', '{machine_name}_{field_label|h2m}', new RequiredMachineName());
$subfield_count_validator = static function (mixed $value): int {
if (!(\is_int($value) || \ctype_digit($value)) || (int) $value <= 0) {
throw new \UnexpectedValueException('The value should be greater than zero.');
}
return (int) $value;
};
$vars['subfield_count'] = $ir->ask('How many sub-fields would you like to create?', '3', $subfield_count_validator);
$type_choices = \array_combine(
\array_keys(self::SUB_TYPES),
\array_column(self::SUB_TYPES, 'label'),
);
// Indicates that at least one of sub-fields needs Random component.
$vars['random'] = FALSE;
// Indicates that all sub-fields can be rendered inline.
$vars['inline'] = TRUE;
// Indicates that at least one of sub-fields has limited allowed values.
$vars['list'] = FALSE;
// Indicates that at least one of sub-fields is required.
$vars['required'] = FALSE;
// Indicates that at least one of sub-fields is of email type.
$vars['email'] = FALSE;
// Indicates that at least one of sub-fields can be rendered as a link.
$vars['link'] = FALSE;
// Indicates that at least one of sub-fields is of datetime type.
$vars['datetime'] = FALSE;
$vars['type_class'] = '{field_label|camelize}Item';
$vars['widget_class'] = '{field_label|camelize}Widget';
$vars['formatter_class'] = '{field_label|camelize}DefaultFormatter';
for ($i = 1; $i <= $vars['subfield_count']; $i++) {
$this->io()->writeln(\sprintf('<fg=green>%s</>', \str_repeat('', 50)));
$subfield = new \stdClass();
$subfield->name = $ir->ask("Label for sub-field #$i", "Value $i");
$subfield->machineName = $ir->ask(
"Machine name for sub-field #$i",
Utils::human2machine($subfield->name),
new RequiredMachineName(),
);
/** @var string $type */
$type = $ir->choice("Type of sub-field #$i", $type_choices, 'Text');
$subfield->dateType = $type === 'datetime' ?
$ir->choice("Date type for sub-field #$i", self::DATE_TYPES, 'Date only') : NULL;
$definition = self::SUB_TYPES[$type];
if ($definition['list']) {
$subfield->list = $ir->confirm("Limit allowed values for sub-field #$i?", FALSE);
}
$subfield->required = $ir->confirm("Make sub-field #$i required?", FALSE);
// Build sub-field vars.
$vars['subfields'][$i] = [
'name' => $subfield->name,
'machine_name' => $subfield->machineName,
'type' => $type,
'data_type' => $definition['data_type'],
'list' => $subfield->list ?? FALSE,
'allowed_values_method' => 'allowed' . Utils::camelize($subfield->name, TRUE) . 'Values',
'required' => $subfield->required,
'link' => $definition['link'],
];
if ($subfield->dateType) {
$vars['subfields'][$i]['date_type'] = $subfield->dateType;
// Back to date type ID.
$vars['subfields'][$i]['date_storage_format'] = $subfield->dateType === 'date' ? 'Y-m-d' : 'Y-m-d\TH:i:s';
}
if ($definition['random']) {
$vars['random'] = TRUE;
}
if (!$definition['inline']) {
$vars['inline'] = FALSE;
}
if ($vars['subfields'][$i]['list']) {
$vars['list'] = TRUE;
}
if ($vars['subfields'][$i]['required']) {
$vars['required'] = TRUE;
}
if ($type === 'email') {
$vars['email'] = TRUE;
}
if ($definition['link']) {
$vars['link'] = TRUE;
}
if ($type === 'datetime') {
$vars['datetime'] = TRUE;
}
}
$this->io()->writeln(\sprintf('<fg=green>%s</>', \str_repeat('', 50)));
$vars['storage_settings'] = $ir->confirm('Would you like to create field storage settings form?', FALSE);
$vars['instance_settings'] = $ir->confirm('Would you like to create field instance settings form?', FALSE);
$vars['widget_settings'] = $ir->confirm('Would you like to create field widget settings form?', FALSE);
$vars['formatter_settings'] = $ir->confirm('Would you like to create field formatter settings form?', FALSE);
$vars['table_formatter'] = $ir->confirm('Would you like to create table formatter?', FALSE);
$vars['key_value_formatter'] = $ir->confirm('Would you like to create key-value formatter?', FALSE);
$assets->addFile('src/Plugin/Field/FieldType/{type_class}.php', 'type.twig');
$assets->addFile('src/Plugin/Field/FieldWidget/{widget_class}.php', 'widget.twig');
$assets->addFile('src/Plugin/Field/FieldFormatter/{formatter_class}.php', 'default-formatter.twig');
$assets->addSchemaFile()->template('schema.twig');
$assets->addFile('{machine_name}.libraries.yml', 'libraries.twig')
->appendIfExists();
$assets->addFile('css/{field_id|u2h}-widget.css', 'widget-css.twig');
if ($vars['table_formatter']) {
$vars['table_formatter_class'] = '{field_label|camelize}TableFormatter';
$assets->addFile('src/Plugin/Field/FieldFormatter/{table_formatter_class}.php', '/table-formatter.twig');
}
if ($vars['key_value_formatter']) {
$vars['key_value_formatter_class'] = '{field_label|camelize}KeyValueFormatter';
$assets->addFile('src/Plugin/Field/FieldFormatter/{key_value_formatter_class}.php', 'key-value-formatter.twig');
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Form;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
/**
* Config form generator.
*
* @todo Clean-up.
*/
#[Generator(
name: 'form:config',
description: 'Generates a configuration form',
aliases: ['config-form'],
templatePath: Application::TEMPLATE_PATH . '/Form/_config',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Config extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: 'SettingsForm');
$vars['raw_form_id'] = \preg_replace('/_form/', '', Utils::camel2machine($vars['class']));
$vars['form_id'] = '{machine_name}_{raw_form_id}';
$vars['route'] = $ir->confirm('Would you like to create a route for this form?');
if ($vars['route']) {
$default_route_path = \str_replace('_', '-', '/admin/config/system/' . $vars['raw_form_id']);
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $vars['raw_form_id']);
$vars['route_path'] = $ir->ask('Route path', $default_route_path);
$vars['route_title'] = $ir->ask('Route title', '{raw_form_id|m2h}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'administer site configuration');
$assets->addFile('{machine_name}.routing.yml')
->template('routing.twig')
->appendIfExists();
if ($vars['link'] = $ir->confirm('Would you like to create a menu link for this route?')) {
$vars['link_title'] = $ir->ask('Link title', $vars['route_title']);
$vars['link_description'] = $ir->ask('Link description');
// Try to guess parent menu item using route path.
if (\preg_match('#^/admin/config/([^/]+)/[^/]+$#', $vars['route_path'], $matches)) {
$vars['link_parent'] = $ir->ask('Parent menu item', 'system.admin_config_' . $matches[1]);
}
$assets->addFile('{machine_name}.links.menu.yml')
->template('links.menu.twig')
->appendIfExists();
}
}
$assets->addFile('src/Form/{class}.php', 'form.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Form;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
/**
* Confirm form generator.
*
* @todo Clean-up.
*/
#[Generator(
name: 'form:confirm',
description: 'Generates a confirmation form',
aliases: ['confirm-form'],
templatePath: Application::TEMPLATE_PATH . '/Form/_confirm',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Confirm extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: 'ExampleConfirmForm');
$vars['raw_form_id'] = \preg_replace('/_form/', '', Utils::camel2machine($vars['class']));
$vars['form_id'] = '{machine_name}_{raw_form_id}';
$vars['route'] = $ir->confirm('Would you like to create a route for this form?');
if ($vars['route']) {
$default_route_path = \str_replace('_', '-', '/' . $vars['machine_name'] . '/' . $vars['raw_form_id']);
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $vars['raw_form_id']);
$vars['route_path'] = $ir->ask('Route path', $default_route_path);
$vars['route_title'] = $ir->ask('Route title', '{raw_form_id|m2t}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'administer site configuration');
$assets->addFile('{machine_name}.routing.yml')
->template('routing.twig')
->appendIfExists();
}
$assets->addFile('src/Form/{class}.php', 'form.twig');
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Form;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
/**
* A generator for a simple form.
*/
#[Generator(
name: 'form:simple',
description: 'Generates simple form',
aliases: ['form'],
templatePath: Application::TEMPLATE_PATH . '/Form/_simple',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Simple extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: 'ExampleForm');
$vars['raw_form_id'] = Utils::camel2machine(Utils::removeSuffix($vars['class'], 'Form'));
$vars['form_id'] = '{machine_name}_{raw_form_id}';
$vars['route'] = $ir->confirm('Would you like to create a route for this form?');
if ($vars['route']) {
$vars['route_name'] = $ir->ask('Route name', '{machine_name}.' . $vars['raw_form_id']);
$default_route_path = \str_replace('_', '-', '/' . $vars['machine_name'] . '/' . $vars['raw_form_id']);
$vars['route_path'] = $ir->ask('Route path', $default_route_path);
$vars['route_title'] = $ir->ask('Route title', '{raw_form_id|m2t}');
$vars['route_permission'] = $ir->askPermission('Route permission', 'access content');
$assets->addFile('{machine_name}.routing.yml')
->template('routing.twig')
->appendIfExists();
}
$assets->addFile('src/Form/{class}.php', 'form.twig');
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\Chained;
use DrupalCodeGenerator\Validator\Choice;
use DrupalCodeGenerator\Validator\Required;
use Symfony\Component\Console\Question\Question;
#[Generator(
name: 'hook',
description: 'Generates a hook',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Hook extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
/** @var \DrupalCodeGenerator\Helper\Drupal\HookInfo $hook_info */
$hook_info = $this->getHelper('hook_info');
$hook_templates = $hook_info->getHookTemplates();
$available_hooks = \array_keys($hook_templates);
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$hook_question = new Question('Hook name');
$validator = new Chained(
new Required(),
new Choice($available_hooks, 'The value is not correct hook name.'),
);
$hook_question->setValidator($validator);
$hook_question->setAutocompleterValues($available_hooks);
$vars['hook_name'] = $this->io()->askQuestion($hook_question);
$vars['file_type'] = $hook_info::getFileType($vars['hook_name']);
$assets->addFile('{machine_name}.{file_type}')
->inlineTemplate($hook_templates[$vars['hook_name']])
->appendIfExists(9);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'install-file',
description: 'Generates an install file',
templatePath: Application::TEMPLATE_PATH . '/_install-file',
type: GeneratorType::MODULE_COMPONENT,
)]
final class InstallFile extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$assets->addFile('{machine_name}.install', 'install.twig');
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'javascript',
description: 'Generates Drupal JavaScript file',
templatePath: Application::TEMPLATE_PATH . '/_javascript',
type: GeneratorType::MODULE_COMPONENT,
)]
final class JavaScript extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['file_name_full'] = $ir->ask('File name', '{machine_name|u2h}.js');
$vars['file_name'] = \pathinfo($vars['file_name_full'], \PATHINFO_FILENAME);
$vars['behavior'] = Utils::camelize($vars['machine_name'], FALSE) . Utils::camelize($vars['file_name']);
if ($ir->confirm('Would you like to create a library for this file?')) {
$vars['library'] = $ir->ask('Library name', '{file_name|h2u}');
$assets->addFile('{machine_name}.libraries.yml', 'libraries.twig')
->appendIfExists();
}
$assets->addFile('js/{file_name_full}', 'javascript.twig');
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
/**
* Interface for generators that provide human-readable label.
*/
interface LabelInterface {
/**
* Returns the human-readable command label.
*/
public function getLabel(): ?string;
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'layout',
description: 'Generates a layout',
templatePath: Application::TEMPLATE_PATH . '/_layout',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Layout extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['layout_name'] = $ir->ask('Layout name', 'Example');
$vars['layout_machine_name'] = $ir->ask('Layout machine name', '{layout_name|h2m}');
$vars['category'] = $ir->ask('Category', '{machine_name|m2h} Layouts');
$vars['js'] = $ir->confirm('Would you like to create JavaScript file for this layout?', FALSE);
$vars['css'] = $ir->confirm('Would you like to create CSS file for this layout?', FALSE);
$assets->addFile('{machine_name}.layouts.yml', 'layouts.twig')
->appendIfExists();
if ($vars['js'] || $vars['css']) {
$assets->addFile('{machine_name}.libraries.yml', 'libraries.twig')
->appendIfExists();
}
$vars['layout_asset_name'] = '{layout_machine_name|u2h}';
$assets->addFile('layouts/{layout_machine_name}/{layout_asset_name}.html.twig', 'template.twig');
if ($vars['js']) {
$assets->addFile('layouts/{layout_machine_name}/{layout_asset_name}.js', 'javascript.twig');
}
if ($vars['css']) {
$assets->addFile('layouts/{layout_machine_name}/{layout_asset_name}.css', 'styles.twig');
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Miscellaneous;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\Chained;
use DrupalCodeGenerator\Validator\Required;
#[Generator(
name: 'misc:apache-virtual-host',
description: 'Generates an Apache site configuration file',
aliases: ['apache-virtual-host'],
templatePath: Application::TEMPLATE_PATH . '/Miscellaneous/_apache-virtual-host',
type: GeneratorType::OTHER,
)]
final class ApacheVirtualHost extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$vars['hostname'] = $this->io()->ask('Host name', 'example.local', self::getDomainValidator());
$vars['docroot'] = $this->io()->ask('Document root', \DRUPAL_ROOT);
$assets->addFile('{hostname}.conf', 'host.twig');
$assets->addFile('{hostname}-ssl.conf', 'host-ssl.twig');
}
/**
* Builds domain validator.
*/
private static function getDomainValidator(): callable {
return new Chained(
new Required(),
static function (string $value): string {
if (!\filter_var($value, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME)) {
throw new \UnexpectedValueException('The value is not correct domain name.');
}
return $value;
},
);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Miscellaneous;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\Chained;
use DrupalCodeGenerator\Validator\Required;
/**
* Nginx config generator.
*/
#[Generator(
name: 'misc:nginx-virtual-host',
description: 'Generates an Nginx site configuration file',
aliases: ['nginx-virtual-host'],
templatePath: Application::TEMPLATE_PATH . '/Miscellaneous/_nginx-virtual-host',
type: GeneratorType::OTHER,
)]
final class NginxVirtualHost extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$socket = \sprintf('/run/php/php%s.%s-fpm.sock', \PHP_MAJOR_VERSION, \PHP_MINOR_VERSION);
$vars['hostname'] = $this->io()->ask('Host name', 'example.local', self::getDomainValidator());
$vars['docroot'] = $this->io()->ask('Document root', \DRUPAL_ROOT);
$vars['file_public_path'] = $this->io()->ask('Public file system path', 'sites/default/files');
$vars['file_private_path'] = $this->io()->ask('Private file system path');
$vars['fastcgi_pass'] = $this->io()->ask('Address of a FastCGI server', 'unix:' . $socket);
if ($vars['file_public_path']) {
$vars['file_public_path'] = \trim($vars['file_public_path'], '/');
}
if ($vars['file_private_path']) {
$vars['file_private_path'] = \trim($vars['file_private_path'], '/');
}
$assets->addFile('{hostname}', 'host.twig');
}
/**
* Builds domain validator.
*/
private static function getDomainValidator(): callable {
return new Chained(
new Required(),
static function (string $value): string {
if (!\filter_var($value, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME)) {
throw new \UnexpectedValueException('The value is not correct domain name.');
}
return $value;
},
);
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Core\Extension\ModuleExtensionList;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\Required;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'module',
description: 'Generates Drupal module',
templatePath: Application::TEMPLATE_PATH . '/_module',
type: GeneratorType::MODULE,
)]
final class Module extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly ModuleExtensionList $moduleList,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container->get('extension.list.module'));
}
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['name'] = $ir->askName();
$vars['machine_name'] = $ir->askMachineName();
$vars['description'] = $ir->ask('Module description', validator: new Required());
$vars['package'] = $ir->ask('Package', 'Custom');
$dependencies = $ir->ask('Dependencies (comma separated)');
$vars['dependencies'] = $this->buildDependencies($dependencies);
$assets->addFile('{machine_name}/{machine_name}.info.yml', 'model.info.yml.twig');
if ($ir->confirm('Would you like to create module file?', FALSE)) {
$assets->addFile('{machine_name}/{machine_name}.module', 'model.module.twig');
}
if ($ir->confirm('Would you like to create install file?', FALSE)) {
$assets->addFile('{machine_name}/{machine_name}.install', 'model.install.twig');
}
if ($ir->confirm('Would you like to create README.md file?', FALSE)) {
$assets->addFile('{machine_name}/README.md', 'README.md.twig');
}
}
/**
* Builds array of dependencies from comma-separated string.
*/
private function buildDependencies(?string $dependencies_encoded): array {
$dependencies = $dependencies_encoded ? \explode(',', $dependencies_encoded) : [];
foreach ($dependencies as &$dependency) {
$dependency = \str_replace(' ', '_', \trim(\strtolower($dependency)));
// Check if the module name is already prefixed.
if (\str_contains($dependency, ':')) {
continue;
}
// Dependencies should be namespaced in the format {project}:{name}.
$project = $dependency;
try {
// The extension list is internal for extending not for instantiating.
// @see \Drupal\Core\Extension\ExtensionList
/** @psalm-suppress InternalMethod */
$package = $this->moduleList->getExtensionInfo($dependency)['package'] ?? NULL;
if ($package === 'Core') {
$project = 'drupal';
}
}
catch (UnknownExtensionException) {
}
$dependency = $project . ':' . $dependency;
}
$dependency_sorter = static function (string $a, string $b): int {
// Core dependencies go first.
$a_is_drupal = \str_starts_with($a, 'drupal:');
$b_is_drupal = \str_starts_with($b, 'drupal:');
if ($a_is_drupal xor $b_is_drupal) {
return $a_is_drupal ? -1 : 1;
}
return $a <=> $b;
};
\uasort($dependencies, $dependency_sorter);
return $dependencies;
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\InputOutput\DefaultOptions;
use DrupalCodeGenerator\InputOutput\IOAwareInterface;
use DrupalCodeGenerator\InputOutput\IOAwareTrait;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
/**
* Implements navigation command.
*/
#[AsCommand(name: 'navigation')]
final class Navigation extends Command implements IOAwareInterface, LoggerAwareInterface {
use IOAwareTrait;
use LoggerAwareTrait;
/**
* Menu tree.
*/
private array $menuTree = [];
/**
* Menu labels.
*/
private array $labels = [
'misc:d7' => 'Drupal 7',
'yml' => 'Yaml',
'misc' => 'Miscellaneous',
];
/**
* {@inheritdoc}
*/
protected function configure(): void {
// As the navigation is default command the help should be relevant to the
// entire DCG application.
$help = <<<'EOT'
<info>dcg</info> Display navigation
<info>dcg plugin:field:widget</info> Run a specific generator
<info>dcg list</info> List all available generators
EOT;
$this
->setName('navigation')
->setDescription('Command line code generator')
->setHelp($help)
->setHidden();
DefaultOptions::apply($this);
}
/**
* {@inheritdoc}
*/
public function getSynopsis($short = FALSE): string {
return 'dcg [options] <generator>';
}
/**
* {@inheritdoc}
*/
protected function initialize(InputInterface $input, OutputInterface $output): void {
parent::initialize($input, $output);
// Build the menu structure.
$this->menuTree = [];
if (!$application = $this->getApplication()) {
throw new \LogicException('Navigation command cannot work without application');
}
foreach ($application->all() as $command) {
if ($command instanceof LabelInterface && !$command->isHidden()) {
/** @var string $command_name */
$command_name = $command->getName();
self::arraySetNestedValue($this->menuTree, \explode(':', $command_name));
// Collect command labels.
if ($label = $command->getLabel()) {
$this->labels[$command_name] = $label;
}
}
}
self::recursiveKsort($this->menuTree);
$style = new OutputFormatterStyle('white', 'blue', ['bold']);
$output->getFormatter()->setStyle('title', $style);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
if ($command_name = $this->selectGenerator($input, $output)) {
if (!$application = $this->getApplication()) {
throw new \LogicException('Navigation command cannot work without application');
}
return $application
->find($command_name)
->run($input, $output);
}
return 0;
}
/**
* Selects a generator.
*
* Returns a generator selected by the user from a multilevel console menu or
* null if user decided to exit the navigation.
*/
private function selectGenerator(InputInterface $input, OutputInterface $output, array $menu_trail = []): ?string {
// Narrow down the menu tree using menu trail.
$active_menu_tree = $this->menuTree;
foreach ($menu_trail as $active_menu_item) {
$active_menu_tree = $active_menu_tree[$active_menu_item];
}
// The $active_menu_tree can be either an array of menu items or TRUE if the
// user has reached the final menu point.
if ($active_menu_tree === TRUE) {
return \implode(':', $menu_trail);
}
$sub_menu_labels = $command_labels = [];
foreach ($active_menu_tree as $menu_item => $subtree) {
$command_name = $menu_trail ? (\implode(':', $menu_trail) . ':' . $menu_item) : $menu_item;
$label = $this->labels[$command_name] ?? \str_replace(['-', '_'], ' ', \ucfirst($menu_item));
\is_array($subtree)
? $sub_menu_labels[$menu_item] = "<comment>$label</comment>"
: $command_labels[$menu_item] = $label;
}
// Generally the choices array consists of the following parts:
// - Reference to the parent menu level.
// - Sorted list of nested menu levels.
// - Sorted list of commands.
\natcasesort($sub_menu_labels);
\natcasesort($command_labels);
$choices = ['..' => '..'] + $sub_menu_labels + $command_labels;
$question = new ChoiceQuestion('<title> Select generator </title>', \array_values($choices));
$answer_label = $this->getHelper('question')->ask($input, $output, $question);
$answer = \array_search($answer_label, $choices);
if ($answer === '..') {
// Exit the application if a user selected zero on the top menu level.
if (\count($menu_trail) === 0) {
return NULL;
}
// Level up.
\array_pop($menu_trail);
}
else {
// Level down.
$menu_trail[] = $answer;
}
return $this->selectGenerator($input, $output, $menu_trail);
}
/**
* Sort multidimensional array by keys.
*
* @param array $array
* An array being sorted.
*/
private static function recursiveKsort(array &$array): void {
foreach ($array as &$value) {
if (\is_array($value)) {
self::recursiveKsort($value);
}
}
\ksort($array);
}
/**
* Sets the property to true in nested array.
*
* @psalm-param list<string> $parents
* An array of parent keys, starting with the outermost key.
*
* @see https://api.drupal.org/api/drupal/includes!common.inc/function/drupal_array_set_nested_value/7
*/
private static function arraySetNestedValue(array &$array, array $parents): void {
$ref = &$array;
foreach ($parents as $parent) {
if (isset($ref) && !\is_array($ref)) {
$ref = [];
}
// @todo Fix this.
/** @psalm-suppress PossiblyNullArrayAccess */
$ref = &$ref[$parent];
}
$ref ??= TRUE;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for configuration entity types.
*/
final class ConfigEntityIds {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$entity_definitions = \array_filter(
$this->entityTypeManager->getDefinitions(),
static fn (EntityTypeInterface $entity_type): bool => $entity_type instanceof ConfigEntityTypeInterface,
);
\ksort($entity_definitions);
$definitions = [];
foreach ($entity_definitions as $type => $entity_definition) {
/** @psalm-var array<string, string> $ids */
$ids = $this->entityTypeManager
->getStorage($type)
->getQuery()
->accessCheck(FALSE)
->execute();
if (\count($ids) > 0) {
$definitions[] = [
'type' => $type,
'label' => $entity_definition->getLabel(),
'class' => Utils::addLeadingSlash($entity_definition->getClass()),
'interface' => ($this->entityInterface)($entity_definition),
'ids' => \array_values($ids),
];
}
}
return File::create('.phpstorm.meta.php/config_entity_ids.php')
->template('config_entity_ids.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\ConfigInfo;
/**
* Generates PhpStorm meta-data for Drupal configuration.
*/
final class Configuration {
/**
* Constructs the object.
*/
public function __construct(
private readonly ConfigInfo $configInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/configuration.php')
->template('configuration.php.twig')
->vars(['configs' => $this->configInfo->getConfigNames()]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Database\Connection;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal database.
*/
final class Database {
/**
* Constructs the object.
*/
public function __construct(
private readonly Connection $connection,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$driver = $this->connection->driver();
$tables = [];
// @todo Support PostgreSQL.
if ($driver === 'mysql') {
/** @psalm-suppress PossiblyNullReference, PossiblyInvalidMethodCall */
$tables = $this->connection->query('SHOW TABLES')->fetchCol();
}
elseif ($driver === 'sqlite') {
$query = <<< 'SQL'
SELECT name
FROM sqlite_schema
WHERE type ='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
SQL;
/** @psalm-suppress PossiblyNullReference, PossiblyInvalidMethodCall */
$tables = $this->connection->query($query)->fetchCol();
}
return File::create('.phpstorm.meta.php/database.php')
->template('database.php.twig')
->vars(['tables' => $tables]);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for date formats.
*/
final class DateFormats {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$date_formats = $this->entityTypeManager
->getStorage('date_format')
->loadMultiple();
$date_formats['custom'] = NULL;
return File::create('.phpstorm.meta.php/date_formats.php')
->template('date_formats.php.twig')
->vars(['date_formats' => \array_keys($date_formats)]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity bundles.
*/
final class EntityBundles {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly EntityTypeBundleInfoInterface $entityTypeBundleInfo,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$definitions = [];
$entity_definitions = $this->entityTypeManager->getDefinitions();
\ksort($entity_definitions);
$bundle_getters = [
'node' => 'getType',
'comment' => 'getTypeId',
];
foreach ($entity_definitions as $entity_type_id => $entity_definition) {
$definitions[] = [
'type' => $entity_type_id,
'label' => $entity_definition->getLabel(),
'class' => Utils::addLeadingSlash($entity_definition->getClass()),
'interface' => ($this->entityInterface)($entity_definition),
'bundle_getter' => $bundle_getters[$entity_type_id] ?? NULL,
'bundles' => \array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)),
];
}
return File::create('.phpstorm.meta.php/entity_bundles.php')
->template('entity_bundles.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity links.
*/
final class EntityLinks {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$definitions = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) {
$definitions[] = [
'type' => $entity_type,
'label' => $definition->getLabel(),
'class' => Utils::addLeadingSlash($definition->getClass()),
'interface' => ($this->entityInterface)($definition),
'links' => \array_keys($definition->getLinkTemplates()),
];
}
\asort($definitions);
return File::create('.phpstorm.meta.php/entity_links.php')
->template('entity_links.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity types.
*/
final class EntityTypes {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$normalized_definitions = [];
foreach ($this->entityTypeManager->getDefinitions() as $type => $definition) {
$normalized_definitions[$type]['class'] = self::normalizeType($definition->getClass());
$normalized_definitions[$type]['storage'] = self::normalizeType($definition->getStorageClass());
$normalized_definitions[$type]['access_control'] = self::normalizeType($definition->getAccessControlClass());
$normalized_definitions[$type]['list_builder'] = self::normalizeType($definition->getListBuilderClass());
$normalized_definitions[$type]['view_builder'] = self::normalizeType($definition->getViewBuilderClass());
}
\ksort($normalized_definitions);
return File::create('.phpstorm.meta.php/entity_types.php')
->template('entity_types.php.twig')
->vars(['definitions' => $normalized_definitions]);
}
/**
* Normalizes handler type.
*/
private static function normalizeType(?string $class): ?string {
if ($class === NULL) {
return NULL;
}
$class = Utils::addLeadingSlash($class);
/** @psalm-var class-string $interface */
$interface = $class . 'Interface';
return \is_a($class, $interface, TRUE) ? $interface : $class;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for extensions.
*/
final class Extensions {
/**
* Constructs the object.
*/
public function __construct(
private readonly ModuleHandlerInterface $moduleHandler,
private readonly ThemeHandlerInterface $themeHandler,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
// Module handler also manages profiles.
$module_extensions = \array_filter(
$this->moduleHandler->getModuleList(),
static fn (Extension $extension): bool => $extension->getType() === 'module',
);
$modules = \array_keys($module_extensions);
$themes = \array_keys($this->themeHandler->listInfo());
\sort($themes);
return File::create('.phpstorm.meta.php/extensions.php')
->template('extensions.php.twig')
->vars(['modules' => $modules, 'themes' => $themes]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for field definitions.
*/
final class FieldDefinitions {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly FieldTypePluginManagerInterface $fieldTypePluginManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$entity_types = \array_keys($this->entityTypeManager->getDefinitions());
\sort($entity_types);
$field_types = \array_keys($this->fieldTypePluginManager->getDefinitions());
\sort($field_types);
return File::create('.phpstorm.meta.php/field_definitions.php')
->template('field_definitions.php.twig')
->vars(['entity_types' => $entity_types, 'field_types' => $field_types]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for entity fields.
*/
final class Fields {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
private readonly EntityFieldManagerInterface $entityFieldManager,
private readonly \Closure $entityInterface,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$definitions = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) {
if (!$definition->entityClassImplements(FieldableEntityInterface::class)) {
continue;
}
$definitions[] = [
'type' => $entity_type,
'label' => $definition->getLabel(),
'class' => Utils::addLeadingSlash($definition->getClass()),
'interface' => ($this->entityInterface)($definition),
'fields' => \array_keys($this->entityFieldManager->getFieldStorageDefinitions($entity_type)),
];
}
return File::create('.phpstorm.meta.php/fields.php')
->template('fields.php.twig')
->vars(['definitions' => $definitions]);
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal filesystem helpers.
*/
final class FileSystem {
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/file_system.php')
->template('file_system.php.twig');
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for miscellaneous Drupal methods.
*/
final class Miscellaneous {
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/miscellaneous.php')
->template('miscellaneous.php.twig');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\PermissionInfo;
/**
* Generates PhpStorm meta-data for permissions.
*/
final class Permissions {
/**
* Constructs the object.
*/
public function __construct(
private readonly PermissionInfo $permissionInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$permissions = $this->permissionInfo->getPermissionNames();
return File::create('.phpstorm.meta.php/permissions.php')
->template('permissions.php.twig')
->vars(['permissions' => $permissions]);
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'phpstorm-meta',
description: 'Generates PhpStorm metadata',
templatePath: Application::TEMPLATE_PATH . '/_phpstorm-meta',
type: GeneratorType::OTHER,
label: 'PhpStorm metadata',
)]
final class PhpStormMeta extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly ContainerInterface $container,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container);
}
/**
* {@inheritdoc}
*
* @noinspection PhpParamsInspection
* @psalm-suppress ArgumentTypeCoercion
*/
protected function generate(array &$vars, Assets $assets): void {
$service = fn (string $name): object => $this->container->get($name);
$entity_interface = static function (EntityTypeInterface $definition): ?string {
$class = Utils::addLeadingSlash($definition->getClass());
// Most content entity types implement an interface which name follows
// this pattern.
$interface = \str_replace('\Entity\\', '\\', $class) . 'Interface';
return $definition->entityClassImplements($interface) ? $interface : NULL;
};
$assets[] = (new ConfigEntityIds($service('entity_type.manager'), $entity_interface))();
$assets[] = (new Configuration($this->getHelper('config_info')))();
$assets[] = (new Database($service('database')))();
$assets[] = (new DateFormats($service('entity_type.manager')))();
$assets[] = (new EntityBundles($service('entity_type.manager'), $service('entity_type.bundle.info'), $entity_interface))();
$assets[] = (new EntityLinks($service('entity_type.manager'), $entity_interface))();
$assets[] = (new EntityTypes($service('entity_type.manager')))();
$assets[] = (new Extensions($service('module_handler'), $service('theme_handler')))();
$assets[] = (new FieldDefinitions($service('entity_type.manager'), $service('plugin.manager.field.field_type')))();
$assets[] = (new Fields($service('entity_type.manager'), $service('entity_field.manager'), $entity_interface))();
$assets[] = (new FileSystem())();
$assets[] = (new Miscellaneous())();
$assets[] = (new Permissions($this->getHelper('permission_info')))();
$assets[] = (new Plugins($this->getHelper('service_info')))();
$assets[] = (new Roles($service('entity_type.manager')))();
$assets[] = (new Routes($this->getHelper('route_info')))();
$assets[] = (new Services($this->getHelper('service_info')))();
$assets[] = (new Settings())();
$assets[] = (new States($service('keyvalue')))();
}
/**
* {@inheritdoc}
*/
protected function getDestination(array $vars): string {
// Typically the root of the PhpStorm project is one level above of the
// Drupal root.
if (!\file_exists(\DRUPAL_ROOT . '/.idea') && \file_exists(\DRUPAL_ROOT . '/../.idea')) {
return \DRUPAL_ROOT . '/..';
}
return \DRUPAL_ROOT;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Plugin\DefaultPluginManager;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\ServiceInfo;
use DrupalCodeGenerator\Utils;
/**
* Generates PhpStorm meta-data for plugins.
*/
final class Plugins {
/**
* Constructs the object.
*/
public function __construct(
private readonly ServiceInfo $serviceInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$plugins = [];
foreach ($this->serviceInfo->getServiceClasses() as $manager_id => $class) {
/** @var class-string $class */
if (!\is_subclass_of($class, DefaultPluginManager::class)) {
continue;
}
/** @var \Drupal\Core\Plugin\DefaultPluginManager $manager */
$manager = $this->serviceInfo->getService($manager_id);
$guessed_interface = $class . 'Interface';
$interface = $manager instanceof $guessed_interface
? $guessed_interface : NULL;
$plugin_ids = \array_keys($manager->getDefinitions());
\sort($plugin_ids);
$plugins[] = [
'manager_id' => $manager_id,
'manager_class' => $class,
'manager_interface' => $interface,
'plugin_interface' => self::getPluginInterface($manager),
'plugin_ids' => $plugin_ids,
];
}
return File::create('.phpstorm.meta.php/plugins.php')
->template('plugins.php.twig')
->vars(['plugins' => $plugins]);
}
/**
* Getter for protected 'pluginInterface' property.
*/
private static function getPluginInterface(DefaultPluginManager $manager): ?string {
$interface = (new \ReflectionClass($manager))
->getProperty('pluginInterface')
->getValue($manager);
return $interface ? Utils::addLeadingSlash($interface) : NULL;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for roles.
*/
final class Roles {
/**
* Constructs the object.
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
// @todo Create a helper for roles.
$roles = $this->entityTypeManager->getStorage('user_role')->loadMultiple();
return File::create('.phpstorm.meta.php/roles.php')
->template('roles.php.twig')
->vars(['roles' => \array_keys($roles)]);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\RouteInfo;
/**
* Generates PhpStorm meta-data for routes.
*/
final class Routes {
/**
* Constructs the object.
*/
public function __construct(
private readonly RouteInfo $routeInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$routes = $this->routeInfo->getRouteNames();
$route_attributes = $this->getRouteAttributes();
return File::create('.phpstorm.meta.php/routes.php')
->template('routes.php.twig')
->vars(['routes' => $routes, 'route_attributes' => $route_attributes]);
}
/**
* Builds attributes suitable for Route autocompletion.
*/
private function getRouteAttributes(): array {
/** @psalm-var array{options: array, requirements: array, defaults: array} $route_attributes */
$route_attributes = [
'options' => [],
'requirements' => [],
'defaults' => [],
];
foreach ($this->routeInfo->getRoutes() as $route) {
$route_attributes['options'] += $route->getOptions();
$route_attributes['requirements'] += $route->getRequirements();
$route_attributes['defaults'] += $route->getDefaults();
}
$is_internal = static fn (string $option_name): bool => \str_starts_with($option_name, '_');
foreach ($route_attributes as $name => $attributes) {
$route_attributes[$name] = \array_filter(\array_keys($route_attributes[$name]), $is_internal);
\sort($route_attributes[$name]);
}
return $route_attributes;
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Helper\Drupal\ServiceInfo;
/**
* Generates PhpStorm meta-data for services.
*/
final class Services {
/**
* Constructs the object.
*/
public function __construct(
private readonly ServiceInfo $serviceInfo,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/services.php')
->template('services.php.twig')
->vars(['services' => $this->serviceInfo->getServiceClasses()]);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\Site\Settings as DrupalSettings;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal settings.
*/
final class Settings {
/**
* Generator callback.
*/
public function __invoke(): File {
return File::create('.phpstorm.meta.php/settings.php')
->template('settings.php.twig')
->vars(['settings' => \array_keys(DrupalSettings::getAll())]);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\PhpStormMeta;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use DrupalCodeGenerator\Asset\File;
/**
* Generates PhpStorm meta-data for Drupal states.
*/
final class States {
/**
* Constructs the object.
*/
public function __construct(
private readonly KeyValueFactoryInterface $keyValueStore,
) {}
/**
* Generator callback.
*/
public function __invoke(): File {
$states = \array_keys($this->keyValueStore->get('state')->getAll());
return File::create('.phpstorm.meta.php/states.php')
->template('states.php.twig')
->vars(['states' => $states]);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RequiredMachineName;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'plugin:action',
description: 'Generates action plugin',
aliases: ['action'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_action',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Action extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel('Action label');
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['category'] = $ir->ask('Action category', 'Custom');
// @todo Create a helper for this.
$definitions = \array_filter(
$this->entityTypeManager->getDefinitions(),
static fn (EntityTypeInterface $definition): bool => $definition instanceof ContentEntityTypeInterface,
);
$entity_type_question = new Question('Entity type to apply the action', 'node');
$entity_type_question->setValidator(new RequiredMachineName());
$entity_type_question->setAutocompleterValues(\array_keys($definitions));
$vars['entity_type'] = $this->io()->askQuestion($entity_type_question);
$vars['configurable'] = $ir->confirm('Make the action configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Action/{class}.php', 'action.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:block',
description: 'Generates block plugin',
aliases: ['block'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_block',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Block extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel('Block admin label');
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Block');
$vars['category'] = $ir->ask('Block category', 'Custom');
$vars['configurable'] = $ir->confirm('Make the block configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$vars['access'] = $ir->confirm('Create access callback?', FALSE);
$assets->addFile('src/Plugin/Block/{class}.php', 'block.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'plugin:ckeditor',
description: 'Generates CKEditor plugin',
aliases: ['ckeditor', 'ckeditor-plugin'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_ckeditor',
type: GeneratorType::MODULE_COMPONENT,
label: 'CKEditor',
)]
final class CKEditor extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['unprefixed_plugin_id'] = Utils::removePrefix($vars['plugin_id'], $vars['machine_name'] . '_');
$vars['class'] = Utils::camelize($vars['unprefixed_plugin_id']);
// Convert plugin ID to hyphen case.
$vars['fe_plugin_id'] = Utils::camelize($vars['unprefixed_plugin_id'], FALSE);
$assets->addFile('webpack.config.js', 'webpack.config.js');
$assets->addFile('package.json', 'package.json.twig');
$assets->addFile('.gitignore', 'gitignore');
$assets->addFile('{machine_name}.libraries.yml', 'model.libraries.yml.twig')
->appendIfExists();
$assets->addFile('{machine_name}.ckeditor5.yml', 'model.ckeditor5.yml.twig')
->appendIfExists();
$assets->addFile('css/{unprefixed_plugin_id|u2h}.admin.css', 'css/model.admin.css.twig');
$assets->addFile('icons/{unprefixed_plugin_id|u2h}.svg', 'icons/example.svg');
$assets->addFile('js/ckeditor5_plugins/{fe_plugin_id}/src/{class}.js', 'js/ckeditor5_plugins/example/src/Example.js.twig');
$assets->addFile('js/ckeditor5_plugins/{fe_plugin_id}/src/index.js', 'js/ckeditor5_plugins/example/src/index.js.twig');
$assets->addDirectory('js/build');
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:condition',
description: 'Generates condition plugin',
aliases: ['condition'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_condition',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Condition extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Condition/{class}.php', 'condition.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\Chained;
use DrupalCodeGenerator\Validator\RegExp;
use DrupalCodeGenerator\Validator\Required;
/**
* Constraint generator.
*
* @todo Clean-up.
* @todo Create SUT test.
*/
#[Generator(
name: 'plugin:constraint',
description: 'Generates constraint plugin',
aliases: ['constraint'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_constraint',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Constraint extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['plugin_label'] = $ir->askPluginLabel();
// Unlike other plugin types, constraint IDs use camel case.
$validator = new Chained(
new Required(),
new RegExp('/^[a-z][a-z0-9_]*[a-z0-9]$/i', 'The value is not correct constraint ID.'),
);
$vars['plugin_id'] = $ir->ask('Plugin ID', '{name|camelize}{plugin_label|camelize}', $validator);
$unprefixed_plugin_id = Utils::removePrefix($vars['plugin_id'], Utils::camelize($vars['machine_name']));
$vars['class'] = $ir->askPluginClass(default: $unprefixed_plugin_id . 'Constraint');
$input_types = [
'raw_value' => 'Raw value',
'item' => 'Item',
'item_list' => 'Item list',
'entity' => 'Entity',
];
$vars['input_type'] = $ir->choice('Type of data to validate', $input_types);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Validation/Constraint/{class}.php')
->template('constraint.twig');
$assets->addFile('src/Plugin/Validation/Constraint/{class}Validator.php')
->template('validator.twig');
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use Drupal\comment\Plugin\EntityReferenceSelection\CommentSelection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\file\Plugin\EntityReferenceSelection\FileSelection;
use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection;
use Drupal\taxonomy\Plugin\EntityReferenceSelection\TermSelection;
use Drupal\user\Plugin\EntityReferenceSelection\UserSelection;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RequiredMachineName;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[Generator(
name: 'plugin:entity-reference-selection',
description: 'Generates entity reference selection plugin',
aliases: ['entity-reference-selection'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_entity-reference-selection',
type: GeneratorType::MODULE_COMPONENT,
)]
final class EntityReferenceSelection extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self($container->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$entity_type_question = new Question('Entity type that can be referenced by this plugin', 'node');
$entity_type_question->setValidator(new RequiredMachineName());
$entity_types = \array_keys($this->entityTypeManager->getDefinitions());
$entity_type_question->setAutocompleterValues($entity_types);
$vars['entity_type'] = $this->io()->askQuestion($entity_type_question);
$vars['plugin_label'] = $ir->askPluginLabel('Plugin label', 'Advanced {entity_type} selection');
$vars['plugin_id'] = $ir->askPluginId(default: '{machine_name}_{entity_type}_selection');
$vars['class'] = $ir->askPluginClass(default: '{entity_type|camelize}Selection');
$vars['configurable'] = $ir->confirm('Provide additional plugin configuration?', FALSE);
$vars['base_class_full'] = match($vars['entity_type']) {
'comment' => CommentSelection::class,
'file' => FileSelection::class,
'node' => NodeSelection::class,
'taxonomy_term' => TermSelection::class,
'user' => UserSelection::class,
default => DefaultSelection::class,
};
$vars['base_class'] = \explode('EntityReferenceSelection\\', $vars['base_class_full'])[1];
$assets->addFile('src/Plugin/EntityReferenceSelection/{class}.php')
->template('entity-reference-selection.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Field;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:field:formatter',
description: 'Generates field formatter plugin',
aliases: ['field-formatter'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Field/_formatter',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Formatter extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Formatter');
$vars['configurable'] = $ir->confirm('Make the formatter configurable?', FALSE);
$assets->addFile('src/Plugin/Field/FieldFormatter/{class}.php', 'formatter.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Field;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:field:type',
description: 'Generates field type plugin',
aliases: ['field-type'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Field/_type',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Type extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Item');
$vars['configurable_storage'] = $ir->confirm('Make the field storage configurable?', FALSE);
$vars['configurable_instance'] = $ir->confirm('Make the field instance configurable?', FALSE);
$assets->addFile('src/Plugin/Field/FieldType/{class}.php', 'type.twig');
$assets->addSchemaFile()->template('schema.twig');
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Field;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:field:widget',
description: 'Generates field widget plugin',
aliases: ['field-widget'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Field/_widget',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Widget extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Widget');
$vars['configurable'] = $ir->confirm('Make the widget configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Field/FieldWidget/{class}.php', 'widget.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:filter',
description: 'Generates filter plugin',
aliases: ['filter'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_filter',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Filter extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$filter_types = [
'TYPE_HTML_RESTRICTOR' => 'HTML restrictor',
'TYPE_MARKUP_LANGUAGE' => 'Markup language',
'TYPE_TRANSFORM_IRREVERSIBLE' => 'Irreversible transformation',
'TYPE_TRANSFORM_REVERSIBLE' => 'Reversible transformation',
];
$vars['filter_type'] = $ir->choice('Filter type', $filter_types);
$vars['configurable'] = $ir->confirm('Make the filter configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Filter/{class}.php', 'filter.twig');
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:menu-link',
description: 'Generates menu-link plugin',
aliases: ['menu-link'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_menu-link',
type: GeneratorType::MODULE_COMPONENT,
)]
final class MenuLink extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askPluginClass('Class', '{machine_name|camelize}MenuLink');
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/Menu/{class}.php', 'menu-link.twig');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Migrate;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:migrate:destination',
description: 'Generates migrate destination plugin',
aliases: ['migrate-destination'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Migrate/_destination',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Destination extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_id'] = $ir->askPluginId(default: NULL);
$vars['class'] = $ir->askPluginClass();
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/migrate/destination/{class}.php', 'destination.twig');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Migrate;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:migrate:process',
description: 'Generates migrate process plugin',
aliases: ['migrate-process'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Migrate/_process',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Process extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_id'] = $ir->askPluginId(default: NULL);
$vars['class'] = $ir->askPluginClass();
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/migrate/process/{class}.php', 'process.twig');
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Migrate;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:migrate:source',
description: 'Generates migrate source plugin',
aliases: ['migrate-source'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Migrate/_source',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Source extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_id'] = $ir->askPluginId(default: NULL);
$vars['class'] = $ir->askPluginClass();
$choices = [
'sql' => 'SQL',
'other' => 'Other',
];
$vars['source_type'] = $ir->choice('Source type', $choices);
$vars['base_class'] = $vars['source_type'] === 'sql' ? 'SqlBase' : 'SourcePluginBase';
$assets->addFile('src/Plugin/migrate/source/{class}.php', 'source.twig');
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:queue-worker',
description: 'Generates queue worker plugin',
aliases: ['queue-worker'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_queue-worker',
type: GeneratorType::MODULE_COMPONENT,
)]
final class QueueWorker extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/QueueWorker/{class}.php', 'queue-worker.twig');
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:rest-resource',
description: 'Generates rest resource plugin',
aliases: ['rest-resource'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/_rest-resource',
type: GeneratorType::MODULE_COMPONENT,
label: 'REST resource',
)]
final class RestResource extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass(suffix: 'Resource');
$assets->addFile('src/Plugin/rest/resource/{class}.php', 'rest-resource.twig');
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Views;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:views:argument-default',
description: 'Generates views default argument plugin',
aliases: ['views-argument-default'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Views/_argument-default',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ArgumentDefault extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['configurable'] = $ir->confirm('Make the plugin configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/views/argument_default/{class}.php')
->template('argument-default.twig');
if ($vars['configurable']) {
$assets->addSchemaFile('config/schema/{machine_name}.views.schema.yml')
->template('argument-default-schema.twig');
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Views;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:views:field',
description: 'Generates views field plugin',
aliases: ['views-field'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Views/_field',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Field extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['configurable'] = $ir->confirm('Make the plugin configurable?', FALSE);
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Plugin/views/field/{class}.php', 'field.twig');
if ($vars['configurable']) {
$assets->addSchemaFile('config/schema/{machine_name}.views.schema.yml')
->template('schema.twig');
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Plugin\Views;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'plugin:views:style',
description: 'Generates views style plugin',
aliases: ['views-style'],
templatePath: Application::TEMPLATE_PATH . '/Plugin/Views/_style',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Style extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['plugin_label'] = $ir->askPluginLabel();
$vars['plugin_id'] = $ir->askPluginId();
$vars['class'] = $ir->askPluginClass();
$vars['configurable'] = $ir->confirm('Make the plugin configurable?');
$assets->addFile('src/Plugin/views/style/{class}.php')
->template('style.twig');
$assets->addFile('templates/views-style-{plugin_id|u2h}.html.twig')
->template('template.twig');
$assets->addFile('{machine_name}.module')
->template('preprocess.twig')
->appendIfExists(9);
if ($vars['configurable']) {
$assets->addSchemaFile()->template('schema.twig');
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\Chained;
use DrupalCodeGenerator\Validator\RegExp;
use DrupalCodeGenerator\Validator\Required;
#[Generator(
name: 'plugin-manager',
description: 'Generates plugin manager',
aliases: ['plugin-manager'],
templatePath: Application::TEMPLATE_PATH . '/_plugin-manager',
type: GeneratorType::MODULE_COMPONENT,
)]
final class PluginManager extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
// Machine name validator does not allow dots.
$validator = new Chained(
new Required(),
new RegExp('/^[a-z][a-z0-9_\.]*[a-z0-9]$/', 'The value is not correct machine name.'),
);
$vars['plugin_type'] = $ir->ask('Plugin type', '{machine_name}', $validator);
$discovery_types = [
'annotation' => 'Annotation',
'attribute' => 'Attribute',
'yaml' => 'YAML',
'hook' => 'Hook',
];
$vars['discovery'] = $ir->choice('Discovery type', $discovery_types, 'Annotation');
$vars['class_prefix'] = '{plugin_type|camelize}';
$assets->addServicesFile()->template('{discovery}/model.services.yml.twig');
$assets->addFile('src/{class_prefix}Interface.php', '{discovery}/src/ExampleInterface.php.twig');
$assets->addFile('src/{class_prefix}PluginManager.php', '{discovery}/src/ExamplePluginManager.php.twig');
switch ($vars['discovery']) {
case 'annotation':
$assets->addFile('src/Annotation/{class_prefix}.php', 'annotation/src/Annotation/Example.php.twig');
$assets->addFile('src/{class_prefix}PluginBase.php', 'annotation/src/ExamplePluginBase.php.twig');
$assets->addFile('src/Plugin/{class_prefix}/Foo.php', 'annotation/src/Plugin/Example/Foo.php.twig');
break;
case 'attribute':
$assets->addFile('src/Attribute/{class_prefix}.php', 'attribute/src/Attribute/Example.php.twig');
$assets->addFile('src/{class_prefix}PluginBase.php', 'attribute/src/ExamplePluginBase.php.twig');
$assets->addFile('src/Plugin/{class_prefix}/Foo.php', 'attribute/src/Plugin/Example/Foo.php.twig');
break;
case 'yaml':
$assets->addFile('{machine_name}.{plugin_type|pluralize}.yml', 'yaml/model.examples.yml.twig');
$assets->addFile('src/{class_prefix}Default.php', 'yaml/src/ExampleDefault.php.twig');
break;
case 'hook':
$assets->addFile('{machine_name}.module', 'hook/model.module.twig')->appendIfExists(7);
$assets->addFile('src/{class_prefix}Default.php', 'hook/src/ExampleDefault.php.twig');
break;
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'readme',
description: 'Generates README file',
templatePath: Application::TEMPLATE_PATH . '/_readme',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Readme extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$assets->addFile('README.md', 'readme.twig');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RequiredMachineName;
#[Generator(
name: 'render-element',
description: 'Generates Drupal render element',
templatePath: Application::TEMPLATE_PATH . '/_render-element',
type: GeneratorType::MODULE_COMPONENT,
)]
final class RenderElement extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['type'] = $ir->ask('Element ID (#type)', validator: new RequiredMachineName());
$vars['class'] = $ir->askClass(default: '{type|camelize}');
$assets->addFile('src/Element/{class}.php', 'render-element.twig');
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RegExp;
#[Generator(
name: 'service:access-checker',
description: 'Generates an access checker service',
aliases: ['access-checker'],
templatePath: Application::TEMPLATE_PATH . '/Service/_access-checker',
type: GeneratorType::MODULE_COMPONENT,
)]
final class AccessChecker extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$validator = new RegExp(
'/^_[a-z0-9_]*[a-z0-9]$/',
'The value is not correct name for requirement name.',
);
$vars['requirement'] = $ir->ask('Requirement', '_foo', $validator);
$vars['class'] = $ir->askClass(default: '{requirement|camelize}AccessChecker');
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/Access/{class}.php', 'access-checker.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:breadcrumb-builder',
description: 'Generates a breadcrumb builder service',
aliases: ['breadcrumb-builder'],
templatePath: Application::TEMPLATE_PATH . '/Service/_breadcrumb-builder',
type: GeneratorType::MODULE_COMPONENT,
)]
final class BreadcrumbBuilder extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}BreadcrumbBuilder');
$vars['services'] = $ir->askServices();
$assets->addFile('src/{class}.php', 'breadcrumb-builder.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:cache-context',
description: 'Generates a cache context service',
aliases: ['cache-context'],
templatePath: Application::TEMPLATE_PATH . '/Service/_cache-context',
type: GeneratorType::MODULE_COMPONENT,
)]
final class CacheContext extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['context_id'] = $ir->ask('Context ID', 'example');
$vars['class'] = $ir->askClass(default: '{context_id|camelize}CacheContext');
// @todo Clean-up.
$base_class_choices = [
'-',
'RequestStackCacheContextBase',
'UserCacheContextBase',
];
$vars['base_class'] = $this->io()->choice('Base class', $base_class_choices);
if ($vars['base_class'] === '-') {
$vars['base_class'] = FALSE;
}
$vars['calculated'] = $ir->confirm('Make the context calculated?', FALSE);
$vars['context_label'] = '{context_id|m2h}';
$vars['interface'] = $vars['calculated'] ?
'CalculatedCacheContextInterface' : 'CacheContextInterface';
$assets->addFile('src/Cache/Context/{class}.php', 'cache-context.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\RequiredServiceName;
#[Generator(
name: 'service:custom',
description: 'Generates a custom Drupal service',
aliases: ['custom-service'],
templatePath: Application::TEMPLATE_PATH . '/Service/_custom',
type: GeneratorType::MODULE_COMPONENT,
label: 'Custom service',
)]
final class Custom extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['service_name'] = $ir->ask('Service name', '{machine_name}.example', new RequiredServiceName());
$default_class = Utils::camelize(
Utils::removePrefix($vars['service_name'], $vars['machine_name']) ?: $vars['machine_name'],
);
$vars['class'] = $ir->askClass(default: $default_class);
$vars['interface'] = $ir->confirm('Would like to create an interface for this class?', FALSE) ?
'{class}Interface' : NULL;
$vars['services'] = $ir->askServices();
$assets->addServicesFile()->template('services.twig');
$assets->addFile('src/{class}.php', 'custom.twig');
if ($vars['interface']) {
$assets->addFile('src/{interface}.php', 'interface.twig');
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:event-subscriber',
description: 'Generates an event subscriber',
aliases: ['event-subscriber'],
templatePath: Application::TEMPLATE_PATH . '/Service/_event-subscriber',
type: GeneratorType::MODULE_COMPONENT,
)]
final class EventSubscriber extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}Subscriber');
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/EventSubscriber/{class}.php', 'event-subscriber.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:logger',
description: 'Generates a logger service',
aliases: ['logger'],
templatePath: Application::TEMPLATE_PATH . '/Service/_logger',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Logger extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass();
$vars['service_id'] = '{class|c2m}';
$vars['services'] = $ir->askServices(forced_services: ['logger.log_message_parser']);
$assets->addFile('src/Logger/{class}.php', 'logger.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:middleware',
description: 'Generates a middleware',
aliases: ['middleware'],
templatePath: Application::TEMPLATE_PATH . '/Service/_middleware',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Middleware extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}Middleware');
$vars['services'] = $ir->askServices(forced_services: ['http_kernel']);
// HTTP kernel argument should not be included to services definition as it
// is added by container compiler pass.
$vars['service_arguments'] = $vars['services'];
unset($vars['service_arguments']['http_kernel']);
$assets->addFile('src/{class}.php', 'middleware.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RegExp;
#[Generator(
name: 'service:param-converter',
description: 'Generates a param converter service',
aliases: ['param-converter'],
templatePath: Application::TEMPLATE_PATH . '/Service/_param-converter',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ParamConverter extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$type_validator = new RegExp('/^[a-z][a-z0-9_\:]*[a-z0-9]$/');
$vars['parameter_type'] = $ir->ask('Parameter type', 'example', $type_validator);
$vars['class'] = $ir->askClass(default: '{parameter_type|camelize}ParamConverter');
$vars['services'] = $ir->askServices();
$vars['controller_class'] = '{machine_name|camelize}Controller';
$assets->addFile('src/{class}.php', 'param-converter.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:path-processor',
description: 'Generates a path processor service',
aliases: ['path-processor'],
templatePath: Application::TEMPLATE_PATH . '/Service/_path-processor',
type: GeneratorType::MODULE_COMPONENT,
)]
final class PathProcessor extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: 'PathProcessor{machine_name|camelize}');
$vars['services'] = $ir->askServices();
$assets->addFile('src/PathProcessor/{class}.php', 'path-processor.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:request-policy',
description: 'Generates a request policy service',
aliases: ['request-policy'],
templatePath: Application::TEMPLATE_PATH . '/Service/_request-policy',
type: GeneratorType::MODULE_COMPONENT,
)]
final class RequestPolicy extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: 'Example');
$vars['services'] = $ir->askServices();
$assets->addFile('src/PageCache/{class}.php', 'request-policy.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:response-policy',
description: 'Generates a response policy service',
aliases: ['response-policy'],
templatePath: Application::TEMPLATE_PATH . '/Service/_response-policy',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ResponsePolicy extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: 'Example');
$vars['services'] = $ir->askServices();
$assets->addFile('src/PageCache/{class}.php', 'response-policy.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:route-subscriber',
description: 'Generates a route subscriber',
aliases: ['route-subscriber'],
templatePath: Application::TEMPLATE_PATH . '/Service/_route-subscriber',
type: GeneratorType::MODULE_COMPONENT,
)]
final class RouteSubscriber extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}RouteSubscriber');
$vars['services'] = $ir->askServices(FALSE);
$assets->addFile('src/EventSubscriber/{class}.php', 'route-subscriber.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:theme-negotiator',
description: 'Generates a theme negotiator',
aliases: ['theme-negotiator'],
templatePath: Application::TEMPLATE_PATH . '/Service/_theme-negotiator',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ThemeNegotiator extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}Negotiator');
$vars['services'] = $ir->askServices();
$assets->addFile('src/Theme/{class}.php', 'theme-negotiator.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:twig-extension',
description: 'Generates Twig extension service',
aliases: ['twig-extension'],
templatePath: Application::TEMPLATE_PATH . '/Service/_twig-extension',
type: GeneratorType::MODULE_COMPONENT,
)]
final class TwigExtension extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['class'] = $ir->askClass(default: '{machine_name|camelize}TwigExtension');
$vars['services'] = $ir->askServices();
$assets->addFile('src/{class}.php', 'twig-extension.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Service;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service:uninstall-validator',
description: 'Generates a uninstall validator service',
aliases: ['uninstall-validator'],
templatePath: Application::TEMPLATE_PATH . '/Service/_uninstall-validator',
type: GeneratorType::MODULE_COMPONENT,
)]
final class UninstallValidator extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->askClass(default: '{name|camelize}UninstallValidator');
$vars['services'] = $ir->askServices();
$assets->addFile('src/{class}.php', 'uninstall-validator.twig');
$assets->addServicesFile()->template('services.twig');
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'service-provider',
description: 'Generates a service provider',
templatePath: Application::TEMPLATE_PATH . '/_service-provider',
type: GeneratorType::MODULE_COMPONENT,
)]
final class ServiceProvider extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['provide'] = $ir->confirm('Would you like to provide new services?');
$vars['modify'] = $ir->confirm('Would you like to modify existing services?');
if (!$vars['provide'] && !$vars['modify']) {
$this->io()->newLine();
$this->io()->writeln('<comment>Congratulations! You don\'t need a service provider.</comment>');
$this->io()->newLine();
return;
}
$interfaces = [
'ServiceProviderInterface' => $vars['provide'],
'ServiceModifierInterface' => $vars['modify'],
];
$vars['interfaces'] = \array_keys(\array_filter($interfaces));
// The class names is required to be a CamelCase version of the module's
// machine name followed by ServiceProvider.
// @see https://www.drupal.org/node/2026959
$vars['class'] = '{machine_name|camelize}ServiceProvider';
$assets->addFile('src/{class}.php', 'service-provider.twig');
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use Drupal\Core\Asset\LibraryDiscoveryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection;
use DrupalCodeGenerator\Asset\File;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\InputOutput\Interviewer;
use DrupalCodeGenerator\Utils;
use DrupalCodeGenerator\Validator\Choice;
use DrupalCodeGenerator\Validator\Optional;
use DrupalCodeGenerator\Validator\Required;
use DrupalCodeGenerator\Validator\RequiredMachineName;
use Symfony\Component\Console\Question\Question;
#[Generator(
name: 'single-directory-component',
description: 'Generates Drupal SDC theme component',
aliases: ['sdc'],
templatePath: Application::TEMPLATE_PATH . '/_sdc',
type: GeneratorType::THEME_COMPONENT,
)]
final class SingleDirectoryComponent extends BaseGenerator implements ContainerInjectionInterface {
/**
* {@inheritdoc}
*/
public function __construct(
private readonly ModuleHandlerInterface $moduleHandler,
private readonly ThemeHandlerInterface $themeHandler,
private readonly LibraryDiscoveryInterface $libraryDiscovery,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self(
$container->get('module_handler'),
$container->get('theme_handler'),
$container->get('library.discovery'),
);
}
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, AssetCollection $assets): void {
$this->askQuestions($vars);
$this->generateAssets($vars, $assets);
}
/**
* Collects user answers.
*/
private function askQuestions(array &$vars): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['component_name'] = $ir->ask('Component name', validator: new Required());
$vars['component_machine_name'] = $ir->ask(
'Component machine name',
Utils::human2machine($vars['component_name']),
new RequiredMachineName(),
);
$vars['component_description'] = $ir->ask('Component description (optional)');
$vars['component_libraries'] = [];
do {
$library = $this->askLibrary();
$vars['component_libraries'][] = $library;
} while ($library !== NULL);
$vars['component_libraries'] = \array_filter($vars['component_libraries']);
$vars['component_has_css'] = $ir->confirm('Needs CSS?');
$vars['component_has_js'] = $ir->confirm('Needs JS?');
if ($ir->confirm('Needs component props?')) {
$vars['component_props'] = [];
do {
$prop = $this->askProp($vars, $ir);
$vars['component_props'][] = $prop;
} while ($ir->confirm('Add another prop?'));
}
$vars['component_props'] = \array_filter($vars['component_props'] ?? []);
}
/**
* Create the assets that the framework will write to disk later on.
*
* @psalm-param array{component_has_css: bool, component_has_js: bool} $vars
* The answers to the CLI questions.
*/
private function generateAssets(array $vars, AssetCollection $assets): void {
$component_path = 'components/{component_machine_name}/';
if ($vars['component_has_css']) {
$assets->addFile($component_path . '{component_machine_name}.css', 'styles.twig');
}
if ($vars['component_has_js']) {
$assets->addFile($component_path . '{component_machine_name}.js', 'javascript.twig');
}
$assets->addFile($component_path . '{component_machine_name}.twig', 'template.twig');
$assets->addFile($component_path . '{component_machine_name}.component.yml', 'component.twig');
$assets->addFile($component_path . 'README.md', 'readme.twig');
$contents = \file_get_contents($this->getTemplatePath() . \DIRECTORY_SEPARATOR . 'thumbnail.jpg');
$thumbnail = new File($component_path . 'thumbnail.jpg');
$thumbnail->content($contents);
$assets[] = $thumbnail;
}
/**
* Prompts the user for a library.
*
* This helper gathers all the libraries from the system to allow autocomplete
* and validation.
*
* @return string|null
* The library ID, if any.
*
* @todo Move this to interviewer.
*/
private function askLibrary(): ?string {
$extensions = [
'core',
...\array_keys($this->moduleHandler->getModuleList()),
...\array_keys($this->themeHandler->listInfo()),
];
$library_ids = \array_reduce(
$extensions,
fn (iterable $libs, $extension): array => \array_merge(
(array) $libs,
\array_map(static fn (string $lib): string => \sprintf('%s/%s', $extension, $lib),
\array_keys($this->libraryDiscovery->getLibrariesByExtension($extension))),
),
[],
);
$question = new Question('Library dependencies (optional). [Example: core/once]');
$question->setAutocompleterValues($library_ids);
$question->setValidator(
new Optional(new Choice($library_ids, 'Invalid library selected.')),
);
return $this->io()->askQuestion($question);
}
/**
* Asks for multiple questions to define a prop and its schema.
*
* @psalm-param array{component_machine_name: mixed, ...<array-key, mixed>} $vars
* The answers to the CLI questions.
*
* @return array
* The prop data, if any.
*/
protected function askProp(array $vars, Interviewer $ir): array {
$prop = [];
$prop['title'] = $ir->ask('Prop title', '', new Required());
$default = Utils::human2machine($prop['title']);
$prop['name'] = $ir->ask('Prop machine name', $default, new RequiredMachineName());
$prop['description'] = $ir->ask('Prop description (optional)');
$choices = [
'string' => 'String',
'number' => 'Number',
'boolean' => 'Boolean',
'array' => 'Array',
'object' => 'Object',
'null' => 'Always null',
];
$prop['type'] = $ir->choice('Prop type', $choices, 'String');
if (!\in_array($prop['type'], ['string', 'number', 'boolean'])) {
/** @psalm-var string $type */
$type = $prop['type'];
$component_schema_name = $vars['component_machine_name'] . '.component.yml';
$this->io()->warning(\sprintf('Unable to generate full schema for %s. Please edit %s after generation.', $type, $component_schema_name));
}
return $prop;
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\GeneratorType;
#[Generator(
name: 'template',
description: 'Generates a template',
aliases: ['template'],
templatePath: Application::TEMPLATE_PATH . '/_template',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Template extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['template_name'] = $ir->ask('Template name', validator: [self::class, 'validateTemplateName']);
if (!\str_ends_with($vars['template_name'], '.twig')) {
$vars['template_name'] .= '.html.twig';
}
$vars['theme_key'] = \preg_replace(
['#(\.html)?\.twig$#', '#[^a-z\d]#'],
['', '_'],
$vars['template_name'],
);
$vars['create_theme'] = $ir->confirm('Create theme hook?');
$vars['create_preprocess'] = $ir->confirm('Create preprocess hook?');
$assets->addFile('templates/{template_name}', 'template.twig');
if ($vars['create_theme'] || $vars['create_preprocess']) {
$assets->addFile('{machine_name}.module')
->template('module.twig')
->appendIfExists(9);
}
}
/**
* Validates template name.
*/
public static function validateTemplateName(mixed $value): string {
if (!\is_string($value) || !\preg_match('/^[a-z\d][a-z\d\.\-]*[a-z\d]$/', $value)) {
throw new \UnexpectedValueException('The value is not correct template name.');
}
return $value;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Test;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RequiredClassName;
#[Generator(
name: 'test:browser',
description: 'Generates a browser based test',
aliases: ['browser-test'],
templatePath: Application::TEMPLATE_PATH . '/Test/_browser',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Browser extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->ask('Class', 'ExampleTest', new RequiredClassName());
$assets->addFile('tests/src/Functional/{class}.php', 'browser.twig');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Test;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Validator\RequiredClassName;
#[Generator(
name: 'test:kernel',
description: 'Generates a kernel based test',
aliases: ['kernel-test'],
templatePath: Application::TEMPLATE_PATH . '/Test/_kernel',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Kernel extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['class'] = $ir->ask('Class', 'ExampleTest', new RequiredClassName());
$assets->addFile('tests/src/Kernel/{class}.php', 'kernel.twig');
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command\Test;
use DrupalCodeGenerator\Application;
use DrupalCodeGenerator\Asset\AssetCollection as Assets;
use DrupalCodeGenerator\Attribute\Generator;
use DrupalCodeGenerator\Command\BaseGenerator;
use DrupalCodeGenerator\GeneratorType;
use DrupalCodeGenerator\Utils;
#[Generator(
name: 'test:nightwatch',
description: 'Generates a nightwatch test',
aliases: ['nightwatch-test'],
templatePath: Application::TEMPLATE_PATH . '/Test/_nightwatch',
type: GeneratorType::MODULE_COMPONENT,
)]
final class Nightwatch extends BaseGenerator {
/**
* {@inheritdoc}
*/
protected function generate(array &$vars, Assets $assets): void {
$ir = $this->createInterviewer($vars);
$vars['machine_name'] = $ir->askMachineName();
$vars['name'] = $ir->askName();
$vars['test_name'] = Utils::camelize($ir->ask('Test name', 'example'), FALSE);
$assets->addFile('tests/src/Nightwatch/{test_name}Test.js', 'nightwatch.twig');
}
}

Some files were not shown because too many files have changed in this diff Show More