Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External User Commands #5

Open
l0gicgate opened this issue Apr 25, 2020 · 17 comments
Open

External User Commands #5

l0gicgate opened this issue Apr 25, 2020 · 17 comments

Comments

@l0gicgate
Copy link
Member

I would like for the console to be easily extensible by the end user. How would we go about this?

I was thinking that we could establish a project base level directory that would give a plugin interface to the user to add commands to the existing console.

Should we make a folder where we loop through files and add the commands in those files? Or could we have the CLI look for ../../commands/external.php which would return an array of user defined commands so we can then call $this->addCommands($external) within the Application class

@ABGEO
Copy link
Collaborator

ABGEO commented Apr 25, 2020

Experience with the Symfony Console has shown that it is important to provide users an interface for defining their own commands. I support this idea.

I think it would be a good idea to store all user commands in a predefined directory and automatically include them in Slim Console.

@l0gicgate
Copy link
Member Author

Two questions arise:

  • What should the default folder be called?
  • Should we let the user override the default folder name?

@ABGEO
Copy link
Collaborator

ABGEO commented Apr 25, 2020

Two questions arise:

* What should the default folder be called?

* Should we let the user override the default folder name?
  1. src/Application/Commands will be a good place for user commands;
  2. We can add the commandsPath parameter to the settings, but I think that users will not override it.

@flangofas
Copy link
Collaborator

What should the default folder be called?

👍 for directory name, also, it may contain subdirectories as well.

Should we let the user override the default folder name?

Makes sense to me.

@zhorton34
Copy link

zhorton34 commented Apr 26, 2020

Sweet, I was looking forward to this issue ~ wanted to comment earlier but know I've added a few scope creep comments :)

Although this may be considered currently outside the current scope, I can't help but think that each core group of setup preset/make commands should be its own repository.

One concern that's jabbing at me is our initial implementation's close coupling
of the slim-console itself to the core-commands under consideration.

Example External Commands Plugin Repo:

PhpDi Container Commands Repo

- composer.json
- config.php
- Plugin.php
- stubs
  - Dummy.stub
- commands
   - PublishStubsCommand.php
   - PublishConfigCommand.php
   - PresetDiContainerCommand.php
   - UnsetDiContainerCommand.php
   // etc... with any associated make or flow commands  

Plugin.php

class Plugin
{
      protected static $commands = [
           Commands/PublishStubsCommand::class,
           Commands/PublishConfigCommand::class,
           Commands/PresetDiContainerCommand::class,
           Commands/UnsetDiContainerCommand::class,
      ];
 
        
       public static function for(SlimConsole $console)
       {
             array_walk(static::$commands, fn ($command) => $console->add(new $command);     
        }  
}

Setup Inside Of Slim Core
mkdir project && cd project && composer require slim/slim
composer require slim/slim-console
./vendor/bin/slim-console init
composer require slim/phpdi-console-plugin
Then We can just install the given group of commands in the console entry point

#!/usr/bin/env php
<?php

/**
 * Slim Framework (https://slimframework.com)
 *
 * @license https://github.com/slimphp/Slim-Console/blob/0.x/LICENSE.md (MIT License)
 */

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Slim\Console\Application;

$console = new Application();

$plugins = [
      \Slim\Console\Commands\PhpDiContainer\Plugin::class,
      // etc...
];

array_walk($plugins, fn ($plugin) => $plugin::for($console));

Then we can simply use the command
php slim preset:php-di-container

@ABGEO
Copy link
Collaborator

ABGEO commented Apr 26, 2020

@zhorton34 I partially agree with you that the preset/make Command Group (but not each command) should have its own separated repository. This group will be dev-dependency and we don't need to have it in a production environment.

@zhorton34
Copy link

@ABGEO07 Which commands/command types are you thinking should be apart of the slim-console core?

@ABGEO
Copy link
Collaborator

ABGEO commented Apr 26, 2020

@zhorton34 I think the help and list commands (which are already in the Symfony Console) will be enough. All other commands must be defined in separate bundles, such as make-bundle, server-bundle, cache-bundle.

The Slim Console Core should be an interface for managing user and third-party commands.

We can also define a "command to create a command" (make:command) in the Core instead of make-bundle.

@zhorton34
Copy link

zhorton34 commented Apr 26, 2020

@ABGEO07 I absolutely agree.

One point, that I may be jumping the gun on, is whether we want to directly integrate 3rd party commands (Phinx for example) or create slim adapter commands that integrate those commands.

The concern for this thought thread in lies our capabilities to use dependency injection with commands.

If a container is implemented, and only if, then we'd want to allow the command's handler function to resolve dependencies from the container.

@ABGEO
Copy link
Collaborator

ABGEO commented Apr 26, 2020

@zhorton34 It would probably be better to use some kind of bridge or adapter, as you said, for integration with 3rd party commands. Let's get some feedback and decide after that.

@zhorton34
Copy link

@l0gicgate What are the expectations for creating issues within this repo?

We'll need a way for commands to run other commands, I have a pretty good idea on how to set that up but want to make sure I'm following how the slim team normally goes about building out core packages.

@l0gicgate
Copy link
Member Author

@zhorton34 feel free to create issues as new problems arise!

I’m also in favour of using a bridge for integrating with third party librairies. This way, it’s easy for other people to create libraries to plug into the console or develop commands for the console. We can also version the integration process this way.

@zhorton34
Copy link

zhorton34 commented Apr 26, 2020

What if we gave devs the option to publish the commands.

Once the commands are registered in slim and everything is setup, that command will take 20 minutes to write.

php slim publish:commands --to=\\Application\\Commands\\

php slim publish:commands

> Which Command Would You Like To Publish
[0] preset:php-di-container
[1] preset:monologger
[2] preset:eloquent

*User Input: 1*

"Successfully Published preset:php-di-container command to \\Application\\Commands\\"

"Would you like to publish another command? [y/n] (yes by default)"

*User Input: y*

"Which command would you like to publish?"
[0] preset:php-di-container
[1] preset:eloquent

*User Input: 1*

"Would you like to publish another command? [y/n] (yes by default)"

"Successfully Published preset:eloquent command to \\Application\\Commands\\"

*User Input: n*

@adriansuter
Copy link
Collaborator

adriansuter commented May 11, 2020

Say a repository example/slim-commands contains a slim command for the slim console. Let there be the following command

/* file: src/Commands/HelloCommand.php */

declare(strict_types=1);

namespace Example\Commands;

use Slim\Console\Command\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class HelloCommand extends AbstractCommand
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('Hello World');

