xp-framework/command

Subcommand integration

Closed this issue · 5 comments

Write XPCLIs as XP subcommands (xp-framework/rfc#303)

<?php namespace com\example\cmd;

class Migrate extends \util\cmd\Command {
  public function run() {
    $this->out->writeLine('Migrating...');
  }
}
# Current
$ xp cmd com.example.cmd.Migrate
$ xp cmd de.thekid.dialog.cmd.AddAlbum

# New
$ xp cmd.migrate
$ xp cmd.add-album

# ...or maybe even
$ xp migrate
$ xp add-album

How could this work?

Subcommands with a dot inside currently don't work, so we could reserve this:

$ xp cmd.migrate                                                        
Uncaught exception: Error (Class 'cmd\migrate' not found)               
  at <source> [line 366 of C:\tools\cygwin\home\Timm\bin\class-main.php]
  at <main>() [line 0 of cmd\migrate.class.php]                         

There could, however, be ambiguities w/ classes, like e.g. cmd.Migrate, so maybe a ":" would be better?

A special handling if the command contains a ":" would be easiest:

  1. If strstr($cmd, ":") -> $spec= explode(":", $cmd) and continue with above, searching for $spec[0]
  2. Pass $spec[1] as further command line argument

...but then again, there's no advantage I can see over just typing "xp cmd migrate"

xp <cmd> will currently search the following for a file called bin/xp.*.<cmd>

  • Module path
  • Current directory
  • ./vendor/bin
  • $COMPOSER_HOME/vendor/bin

To create a subcommand from an XPCLI, the following steps would be necessary:

  1. Create bin/xp.example.cmd.migrate (assuming example/cmd is the module name)
  2. Rename the Migrate class to MigrateRunner
  3. Move it to the xp.cmd namespace (the package name cmd comes from the above module name)
  4. Create a static main method inside it, and place return \xp\command\CmdRunner::main($args) in it

The missing piece between xp migrate and com.example.cmd.Migrate really only is the package name, com.example.cmd.

The XPCLI package could install an autoloader (if we had a possibility for globally installed modules to do so, but the compiler would benefit from this, too!) to check for this package, search the class path recursively for a class Migrate extending util.cmd.Command, though that would be slowish:

private function findCommand($cl, $name, $package) {
  $file= $name.\xp::CLASS_FILE_EXT;
  $prefix= ($package ? $package.'.' : '');
  foreach ($cl->packageContents($package) as $resource) {
    if ($file === $resource) {
      return $prefix.$name;
    } else if ('/' === $resource{strlen($resource) - 1}) {
      if ($uri= $this->findCommand($cl, $name, $prefix.substr($resource, 0, -1))) return $uri;
    }
  }
  return null;
}

private function findClass($name) {
  $class= implode('', array_map('ucfirst', explode('-', $name)));
  foreach (ClassLoader::getLoaders() as $cl) {
    if ($command= $this->findCommand($cl, $class, null)) return $cl->loadClass($command);
  }
  throw new ClassNotFoundException($class);
}

$class= $this->findClass('add-album');   // de.thekid.dialog.cmd.AddAlbum

...but then again, there's no advantage I can see over just typing "xp cmd migrate"

Implemented via Commands::registerPath('path.to.migrate.class') - see #6