php-school / cli-menu

🖥 Build beautiful PHP CLI menus. Simple yet Powerful. Expressive DSL.
http://www.phpschool.io
MIT License
1.94k stars 106 forks source link

Feature request - ability to add sub-menu from addItem's callable #277

Open temuri416 opened 11 months ago

temuri416 commented 11 months ago

Something along these lines:

    $builder = (new CliMenuBuilder)
        ->addItem('Load Available Options', function (CliMenu $menuItem) {
            /**
             * Here I would load available options from DB and add them as $menu's subMenu
             */
            $loadedOptions = ['Opt1', 'Opt2', 'Opt3'];

            $builder = $menuItem->getBuilder();
            $subMenu = $builder->addSubMenu('Options');

            foreach ($loadedOptions as $opt) {
                $subMenu->addItem($opt);
            }

            $builder->build();
        })
        ->build();

    $builder->open();

In other words, a support for dynamically adding submenus when a menu item is selected would be an amazing feature.

Keep up the great work!

Thank you :)

AydinHassan commented 11 months ago

I guess you can already achieve this, maybe not so simple, let me take a look

AydinHassan commented 11 months ago

Is this what you mean?

<?php
declare(strict_types=1);

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\Builder\CliMenuBuilder;
use PhpSchool\CliMenu\MenuItem\MenuItemInterface;
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;

require_once(__DIR__ . '/../vendor/autoload.php');

$itemCallable = function (CliMenu $menu) {
    echo $menu->getSelectedItem()->getText();
};

$menu = (new CliMenuBuilder)
    ->setTitle('CLI Menu')
    ->addSubMenu('Options', function (CliMenuBuilder $b) {
        $b->setTitle('CLI Menu > Options');
    })
    ->addItem('Load Available Options', function (CliMenu $menu) {
        $items = $menu->getItems();

        //find options menu
        /** @var MenuMenuItem $optionsMenu */
        $optionsMenu = current(array_values(array_filter($items, function (MenuItemInterface $item) {
            return $item instanceof MenuMenuItem && $item->getText() === 'Options';
        })));

        if ($optionsMenu === false) {
            //menu not found
        }
        $optionsMenu->getSubMenu()->setItems([
            new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 1', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            }),
            new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 2', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            })
        ]);

        echo "Loaded";
    })
    ->setBackgroundColour('yellow')
    ->build();

$menu->open();
temuri416 commented 11 months ago

First of all, thank you for your time and the example.

The solution above kind of works, but I wonder if it can be improved to the following flow.

Step 1. Menu items are:

Cli Menu

● OPTIONS ● EXIT

Step 2. User enters OPTIONS menu item by pressing ENTER. The $itemCallable registered with OPTIONS menu item then loads options from the database and programmatically builds submenu with its options, so that:

Step 3. Menu items are:

Cli Menu > Options

● Option 1 ● Option 2 ● Option 3 ● GO BACK

Selecting any of the options above would take user to the root, which would look as:

Step 4. Upon return to the root menu the items are:

Cli Menu

● OPTIONS ● EXIT

That's it.

In other words, I'd like to generate sub-menu of a root menu item dynamically. I hope this explains it :-)

Cheers!

AydinHassan commented 11 months ago

Ah yeah, I thought that's what you really meant, you can do that by extending the submenu item:

<?php
declare(strict_types=1);

use PhpSchool\CliMenu\CliMenu;
use PhpSchool\CliMenu\Builder\CliMenuBuilder;
use PhpSchool\CliMenu\MenuItem\MenuMenuItem;

require_once(__DIR__ . '/../vendor/autoload.php');

$itemCallable = function (CliMenu $menu) {
    echo $menu->getSelectedItem()->getText();
};

class DynamicSubMenu extends MenuMenuItem
{
    public function __construct(string $text, string $title)
    {
        parent::__construct($text, new CliMenu($title, []), false);
    }

    public function getSelectAction() : ?callable
    {
        return function (CliMenu $menu) {

            $this->getSubMenu()->setItems([
                new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 1', function (CliMenu $menu) {
                    echo $menu->getSelectedItem()->getText();
                }),
                new \PhpSchool\CliMenu\MenuItem\SelectableItem('Option 2', function (CliMenu $menu) {
                    echo $menu->getSelectedItem()->getText();
                })
            ]);

            $this->showSubMenu($menu);
        };
    }
}

$menu = (new CliMenuBuilder)
    ->setTitle('CLI Menu')
    ->addMenuItem(new DynamicSubMenu('Options', 'CLI Menu > Options'))
    ->setBackgroundColour('yellow')
    ->build();

$menu->open();

It would be cool to support this natively though. I guess we could accept a callable parameter to MenuMenuItem constructor, and if you specify it, we call it with the existing select action as a parameter (which just calls showSubMenu). Then the consumer decides when to forward, instead of us deciding whether we open before or after. If you don't provide the callable arg then we just use our existing action. I would be happy to accept this as a PR if you wanna work on it :)

So MenuMenuItem's getSelectAction method would look something like:

public function getSelectAction() : ?callable
{
    $show = function (CliMenu $menu) {
        $this->showSubMenu($menu);
    };

    if ($this->customAction === null) {
        return $open;
    }

    ($this->customAction)($open);
}
temuri416 commented 11 months ago

Yes, that's what I had in mind. I'd have to dig deep into the source code to prepare a PR. I'll try to spend some time on it.

Cheers!

temuri416 commented 11 months ago

Got a question regarding your latest solution.

The "GoBack" action does not return from the DynamicSubMenu back to its parent:

        $this->getSubMenu()->setItems([
            new SelectableItem('Option 1', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            }),
            new SelectableItem('Option 2', function (CliMenu $menu) {
                echo $menu->getSelectedItem()->getText();
            }),
            new SelectableItem('Back', new GoBackAction)
        ]);

I suppose that's because there's no proper support for dynamic submenus?

temuri416 commented 11 months ago

Yeah, that seems to be the case:

public function __invoke(CliMenu $menu) : void
{
    if ($parent = $menu->getParent()) {
        $menu->closeThis();
        $parent->open();
    }
}

$parent is missing.

AydinHassan commented 11 months ago

Yes probably a few things don't work properly, I'm sure you could hack them in, but would be nice to have baked in support, feel free to ping if you have any questions !

eg something hacky like $this->getSubMenu()->setParent($menu); in the select action of DynamicSubMenu

temuri416 commented 11 months ago

Maybe the key is to make the following work:

->addSubMenu(new DynamicSubMenu('Options', 'CLI Menu > Options'))

Started digging in the source now, hopefully will be able to figure out proper solution.