CLI entry point in Drupal Core (Dex)

Created on 9 June 2024, 19 days ago
Updated 14 June 2024, 13 days ago

dex module:command --options

This is a proposal for a common entry point for CLI commands relying on a booted Drupal install.

This issue comes out of many years of maintaining a slim console, and building the console work for the SM project . Now the hope is the skeleton can move to core.

Summary

I'd like to propose a new slim console initiative. All about getting a minimal viable console entrypoint which first and foremost an easy way for Drupal modules to introduce CLI commands.

This is a continuation of the discussions at the decade old #2242947: Integrate Symfony Console component to natively support command line operations . I've read through the remarks and address points raised below.

Scope/Proposal

  • To add a single entry point of entry, for which users can run commands.
  • For module developers to be able to add commands in a consistent manner.
  • Only commands defined by enabled modules/extensions.

Exclusions/Out of scope

Mostly since its been proven these things have caused other issues to stall.

  • Anything supporting a non-booted site. This could be addressed in the future. Projects like Drush can continue to work, and can even make use of the new commands.
  • Conversion of existing core scripts.
  • Adding commands to core itself.
  • A base/abstract command. The Symfony Console class is recommended.

Addressing the original Symfony Console POC

The discussion at #2242947: Integrate Symfony Console component to natively support command line operations is certainly valuable, however is long in the tooth at 10 years old. Discussions indicate (Comments #129 / #131 / #193 / #207 ) a preference to close out in favour of a newer issue which will provide commands for core . Notably, that issue also has stalled.

This issue is designed to replace #2242947: Integrate Symfony Console component to natively support command line operations in its entirely. With respect and acknowledgement of the discussion there.

Addressing the Issue summary of the original Symfony Console POC

This CLI integration should be viewed as a separate application which ships with Drupal. This might include going so far as to turn it into a subtree split similar to Drupal components.

Since this proposal is so small, the main systems are included. If others, including Drush, want to recycle commands. They certainly can since the command loader system is reusable.

Some commands will need to run without an installed Drupal, or even without a booted kernel. Different boot levels in functional vs. kernel vs. unit tests is a useful metaphor for what is needed.

The proposal here is simpler, requiring a fully booted install.

We use a separate service discovery container as a place to resolve dependencies and commands. This is independent of the Drupal container because it's not a Drupal, it's a console application that happens to use some Drupal as needed.

This proposal would use the same container, and thusly any and all services a booted Drupal install would have access to.

We use symfony/finder to locate extensions and find service definition files

This proposal does not need any new extension/module handling. The closest thing to this would be the proposed plugin-directory auto service registration.

Discussions about core commands

#2242947: Integrate Symfony Console component to natively support command line operations had plenty of discussion about how and where core would include commands, eventually leading to Directory based automatic service creation Needs review . I'd like to draw the line and not consider any core commands, other than test-only commands. The proposal here is first and foremost a benefit for contrib. Anything more can be handled in the future.

Proposed name

Not going to bikeshed too much with a name, so putting it out there:

dex

Rationale

  • Origin story, at least for now, its for Drupal EXtensions (modules, themes, profiles), as in: it requires a booted Drupal install. "Drupal" itself is more ideal, however this namespace is taken, for now. And makes sense to reserve that name for an all-encompassing version supporting non-booted installs.
  • It's easy to type, and memorable.
  • Its Searchable (Googleable), much like drush, and unlike drupal.

Technical proposal

Core implementation

  • Provide an entry point bin file, which is then wrapped by symfony/runtime.
    • Includes just enough information to synthesise HTTP requests / base URL. (Like Drush's uri option)
    • Brings in command loader, and legacy commands (uses configure() instead of Attributes).
  • composer.json changes:
    • Add symfony/runtime , so we dont need to deal with low level / bootstrap / autoloader.
    • Adds bin file so it can be placed in vendor/bin, available in PATH
  • Add a console.command_loader service to core; the registry of commands.
  • Adds a compiler pass to core.
    • Which scans src/Command/ directories in enabled extensions for commands, and creates tagged command services from them.
    • Adds tagged command services to the console.command_loader service.
  • Associated tests.

Command Loader

The main meat of registering commands to the command loader to be the same, or as close to, how Symfony does it in \Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass (Code)

Runtime

The proposal here relies on symfony/runtime, which we've also discussed at Use symfony/runtime for less bespoke bootstrap/compatibility with varied runtime environments Active . I dont think we need to block on either of these issues. In fact we can preempt the runtime issue entirely, since we are providing a real use case with real motivation. It may be used in the future for non-cli purposes. Interestingly, there seems to be some community interest in symfony/runtime, as after 18 months, the runtime issue has 40 followers already. Despite little discussion between 4 people.

The maturity of symfony/runtime proves we can implement this idea without needing to invent new things for Drupal.

Testing is simplified, the surface of our testing can focus on the integration since we are not maintaining significant amounts of new code.

Expected UX

Since runtime sets up linking to a projects' bin directory, if you have it in your PATH, then the expected UX is:

dex my:command --blah --foo=bar

Expected DX

  • Developers create a class in src/Command/. Such that namespace is Drupal\mymodule\Command
  • Class extends \Symfony\Component\Console\Command\Command, as required by AddConsoleCommandPass.
  • Class has attribute #[\Symfony\Component\Console\Attribute\AsCommand]
  • The class must implement only one method: protected function execute(), featuring the necessary custom logic for the command.
  • The command has access to the container for autowiring.
  • Discovery is as simple as a container reset (cache clear).
  • There is no requirement for a services.yml entry, though advanced 1% use cases can do so.

The expected code is concise:


declare(strict_types=1);

namespace Drupal\dex_test\Command;

use Drupal\Component\Datetime\TimeInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'example:command', description: 'An example command.')]
final class DexExampleCommand extends Command {

  public function __construct(
    private readonly TimeInterface $dateTime,
  ) {
    parent::__construct();
  }

  protected function execute(InputInterface $input, OutputInterface $output): int {
    $io = new SymfonyStyle($input, $output);

    $now = new \DateTimeImmutable('@' . $this->dateTime->getRequestTime());
    $io->note('The current time is ' . $now->format('r'));

    return static::SUCCESS;
  }

}

Drush, Core Commands, Future Non-Booted version

Drush may continue to exist for now as it provides use cases outside of a booted Drupal installation, including code generation, install, etc. In fact Drush may choose to provide a layer to register the commands we're adding here, to Drush itself.

Perhaps a future all-encompassing Drush replacement would utilise the drupal namespace and cleanly replace dex.

#3089277: Provide core CLI commands for the most common features of drush is a good place for working towards core commands or non bootstrap commands. Which would not block or be postponed on the issue here.

MR notes

  • My vision for this issue is not too much larger than the companion MR, please take a look.
  • Very basic env var pieces are included in the Dex command in order to resolve issues with not being in web requests.
  • File system auto discovery is limited to Drupal extensions, not vendor. Though if a Drupal extension wants to register a command located in vendor, they can via manually tagged service definitions instead of #[AsCommand]. vendor doesn't participate in autodiscovery for modules, either.
  • Dependencies are provided by autowiring. If aliases don’t exist for dependencies, they can be defined in the custom project, create a patch in the service-defining project, or ultimately can fall back to defining a command in services.yml

Other notes

Contrib Project

I have created an intentionally short lived project that gives contrib all its needs for command execution. It contains just the pieces needed to evaluate the UX/DX of the MR for this issue. And to avoid the inability to have runtime set things up when used as a patch, since patches can’t modify composer.json until after Composer does its thing. Unless of course you apply the patch to a branch of some kind and require it.

https://www.drupal.org/project/dex_console

Feature request
Status

Needs work

Version

11.0 🔥

Component
Base 

Last updated about 3 hours ago

Created by

