krystal-sf/ux-menus

Tagged menus for Krystal Symfony UX Projects

Maintainers

Package info

gitlab.com/krystal-sf/ux-menus

Homepage

Issues

Type:symfony-bundle

pkg:composer/krystal-sf/ux-menus

Transparency log

Statistics

Installs: 2 068

Dependents: 10

Suggesters: 0

Stars: 0

1.0.x-dev 2026-08-19 10:45 UTC

This package is auto-updated.

Last update: 2026-08-19 10:45:59 UTC


README

Tagged Knp Menus for Krystal Symfony UX Projects: menus providers collected by tags, fluent menu factory, presets processors and live menu items.

PHP Version Symfony

Installation

composer require krystal-sf/ux-menus

Enable the bundle in config/bundles.php:

return [
    // ...
    Ksf\Core\Menus\KsfMenusBundle::class => ['all' => true],
];

Tagged Menus Providers

Menus are built by providers, plain services registered on a tag with the #[AsMenu] attribute. All providers sharing a tag are collected and merged (by priority) when the menu is rendered:

use Knp\Menu\ItemInterface;
use Ksf\Core\Menus\Attribute\AsMenu;
use Ksf\Core\Menus\Model\AbstractUxMenuProvider;

#[AsMenu(MyMenusTags::SIDEBAR, priority: 100)]
class UserMenu extends AbstractUxMenuProvider
{
    public function get(string $name, array $options = array()): ItemInterface
    {
        return $this->factory
            ->autoload("menus.user", "MyDomain")
            ->createItem("user", array('route' => MyRoutes::USER))
        ;
    }
}

Providers may target a parent item via getParent(), or declare themselves as root with const IS_ROOT = true.

Render the menu anywhere with the Ksf:Menu Twig component:

<twig:Ksf:Menu :subject="ux.menus.app.SIDEBAR" :options="{...}"/>

Menu tags are declared in dictionaries exposed to Twig via #[AsMenusDictionary("name")]ux.menus.name.*.

Menu Factory

MenuFactory wraps the Knp factory with a fluent options basket, consumed and purged by createItem():

$item = $this->factory
    ->setIcon("fas fa-user")
    ->setBadge("12")
    ->addPreset(BtnStylesPresets::PRIMARY)
    ->liveAction("doAction", array("id" => 27))
    ->autoload("menus.user.details", "MyDomain")
    ->createItem("details")
;

Fluent helpers: setOption, mergeOptions, setIcon, setImage, setBadge, setTooltip, setTranslationDomain, addPreset, addPresets, setPresets, liveEvent, liveAction, liveController, autoload.

Warning: the factory is a shared service — options accumulated without a final createItem() would leak into the next menu construction. Call reset() at the beginning of a build when in doubt.

BootBox confirmations helpers (bootboxConfirm...) are available when krystal-sf/ux-bootbox is installed (suggested dependency).

Presets & Processors

Menu items may carry presets (CSV or array), expanded by tagged providers and applied by processors:

  • #[AsMenuPresetProvider] services expose preset values
  • #[AsMenuProcessor] services post-process built items (see CompactModeProcessor, JumboModeProcessor)

Live Menu Items

Rendered items are Live Components (Ksf:MenuItem) supporting:

  • data-event → dispatch a Live Event (LiveEventArgs)
  • data-action → trigger a Live Action (LiveActionArgs)
  • action / actionArgs → forward to a Symfony controller
  • Badges with live counters (Ksf:MenuItemWithBadge)

Active Items & Page Context

KnpMenu native mechanisms remain available to mark items active ("current", rendered with the active class - parents follow automatically):

// Force state (bypasses all voters):
$this->factory->createItem("item", ['current' => true]);
// Native RouteVoter - routes & patterns covered by this item:
$item->setExtra('routes', ['app_users', ['pattern' => '/^myplugin_/']]);

For apps where urls have no usable hierarchy (custom plugins, variable routing), bind activation to the Ksf Page Context instead: the page declares its working subject (exactly like for the blocks), items declare what they cover - no route coupling at all.

// The page (controller) declares its subject, as usual:
$this->contextManager->setGlobal($myObject, ActionContext::EDIT);

// The menu provider declares its coverage:
$this->factory
    ->setActiveContext(MyObjectClass::class)                          // any MyObjectClass page
    // ->setActiveContext(MyObjectClass::class, ActionContext::EDIT)  // edit pages only
    // ->setActiveContext(                                       // custom rule
    //     MyObjectClass::class,
    //     callback: fn (MyObjectClass $object) => $object->getType() === $type
    // )
    ->createItem("my-objects");

Subjects match by class with inheritance (an item may declare an interface), plain strings match strictly. The ContextVoter reads the GLOBAL context only & ABSTAINS on mismatch, so route based items keep working alongside.

A strongly typed callback may even be used ALONE: a TypeError on invocation counts as an abstention, so the declared argument type acts as the subject filter.

->setActiveContext(callback: fn (MyObjectClass $object) => $object->isEnabled())

Testing

make up          # boot the docker stack
make phpunit     # run the test suite
make quality     # lint + style + phpstan

License

MIT — see LICENSE.