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

Bruno Laravel Package

optimus/bruno

Bruno is a Laravel package by Optimus that provides a simple, fluent way to manage and organize your application's data, helping developers build cleaner, more maintainable Laravel projects with less boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Bruno is a base API controller for Laravel that turns a plain Eloquent model into a resource endpoint with client-driven sort, filter, include, and limit/page behavior. It is intentionally small: you extend a controller, point it at a model, and get a conventional REST surface for free.

Minimal install and first controller:

composer require optimus/bruno
php artisan vendor:publish --tag=bruno   # only if you need to tune defaults
namespace App\Http\Controllers\Api;

use App\Models\Post;
use Optimus\Bruno\BrunoController;

class PostController extends BrunoController
{
    protected function getModel()
    {
        return new Post;
        // newer/older versions may expose this as a $model property instead —
        // confirm against the README tag that matches your locked version
    }
}

Register a normal resource route; the base controller supplies index, show, store, update, and destroy:

Route::apiResource('posts', PostController::class);

That single route now accepts the query shape Bruno is built around:

GET /api/posts?sort=-created_at&filter[status]=published&include=author,comments&limit=15&page=2

Where to look first:

  1. The package README's parameter table — this is the contract your frontend will depend on.
  2. config/bruno.php (if published) for the default and maximum limit.
  3. The base controller source (vendor/optimus/bruno/src) for the protected hooks you can override; that is the real extension surface.

Your first useful case is a read-only index endpoint for an admin table or a mobile list view. Sorting, filtering, and eager loading all land in the first sprint without hand-writing a query builder, and the response is paginated JSON your client can consume directly.

Implementation Patterns

Whitelist everything as a policy, not an afterthought. Bruno applies filters and sorts from request input; the safe pattern is to declare exactly what each endpoint allows on the controller — sortable columns, filterable columns, and includable relations — and let everything else be a 422 or be ignored. Treat this as part of the controller's contract, and keep the whitelist next to the model it applies to.

Filtering is a query-string facade over where. filter[status]=published becomes a column constraint; filter[status][eq]=published style operators (where supported in your version) let you express ranges and in clauses. Keep filter keys aligned to real columns or whitelisted relation paths, and prefer explicit operator syntax over magic parsing for anything beyond equality.

Sorting is sign-prefixed and multi-valued. sort=-created_at,title means "newest first, then title ascending." Always include a deterministic tiebreaker column (usually id) when the primary sort may have duplicates, otherwise pagination drifts between pages.

Includes are eager loads, and they are the N+1 fix. include=author,comments.user compiles to with() on the underlying query. The pattern that works well: expose only relations you actually want clients to traverse, and pair each include with a resource that renders it deterministically. Never let the client request arbitrary relations by name.

Override the query hook for scoping, not the action methods. The standard extension point is the protected method that builds the base query. Use it for tenant scoping, soft-delete/visibility rules, and default eager loads — applying ->where('account_id', auth()->id()) there means every verb (index, show, update, destroy) inherits the constraint. Reserve overriding index/show for cases where you need to change the response envelope.

Wrap writes in Form Requests. Bruno covers reads cleanly; store and update benefit from the usual StorePostRequest/UpdatePostRequest validation. Keep authorization in middleware and policies, not in the base controller, so apiResource routes stay uniform.

Nested resources need parent scoping. For /posts/{post}/comments, either bind the parent in the route and constrain in your query hook, or use a dedicated controller whose base query is Comment::where('post_id', $post->id). Don't rely on filter[post_id] for access control — that is client-controlled.

Reuse the parser outside controllers. Custom reporting or search endpoints can call Bruno's parsing layer directly to normalize sort/filter/limit input, then build their own query. This keeps parameter semantics identical across the API instead of drifting per endpoint.

Test the query contract, not the internals. A feature test that hits ?sort=-created_at&include=author&filter[status]=published and asserts both the JSON shape and the generated SQL (via DB::listen or assertDatabaseQuery) catches the regressions that matter when you upgrade.

Gotchas and Tips

  • Filter keys are user input. If your version allows unwhitelisted filters, a client can probe columns and, depending on parsing, attempt operator injection. Always enumerate filterable columns.
  • Sorting on relations is usually not supported. Sorting by author.name requires a join or subquery; either add a whitelisted sort that does the join yourself or expose a denormalized column. Do not assume sort=author.name works.
  • limit needs a ceiling. Set a max in config; without it a client can request limit=100000 and take down the database. Also confirm whether limit interacts with page or is a separate "take N" mode in your version — behavior differs across releases.
  • Eager loading is not free. include=comments.user on a large index is a multi-query hit; combine includes with the max limit, and consider withCount for list views that only need counts.
  • Route model binding vs. base controller lookup. The base controller often performs its own findOrFail using the model and route key. That means soft deletes, custom route keys, and 404 response shapes may differ from a hand-written show. Override show if you need policy checks before the lookup.
  • Pagination metadata is part of the contract. Capture an example response in your API docs/tests early; Bruno's envelope is easy to swap but painful to change once a mobile client ships against it.
  • Config caching. If you publish bruno.php, remember php artisan config:cache in deploys; a stale cached limit is a common "why is this endpoint returning 10 items" mystery.
  • Debug with the query log. DB::listen(fn ($q) => logger($q->sql, $q->bindings)) around a request shows exactly what sort/filter/include produced. This is faster than reading the parser source.
  • Override and call parent. When customizing a protected hook, call the parent implementation and merge your constraint rather than replacing it — otherwise you silently drop sorting or pagination.
  • Maintenance reality check. 163 stars and zero listed dependents means the package is a utility you own, not a platform you inherit. Pin the version in composer.json, keep an upgrade test around the query contract, and be prepared to fork the single base controller if Laravel major versions move under it. If you need joins, aggregates, or nested filter groups, evaluate spatie/laravel-query-builder before committing — Bruno's sweet spot is a straightforward CRUD resource with client-driven sorting, filtering, includes, and pagination.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator