«Doctor, mi Query Builder está generando algo extraño…»
\n¿Situación familiar? Escribes una elegante consulta encadenada con Query Builder, estás seguro de que todo funciona perfectamente, pero el resultado es un error o datos incorrectos. Lo primero que se le viene a la mente a cualquier desarrollador PHP: «¡Muéstrame lo que realmente estás generando!».
\nLa pregunta «How do I get the query builder to output its raw SQL query as a string?» es una de las más populares en Stack Overflow (miles de visitas y decenas de respuestas). Y no es casualidad. Saber cómo obtener SQL limpio rápidamente es un superpoder que ahorra horas de depuración.
\nEn este artículo analizaremos 3 métodos prácticos para frameworks PHP (Laravel, Doctrine, CakePHP), mostraremos ejemplos de código reales y explicaremos cuándo usar cada método. ¡Allá vamos!
\n\n¿Por qué es necesario? (Breve y al grano)
\n- \n
- Depuración — entender por qué la consulta devuelve datos incorrectos. \n
- Optimización — ver qué JOINs y WHEREs se ejecutan realmente. \n
- Aprendizaje — los principiantes a menudo no entienden cómo Query Builder traduce los métodos a SQL. \n
- Registro — guardar la consulta cruda para auditoría o reproducción. \n
¿Listo? ¡Entonces pasemos al código!
\n\nMétodo 1: Laravel Eloquent / Query Builder — el clásico
\nEn Laravel hay varias formas, pero la más fiable es usar toSql() junto con getBindings(). ¿Por qué juntos? Porque toSql() muestra la consulta con marcadores de posición ?, y sin sustituir los valores es inútil.
Ejemplo simple: SELECT con WHERE
\nuse App\\Models\\User;\n\n$query = User::where('email', 'like', '%@gmail.com')\n ->where('active', 1)\n ->orderBy('created_at', 'desc');\n\n// Obtenemos SQL con marcadores de posición\n$sql = $query->toSql();\n$bindings = $query->getBindings();\n\n// Unimos todo en un formato legible\n$fullSql = vsprintf(str_replace('?', '%s', $sql), $bindings);\n\necho $fullSql;\n// Resultado:\n// SELECT * FROM users WHERE email LIKE '%@gmail.com' AND active = 1 ORDER BY created_at DESC\nImportante: vsprintf no es una solución universal. Para tipos de datos complejos (por ejemplo, NULL o arrays) es mejor usar Str::replaceArray() o el paquete laravel-query-builder-debug.
Variante avanzada: con join y whereIn
\n$query = DB::table('orders')\n ->join('users', 'orders.user_id', '=', 'users.id')\n ->whereIn('orders.status', ['pending', 'processing'])\n ->where('orders.total', '>', 100)\n ->select('orders.*', 'users.name');\n\n$bindings = $query->getBindings();\n$sql = $query->toSql();\n\n// Formateamos de manera elegante\n$fullSql = preg_replace_array('/\\?/', $bindings, $sql);\necho $fullSql;\n// SELECT orders.*, users.name FROM orders INNER JOIN users ON orders.user_id = users.id WHERE orders.status IN ('pending', 'processing') AND orders.total > 100\nConsejo: Use preg_replace_array() — es más seguro que vsprintf, especialmente cuando los datos contienen el carácter %.
Método 2: Doctrine DBAL — para los que aman el rigor
\nDoctrine Query Builder (usado a menudo en Symfony) es una bestia más compleja. Aquí no hay un toSql() listo, pero existe getSQL() y getParameters(). Además, hay que tener en cuenta los tipos de parámetros.
Ejemplo básico
\nuse Doctrine\\DBAL\\Connection;\nuse Doctrine\\DBAL\\Query\\QueryBuilder;\n\n/** @var Connection $connection */\n$queryBuilder = $connection->createQueryBuilder();\n$queryBuilder\n ->select('p.*', 'c.name as category_name')\n ->from('products', 'p')\n ->join('p', 'categories', 'c', 'p.category_id = c.id')\n ->where('p.price > :minPrice')\n ->andWhere('p.sto