skip to content

In Laravel, what do ->dump(), ->dd() and ->ddRawSql() print when chained on a collection or a query builder?

level: middleimportance: should knowfreq 45%

answer

  1. collections dump their items
  2. builders dump SQL, not results
  3. placeholders plus a bindings array
  4. RawSql substitutes the bindings
  5. toRawSql() returns a string

basics

~20 s

On a collection they dump its items; ->dump() returns the collection so the chain continues, ->dd() stops. On a query builder they dump the SQL, not results: ->dump() shows ? placeholders plus bindings, ->dumpRawSql() and ->ddRawSql() show the SQL with bindings substituted.

solid answer

~40 s

A collection's `->dump()` calls `dump($this->all())` and returns the same collection, so you can drop it between `filter()` and `sum()` to watch data change; `->dd()` dumps the items and ends the script. On a query builder the methods dump the **query, not its results**, and nothing is executed: `->dump()` prints `toSql()` with `?` placeholders plus the bindings array and returns the builder, `->dd()` prints the same and stops, while `->dumpRawSql()` and `->ddRawSql()` print `toRawSql()`, the SQL with bindings substituted, ready to paste into a database client. Eloquent forwards all four to the underlying query builder with global scopes applied, so the dump shows scope conditions too. When you need the SQL in a log instead, `toRawSql()` returns it as a string.

code

php · 16 lines
php
<?php

use Illuminate\Support\Facades\DB;

DB::table('promotions')
    ->where('active', true)
    ->where('category_id', 7)
    ->dump();
// "select * from "promotions" where "active" = ? and "category_id" = ?"
// [true, 7]

DB::table('promotions')
    ->where('active', true)
    ->where('category_id', 7)
    ->ddRawSql();
// select * from "promotions" where "active" = 1 and "category_id" = 7

go deeper

for a junior

Recall that collections and query builders have ->dump() and ->dd(), and that on a query they print SQL rather than results.

for a middle

Explain placeholders plus bindings versus dumpRawSql, what each method returns, and why toRawSql suits logging.

for a senior

Point out the Eloquent passthru return type, global scopes in dumps, and the LazyCollection cost of dumping mid-pipeline.

for a principal

Decide which query-inspection tools belong in everyday workflow versus tests and logs, so SQL debugging does not leak into shipped code.

## Chainable dumps Laravel adds dump methods to the objects you most often chain, so you can inspect a value mid-chain without breaking it into variables. The global `dump()` and `dd()` helpers underneath are the same Symfony VarDumper helpers, so the output format and the "dd stops the script" rule are identical. | Called on | `->dump()` prints | `->dump()` returns | `->dd()` | |---|---|---|---| | `Collection` / `LazyCollection` | `$this->all()`, the items | the same collection | prints items, ends the script | | Query builder (`DB::table()`) | SQL with `?` and the bindings array | the builder | prints SQL and bindings, ends | | Eloquent builder (`Product::where()`) | forwarded to the base query builder | the **base** query builder | forwarded, ends | Other everyday objects have the same pair: `Str::of()` stringables, `Carbon` dates, the request (`$request->dump('coupon')`), validated input, and HTTP client requests and responses. ## Collections: watching data change For the grocery discount bug, a collection pipeline might compute the discount per item: ```php $discount = $cart->items ->filter(fn ($item) => $item->product->on_sale) ->dump() // which items qualified? ->map(fn ($item) => $item->price * $item->rule->percent / 100) ->dump() // per-item amounts ->sum(); ``` Because `->dump()` returns `$this`, the chain continues and the final value is unchanged. On a `LazyCollection`, `dump()` calls `all()`, which enumerates the whole source, so a dump inside a lazy pipeline over a large query loads everything at once. ## Query builders: the SQL, not the rows On a query builder the methods never run the query. They show what **would** run: 1. `->dump()` prints `toSql()` (for example `select * from "products" where "on_sale" = ? and "category_id" = ?`) and `getBindings()` (`[1, 7]`), then returns the builder so `->get()` can follow. 2. `->dd()` prints the same pair and stops. 3. `->dumpRawSql()` prints `toRawSql()`, the statement with each binding substituted and quoted by the connection's grammar: `... where "on_sale" = 1 and "category_id" = 7`. It returns the builder. 4. `->ddRawSql()` prints that raw SQL and stops. The raw form is what you paste into a database client to reproduce a wrong total. The placeholder form is closer to what the driver actually sends, and shows each binding's PHP type. `toRawSql()` itself returns a string, so it is the one to use in code that should keep running without output: ```php Log::debug('Discount query.', ['sql' => $query->toRawSql()]); ``` Treat raw SQL as a debugging aid only: substituting bindings for display is not how the query is executed, and the string should never be run back against the database with user input in it. ## The Eloquent detail worth knowing `Illuminate\Database\Eloquent\Builder` lists `dd`, `ddrawsql`, `dump` and `dumprawsql` in its **passthru** array. A passthru call runs `$this->toBase()->{$method}()`, and `toBase()` applies the model's global scopes first. Two consequences follow: - the dumped SQL includes global scope conditions, such as `"deleted_at" is null` from `SoftDeletes`, which is often exactly the clue a wrong total needs; - the method returns the **base query builder**, not the Eloquent builder, so `Product::where(...)->dump()->get()` gives plain `stdClass` rows rather than `Product` models. Dump in a separate statement, or use `dumpRawSql()` on a query variable, when you need models afterwards. ## More chainable dumps - `$request->dump('coupon', 'cart_id')` dumps only those input keys and returns the request. - `Http::dump()->post(...)` and `Http::dd()->post(...)` dump the outgoing request and its options just before sending; `dd()` ends the script without sending it. - `Benchmark::dd(fn () => $service->apply($cart), iterations: 10)` runs the closure, then dumps its average time in milliseconds and stops. ## Choosing the right call - Collection pipeline behaving oddly: `->dump()` between steps. - Query returns unexpected rows: `->dumpRawSql()` and run the SQL by hand. - Need the SQL in a log or test failure message: `toRawSql()`. - Need timing and every query of the request: a query log or Debugbar, not a dump.

  • Why does Product::where('on_sale', true)->dump()->get() return stdClass objects instead of Product models?
    The Eloquent builder passes `dump` through to its base query builder via `toBase()->dump()`, and `dump()` returns that base builder. The following `get()` therefore runs on the plain query builder, which returns `stdClass` rows. Call `->dump()` in its own statement, or keep the Eloquent builder in a variable and dump a clone.
  • The raw SQL from ddRawSql() shows "deleted_at" is null, which you never wrote. Where does it come from?
    Eloquent applies global scopes before forwarding the dump call, because passthru methods run on `toBase()`, which calls `applyScopes()`. A model using `SoftDeletes` adds `whereNull('deleted_at')` as a global scope, so the dump reflects the query that will really run.

saying these in an interview costs you the question

  • Calling ->dump() on a query builder executes the query and shows the rows.
  • ->dump() on a collection returns null, ending the chain.
  • toRawSql() prints the query and stops the script.
  • Eloquent dumps omit global scope conditions.
  • The raw SQL string is what the driver actually sends to the database.