mmoreram/controller-extra-bundle
Symfony bundle that cuts controller boilerplate with annotations: inject request params, Doctrine entities, paginators, logged-in users, and cache or log behavior directly into controller actions. Keeps code small and declarative, with custom resolvers for extending it.
First, a reality check: this is a Symfony2/Symfony3 bundle, not a Laravel package. There is no service provider, no facade, no config file Laravel will read. composer require mmoreram/controller-extra-bundle in a Laravel app will succeed (it pulls symfony/http-kernel, doctrine/annotations, etc.) but nothing will be wired up — the ControllerExtraBundle class extends Symfony\Component\HttpKernel\Bundle\Bundle, and Laravel never instantiates bundles. So your "getting started" depends on which of two situations you're in.
composer require mmoreram/controller-extra-bundleapp/AppKernel.php: new Mmoreram\ControllerExtraBundle\ControllerExtraBundle()framework.annotations: true, plus doctrine/annotations and sensio/framework-extra-bundle present), and that the routing.yml for the controller uses type: annotation.Annotation/ directory — that is the authoritative API surface. The headline annotations are @Paginator, @Entity, @Cache and @Log (with a couple of smaller helpers depending on version). The README is thin; the annotation classes and their EventSubscriber//EventListener/ counterparts tell you exactly which request attributes and controller arguments they populate./**
* @Paginator(
* entityManager = "doctrine.orm.default_entity_manager",
* query = "SELECT u FROM AppBundle:User u",
* page = "page",
* limit = "limit"
* )
*/
public function listAction($paginator)
{
return ['paginator' => $paginator];
}
(Exact parameter names drift between versions — read Annotation/Paginator.php before copying snippets from blog posts.)
Don't install it. Read the README and the Annotation/ folder as a design checklist for controller boilerplate, then map each annotation to what Laravel already gives you:
| Bundle annotation | Laravel equivalent, out of the box |
|---|---|
@Entity (request param → entity, 404 if missing) |
Implicit route model binding: Route::get('/users/{user}', ...) |
@Paginator |
User::paginate(15) + $users->links() |
@Cache (cache a controller response for a TTL) |
Cache::remember() around the query, or spatie/laravel-responsecache for whole-response caching |
@Log |
A LogRequests middleware with Log::info(), or spatie/laravel-activitylog |
Your first "use case" is therefore: pick the one action in your app with the most boilerplate (usually a filtered, paginated index), and confirm Laravel's native tools already cover it. They almost always do.
The bundle's whole value proposition is that a controller action declares its inputs and side effects in the docblock, and event listeners on kernel.controller / kernel.controller_arguments resolve them before your method body runs. A typical annotated action:
/**
* @Entity(
* name = "user",
* class = "AppBundle:User",
* mapping = {"id" = "id"}
* )
* @Cache(ttl = 3600, key = "user_show")
* @Log(message = "User profile viewed", level = "info")
*/
public function showAction(User $user)
{
return ['user' => $user];
}
Workflow tip: keep these actions thin. The bundle is designed so the method body is 1–3 lines. If you find yourself unpacking the paginator and re-querying, you're fighting the abstraction.
// routes/web.php
Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show');
// app/Http/Controllers/UserController.php
public function show(User $user)
{
return view('users.show', compact('user'));
}
Route model binding does @Entity's job; returning a view/JSON does the response shaping. Add ->middleware('cache.headers:public;max_age=3600') or wrap the payload in Cache::remember("user.{$user->id}", 3600, fn () => ...) for the @Cache behaviour.
@LogThe bundle's @Log is a per-action cross-cutting concern. In Laravel, cross-cutting concerns belong in middleware, not annotations, because you get one place to change and it applies to whole route groups:
// app/Http/Middleware/LogControllerAction.php
public function handle($request, Closure $next)
{
Log::info('controller.action', [
'route' => $request->route()->getName(),
'action' => $request->route()->getActionName(),
'user' => optional($request->user())->id,
]);
return $next($request);
}
Register it globally in bootstrap/app.php (Laravel 11+) or Kernel::$middleware. This is the direct upgrade path from "annotate each action."
Integrate the bundle only where it replaces existing code — don't annotate new actions for the sake of it. The pragmatic migration order is:
@Entity on detail/show actions (highest boilerplate savings, lowest risk).@Paginator on index actions.@Cache last, and only on read-only, non-authenticated-variant endpoints (see Gotchas).@Log never — use a Monolog channel or a kernel listener you control.If you like the annotation ergonomics, the durable version is to write your own small kernel.controller_arguments listener (Symfony) or a FormRequest + policy (Laravel). The bundle is 151 stars, zero dependents, last released 2017 — treat it as a reference implementation of the pattern, not a runtime dependency.
It will not run on Symfony 4+ (Flex). Symfony 4 removed bundle registration from AppKernel, and the bundle predates the Flex recipe era — there is no recipe, no config/bundles.php entry, and its service definitions assume Symfony 2/3 container semantics. Installing it on a modern Symfony is a dead end. On Laravel it is a no-op.
Version drift in annotation parameters. The README on GitHub does not match every tagged release. The single most common time-waster is copying a @Paginator(query = "...") snippet that belongs to a different minor version. Always open vendor/mmoreram/controller-extra-bundle/Annotation/<Annotation>.php and read the public properties — those are the accepted argument names.
@Entity overlaps with @ParamConverter. If sensio/framework-extra-bundle is installed, both listeners may try to resolve the same controller argument. Whichever listener has the higher priority runs first, and the second may overwrite or 404 unexpectedly. Fix by using one or the other per action, and if debugging, dump $event->getRequest()->attributes->all() in a kernel.controller listener to see which one won.
@Cache caches the response, not the data — and it does not understand user context. An action that varies by logged-in user, by Accept header, or by query string can serve one user's response to another if the cache key is static. Always make key include the varying inputs (e.g. key = "user_show_" ~ id if the annotation supports expression keys in your version) or skip @Cache for authenticated endpoints entirely. In Laravel the equivalent trap is Cache::remember without the user id in the key.
Cache invalidation is manual. The bundle has no tag-based invalidation and no observer on Doctrine writes. You will be clearing the pool (cache:pool:clear, or cache:clear in older Symfony) after every content change. This is the main reason the pattern lost to HTTP-cache middleware (symfony/http-cache, or spatie/laravel-responsecache on the Laravel side), which can purge by tag.
@Log is synchronous and in-request. It writes during the request lifecycle; a slow log handler directly increases response time, and it logs before you know whether the action threw. For anything you actually rely on for audit, use a kernel.terminate listener or Laravel's terminable middleware.
Annotations are parsed on every request in dev, cached in prod. If you add an annotation and see no effect, you almost certainly need to clear the container/annotation cache (bin/console cache:clear --env=prod). This accounts for the majority of "the bundle is broken" reports.
No PHP 7.2+ / 8.x guarantees. Last release 2017-12-04 means it predates PHP 7.2's object type, PHP 7.4 typed properties, and PHP 8 attributes entirely. On PHP 8 you may hit deprecation noise from doctrine/annotations or reflection-based code. Pin your PHP version or run with E_DEPRECATED suppressed.
Extension points, if you're stuck on it: each annotation is paired with a listener that you can override by registering your own service with the same event and a higher priority in the tag (kernel.event_listener). You can also decorate the paginator factory if the bundle exposes one as a service — check Resources/config/services.yml for the service ids; that file is the real table of contents for the bundle.
For a Laravel team, the actionable takeaway: the four annotations map 1:1 onto route model binding, Eloquent pagination, response caching middleware, and logging middleware. There is nothing here you need to import. Re-derive the ergonomics, skip the dependency.
How can I help you explore Laravel packages today?