intellect-web-development/symfony-presentation-bundle
Symfony Presentation Bundle integrates presentation-layer tooling into Symfony applications, helping organize and render reusable presentational components with configurable services and Twig support.
First, the framework reality check. This is a Symfony bundle, not a Laravel package. There is no service provider, no auto-discovery, and no config/presentation.php to publish. If you tried composer require intellect-web-development/symfony-presentation-bundle in a Laravel app, you would get the classes on the autoloader and nothing wired up. So treat this assessment as: patterns I can learn from / port, unless you are actually on a Symfony codebase.
If you are on Symfony (e.g. inheriting a project, or working across both stacks), the setup is conventional:
composer require intellect-web-development/symfony-presentation-bundle
Then register the bundle in config/bundles.php and check for a config/packages/ YAML file after bin/console cache:clear. Because the package has zero dependents and 3 stars, do not assume a README exists or is current — read the composer.json (require, autoload, extra.symfony) and the src/ tree directly before trusting any docs.
Where to look first
src/ directory structure — look for the Command bus interface, Query bus interface, and a Search / Filter / Criteria namespace. That trio is the whole value proposition.DependencyInjection/*Extension.php) — this tells you which services are auto-registered and with what tags. This is your real "config reference."First use case to try
Pick your most filter-heavy list endpoint — the one with query params like ?status=active&sort=-created_at&page=2&per_page=25. That is exactly what this bundle targets. Build a single Query object for it, hand it to the query bus, and compare the resulting controller code against what you'd write today.
Laravel equivalents so you can benchmark
| Bundle concept | Laravel idiomatic equivalent |
|---|---|
| Command bus | symfony/messenger has no Laravel analogue; use queued Jobs or a simple CommandBus service |
| Query bus | A dedicated QueryBus you write, or just inject query handler services |
| Filter/Sort/Pagination search object | spatie/laravel-query-builder — this is the closest 1:1 and is battle-tested |
| Query DTO validation | Form Requests + ->validated() |
If your goal is purely "filtering, sorting, pagination in Laravel," stop here and use spatie/laravel-query-builder. This bundle's value is the discipline of CQRS, not a Laravel feature gap.
1. Query object per use case, not per entity.
The core pattern worth stealing: an immutable query DTO that carries the filter set, sort, and pagination bounds together.
final class ListOrdersQuery
{
public function __construct(
public readonly ?string $status,
public readonly ?string $search,
public readonly string $sort = '-created_at',
public readonly int $page = 1,
public readonly int $perPage = 25,
) {}
}
Then a handler that consumes it and returns a paginated result. This is the same shape whether you're in Symfony or Laravel — in Laravel you'd construct it in the controller from a Form Request and inject the handler directly rather than going through a bus.
2. Separate the bus from the search engine.
The package couples two things that are independent: (a) command/query dispatch and (b) the filter/sort/paginate engine. In practice you will often want (b) without (a). When evaluating or porting, isolate the search/filter logic — it's the part with real reuse value. The bus is a thin layer you can replace with a single service class in ~40 lines.
3. Filter criteria as data, not as if chains.
The right implementation is a collection of filter objects applied to a query builder, driven by the request params. Watch for whether the bundle does allowlisting. If it maps arbitrary request params to columns, that is a security and correctness problem — see Gotchas.
4. Integration tip: keep the boundary at the controller.
Controller → build Query DTO from validated input → Query Bus / Handler → PaginatedResult → serialize
Do not let the search object leak into your entities or repositories as a query-builder mutation. The DTO-in, result-out shape is what makes it testable. If the bundle's implementation hands you back a raw ORM QueryBuilder you can keep mutating, you've lost the main benefit.
5. Response shaping is yours, not the bundle's.
Expect to write your own serializer. Return a stable envelope:
{ "data": [...], "meta": { "page": 2, "per_page": 25, "total": 413 } }
Consistency here matters far more than which pagination internals the package uses.
License is NOASSERTION — resolve this before production. GitHub's classifier could not determine the license from the LICENSE file. That usually means a custom or malformed license file. Open the repo and read it. A missing or ambiguous license is a genuine legal risk for commercial use, and for a package with 3 stars it's unlikely anyone has vetted it. This is a hard blocker for some orgs.
Stale and unproven. Last release was 2023-08-04, with 0 dependents. That is roughly the profile of an in-house bundle extracted from one company's project. Expect: no issue triage, no Symfony 7 / PHP 8.3+ compatibility work, and a real chance of abandoned code. Pin it and be prepared to fork. If you're on a recent Symfony, verify composer.json constraints actually allow your version before you invest.
The Symfony/Laravel impedance mismatch is the whole assessment. Nothing here installs cleanly into Laravel. If you're evaluating for a Laravel project, the honest conclusion is: read the source for design ideas, then implement the same shape with spatie/laravel-query-builder plus your own QueryBus if you actually want CQRS. Don't try to adapter-shim a Symfony bundle into a Laravel container.
Allowlist your sortable and filterable fields. This is the pitfall in every filter/sort/paginate implementation. Dynamic ORDER BY from request input is a classic injection vector if it's string-concatenated, and an error-magnet if it isn't validated. Verify whether the bundle validates sort columns against a known set, or whether it trusts the caller. If you're building your own, make it explicit:
private const SORTABLE = ['created_at', 'total', 'status'];
private const FILTERABLE = ['status', 'customer_id'];
Debug the DI wiring, not your code. When a bus returns "no handler for message," the problem is almost always tag registration, not your handler. In Symfony: bin/console debug:container --tag=<whatever-tag-the-extension-applies>. Confirm the handler is in the tagged list before touching anything else.
Bound your pagination. Clamp per_page server-side (e.g. min($perPage, 100)). Query DTOs that accept the raw client value are a trivial DoS surface, and a page of 0 or a negative value should be normalized, not passed to the ORM.
Test the query object directly. Because the input is an immutable DTO, you can unit-test filtering and sorting without an HTTP layer. This is the pattern's biggest practical win — build a small fixture table and assert on the returned collection, not on JSON.
Extension points to check for. Look for an interface your own filter classes can implement, and for a way to register custom sort strategies. If the bundle offers neither, it's a closed implementation and you'll be editing vendor code — fork early rather than patching in place.
How can I help you explore Laravel packages today?