How to maintain Drush commands for Drush 8 and 9 and Drupal console with the same code base.
Config Split treats all the cli the same.
Drupal 8.4 and its upgrade to Symfony 3 has made the compatibility of the global Drush 8 a bit more challenging. Drush 9 works with Drupal 8.4 but it is not stable yet and the format of how third party Drush commands are made has changed significantly.
While Drush 9 comes with a command that helps porting Drush 8 commands you will still end up maintaining very similar code in two places one with calls to drush_confirm('...')
and one with $this->io()->confirm('...')
. If you decide to also provide your commands for Drupal console you now have three times the burden.
Because we tried to provide the commands for Config Split for both Drush and Drupal console early on we faced this problem already more than a year ago. And now it has paid off because porting the commands to Drush 9 was very quick.
The solution is actually really simple and brings the added benefit of being able to test the business logic of the commands in the absence of Drush or Drupal console. It is all about separating the command discovery from the command logic. Drush 8, 9 and Drupal console all have a bit different ways to discover and invoke commands, but the business logic you want to implement is the same so all we have to do is to extract a common "interface" our custom service can implement and then make the command definitions wrap that and keep things DRY.
The CliService
Config Split defines a config_split.cli
service with the class ConfigSplitCliService with all its dependencies injected. It has the methods \Drupal\config_split\ConfigSplitCliService::ioExport and \Drupal\config_split\ConfigSplitCliService::ioImport that implement all the commands logic and delegate the actual importing and exporting to specific methods.
The method signature for both the export and import method are more or less the same: CliService::ioMethod($arguments, $io, callable $t)
.
- $arguments: The arguments passed to the command.
- $io: This is an object that interacts with the command line, in Drush 9 and Drupal console this comes from the Symfony console component, for Drush 8 we created a custom wrapper around
drush_confirm
anddrush_log
called ConfigSplitDrush8Io. - $t: This is essentially a
t
function akin to how Drupal translates strings. Because Drupal console translates things differently we had to be a bit creative with that by adding a t method to the command.
Commands wrap the service
The Drush 8 command is essentially:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">drush_config_split_export</span><span style="color: #007700">(</span><span style="color: #0000BB">$split </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// Make the magic happen.<br> </span><span style="color: #007700">\</span><span style="color: #0000BB">Drupal</span><span style="color: #007700">::</span><span style="color: #0000BB">service</span><span style="color: #007700">(</span><span style="color: #DD0000">'config_split.cli'</span><span style="color: #007700">)-></span><span style="color: #0000BB">ioExport</span><span style="color: #007700">(</span><span style="color: #0000BB">$split</span><span style="color: #007700">, new </span><span style="color: #0000BB">ConfigSplitDrush8Io</span><span style="color: #007700">(), </span><span style="color: #DD0000">'dt'</span><span style="color: #007700">);<br>}<br></span><span style="color: #0000BB">?></span></span>
For Drush 9 we can use dependency injection and the Drush 9 command becomes essentially:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">class </span><span style="color: #0000BB">ConfigSplitCommands </span><span style="color: #007700">extends </span><span style="color: #0000BB">DrushCommands </span><span style="color: #007700">{<br> public function </span><span style="color: #0000BB">splitExport</span><span style="color: #007700">(</span><span style="color: #0000BB">$split </span><span style="color: #007700">= </span><span style="color: #0000BB">NULL</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">cliService</span><span style="color: #007700">-></span><span style="color: #0000BB">ioExport</span><span style="color: #007700">(</span><span style="color: #0000BB">$split</span><span style="color: #007700">, </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">io</span><span style="color: #007700">(), </span><span style="color: #DD0000">'dt'</span><span style="color: #007700">);<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
And very similar the Drupal console command:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">class </span><span style="color: #0000BB">ExportCommand </span><span style="color: #007700">extends </span><span style="color: #0000BB">SplitCommandBase </span><span style="color: #007700">{<br> protected function </span><span style="color: #0000BB">execute</span><span style="color: #007700">(</span><span style="color: #0000BB">InputInterface $input</span><span style="color: #007700">, </span><span style="color: #0000BB">OutputInterface $output</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">setupIo</span><span style="color: #007700">(</span><span style="color: #0000BB">$input</span><span style="color: #007700">, </span><span style="color: #0000BB">$output</span><span style="color: #007700">);<br> </span><span style="color: #FF8000">// Make the magic happen.<br> </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">cliService</span><span style="color: #007700">-></span><span style="color: #0000BB">ioExport</span><span style="color: #007700">(</span><span style="color: #0000BB">$input</span><span style="color: #007700">-></span><span style="color: #0000BB">getOption</span><span style="color: #007700">(</span><span style="color: #DD0000">'split'</span><span style="color: #007700">), </span><span style="color: #0000BB">$this</span><span style="color: #007700">-></span><span style="color: #0000BB">getIo</span><span style="color: #007700">(), [</span><span style="color: #0000BB">$this</span><span style="color: #007700">, </span><span style="color: #DD0000">'t'</span><span style="color: #007700">]);<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
Testing
The ConfigSplitCliServiceTest is a KernelTest which asserts that the export works as expected by exporting to a virtual file system. The test coverage is not 100% (patches welcome) but the most important aspects for the complete and conditional splitting (blacklist/graylist) is thoroughly tested.
There are no limitations on what or how you can test your CliService since it is self contained in your module and does not depend on Drush or the Drupal console. For example one could write a unit test with a mocked $io object that asserts that the messages printed to the cli are correct.
Tags: Drupal 8Drupal PlanetDrush