🇦🇺Australia dpi Perth, Australia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @dpi
  • Merge request !8344Dex → (Open) created by dpi
  • 🇦🇺Australia dpi Perth, Australia
  • 🇦🇺Australia dpi Perth, Australia
  • Pipeline finished with Failed
    19 days ago
    Total: 761s
    #194757
  • Pipeline finished with Failed
    19 days ago
    Total: 190s
    #194850
  • Pipeline finished with Canceled
    19 days ago
    Total: 38s
    #194855
  • Pipeline finished with Failed
    19 days ago
    Total: 448s
    #194857
  • Pipeline finished with Failed
    19 days ago
    Total: 508s
    #194862
  • Pipeline finished with Failed
    19 days ago
    Total: 128s
    #194874
  • Pipeline finished with Failed
    18 days ago
    #194876
  • Status changed to Needs review 18 days ago
  • 🇦🇺Australia dpi Perth, Australia

    The intricacies of how to match versions in Composer with Drupal build system needs work, otherwise seeking feedback on the IS/proposal and the non Composer/build-system related pieces of the MR.

  • Pipeline finished with Failed
    18 days ago
    Total: 634s
    #194935
  • Pipeline finished with Success
    18 days ago
    Total: 658s
    #194945
  • 🇷🇺Russia Chi

    +1

    I've built a POC a few years ago. That proves how easy to create a CLI endpoint for commands that only working on fully booted Drupal site. That's an example of Pareto principal. Just a few commands (like drush site-install) that need to support non-booted environment, brought so much complexity to Drush.

    One thing I'd like see in this implementation is supporting command discovery in vendor directory. So you would be able bundle console commands to a Composer package (not Drupal module).

  • 🇦🇺Australia dpi Perth, Australia

    Cleaning up markup

  • 🇦🇺Australia dpi Perth, Australia

    That's an example of Pareto principal. Just a few commands

    This is exactly what I was thinking. Well put.

    One thing I'd like see in this implementation is supporting command discovery in vendor directory. So you would be able bundle console commands to a Composer package (not Drupal module).

    I've tried to address this in MR Notes CLI entry point in Drupal Core (Dex) Needs review . You can absolutely set up a command that exists in vendor (via services yml, etc). However autodiscovery is limited to the same rules as Drupal, i.e. only in module directories. Until Drupal has the ability to discover modules/plugins/etc I dont think it makes sense for this issue/initiate or related issues like Directory based automatic service creation Needs review to go out of their way and look in vendor. Its a large can of worms that doesnt need a decision now; we can easily change it in the future.

    I've built a POC a few years ago.

    Neat, I'm glad we've evolved even further since then. We're in such a nice spot in 2024.

  • 🇦🇺Australia dpi Perth, Australia

    File was deleted?!

  • 🇫🇷France andypost

    +1 sounds like solution to long lasting set of issues, it could be extendable via dex drush

    PS looking how to extend it for xhprof but SF runtime backend is one more strong reason

  • Status changed to Needs work 17 days ago
  • The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

    This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

    Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

  • 🇺🇸United States greg.1.anderson

    Overall, I like the proposal. Drush can already accept Symfony Console commands in its command list, so if the command loader was available as a public method of a discoverable class, then all dex-provided commands from modules could also be available via drush commandname, for existing Drush users, as desired.

    Regarding the Drupal core cli, though, I think that the command name should remain drupal, and the work here should be limited to providing a mechanism whereby modules can add new commands to this existing script.

  • 🇺🇸United States mradcliffe USA

    I think "Anything supporting a non-booted site." should be in-scope so that we can implement this as part of the existing drupal core CLI. Having an intermediary step where we have dex and drupal and then switch back to drupal later on would seem like a lot of wasted time updating docs, training materials, tests, etc..

    If we can have a resolution for both existing sites and new sites, then it would be easier to adopt to using this rather than a new command.

  • 🇷🇺Russia Chi

    How many commands in Drush core can work on non-booted Drupal installation?

    I found just three.

    • site:install
    • archive:dump
    • archive:restore

    I believe those commands deserve own console endpoint.

  • 🇺🇸United States greg.1.anderson

    The main use-case for non-bootstrapped commands is the existing drupal install.

  • 🇦🇺Australia acbramley

    I think "Anything supporting a non-booted site." should be in-scope

    I think the IS and other comments here make a compelling argument against this.

  • 🇺🇸United States mradcliffe USA

    @acbramley, I disagree based on my previous comment about multiple commands being more confusing so solving the problem of bootstrap/non-bootstrap is important now rather than later.

  • 🇺🇸United States greg.1.anderson

    The issue summary says:

    Perhaps a future all-encompassing Drush replacement would utilise the drupal namespace and cleanly replace dex.

    It is not necessary to build an entire Drush replacement to unify the dex and drupal front controllers. We have a paved path for doing this in Drush that wouldn't be too difficult to use here. It's harder to remove things that you don't want than it is to add something new, so I do not find it to be a compelling argument that we should introduce a new script only to immediately deprecate it.

    I think we could get this merged if we unified on the drupal script name. I personally feel like we have too many different command names that are trying to do the same thing, and expanding that collection further is not moving in the right direction.

  • 🇬🇧United Kingdom AaronMcHale Edinburgh, Scotland

    For commands like site:instlall, what if those were just Compsoer plugins? We already recommend the composer create-project command as the first command you run, what if the next command was just composer drupal-install.

    Then you have a installed site, and move onto using drupal (or whatever the name is).

    For the record, I prefer just drupal as the command name.

    I don't know too much about the existing drupal command, but it seems to not require a booted site, so if the non-booted parts of that were just moved to Composer commands, that would free up the drupal command to become one which can be used for a booted site.

    drupal at core/scripts could then be deprecated, and eventually replaced, while drupal at core/bin is what we are building here.

  • 🇺🇸United States greg.1.anderson

    I can't help but feel like some of the strong opinions against combining dex and drupal might be motivated by an impression that taking that step would pull in a lot of complexity from the Drush bootstrap logic in order to support both bootstrapped and non-bootstrapped commands. In actuality, though, this can be achieved with a little over a dozen lines of code by overriding the Application::find() method. Drush has been doing this successfully for years, since Drush 9, and it has worked well across multiple versions of Symfony. The rest of Drush's preflight and bootstrap can be left behind. The key part look like this (in the derived Application class):

      public function find($name): Command {
        try {
          $command = parent::find($name);
          if ($command instanceof ListCommand || $command instanceof HelpCommand) {
            $this->bootstrap();
          }
          return $command;
        } catch (CommandNotFoundException $e) {
          if (!$this->bootstrap()) {
            throw $e;
          }
          return parent::find($name);
        }
      }
    

    I have created a branch that demonstrates the above technique in action. Bootstrapped and non-bootstrapped commands live side-by-side in the drupal script. This fulfills the vision from the IS: to eventually replace dex with a unified drupal command. The rest of the code in the branch needed to make this work, beyond the fourteen lines shown above, is pretty much the same code that is in the current MR, with some refactoring. I have left the original dex script in the branch, so that it can be tried alongside the enhanced drupal script. Some enhancements from dex, e.g. the wrapper in vendor/bin, have been carried over from the original MR and applied to the drupal script.

    I think that the work done in the original MR here is really excellent. The advent of attribute-based discovery, and conformance to the latest Symfony conventions around command definition gives us a really clean interface to allow Drupal modules to add cli commands to a lightweight front controller in core. I think that we have a really good chance at getting this merged, if the community can reach an agreement on whether the script that is used to call Drupal module commands should be called dex or drupal. I hope that the minimum additions I've shown here are sufficient to convince folks that we should integrate with the existing drupal script now rather than later.

  • Pipeline finished with Failed
    13 days ago
    Total: 194s
    #199305
  • Pipeline finished with Failed
    13 days ago
    Total: 197s
    #199310
  • Pipeline finished with Failed
    13 days ago
    Total: 623s
    #199329
  • Pipeline finished with Success
    13 days ago
    Total: 627s
    #199351
Production build 0.69.0 2024