Doctrine2 + Symfony2: Cast values before bind to DQL query - php

I'm building a function for filter some records based on four parameters: $codigo, $anno, $term and $comite_tecnico. This is what I build until now:
public function filtrarNorma($codigo = null, $anno = null, $term = null, $comite_tecnico = null)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('n')
->from("AppBundle:Norma", "n");
if ($codigo != NULL) {
$qb->where($qb->expr()->like('n.numero', ':codigo'));
$qb->setParameter('codigo', '%' . $codigo . '%');
}
if ($anno != NULL) {
$qb->orWhere($qb->expr()->like('n.anno', ':anno'));
$qb->setParameter('anno', '%' . $anno . '%');
}
if ($term != NULL) {
$qb->orWhere($qb->expr()->like('n.nombre', ':term'));
$qb->setParameter('term', '%' . $term. '%');
}
if ($comite_tecnico != NULL) {
$qb->orWhere($qb->expr()->like('n.comite_tecnico', ':comite_tecnico'));
$qb->setParameter('comite_tecnico', '%' . $comite_tecnico . '%');
}
return $qb->getQuery()->getResult();
}
Any time I try to perform a query I get this error:
An exception occurred while executing 'SELECT n0_.numero AS numero0,
n0_.anno AS anno1, n0_.id AS id2, n0_.nombre AS nombre3, n0_.activo AS
activo4, n0_.comite_tecnico_id AS comite_tecnico_id5 FROM
nomencladores.norma n0_ WHERE n0_.numero LIKE ? OR n0_.anno LIKE ?'
with params ["34", 45]:
SQLSTATE[42883]: Undefined function: 7 ERROR: operator does not exist:
integer ~~ unknown LINE 1: ...dores.norma n0_ WHERE n0_.numero LIKE $1
OR n0_.anno LIKE $2 ^ HINT: No operator matches the given name and
argument type(s). You might need to add explicit type casts.
That's telling me that I need to cast some of those parameters before send it to the PgSQL DB and execute the query to get results but my question is, how I do that on Doctrine2 DQL? It's possible? Any workaround or trick or something else? I've found this documentation but don't know which function apply and also how, can any give me some help or advice around this?
Edit with new tests
After users suggestions I made some changes to my code and now it looks like:
public function filtrarNorma($codigo = null, $anno = null, $term = null, $comite_tecnico = null)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('n')
->from("AppBundle:Norma", "n");
if ($codigo != NULL) {
$qb->where($qb->expr()->like('n.numero', ':codigo'));
$qb->setParameter('codigo', '%'.$codigo.'%', PDO::PARAM_STR);
}
if ($anno != NULL) {
$qb->orWhere($qb->expr()->like('n.anno', ':anno'));
$qb->setParameter('anno', $anno, PDO::PARAM_INT);
}
if ($term != NULL) {
$qb->orWhere($qb->expr()->like('n.nombre', ':term'));
$qb->setParameter('term', '%'.$term.'%', PDO::PARAM_STR);
}
if ($comite_tecnico != NULL) {
$qb->orWhere($qb->expr()->like('IDENTITY(n.comite_tecnico)', ':comite_tecnico'));
$qb->setParameter('comite_tecnico', '%'.$comite_tecnico.'%', PDO::PARAM_INT);
}
return $qb->getQuery()->getResult();
}
But once again, get the same error:
An exception occurred while executing 'SELECT n0_.numero AS numero0,
n0_.anno AS anno1, n0_.id AS id2, n0_.nombre AS nombre3, n0_.activo AS
activo4, n0_.comite_tecnico_id AS comite_tecnico_id5 FROM
nomencladores.norma n0_ WHERE n0_.numero LIKE ? OR n0_.anno LIKE ?'
with params ["%4%", "4"]:
SQLSTATE[42883]: Undefined function: 7 ERROR: operator does not exist:
integer ~~ unknown LINE 1: ...dores.norma n0_ WHERE n0_.numero LIKE $1
OR n0_.anno LIKE $2 ^ HINT: No operator matches the given name and
argument type(s). You might need to add explicit type casts.
And as you may notice in this case params are passed as should be: ["%4%", "4"] but why the error? Still not getting where it's
Another test
So, getting ride of Doctrine Query Builder and applying some Doctrine Query Language I moved the query from the code above to this one:
$em = $this->getEntityManager();
$query = $em->createQuery("SELECT n from AppBundle:Norma n WHERE n.numero LIKE '%:codigo%' OR n.anno LIKE '%:anno%' OR n.nombre LIKE '%:term%' OR IDENTITY(n.comite_tecnico) LIKE '%:comite_tecnico%'");
$query->setParameters(array(
'codigo' => $codigo,
'anno' => $anno,
'term' => $term,
'comite_tecnico' => $comite_tecnico
));
return $query->getResult();
But in this case I get this message:
Invalid parameter number: number of bound variables does not match
number of tokens
If the query is made by OR should be the four parameters required?