        return 0;
    }
}

Say a developer wants to use that, so he/she runs

$ composer require slim/console
$ composer require example/slim-commands

Now the file containing the command is stored in vendor/example/slim-commands/src/Commands/HelloCommand.php. But the slim console does not know anything about it. How do we inform the slim console about that command?

Idea

We define a file (like phpunit.xml.dist) in the root of a project that holds all command loaders available (see https://symfony.com/doc/current/console/lazy_commands.html). For example

/* file: slim-console.php */

declare(strict_types=1);

return [
];

So the dev just needs to add the command loader provided by the example/slim-commands repository by adding it to the array above, so

/* file: slim-console.php */

declare(strict_types=1);

return [
    \Example\CommandLoader::class
];

The slim console app would require this file, iterate over all these classes (command loaders), check if they are really instances of SlimConsoleCommandLoaderInterface and if so, create an instance and call the method getCommandLoader() (or call it as static method). This method would then return the command loader and the slim console app can add it to itself.

Below is an example of the command loader provided by the example/slim-commands repository.

<?php
/* file: vendor/example/slim-commands/src/Example/CommandLoader.php */

declare(strict_types=1);

namespace Example;

use Example\Commands\HelloCommand;
use Slim\Console\Interfaces\SlimConsoleCommandLoaderInterface;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;

class CommandLoader implements SlimConsoleCommandLoaderInterface
{
    public function getCommandLoader(): CommandLoaderInterface
    {
        return new FactoryCommandLoader([
            'example:hello' => function () {
                return new HelloCommand();
            }
        ]);
    }
}

What are your thoughts about that?

@adriansuter
Copy link
Collaborator

Actually Symfony Console allows to have one CommandLoader only. So if we would follow the idea above, the repositories providing slim commands, should not return a command loader, but an array of factories.

Idea

The App loads the file slim-console.php from the project root directory. The file simply returns an array of class names - it is the duty of the dev to put those class names into that file.

The App then iterates over these class names and tries to call the static method getCommandFactories(). This method would return an associative array where the keys are command names and the values are closures that would be called by Symfony Console if the command actually has to be instantiated. For example

/* ... */
/**
 * @return array<string, Closure>
 */
public static function getCommandFactories(): array
{
    return [
         'example:hello' => function () {
                return new HelloCommand();
            }
    ];
}
/* ... */

Once the App has collected and merged all those arrays, it can create the FactoryCommandLoader and add it to itself.

I guess that way, the Slim Console can be extended simply by requiring new packages. So Doctrine, PHP-DI and all the others could provide their own commands that would work together with the Slim Console.

And if a dev would like to add his/her own commands (specific to the project), then it is only a matter of writing a CommandFactoriesProvider which implements the getCommandFactories() method and put it to the slim-console.php file.

Please tell me, if I am completely wrong.

@l0gicgate
Copy link
Member Author

l0gicgate commented May 13, 2020

@adriansuter this makes sense to me. We should provide an interface for a custom command loader which merges them into the application. I do think we might need to pass in InputInterface and OutputInterface to those command when initializing them.

I think we may need to add that into the Config object. A property that points to a file which needs to return an array of classes or closures in case they need to do dependency injection. Which would be something like:

app/Application/Commands/load-these-commands.php

<?php

use Package\Commands\Command;

return [
    // Can be a class name
    Command::class,

    // Can be a closure and must return a Command
    static function (): CommandInterface {
      return new MyCommand($dependency);
    },
];

Then our command loader parses that file and iterates over those and merges them into the App

@adriansuter
Copy link
Collaborator

adriansuter commented May 13, 2020

Symfony has already two loaders, the FactoryCommandLoader and the ContainerCommandLoader. Shouldn't we use one of them?

They (one of them) could be used to bring these commands into our app - best of all, they allow some sort of deferred instantiation of the command classes (the class would be created only if needed).

I don't think we should mix class files and pure configuration files in the same directory. So I would rather put the file containing the command factories providers to the root of the project by default. But yes, that should be configurable.

The idea is, that the Slim Console provides the most fundamental commands only. Really just a slim set of commands. If a dev needs more commands, he/she just requires one or more packages via composer and adds the command factories provider class name(s) to the php array in the file mentioned above. Of course, he/she could always add custom developed commands. That's just a matter of writing the commands, writing the command factories provider and adding it to the php array.

This is because the set of commands for an API style app would be different than the set of commands used for a Web style app. So those could be separated into two different repositories (packages). Commands for database queries are again another isolated use case, as not every app needs a database (or needs console access to the database).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants