imanghafoori/laravel-widgetize
Laravel Widgetize turns Blade partials into self-contained widget classes that render and cache their own HTML. Keep controllers and templates lean, cache expensive page fragments, and compose complex pages from small, testable, reusable pieces.
Install it like any other Laravel package:
composer require imanghafoori/laravel-widgetize
Laravel's package auto-discovery registers the service provider, so there is nothing to add to config/app.php on modern Laravel (5.5+). Version support has moved recently: v2.2.51 explicitly adds Laravel 11 installability, so if you are on Laravel 11 make sure you pull at least that tag ("imanghafoori/laravel-widgetize": "^2.2.51") rather than whatever happens to be locked in your composer.json from an older install. Anything past Laravel 11 is still unproven — pin the version and run a smoke test before rolling it into a large app.
The mental model to internalize first: a widget is a tiny controller for a fragment of a page. Instead of a controller returning a full view, each widget owns one chunk (a sidebar, a "latest posts" list, a stats block), fetches its own data, and renders its own template. The package adds the plumbing to call those widgets from Blade and to cache each one independently.
Start by looking at the vendor source rather than guessing:
ls vendor/imanghafoori/laravel-widgetize/src
# BaseWidget / Widget are the two things you actually touch
Then scaffold your first widget:
php artisan make:widget PostsWidget
(If that command is not listed in your version, run php artisan list | grep -i widget and check the README's generator section — the naming has shifted across releases.) A generated widget looks roughly like this:
namespace App\Widgets;
use Imanghafoori\Widgets\BaseWidget;
class PostsWidget extends BaseWidget
{
// Which Blade view renders this widget
protected $template = 'widgets.posts';
// Minutes; 0 (the default) disables caching
protected $cacheLifeTimeInMinutes = 30;
public function html()
{
$posts = \App\Post::latest()->take(10)->get();
return view($this->template, compact('posts'));
}
}
Render it from any Blade file:
{!! \App\Widgets\PostsWidget::render() !!}
First use case to try: open your layouts/app.blade.php (or whatever the master layout is) and pull the sidebar, navbar notification bell, or footer out of the layout into widgets one at a time. This is the lowest-risk introduction because layout partials are exactly the pieces that benefit most from per-fragment caching, and you immediately see the win: the layout stays a dumb shell, and each fragment gets its own cache lifetime.
Convention layout. Keep widgets in app/Widgets and their views in resources/views/widgets. Name the widget after the fragment (LatestPostsWidget, TopCategoriesWidget) rather than after the page. One widget = one view = one query set. When a widget grows past ~40 lines of html(), it usually wants to be split.
Widgets are pull-based, not slot-based. Blade components are for markup reuse with slots; widgets are for "give me this rendered chunk of the page, with its own data and its own cache." A useful rule: if the fragment needs its own database query, make it a widget; if it is just markup parametrized by data the parent already has, make it a component. On Laravel 11 the built-in anonymous-component story is stronger than it was, so be honest with yourself about which fragments actually need the caching layer.
Passing arguments. render() accepts arguments that flow into html(), so the same widget can be reused at different sizes:
{!! \App\Widgets\PostsWidget::render(['limit' => 5]) !!}
public function html($args = [])
{
$posts = Post::latest()->take($args['limit'] ?? 10)->get();
return view($this->template, compact('posts'));
}
Remember that arguments become part of the cache key, so every distinct argument creates a separate cache entry.
Caching is the whole point. Set $cacheLifeTimeInMinutes per widget to match how stale that fragment can afford to be. A footer link list can be 1440 minutes; a "latest comments" block might be 1. Use $cacheTags if you want to invalidate a group at once, and (in recent versions) override the extra key dependency hook so the cache busts when the underlying data changes:
protected $cacheLifeTimeInMinutes = 60;
protected $cacheTags = ['posts'];
// Folded into the cache key — busts automatically when data moves
public function extraCacheKeyDependency()
{
return Post::max('updated_at');
}
This is the cleanest invalidation story the package offers: rather than manually flushing keys in observers, you express "this widget is only valid as long as the max updated_at hasn't moved." For user-specific fragments, either include auth()->id() in that dependency or disable caching for that widget entirely.
AJAX / partial refresh. Because the widget is just a class that returns rendered output, it also works well as a JSON or HTML endpoint for a poll/refresh button — expose a route that calls the widget and return the string. Some versions ship a JSON helper alongside render(); check BaseWidget for it before hand-rolling one.
Testing. Widgets are far easier to test than controller+view pairs, because render() returns a plain string:
$html = \App\Widgets\PostsWidget::render();
$this->assertStringContainsString('Latest posts', $html);
Disable caching in the test environment (lifetime 0) or use the array cache driver so tests don't leak state between cases. If you are upgrading to Laravel 11, this one-assertion test is also your cheapest compatibility check — put it in CI.
Composition. A page controller can stay thin:
public function index()
{
return view('pages.home'); // the view is mostly @widget calls
}
and pages/home.blade.php becomes a readable map of the page's fragments. That readability is the strongest argument for adopting the package on a content-heavy site.
{!! ... !!} is required because render() returns HTML. That's fine for trusted widget output, but never let user input flow into a widget template unescaped — you have now bypassed Blade's default protection at the call site.csrf_token() / session id into the cache key dependency — but then you've effectively disabled caching, so usually the right answer is "don't cache forms."auth(), session(), or request() usage, and key or disable accordingly.$cacheTags; the file and database drivers do not. On file (the Laravel default) tags are ignored or throw depending on version — if tag invalidation "doesn't work," check CACHE_DRIVER first. Laravel 11 changed the default cache/session/database setup in fresh apps, so re-check what your CACHE_STORE actually resolves to after an upgrade rather than assuming it is still file.extraCacheKeyDependency() (or an observer that flushes tags) for anything where a stale fragment would be embarrassing, e.g. prices or stock counts.DB::listen() or Laravel Debugbar and count once, then decide which widgets actually deserve a cache.$cacheLifeTimeInMinutes = 0 temporarily and confirm the widget renders correctly uncached; if it does, the problem is invalidation, not the widget. Dumping the computed cache key (via the widget's key method) tells you immediately whether your extraCacheKeyDependency() is actually varying.local/testing so developers aren't chasing ghosts, and keep it on in production.Widget::render()-style static entry points are convenient in Blade but invisible to autocompletion and static analysis. If that bothers the team, wrap the calls in a small helper or add @method docblocks.^2.2.51, and keep a smoke test that renders one widget so a future Laravel upgrade failure surfaces in CI rather than in production. The maintainer count is still one — do not float this dependency with * or dev-master.How can I help you explore Laravel packages today?