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

Persist Sql Query Laravel Package

jasny/persist-sql-query

A lightweight PHP package for persisting SQL queries, offering a simple way to store reusable query definitions and execute them later with bound parameters. Useful for keeping complex SQL organized, reducing duplication, and maintaining cleaner database-driven applications.

Deep Wiki
Context7

Getting Started

composer require jasny/persist-sql-query

Where to look first. The repo is small and trait-organised: each SQL clause lives in its own trait (SelectTrait, WhereTrait, JoinTrait, OrderTrait, LimitTrait, plus InsertTrait/UpdateTrait/DeleteTrait/ReplaceTrait for DML). Read SQLQuery.php to see the composed surface, then skim the test suite — for a 33-star package the tests are the documentation, and they pin down the exact edge cases (empty IN(), IS NULL, nested subqueries, quoting).

Note that the method names below follow the library's fluent, clause-per-method shape. Before you build on them, confirm against the README/src/ — the package is at a 2020 API and it is worth ten minutes to match names exactly.

First use case: replace string-concatenated reporting SQL. The value proposition is that you get SQL with ? placeholders plus a separate bind array, so nothing user-supplied ever lands in the SQL text.

use Jasny\DB\MySQL\Query\SQLQuery;

$query = SQLQuery::create()
    ->select('u.id', 'u.email')
    ->from('users', 'u')
    ->where('u.status', 'active')
    ->orderBy('u.created_at', 'DESC')
    ->limit(100);

$stmt = $pdo->prepare((string)$query);
$stmt->execute($query->getBindValues());

Second, lower-risk entry point: the parser. If you have an existing codebase full of hand-written SQL, start by parsing instead of building. SQLQuery::fromString($sql) gives you a query object you can inspect, tweak, and re-emit — useful for adding a LIMIT, injecting a tenant filter, or normalising SQL for logging without touching the original code paths.

Implementation Patterns

Wrap build-cast-bind in one executor. The package deliberately does not talk to a database, so the first thing worth writing is a thin helper so callers never juggle the SQL string and the bind array separately:

final class Sql
{
    public function __construct(private \PDO $pdo) {}

    public function rows(SQLQuery $query): array
    {
        $stmt = $this->pdo->prepare((string)$query);
        $stmt->execute($query->getBindValues());
        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }
}

Every other pattern below then becomes a one-liner at the call site, and you have a single choke point for logging, timing, and slow-query alerts.

Builder for new code, parser for legacy code, and both together for composition. The genuinely differentiating feature versus Laravel's query builder is round-tripping. Typical uses:

  • Parse a stored/report SQL fragment, wrap or constrain it, and emit it again.
  • Build a query programmatically but accept a raw subquery from configuration.
  • Normalise SQL before it goes into a query log or an audit table (stable quoting, stable clause order) so you can group identical queries.

Compose reports from a shared base. Build the tenant/visibility filters once, then clone and extend per report branch:

$base = SQLQuery::create()
    ->select('o.id', 'o.total', 'o.created_at')
    ->from('orders', 'o')
    ->where('o.tenant_id', $tenantId)
    ->where('o.deleted_at', null);

$recent = (clone $base)->where('o.created_at', '>', $since)->limit(50);
$big    = (clone $base)->where('o.total', '>=', 1000);

Cloning is the cleanest way to share filter logic; just remember to cast to string after the final mutation, and to read bind values from the same object you cast.

Let arrays and null do the SQL work. ->where('u.status', ['active', 'pending']) becomes IN (?, ?), and ->where('u.deleted_at', null) becomes IS NULL. Prefer these over string-building IN lists — the parameter count and ordering are then the library's problem, not yours.

DML is where the bind-value discipline pays off. Inserts and updates built this way are parameterised by construction, which removes the most common injection vector in legacy PHP:

$insert = SQLQuery::create()
    ->insert()
    ->into('users')
    ->values(['email' => $email, 'name' => $name]);

Treat raw expressions as an explicit, audited escape hatch. Any API that accepts a raw SQL fragment (expressions, function calls, ORDER BY field references) inserts text verbatim. Keep those call sites few, grep-able, and never fed from request data. Identifiers passed as dotted names ('u.created_at') are the safe path for column references.

In a Laravel app, use it beside Eloquent, not instead of it. There is no service provider and no integration. For read-only analytics and reporting against a replica, generate the SQL here and hand it to the framework:

$rows = DB::connection('replica')->select((string)$query, $query->getBindValues());

This gets you the parser and the parameterisation without fighting Eloquent's grammar.

Test by snapshotting two things. (string)$query and $query->getBindValues() are both plain data, so unit tests need no database at all. Assert on the pair; it catches both clause-ordering regressions and placeholder/bind-array drift, which is exactly the class of bug that bites in production.

Gotchas and Tips

It builds and parses; it does not execute. No connection, no result hydration, no transaction handling. If you expect a DB-style facade you will be surprised. Plan for PDO (or your framework's connection) from day one.

MySQL only, and visibly so. Backtick quoting, LIMIT n, m, REPLACE INTO, ON DUPLICATE KEY UPDATE are baked into the emitter. If you support multiple engines, keep this confined to the MySQL code path rather than abstracting over it.

Cast-to-string and bind values must be read from the same, final state. (string)$query emits ? in clause order, and the bind array is generated alongside it. Mutate the query afterwards and the old string and old array no longer correspond. Build, cast, execute — don't cache half of the pair.

Parsing is the riskier half; round-trip your real corpus first. MySQL's grammar is enormous. Anything exotic — CTEs, window functions, JSON_TABLE, complex CASE nesting — may not survive parse → rebuild intact. Before you rely on rewriting in production, take your actual slow-query log, round-trip every statement, and diff the strings. Keep a fallback path that passes the original SQL through untouched when parsing fails or the diff is non-trivial.

Mind empty and null edge cases in generated IN clauses. An empty array should short-circuit to a false predicate, not emit IN (). Verify the behaviour in the tests for your version; it is a classic source of production syntax errors.

It is unmaintained since October 2020. The last release predates PHP 8.0, let alone 8.1+. Expect deprecation noise on modern runtimes (dynamic properties, implicit nullable parameters, missing return types) and no upstream fixes. Decide deliberately: pin the version and vendor/fork it with a small compatibility patch if you need current PHP, and accept that CVE fixes are yours to make. The MIT license makes forking clean, which is the main reason this is viable at all.

33 stars means no community safety net. Stack Overflow will not save you. Budget for reading src/ and tests/, and write down the behaviours you depend on (in your own tests) so a future fork is cheap.

Build a "readable SQL" helper for logs only. Interpolating values into the string is invaluable when debugging, and forbidden for execution. Keep it in a debug/ namespace with an unmissable name.

Extension points are traits and subclassing. SQLQuery is a composed set of clause traits, so a custom clause means a custom trait and a subclass. Powerful, but it welds you to the package internals. Prefer wrapping over subclassing unless you genuinely need a new clause.

Debugging checklist when the SQL looks wrong: dump (string)$query and the bind array together as a pair; check clause ordering against what MySQL actually accepts (HAVING before ORDER BY, etc.); check whether a raw expression sneaked in unquoted; and confirm you're not looking at a query object that was mutated after the string was generated.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin