Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Menu Builder Client Bundle Laravel Package

forci/menu-builder-client-bundle

A Symfony bundle that integrates the Forci menu builder client into your application, providing ready-made services, dependency injection wiring and configuration options for building, managing and rendering menus within the Symfony framework.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. Every "zero-config" claim in the description applies to a Symfony kernel — config/bundles.php registration, a Configuration tree, and autowired services. In Laravel there is no bundle mechanism, so "getting started" splits into two honest paths: (1) you're integrating into a Symfony app, or (2) you're using this as reference code to build a Laravel client. Assume the second until you've read the source.

For a Symfony app:

  1. composer require forci/menu-builder-client-bundle
  2. Register in config/bundles.php if Flex hasn't already:
    Forci\MenuBuilderClientBundle\ForciMenuBuilderClientBundle::class => ['all' => true],
    
  3. Configure in config/packages/forci_menu_builder_client.yaml — typically base URI, API key/token, timeout, and any cache service.
  4. Inject the client service into a controller or service via autowiring, call the menu-fetch method, hand the returned DTO/tree to Twig.

For a Laravel dev, the first real step is a source audit, not composer require. Clone the repo and read, in this order:

  • composer.json — check if it requires symfony/framework-bundle or symfony/twig-bundle. If yes, it is not portable; reimplement.
  • DependencyInjection/ — tells you exactly which services are wired and with what config. This is effectively your config schema for a Laravel translation.
  • Client/ or Service/ (whatever the HTTP layer is called) — this is the part worth reusing. If it's PSR-18/PSR-7 with no container or Twig coupling, it ports cleanly as a package under app/Services/MenuBuilder/.

The first use case in either framing is the same: fetch a named menu (e.g. main-nav) for a locale, render it as a nested <ul>/Blade recursive partial, and cache the raw response. Build that vertical slice before anything else; it will force you to answer the questions that matter — auth shape, error semantics, tree depth, and idempotency.

Implementation Patterns

Bind once, inject everywhere. In Symfony the bundle already registers the client as a service; consumers autowire it. When porting to Laravel, mirror that with a singleton binding in a ServiceProvider:

$this->app->singleton(MenuBuilderClient::class, fn ($app) => new MenuBuilderClient(
    http: $app->make(\Illuminate\Http\Client\Factory::class),
    baseUrl: config('menu-builder.base_url'),
    token: config('menu-builder.token'),
    timeout: config('menu-builder.timeout', 2),
));

Consumers should depend on an interface you own (MenuRepository / MenuProvider), never on the vendor class directly. That's the swap-out seam if the vendor disappears.

Cache, don't cache-per-request-forever. Menus are highly cacheable and should almost never be fetched on the hot render path. The pattern that holds up:

public function get(string $slug, string $locale): Menu
{
    return Cache::remember("menu:{$slug}:{$locale}", now()->addMinutes(10), function () use ($slug, $locale) {
        $raw = $this->client->fetch($slug, $locale);
        return Menu::fromApi($raw);
    });
}

Cache the raw response (or a serialized DTO), not the rendered HTML — otherwise you can't invalidate on locale switch or re-render with different templates. Invalidate via a webhook if the vendor offers one, otherwise short TTLs with jitter (e.g. 10m ± 60s) to avoid synchronized stampedes across app instances.

Wrap the fetch in a resilient boundary. A remote menu service in the render path is a latency and availability coupling. Every fetch should be:

  • Timeout-bounded (500–800ms connect+response is a reasonable ceiling for nav).
  • Retried once with backoff for transient 5xx/timeouts — Http::retry(1, 200).
  • Fallback-aware: on failure, serve last-known-good from cache; if no cache exists, fall back to a static nav config so pages never render without a menu.

Treat empty results as anomalies. A menu returning [] should be logged and alerted, not silently rendered as an empty <ul>. On a vendor schema change, deserialization often "succeeds" into an empty collection — this is your canary.

Render with a recursive partial. Whatever the DTO shape, flatten or recurse — Blade makes this clean:

@foreach ($menu->items as $item)
    <li>
        <a href="{{ $item->url }}">{{ $item->label }}</a>
        @if ($item->children)
            <ul>@include('partials.menu-items', ['items' => $item->children])</ul>
        @endif
    </li>
@endforeach

Don't try to port the Twig extension; it won't survive the trip, and Blade recursion is trivial anyway.

Testing seam. The bundle advertises a PSR-compliant HTTP layer "easy to test." In Laravel, this maps to Http::fake() in feature tests and a MenuBuilderClientContract with a fake implementation for unit tests. Write one contract test against a recorded fixture of the real API response — this is your early-warning system for schema drift.

Gotchas and Tips

License is the first blocker, not a technical one. NOASSERTION in composer.json means no rights are explicitly granted — not even internal use, depending on legal's reading. Get this resolved in writing before any code lands in a product repo. This is a hard gate that outranks every other concern below.

You are the maintainer from day one. Zero stars, zero dependents, zero release activity means no one else has validated this in production. Assume no PHP/Symfony version bumps, no security patches, no issue triage. Vendor the code (fork + VCS/path repository in composer.json) rather than depending on Packagist, and assign a named owner.

"Bundle" is the tell. If a composer require pulls in symfony/framework-bundle or symfony/twig-bundle, the correct move is to reimplement — not to try to shoehorn it. The reusable surface is small (likely <200 lines of HTTP + DTO); writing it yourself removes the license question, the maintenance burden, and the framework mismatch in one go.

Configuration quirks to check in DependencyInjection/Configuration.php:

  • Auth model: API key header, bearer token, or query param? This determines how you scope secrets and whether per-tenant credentials mean multiple client instances.
  • Endpoint shape: is the base URI hardcoded or configurable? The description says "configurable endpoints," but verify — a hardcoded prod URL in a bundle is a footgun for staging.
  • Timeout defaults: often missing or overly generous (30s). Set them explicitly in your config; never inherit vendor defaults for a render-path dependency.
  • Caching: if the bundle accepts a cache service, understand whether it caches raw responses or DTOs. If it doesn't cache at all, treat caching as your layer, not the bundle's.

Failure modes and what to do about them:

  • Service down or slow → hard timeout + last-known-good cache + static fallback. Test this by mocking 5xx and by Http::fake(['*' => Http::response([], 503)]) and asserting the page still renders nav.
  • Auth token expired/rotated → detect 401 explicitly and alert; don't fail silently into empty menus. Credential rotation is your job, not the vendor's.
  • Schema drift → contract test against a recorded fixture; alert on deserialization errors rather than letting them degrade to [].
  • Cold cache on deploy → warm the menu cache in your deploy/artisan hook, or use Cache::lock() single-flight so only one instance fetches on stampede.
  • Deep trees blowing payload/template time → consider capping depth at the fetch layer, or pre-flattening in the DTO.

Extension points worth knowing. If the client is PSR-18-based (likely, given the description), you can swap the underlying HTTP client, decorate it with a caching decorator, or wrap it in a retry middleware — all without touching the vendor class. In Laravel, that means binding the vendor's PSR-18 client into the container and using Http::withClient() or a Psr18Client adapter. Look for constructor injection of Psr\Http\Client\ClientInterface in the source; if it's a hardcoded GuzzleHttp\Client inside a method, that's a portability ding.

Rollout tip. Ship behind a feature flag with the current nav as fallback, cut over to the remote source on a low-traffic page first, and watch latency p95 and error rate for a week before it touches the primary layout. Navigation is every page — the blast radius of a bad integration is the whole site.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource