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

Laravel Prose Laravel Package

sulimanbenhalim/laravel-prose

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the surface area first. This is a small package (14 stars, MIT), so before designing anything around it, spend five minutes verifying exactly what it registers:

composer require sulimanbenhalim/laravel-prose
php artisan vendor:publish --tag=prose-config   # confirm the tag in the README

Laravel auto-discovery should register the service provider. Then check vendor/sulimanbenhalim/laravel-prose/src/ for the provider — the thing you care about is which extension point it uses: a Builder macro (->toProse()), a Prose::describe($query) facade, or a model trait. All three shapes appear in packages like this, and the rest of your integration depends on which one you get.

First call to make:

$prose = User::query()
    ->where('active', true)
    ->where('created_at', '>=', now()->subDays(30))
    ->orderBy('name')
    ->limit(10)
    ->toProse();

// "Active users created in the last 30 days, ordered by name, limited to 10."

First real use case: saved filters in an admin panel. Any app with a "segments" or "saved views" feature eventually has users asking "what does this filter actually do?" Rendering the builder through the package gives you a self-documenting label for free, and it stays in sync because it is derived from the query object rather than a hand-written string.

Secondary first use cases, in order of payoff: attaching human-readable context to slow-query log entries, and generating internal endpoint documentation from the builders in your repositories.

Implementation Patterns

Centralize the call in one place. Don't scatter ->toProse() through controllers. Wrap it so you can swap in toSql() as a fallback and control redaction:

final class QueryDescriber
{
    public function describe(Builder $query, bool $redact = true): string
    {
        try {
            $prose = $query->toProse();
        } catch (\Throwable $e) {
            report($e);
            return 'Custom query (description unavailable)';
        }

        return $redact ? $this->redact($prose) : $prose;
    }
}

Saved-filter labels. Store the builder-affecting state (filters, sorts) as JSON, rebuild the builder from it, then describe it. The prose is a projection of the builder, never the source of truth:

$query = $this->applyFilters(User::query(), $segment->filters);

return [
    'id'         => $segment->id,
    'name'       => $segment->name,
    'summary'    => $this->describer->describe($query),
    'sql'        => $query->toSql(),   // always ship this alongside in admin UIs
];

Slow-query and audit logging. In a DB::listen handler you only have raw SQL, so this package is more useful upstream — wrap your repository methods and log the prose next to the timing:

return tap($this->repository->expensiveReport($params), function ($result) use ($params) {
    Log::channel('audit')->info('Report generated', [
        'query'  => $this->describer->describe($this->repository->lastBuilder()),
        'params' => $params,
    ]);
});

Local scopes are your friend, and they get described. Because scopes mutate the builder, User::active()->recent()->toProse() produces a sentence covering the expanded wheres. That makes scopes testable as prose:

public function test_active_scope_is_described(): void
{
    $this->assertStringContainsString(
        'active',
        User::query()->active()->toProse()
    );
}

Use this sparingly — asserting on sentences is brittle. Assert on substrings for the handful of scopes whose semantics are non-obvious, and don't build a whole test suite around prose output.

API responses for filter builders. If you have a UI where users compose conditions, describe the draft builder on every change so the user sees "Users in Germany, verified, with at least one order" before saving. Debounce it; the description is cheap but not free.

Docs generation command. A scheduled or on-demand artisan command that walks a registry of named queries and dumps prose into a markdown file is a genuinely good fit for this package — it gives non-engineers a readable catalog of what your reporting layer does.

Gotchas and Tips

  • Bindings vs. inlined values. The central design question is whether the output renders email = ? or the actual bound value. If values are inlined, treat every description as potentially containing PII and redact before it reaches logs, Slack, or an error tracker. Verify this early with a query containing a real email address — it changes your entire integration.
  • whereRaw, selectRaw, DB::raw are opaque. Any raw expression will either be skipped or dumped verbatim. Audit your repositories for raw clauses before assuming descriptions are complete; a silently incomplete description is worse than none.
  • Global scopes show up. Soft deletes, multi-tenancy, and active() global scopes are appended to every builder, so descriptions may say "excluding deleted records" or "for tenant 42" on every single line. Decide whether that's signal or noise, and strip it in your wrapper if it's noise.
  • Nested closures and whereHas. Deeply nested whereHas('orders.items', fn ($q) => ...) structures are where prose generators degrade into unreadable run-ons. Cap the depth in your wrapper, or fall back to SQL when the description exceeds ~200 characters.
  • Descriptions describe the builder, not the executed query. No execution means no N+1 risk and no cost, but it also means you cannot describe anything the DB adds at runtime — no index hints, no optimizer behavior, no actual row counts.
  • groupBy / having / union are the weak spots. These are the constructs most likely to produce vague output ("grouped records"). If your reporting layer leans on aggregates, test those paths explicitly before shipping prose to end users.
  • Pin the version and read the source. At this size, the package has no compatibility matrix you can rely on. composer require sulimanbenhalim/laravel-prose:^x.y with a committed composer.lock, and check the provider for Laravel version guards before upgrading the framework.
  • Extension points to look for: translation/config file for phrase templates (enables localization, which matters if you show prose to users) and any registered describer classes (lets you add support for your own custom query builders). If neither exists, keep all prose output behind your own QueryDescriber so you retain a single place to swap the implementation — including replacing it with a hand-rolled describer if the package stalls.
  • Always pair prose with toSql() in developer-facing contexts. Prose is for humans deciding what a filter means; SQL is for humans debugging why it's slow. Never let prose replace the SQL view in an admin tool.
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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource