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

Dashboard Bundle Laravel Package

zenstruck/dashboard-bundle

A Symfony bundle from zenstruck for adding dashboards to your application. It provides building blocks to assemble dashboard pages that surface your app's data in one place, following standard Symfony conventions. Suited to admin panels and internal tools.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. zenstruck/dashboard-bundle is a Symfony2 bundle, not a Laravel package. Its last release was 2013-12-11, it has 3 stars, 0 dependents, and a composite score of 0.175. It targets PHP 5.3/5.4, Symfony ~2.2, and Twig 1.x. You cannot composer require it into a Laravel app: it registers through AppKernel/Bundle classes, wires services via Resources/config/*.xml, and depends on Symfony 2.x EventDispatcherInterface and DependencyInjection contracts that Laravel does not satisfy.

So the only realistic "first step" is to read it as a design spec, then reimplement the two ideas it actually delivers — a config-driven admin menu and a widget grid — in Laravel idioms.

Where to look (5–10 min read):

ZenstruckDashboardBundle.php              # bundle entry point
DependencyInjection/Configuration.php     # the config tree = your config/dashboard.php
DependencyInjection/*Extension.php        # service wiring = your ServiceProvider
Resources/config/*.xml                    # container tags = Laravel container tags
Resources/views/                          # Twig layout + widget partials = Blade components
README.md                                 # the YAML config example

First use case. Copy the config shape into a Laravel config file and stand up a DashboardServiceProvider that resolves a list of widget classes. That is 90% of the package's value and takes an afternoon:

// config/dashboard.php
return [
    'menu' => [
        ['label' => 'Users',   'route' => 'admin.users.index',   'icon' => 'users',   'can' => 'viewAny,App\Models\User'],
        ['label' => 'Orders',  'route' => 'admin.orders.index',  'icon' => 'cart',    'can' => 'viewAny,App\Models\Order'],
    ],
    'widgets' => [
        \App\Dashboard\Widgets\NewUsersWidget::class   => ['priority' => 10],
        \App\Dashboard\Widgets\RevenueWidget::class    => ['priority' => 20],
        \App\Dashboard\Widgets\RecentOrdersWidget::class => ['priority' => 30],
    ],
];

Then render with a Blade layout that loops config('dashboard.menu') for the sidebar and the resolved widgets for the grid.

Implementation Patterns

The bundle's architecture maps cleanly onto Laravel constructs. Use this table as your translation key:

Symfony bundle idiom (2013) Laravel equivalent
Configuration.php tree + YAML config/dashboard.php + mergeConfigFrom()
XML service definitions ServiceProvider::register() + $this->app->bind()
Service tags (dashboard.widget) Container tags + $this->app->tagged()
Twig widget partials Blade components / @include
Event dispatcher hooks Laravel events, or a WidgetCollection
Menu builder service A plain Menu value object injected into the layout

1. Widget contract + tagged collection. This is the core pattern worth stealing:

namespace App\Dashboard;

interface Widget
{
    public function label(): string;
    public function render(): \Illuminate\Contracts\View\View|\Illuminate\Support\HtmlString;
    public function priority(): int;
    public function canView(\App\Models\User $user): bool;
}
// App\Providers\DashboardServiceProvider
public function register(): void
{
    $this->mergeConfigFrom(base_path('config/dashboard.php'), 'dashboard');

    $this->app->singleton(WidgetCollection::class, function ($app) {
        $map = $app['config']->get('dashboard.widgets', []);

        return new WidgetCollection(
            collect($map)->map(function ($opts, $class) use ($app) {
                return $app->make($class, is_array($opts) ? $opts : []);
            })->all()
        );
    });
}

2. Ordering by priority, ties broken deterministically. The original used a priority integer; do the same but sort stable on [priority, class] so two widgets with priority 10 don't reorder between requests:

return collect($widgets)
    ->sortBy(fn (Widget $w) => sprintf('%05d-%s', $w->priority(), $w::class))
    ->values();

3. Permission-gated menu + widgets. Rather than the bundle's all-or-nothing menu, gate each entry. This is the single biggest day-to-day improvement over the original:

{{-- resources/views/layouts/admin.blade.php --}}
<aside class="w-56 shrink-0">
    @foreach (config('dashboard.menu') as $item)
        @if (! isset($item['can']) || Gate::allows(...explode(',', $item['can'])))
            <a href="{{ route($item['route']) }}"
               class="{{ request()->routeIs($item['route'].'*') ? 'bg-gray-800' : '' }}">
                {{ $item['label'] }}
            </a>
        @endif
    @endforeach
</aside>

4. Failure isolation. A single widget throwing must not blank the dashboard. Wrap render, log, and degrade:

public function render(WidgetCollection $widgets, Request $request)
{
    $visible = $widgets->filter(fn (Widget $w) => $w->canView($request->user()));

    $rendered = $visible->map(function (Widget $w) {
        try {
            return ['widget' => $w, 'html' => $w->render()->render()];
        } catch (\Throwable $e) {
            report($e);
            return ['widget' => $w, 'html' => view('dashboard.widget-error', ['widget' => $w])];
        }
    });

    return view('dashboard.index', compact('rendered'));
}

5. Cache per user, not per role. Widget queries are the expensive part; a Cache::remember keyed on user_id avoids leaking data across users while keeping the hit rate reasonable:

$html = Cache::remember("dash:{$user->id}:{$w::class}", now()->addMinutes(5),
    fn () => $w->render()->render());

6. Extension points for your own project. Rather than a service tag, expose a dashboard.register hook in a service provider so feature modules can push widgets/menu items without editing config:

// in a feature module's ServiceProvider::boot()
Dashboard::menu('Billing', route: 'admin.billing.index', icon: 'card');
Dashboard::widget(BillingSummaryWidget::class, priority: 15);

That is the "plugin" behaviour the Symfony bundle got from DIC tags, expressed the Laravel way.

7. When not to build it. If this is a real admin panel, use Filament (->widgets([...]) on the page class is the exact same concept, with auth, caching, and a resource layer included), Laravel Nova, or Backpack. Reimplementing this bundle's three screens is defensible only when you already have a bespoke Blade admin and just need the config-driven grid.

Gotchas and Tips

Do not attempt to install it. Composer will fail to resolve on PHP 8.x: symfony/symfony:~2.2, Twig 1.x, and the Bundle/AppKernel registration model all conflict. Even with --ignore-platform-reqs you'd be shipping an unpatched 2013 dependency tree into production. There is no route from this package to a modern Laravel app; treat the tarball as source-of-inspiration only.

The vendor/repo names disagree. Packagist is zenstruck/dashboard-bundle, the canonical repo is kbond/ZenstruckDashboardBundle. Older tutorials reference zenstruck/dashboard — same code, different era. If you Google for docs, expect German blog posts from 2012 and Symfony 2.1–2.3 API references.

Abandonment signals to weigh. 3 stars, 0 dependents, no releases in 12 years, no security policy, no PHP 8 support. Any bug you hit is yours to fix; any CVE in its transitive deps is your responsibility. That alone justifies the reimplementation.

Event dispatcher is not a bridge point. Laravel's dispatcher implements Illuminate\Contracts\Events\Dispatcher (and PSR-14 in newer versions), not Symfony 2.x's EventDispatcherInterface. Do not try to shim it — you'll burn a day on signature mismatches before giving up.

Config-tree quirks to carry over (carefully). The bundle's YAML used implicit defaults per widget; Laravel's mergeConfigFrom only merges one level deep. If your dashboard.widgets array is associative, publishing an overriding config file will replace rather than merge. Do the merge yourself:

$widgets = array_replace_recursive(
    require __DIR__.'/../../config/dashboard.php',
    config('dashboard', [])
);

Ordering ties are a real bug source. Two widgets at the same priority will swap positions across deploys if you sort only on priority(). Always add a stable secondary key (class name or explicit order).

N+1 in widgets is the number-one performance trap. Ten widgets each running User::count(), Order::where(...)->count(), Order::latest()->take(5)->get() is 30 queries before the page paints. Batch with a single DashboardMetrics service injected into widgets, and cache the result for the request or for 5 minutes.

Cache keys collide across roles. If you cache on role instead of user_id, admins and support staff will see each other's data. Key on the user (or on a hash of the gate decisions) — never on the role name.

Widget render failures should be visible, not silent. Log with report($e) and render a placeholder card labelled with the widget name; a silently missing widget is far harder to debug than a red box.

Debugging in the Symfony original (if you're reading a fork in a legacy app): bin/console debug:container --tag=dashboard.widget lists registered widgets, and YAML config errors surface as InvalidConfigurationException at kernel boot, not at render time.

Debugging your Laravel port: php artisan about for config cache state, app(WidgetCollection::class) in tinker to check resolution, and php artisan config:clear — a stale bootstrap/cache/config.php makes config('dashboard.*') edits appear to do nothing, which is the most common "it's broken" report you'll get from a teammate.

Extension point to keep open. Whatever you build, expose widget registration through a facade or a dashboard.widget container tag rather than hardcoding the list. The moment a second developer wants to add "signups this week", you want them adding one class and one line, not editing a controller.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib