PHPackages                             jdz/menu - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. jdz/menu

ActiveLibrary

jdz/menu
========

Menu tree structure &amp; rendering library

1.0.0(2mo ago)04↓75%MITPHPPHP &gt;=8.2

Since Jun 3Pushed 2mo agoCompare

[ Source](https://github.com/joffreydemetz/menu)[ Packagist](https://packagist.org/packages/jdz/menu)[ Docs](https://jdz.joffreydemetz.com/menu)[ RSS](/packages/jdz-menu/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (2)Versions (2)Used By (0)

JDZ Menu
========

[](#jdz-menu)

A framework-agnostic PHP library for building, activating, and rendering hierarchical menu trees.

You feed it a flat (or nested) list of items; it assembles a tree of nodes, flags the active branch for the current route, and renders a template-ready nested array. Where the items *come from* (database, config file, hardcoded array) is entirely up to you — this package is intentionally **tree-only** and has no persistence layer.

Features
--------

[](#features)

- 🌳 **Tree builder** — turns a flat/nested item list into a parent/child `MenuNode` tree
- 🎯 **Active-state resolution** — marks the matching node and propagates `active` up its ancestors
- 🧩 **Template-ready output** — `toTemplate()` returns a plain nested array your view layer can loop over
- 🪝 **Pluggable callbacks** — customise active detection, titles, routes, per-component parsing
- 🎨 **Attribute hooks** — override link/container attribute formatting for your markup
- 🚫 **Class filtering** — include/ignore nodes by CSS class (e.g. hide `guest`/`logged` items)
- ✅ **Type-safe, zero coupling** — PHP 8.2+, depends only on `jdz/data`; no DB, no framework

Installation
------------

[](#installation)

```
composer require jdz/menu
```

Requirements
------------

[](#requirements)

- PHP 8.2 or higher
- [`jdz/data`](https://jdz.joffreydemetz.com/data) (pulled automatically — used by `MenuConfig`)

How it works
------------

[](#how-it-works)

`AbstractMenu` drives a three-stage pipeline:

```
prepareItems()  →  appendItems()  →  setActiveItems()  →  toTemplate()
   (your data)      (build tree)     (flag active)        (render array)

```

You extend `AbstractMenu` and implement `prepareItems()` to supply the items. Everything else (tree assembly, active resolution, rendering) is handled for you.

Quick Start
-----------

[](#quick-start)

```
use JDZ\Menu\Tree\AbstractMenu;

/** Supplies its items from a plain array. */
final class ArrayMenu extends AbstractMenu
{
    public function __construct(private array $items) {}

    protected function prepareItems(): array
    {
        return $this->items;
    }
}

// Build the item list (see "The item contract" below for the required fields).
$item = fn (array $o) => (object) array_merge([
    'id' => 0, 'title' => '', 'link' => '', 'component' => '',
    'class' => '', 'icon' => '', 'target' => '', 'slug' => '',
    'modal' => '', 'params' => [], 'separator' => false, 'children' => [],
], $o);

$menu = new ArrayMenu([
    $item(['id' => 1, 'title' => 'Home', 'link' => '/']),
    $item(['id' => 2, 'title' => 'Blog', 'link' => '/blog/', 'children' => [
        $item(['id' => 3, 'title' => 'News', 'link' => '/blog/news/']),
    ]]),
]);

$menu->activeRoute = '/blog/news/';

$tree = $menu->setMenu()->toTemplate();
// $tree is a nested array; the "Blog" and "News" nodes are marked active.
```

The item contract
-----------------

[](#the-item-contract)

`prepareItems()` must return an array of objects (e.g. `stdClass`) exposing these public properties — `appendItems()` reads them directly:

PropertyTypeNotes`id`int|string`title`string`link`string`#` and absolute `http(s)://` URLs are treated as external`component`stringused to dispatch per-component parser callbacks`class`stringspace-separated CSS classes`icon`string`target`string`blank` / `self` / `parent` → rendered as `_blank` etc.`slug`string`modal`stringnon-empty marks the node as a modal trigger`params`array`separator`bool`true` renders a divider (title/class only)`children`arraynested items, same shapeRendered output
---------------

[](#rendered-output)

`toTemplate()` returns an array of items, each shaped like:

```
[
    'route'     => '/blog/',   // null when link is '#' or empty
    'separator' => false,
    'active'    => true,
    'modal'     => false,
    'slug'      => '',
    'icon'      => '',
    'target'    => '',
    'id'        => 2,
    'class'     => '',
    'title'     => 'Blog',
    // 'containerAttrs' => [...]  // present if formatContainerAttributes() returns any
    // 'linkAttrs'      => [...]  // present if formatLinkAttributes() returns any
    'children'  => [ /* same shape, only if the node has rendered children */ ],
]
```

Add your own keys per node by overriding `extraNodeData(MenuNode $node): array`.

MenuNode
--------

[](#menunode)

`JDZ\Menu\Tree\MenuNode` is the tree node. Useful methods when writing callbacks or attribute formatters:

- `getId() / getTitle() / getLink() / getClass() / getIcon() / getTarget() / getSlug() / getComponent() / getModal() / getParams() / getRoute()`
- `isActive() / isSeparator() / isModal() / isRoot() / isInternal() / hasChildren() / hasClass($classes)`
- `getChildren() / getParent()`
- `addClass(string $value, bool $merge = false)` / `setActive()` (propagates to ancestors) / `setAnchorAttribute($key, $value)`

Callbacks
---------

[](#callbacks)

Register closures before calling `setMenu()`:

```
$menu
    // Decide active state yourself (return bool). Runs when the default
    // link === activeRoute check doesn't already match.
    ->setNodeActiveCallback(fn (MenuNode $n) => str_starts_with($n->getLink(), '/blog/'))

    // Rewrite the displayed title (return string).
    ->setNodeTitleCallback(fn (MenuNode $n) => strtoupper($n->getTitle()))

    // Post-process the route just before render.
    ->setNodeRouteCallback(fn (MenuNode $n) => $n)

    // Per-component item parsing ('_' is the fallback for any component).
    ->setNodeParserCallback('blog', fn (object $item) => $item)

    // Inject extra children onto a node as it's appended.
    ->setNodeAppendChildrenCallback(fn (MenuNode $n) => null);
```

Public tuning properties: `$activeRoute`, `$showChildren`, `$dropdown`, `$dropdownClass`, `$ignoreClasses` (nodes carrying any listed class are skipped during render).

Configuration
-------------

[](#configuration)

`JDZ\Menu\Config\MenuConfig` (extends `JDZ\Utils\Data`) carries render-time context to your subclass via `setConfig()`:

```
use JDZ\Menu\Config\MenuConfig;

$config = (new MenuConfig())
    ->set('publicPath', '/var/www/site/public')
    ->set('language', 'fr');

$menu->setConfig($config);
// inside your subclass: $this->config->getPublicPath(), getLanguage(), getCacheThumbs(), getBaseUrl()...
```

Not included
------------

[](#not-included)

This package builds and renders the **tree only**. It deliberately ships no database access, entities, or repositories — provide your items however you like by implementing `prepareItems()`.

Testing
-------

[](#testing)

```
composer test
# or
vendor/bin/phpunit
```

License
-------

[](#license)

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance83

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

88d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/5e83e3701566e43438525ed14578487e732b849d152b5071aa1613a0dad96913?d=identicon)[jdz](/maintainers/jdz)

---

Top Contributors

[![joffreydemetz](https://avatars.githubusercontent.com/u/15113527?v=4)](https://github.com/joffreydemetz "joffreydemetz (3 commits)")

---

Tags

contentJDZ

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/jdz-menu/health.svg)

```
[![Health](https://phpackages.com/badges/jdz-menu/health.svg)](https://phpackages.com/packages/jdz-menu)
```

###  Alternatives

[willdurand/negotiation

Content Negotiation tools for PHP provided as a standalone library.

1.4k133.7M236](/packages/willdurand-negotiation)[laurentvw/scrapher

A web scraper for PHP to easily extract data from web pages

192.5k1](/packages/laurentvw-scrapher)[coldtrick/thewire_tools

Extend the functionality of The Wire

143.3k](/packages/coldtrick-thewire-tools)[coldtrick/static

Create static content pages

103.9k](/packages/coldtrick-static)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