Your first try actually works for me all the time. You can convert your integers using strval()'.
'%' . strval($anno) . '%';

After a deep research I've found the solution to my problem and want to share with others too. I should said also thanks to #ErwinBrandstetter, #b.b3rn4rd for their time and support and to #Pradeep which finally give me the idea for research and finally get problem fixed and I did by enabling implicit casting support in PostgreSQL.
For enable implicit casts you must therefore execute the following commands in your PostgreSQL console when connected to the template1 database, so that any database created afterward will come with the required CASTs (if your database is already created, execute the commands in your database as well):
CREATE FUNCTION pg_catalog.text(integer) RETURNS text STRICT IMMUTABLE LANGUAGE SQL AS 'SELECT textin(int4out($1));';
CREATE CAST (integer AS text) WITH FUNCTION pg_catalog.text(integer) AS IMPLICIT;
COMMENT ON FUNCTION pg_catalog.text(integer) IS 'convert integer to text';
CREATE FUNCTION pg_catalog.text(bigint) RETURNS text STRICT IMMUTABLE LANGUAGE SQL AS 'SELECT textin(int8out($1));';
CREATE CAST (bigint AS text) WITH FUNCTION pg_catalog.text(bigint) AS IMPLICIT;
COMMENT ON FUNCTION pg_catalog.text(bigint) IS 'convert bigint to text';
That's all, after running that on the current DB I'm using and also on template1 for future ones and keeping conditions on my code as follow, all works fine and without any errors:
if ($codigo != null) {
$qb->where($qb->expr()->like('n.numero', ':codigo'));
$qb->setParameter('codigo', '%'.$codigo.'%', PDO::PARAM_STR);
}
if ($anno != null) {
$qb->orWhere($qb->expr()->like('n.anno', ':anno'));
$qb->setParameter('anno', '%'.$anno.'%', PDO::PARAM_STR);
}
if ($term != null) {
$qb->orWhere($qb->expr()->like('n.nombre', ':term'));
$qb->setParameter('term', '%'.$term.'%', PDO::PARAM_STR);
}
if ($comite_tecnico != null) {
$qb->orWhere($qb->expr()->like('IDENTITY(n.comite_tecnico)', ':comite_tecnico'));
$qb->setParameter('comite_tecnico', '%'.$comite_tecnico.'%', PDO::PARAM_STR);
}
Happy coding!!

