Introduction: When Query Builder is Silent, But You Need to See the SQL
\n\nHave you ever written a complex query through Query Builder, run it — and got an error or wrong data? You itch to see what your method chain actually generates, but the framework stubbornly returns an object, not a string. Sound familiar?
\n\nThis question is one of the most popular on Stack Overflow (tens of thousands of views). And for good reason. The ability to "extract" raw SQL from Query Builder is a superpower that saves hours of debugging. Especially when you're working with ORMs like Doctrine or Eloquent in Laravel, where queries are built through method chains and the actual SQL is hidden inside.
\n\nIn this article, we'll cover how to get a raw SQL string from Query Builder in different PHP frameworks, show code examples, and explain why this is important for every developer. Let's go!
\n\n1. Why Might You Need Raw SQL?
\n\nBefore diving into the code, let's understand why you'd want to look at "raw" SQL when Query Builder does everything for you.
\n\n- \n
- Debugging errors. When a query doesn't work, you only see an object. But SQL is the truth. You'll see which tables are joined, which WHERE conditions are applied, and where the syntax error is. \n
- Performance optimization. Query Builder can generate an inefficient query. By looking at it, you'll notice unnecessary JOINs or missing indexes. \n
- Learning. If you're a beginner, viewing the generated SQL helps you understand how the ORM translates your PHP methods into real SQL commands. \n
- Export and logging. Sometimes you need to log a query or send it to another service. Without a string, this is impossible. \n
Now let's move on to the most interesting part — the code.
\n\n2. Laravel Eloquent: toSql() and getBindings()
\n\nLaravel is the most popular PHP framework, and its Query Builder (part of Eloquent) provides built-in methods for getting SQL. Here's how it works.
\n\nSimple Example: toSql()
\n\nuse Illuminate\\Support\\Facades\\DB;\n\n$query = DB::table('users')\n ->where('age', '>', 18)\n ->where('status', 'active')\n ->orderBy('name', 'asc');\n\n$sql = $query->toSql();\n\necho $sql;\n// Output: select * from `users` where `age` > ? and `status` = ? order by `name` asc\n\nGreat! But did you notice the question marks? These are placeholders for prepared statements. To see the actual values, you need to use getBindings().
Full SQL with Value Substitution
\n\n$bindings = $query->getBindings();\n\n$fullSql = vsprintf(str_replace('?', '%s', $sql), array_map(function ($value) {\n return is_numeric($value) ? $value : "'" . $value . "'";\n}, $bindings));\n\necho $fullSql;\n// Output: select * from `users` where `age` > 18 and `status` = 'active' order by `name` asc\n\nThis trick replaces placeholders with actual values. But be careful: do not use this method to execute the query — it does not escape data, it's for debugging only.
\n\nThe dd() Method for Quick Debugging
\n\nLaravel has a convenient dd() method (dump and die) that outputs SQL and bindings:
DB::table('users')->where('age', '>', 18)->dd();\n// Stops execution and shows:\n// sql: select * from `users` where `age` > ?\n// bindings: [18]\n\nTip: Use dd() only in a development environment. In production, it will kill the query.
3. Doctrine ORM: getSQL() and Parameters
\n\nDoctrine is a powerful ORM used in Symfony and other projects. Its Query Builder is slightly different, but the principle is the same.
\n\nBasic Example
\n\nuse Doctrine\\ORM\\EntityManagerInterface;\n\n// Assume $entityManager is already obtained via DI\n$queryBuilder = $entityManager->createQueryBuil