I think in your last try the raw SQL string should look like this:
$query = $em->createQuery("SELECT n.*
FROM nomencladores.norma n
WHERE n.numero LIKE '%' || :codigo || '%' OR
cast(n.anno AS text) LIKE '%' || :anno || '%' OR
n.nombre LIKE '%' || :term || '%' OR
IDENTITY(n.comite_tecnico) LIKE '%' || :comite_tecnico || '%'");
Any other column here not text or varchar? Cast it, too.
Don't know the IDENTITY() function. A spillover from Doctrine, as well?
Still, I don't know much about Doctrine.

You're trying to use LIKE on an integer, which doesn't make sense.
Cast the integer to its text representation. This might work:
$qb->where($qb->expr()->like('CAST(n.numero AS text)', ':codigo'));

Related

Semantical error when using QueryBuilder with an expression

I have a problem with getting data from the database using the expr() method function. I would like to get data where isPublic = true and objectType = $objectType OR user = $user and objectType = $objectType, no matter what the value of isPublic is.
I'm getting this error:
[Semantical Error] line 0, col 76 near 'user-avatar)': Error: 'user' is not defined.
My code in repository:
public function findByObjectType($objectType, $user)
{
$qb = $this->createQueryBuilder('s');
return $qb->where($qb->expr()->andX(
$qb->expr()->eq('s.isPublic', true),
$qb->expr()->eq('s.objectType', $objectType)
))
->orWhere($qb->expr()->andX(
$qb->expr()->eq('s.user', $user->getId()),
$qb->expr()->eq('s.objectType', $objectType)
))
->getQuery()
->getResult();
}
where: $objectType = 'user-avatar'; $user = UserInterface
expr()->eq() will treat the expression as literals, trying to use them literally as they appear on method call.
As mentioned by the library author:
You are not using parameter binding. Expressions use string concatenation internally, so this outcome is actually expected.
In your case, you should be doing something like::
return $qb->where($qb->expr()->andX(
$qb->expr()->eq('s.isPublic', ':true'),
$qb->expr()->eq('s.objectType', ':objectType')
))
->orWhere($qb->expr()->andX(
$qb->expr()->eq('s.user', ':userId'),
$qb->expr()->eq('s.objectType', ':objectType')
))
->setParameter('true', true)
->setParameter('userId', $user->getId())
->setParameter('objectType', $objectType)
->getQuery()
->getResult();
This way your code is easier to read, safer and more portable.
When using $qb->expr()->eq() you will need supply the exact value for the query. In this case you need to change your query to something like this:
$qb->expr()->eq('s.objectType', '"' . $objectType .'"')
This way to string will be correctly quoted in the db query. The same goes for booleans by the way. Casting true to a string will result in 1. That's why you didn't encounter an error in this case. false however gets cast to an empty string, which would result in an error.
To better understand what's going on, here is the part of code that converts the eq() expression to the query:
/**
* #return string
*/
public function __toString()
{
return $this->leftExpr . ' ' . $this->operator . ' ' . $this->rightExpr;
}

Replace Like in SQL for ENUM

Currently I am having success with the following query:
if (isset($data->action_needed_status) && $data->action_needed_status != '') {
$query->where('status', 'like', '%'.$data->action_needed_status.'%');
$query_count->where('status', 'like', '%'.$data->action_needed_status.'%');
The complaint was this:
Since status is an ENUM field you know that any valid status value
passed should match exactly. There shouldn't be a need for using LIKE
in the where statements.
What would be the best way to have same result without using LIKE?

Doctrine2 query builder does not quote the string

Below is the code excerpt I have
$column_name = "ipAddress";
$qb = EntityManagerContainer::get()->createQueryBuilder();
$qb->select('u')
->from(BlacklistedIps::class, 'u');
if($search_term)
{
$clause = $qb->expr()->like("u.".$column_name, "'%$search_term%'");
$qb->where($clause);
}
$query = $qb->getQuery();
$result = $query->getResult();
It works absolutely fine (although it's open to SQL injection but that's another story).
My problem with this is the need to have "'%$search_term%'". Without this extra set of single quotes the query fails
Uncaught exception 'Doctrine\ORM\Query\QueryException' with message
'SELECT u FROM Orm\Entity\BlacklistedIps u WHERE u.ipAddress LIKE
%123% ORDER BY u.reason desc' in ***
I am not entirely sure I am doing it the right way. Because if I do, then there is a bug (mssing feature?) in Doctrine2. When I do
$qb->expr()->like("u.".$column_name, "%$search_term%");
then I am ABSOLUTELY sure that I am dealing with a string. When integers or booleans or floats, etc are compared to each other different operators are used, but definitely not LIKE. LIKE is used ONLY when dealing with strings, so quoting the string in DQL is exactly the only possible ->like method use case.
Please tell me I am doing something wrong. I've been using Doctrine2 for couple of days only and feel fascinated by it. But don't like strings not being quoted automatically for me.
it looks like a problem of how you use querybuilder. You should do something like that :
$qb ->where($qb->expr()->orX($qb->expr()->like('u.'.$column_name, $qb->expr()->literal("%$searchTerm%"))))
or
$qb->where($qb->expr()->like("u.".$column_name, array("%$searchTerm%")));
also to avoid sql injection, a good practice is to not pass user input in any querybuilder methods, use setParameter with ? or : instead.
$qb->where('u.'.$column_name.' LIKE :searchTerm')
$qb->setParameter('searchTerm', '%'.$searchTerm.'%')
or something like :
$qb->expr()->like('u.'.$column_name, '?1')
$qb->getQuery()->setParameter(1, '%' . $searchTerm . '%');
Notice the following:
how I've broken the query up for increased security of your database.
my use of "andWhere" throughout the query being built
how I've assigned the value of the executed query to $result
public function findPending($id)
{
$qb = $this->createQueryBuilder('o')
->addSelect('s')
->leftJoin('MyApp\\Model\\Entity\\Shipment', 's')
->orderBy('o.date_sent', 'DESC');
// Order has been sent and was not cancelled
$qb
->andWhere($qb->expr()->andX(
$qb->expr()->eq('o.date_cancelled','0000-00-00 00:00:00'),
$qb->expr()->neq('o.date_sent','0000-00-00 00:00:00')
));
$qb
->andWhere($qb->expr()->orX(
// Order doesn't have a shipment
$qb->expr()->isNull('s.order'),
// OR Order has a shipment
$qb->expr()->orX(
// Shipment has not been sent
$qb->expr()->eq('s.date_sent','0000-00-00 00:00:00'),
// OR Shipment has been sent AND it was cancelled
$qb->expr()->andX(
$qb->expr()->neq('s.date_sent','0000-00-00 00:00:00'),
$qb->expr()->eq('s.date_cancelled','0000-00-00 00:00:00')
)
)
));
$qb
->setMaxResults(6);
$result = $qb->getQuery()
->getResult();
return $result;
}
To see the query you've created add this before "$result"
$qb->getQuery():

How to assemble a query with various optional parameters in Laravel?

I have a research to do in a database. Not always I'll be using all of parameters. The user may want to research for a name, but not address or the other way around.
I've tried to use advanced wheres and even unions, but none seems to work. All of them give me a SQL error "General error: 1221 Incorrect usage of UNION and ORDER BY".
Here's a piece of code I've tried
$city_name = ($city_name != null) ? DB::table('cities')->where('name', 'LIKE', "%$city_name%") : DB::table('cities');
$state = ($state_id != '--') ? DB::table('cities')->where('state_id', '=', $state_id) : DB::table('cities');
$cities = DB::table('cities')->union($city_name)->union($state)->orderBy('name')->get();
But it gives me the above described error.
What I really want to do is to select, dinamically, what parameters I put in the query and even assemble it "on the fly". Does anyone knows how to do that?
If I couldn't make myself clear, please let me know in the comments...
I think you need somethink like this:
$query = DB::table('cities');
if ($city_name != null)
{
$query->where('name', 'LIKE', "%$city_name%");
}
if ($state_id != '--')
{
$query->where('state_id', '=', $state_id);
}
$cities = $query->orderBy('name')->get();

Dealing with null values in Laravel database query?

I am finding that I often need to select a field, based on a condition other than the id.
So, $user = User::where('last_login', $lastLogin)->where('last_warning', $lastWarning)->get(); works perfectly.
That is until you set one of the where's to allow nulls (let's do last_login).
That is, it can either have a value or be null.
That means you need to use one of two function where() or whereNull() and to do that you need to break the chain, so it becomes
$user = User::where('last_warning', $lastWarning);
is_null($lastLogin) ? $user->whereNull('last_login') : $user->where('last_login', $lastLogin);
$user = $user->get();
I am wondering if where has a way to deal with this? as currently if you pass null through to where you get where column = null which doesn't work!
Two options:
Option 1:
if (is_null($lastLogin))
{
$user = User::whereNull('last_login')->where('last_warning', $lastWarning)->get();
}
else
{
$user = User::where('last_login', $lastLogin)->where('last_warning', $lastWarning)->get();
}
Option 2:
$user = User::where('last_login', (is_null($lastLogin) ? 'IS' : '=') ,$lastLogin)->where('last_warning', $lastWarning)->get();
Option two makes the query 'where last_login = x' or 'where last_login IS null'
You can try this:
User::where(function($query) use ($lastlogin)
{
if(is_null($lastLogin))
{
$query->whereNull('last_login');
}
else
{
$query->where('last_login', '=', $lastLogin);
}
})->get();
It is a good solution when dealing with long queries with more than one parameter.
You can use DB::raw() as well:
User::where('last_login', 'IS', DB::raw('null'))->where_last_warning($lastWarning)->get();
As of Laravel 5.3 you are now able to ->where('column', null) to automatically produce WHERE column IS NULL.
If using variable, make sure that they have PHP null strict value.

Categories