How do I use the between() after find() [duplicate] - cakephp-3.0

Is it possible to do a "BETWEEN ? AND ?" where condition LIKE in cakephp 2.5?
In cakephp 2.5 I write something like
'conditions' => ['start_date BETWEEN ? AND ?' => ['2014-01-01', '2014-12-32']]
how can I migrate that?
additionally I would write something like
'conditions' => [ '? BETWEEN start_date AND end_date'] => '2014-03-31']

Expressions
Between expression are supported out of the box, however they only support the first case without additional fiddling:
$Query = $Table
->find()
->where(function($exp) {
return $exp->between('start_date', '2014-01-01', '2014-12-32', 'date');
});
If you'd wanted to handle the second case via the between method, then you'd have to pass all values as expressions, which can easily go wrong, as they will not be subject to escaping/parameter binding in that case, you'd have to do that on your own (which is anything but recommended! See the security notes in the manual for PDO::quote()), something along the lines of:
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\ORM\Query;
// ...
$Query = $Table
->find()
->where(function(QueryExpression $exp, Query $query) {
return $exp->between(
$query->newExpr(
$query->connection()->driver()->quote(
'2014-03-31',
\PDO::PARAM_STR
)
),
new IdentifierExpression('start_date'),
new IdentifierExpression('end_date')
);
});
That might feel a little inconvenient for such a basic SQL expression that is supported by all SQL dialects that CakePHP ships with, so you may have a reason here to use a raw SQL snippet with value bindig instead.
It should be noted however that expressions are often the better choice when it comes to for example cross dialect support, as they can be (more or less) easily transformed at compile time, see the implementations of SqlDialectTrait::_expressionTranslators(). Also expressions usually support automatic identifier quoting.
Value binding
Via manual value binding you can pretty much create anything you like. It should however be noted that whenever possible, you should use expressions instead, as they are easier to port, which happens out of the box for quite a few expressions already.
$Query = $Table
->find()
->where([
'start_date BETWEEN :start AND :end'
])
->bind(':start', '2014-01-01', 'date')
->bind(':end', '2014-12-31', 'date');
That way the second case can also be solved very easily, like:
$Query = $Table
->find()
->where([
':date BETWEEN start_date AND end_date'
])
->bind(':date', '2014-03-31', 'date');
A mixture of both (safest and most compatible approach)
It's also possible to mix both, ie use an expression that makes use of custom bindings, something along the lines of this:
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\ORM\Query;
// ...
$Query = $Table
->find()
->where(function(QueryExpression $exp, Query $query) {
return $exp->between(
$query->newExpr(':date'),
new IdentifierExpression('start_date'),
new IdentifierExpression('end_date')
);
})
->bind(':date', '2014-03-31', 'date');
That way you could handle the second case using possibly portable expressions, and don't have to worry about quoting/escaping input data and identifiers manually.
Regular comparison using array syntax
All that being said, in the end BETWEEN is just the same as using two separate simple conditions like this:
$Query = $Table
->find()
->where([
'start_date >=' => '2014-01-01',
'start_date <=' => '2014-12-32',
]);
$Query = $Table
->find()
->where([
'start_date >=' => '2014-03-31',
'end_date <=' => '2014-03-31',
]);
But don't be mad, if you read all the way down to here, at least you learned something about the ins and outs of the query builder.
See also
Cookbook > Database Access & ORM > Query Builder > Advanced Conditions
API > \Cake\Database\Query::bind()
Currently there seems to be only two options. The core now supports this out of the box, the following is just kept for reference.
Value binding (via the database query builder)
For now the ORM query builder (Cake\ORM\Query), the one that is being retrived when invoking for example find() on a table object, doesn't support value binding
https://github.com/cakephp/cakephp/issues/4926
So, for being able to use bindings you'd have to use the underlying database query builder (Cake\Database\Query), which can for example be retrived via Connection::newQuery().
Here's an example:
$conn = ConnectionManager::get('default');
$Query = $conn->newQuery();
$Query
->select('*')
->from('table_name')
->where([
'start_date BETWEEN :start AND :end'
])
->bind(':start', new \DateTime('2014-01-01'), 'date')
->bind(':end', new \DateTime('2014-12-31'), 'date');
debug($Query->execute()->fetchAll());
This would result in a query similar to this
SELECT
*
FROM
table_name
WHERE
start_date BETWEEN '2014-01-01' AND '2014-12-31'
A custom expression class
Another option would be a custom expression class that generates appropriate SQL snippets. Here's an example.
Column names should be wrapped into identifier expression objects in order to them be auto quoted (in case auto quoting is enabled), the key > value array syntax is for binding values, where the array key is the actual value, and the array value is the datatype.
Please note that it's not safe to directly pass user input for column names, as they are not being escaped! Use a whitelist or similar to make sure the column name is safe to use!
Field between values
use App\Database\Expression\BetweenComparison;
use Cake\Database\Expression\IdentifierExpression;
// ...
$between = new BetweenComparison(
new IdentifierExpression('created'),
['2014-01-01' => 'date'],
['2014-12-31' => 'date']
);
$TableName = TableRegistry::get('TableName');
$Query = $TableName
->find()
->where($between);
debug($Query->execute()->fetchAll());
This would generate a query similar to the one above.
Value between fields
use App\Database\Expression\BetweenComparison;
use Cake\Database\Expression\IdentifierExpression;
// ...
$between = new BetweenComparison(
['2014-03-31' => 'date'],
new IdentifierExpression('start_date'),
new IdentifierExpression('end_date')
);
$TableName = TableRegistry::get('TableName');
$Query = $TableName
->find()
->where($between);
debug($Query->execute()->fetchAll());
This on the other hand would result in a query similar to this
SELECT
*
FROM
table_name
WHERE
'2014-03-31' BETWEEN start_date AND end_date
The expression class
namespace App\Database\Expression;
use Cake\Database\ExpressionInterface;
use Cake\Database\ValueBinder;
class BetweenComparison implements ExpressionInterface {
protected $_field;
protected $_valueA;
protected $_valueB;
public function __construct($field, $valueA, $valueB) {
$this->_field = $field;
$this->_valueA = $valueA;
$this->_valueB = $valueB;
}
public function sql(ValueBinder $generator) {
$field = $this->_compilePart($this->_field, $generator);
$valueA = $this->_compilePart($this->_valueA, $generator);
$valueB = $this->_compilePart($this->_valueB, $generator);
return sprintf('%s BETWEEN %s AND %s', $field, $valueA, $valueB);
}
public function traverse(callable $callable) {
$this->_traversePart($this->_field, $callable);
$this->_traversePart($this->_valueA, $callable);
$this->_traversePart($this->_valueB, $callable);
}
protected function _bindValue($value, $generator, $type) {
$placeholder = $generator->placeholder('c');
$generator->bind($placeholder, $value, $type);
return $placeholder;
}
protected function _compilePart($value, $generator) {
if ($value instanceof ExpressionInterface) {
return $value->sql($generator);
} else if(is_array($value)) {
return $this->_bindValue(key($value), $generator, current($value));
}
return $value;
}
protected function _traversePart($value, callable $callable) {
if ($value instanceof ExpressionInterface) {
$callable($value);
$value->traverse($callable);
}
}
}

You can use one of following 2 methods.
Method 1 :
$start_date = '2014-01-01 00:00:00';
$end_date = '2014-12-31 23:59:59';
$query = $this->Table->find('all')
->where(function ($exp, $q) use($start_date,$end_date) {
return $exp->between('start_date', $start_date, $end_date);
});
$result = $query->toArray();
Method 2:
$start_date = '2014-01-01 00:00:00';
$end_date = '2014-12-31 23:59:59';
$query = $this->Table->find('all')
->where([
'start_date BETWEEN :start AND :end'
])
->bind(':start', new \DateTime($start_date), 'datetime')
->bind(':end', new \DateTime($end_date), 'datetime');
$result = $query->toArray();

I'm using it like this
$this->Table->find()->where(['data_inicio BETWEEN '.'\''.$data_inicio.'\''.' AND .'\''.$data_final.'\''.' ']);

Hello guys please use this query to get data on the basis of range of value
$query = $this->Leads->find('all',
array('conditions'=>array('postcode BETWEEN '.$postcodeFrom.' and'.$postcodeTo.''), 'recursive'=>-1));
debug($query);
print_r($query->toArray());

Related

How to conditionally select an aggregate function return value?

Currently I'm asking min and max values like this:
$query = $query
->where(['date >=' => $today])
->select([
'minvalue' => $this->Daten->find()->func()->min('brennstoff'),
'maxvalue' => $this->Daten->find()->func()->max('brennstoff')
])
->hydrate(false)
->toArray();
Sometimes the min or maxvalues could be NULL, so there will be no result; but I want to have 0 (zero) to be given out.
In SQL I'd use IF(MIN(value), MIN(value), 0)). But how to translate this in the ORM syntax?
IF is very MySQL specific, I'd suggest using a CASE expression instead, which is understood by all SQL dialects that are supported by CakePHP.
While the query builder can be used to create any type of SQL function call by simply calling a magic method with the same name via the functions builder, like:
$minValue = $query->func()->IF([
$query->newExpr()->isNotNull($query->func()->min('brennstoff')),
$query->func()->min('brennstoff'),
0
]);
$maxValue = $query->func()->IF([
$query->newExpr()->isNotNull($query->func()->max('brennstoff')),
$query->func()->max('brennstoff'),
0
]);
or IFNULL for something more compact:
$minValue = $query->func()->IFNULL([
$query->func()->min('brennstoff'),
0
]);
$maxValue = $query->func()->IFNULL([
$query->func()->max('brennstoff'),
0
]);
there are concrete helper methods for CASE expressions:
$minValue = $query
->newExpr()
->addCase(
[$query->newExpr()->isNotNull($query->func()->min('brennstoff'))],
[$query->func()->min('brennstoff'), 0],
[null, 'integer']
);
$maxValue = $query
->newExpr()
->addCase(
[$query->newExpr()->isNotNull($query->func()->max('brennstoff'))],
[$query->func()->max('brennstoff'), 0],
[null, 'integer']
);
$query = $query
->select([
'minvalue' => $minValue,
'maxvalue' => $maxValue
])
// ...
See also
Cookbook > Database Access & ORM > Query Builder > Case statements
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
MySQL Reference Manual / Functions and Operators / Control Flow Functions > CASE

MySQL NOW() in Doctrine DBAL insert?

I'm staring at http://doctrine-dbal.readthedocs.org/en/latest/reference/data-retrieval-and-manipulation.html#insert
I'd love to be able to do something like
$db->insert('mytable', [
'foo' => 'bar',
'created_on' => new MagicThatMakesNowWork()
]);
Is this impossible? The best solution I've seen is using PHP to get the datetime while setting the timezone, which is less than ideal. For some reason it seems only the ORM or query builder can handle expressions. I know in Zend I can do something like new Zend_Db_Expr('NOW()') and it knows by the object type not to quote NOW() in the built query. No query builder or ORM required.
Not sure if it's not possible or not documented well. The second answer on Doctrine DBAL: Updating timestamp field with 'NOW()' value shows a random string of datetime in the types array which seems weird/bad as well.
it seems you're trying to accomplish what the TIMESTAMP column type does automatically. That's been around for way longer than 2014. Use it, and stop trying to manage your created_on and updated_on fields manually. https://dev.mysql.com/doc/refman/5.1/en/datetime.html
A late answer but the easiest way I've found to do this is to run a query first to get the value of NOW() and then insert that string.
An example:
$now = $db->fetchColumn("SELECT NOW()");
$db->insert('table', array(
'field1' => $field1
,'created' => $now
));
A slightly less desirable alternative would be if you are able to make sure you PHP/MySQL times are in sync, you could always create the DateTime object in PHP and format it as a string ready for MySQL:
$created = new \Datetime('now', new \DateTimeZone('Europe/Paris'));
$created->format('Y-m-d H:i:s')
insert isn't much of an option. So the answer at Expression mysql NOW() in Doctrine QueryBuilder shows manually knowing what's coming for that variable.
This requires DBAL >= 2.5 which was recently released. You'd be out of luck in 2014. Note that datetime is a valid string, but doesn't appear to have a constant http://php.net/manual/en/pdo.constants.php.
public function insert($params) {
if (!isset($params[static::CREATED_ON])) {
$params[] = [static::CREATED_ON, new \Datetime('now'), 'datetime'];
}
if (!isset($params[static::MODIFIED_ON])) {
$params[] = [static::MODIFIED_ON, new \Datetime('now'), 'datetime'];
}
$conn = $this->_dbs['write'];
$query_builder = $conn->createQueryBuilder();
$query_builder->insert(static::TABLE);
foreach ( $params as $location => $data ) {
$field = $data[0];
$value = $data[1];
$type = (isset($data[2])) ? $data[2] : \PDO::PARAM_STR;
$prep = (isset($data[3])) ? $data[3] : '?';
$query_builder
->setValue($field, $prep)
->setParameter($location, $value, $type);
}
$return = $query_builder->execute();
if (!$return) {
throw new \Exception("Failed to execute statement");
}
return $conn->lastInsertId();
}
public function update($params, $where) {
$conn = $this->_dbs['write'];
$query_builder = $conn->createQueryBuilder();
$query_builder->update(static::TABLE);
foreach ( $params as $location => $data ) {
// can do arbitrary expressions via ['field', null, null, 'ANY EXPRESSION YOU WANT']
// ie ['votes', null, null,'votes=votes+25']
// or ['votes', '1', \PDO::PARAM_INT, 'votes + ?']
$field = $data[0];
$value = $data[1];
$type = (isset($data[2])) ? $data[2] : \PDO::PARAM_STR;
$prep = (isset($data[3])) ? $data[3] : '?';
$query_builder
->set($field, $prep)
->setParameter($location, $value, $type);
}
$query_builder->where($where);
return $query_builder->execute();
}

Laravel 4.x, Eloquent multiple conditions

I've tried to this:
Product::where(['product_id' => $product->id, 'catalog_id' => $key])->first();
This isn't working at all. When I'm doing this:
Product:where('product_id', $product->id)->where('catalog_id', $key)->first();
It just works fine. I've searched in the documentation of Laravel,
and found nothing.
Is there any option to using the where function with an array in it ?
You need to use where() individually. If you want to dynamically building the query you can do something like:
$wheres = array('product_id' => $product->id, 'catalog_id' => $key);
$q = new Product;
foreach ( $wheres as $k => $v ) {
$q = $q->where($k, $v);
}
$products = $q->first();
In fact we were all wrong ;)
As of latest version of the framework you can do exactly what you wanted.
Check this commit and update Laravel if you need that feature.
https://github.com/laravel/framework/commit/87b267a232983abdac7c23c2dc6b1b270dd24b8a
Product::whereNested(function($query) use ($key, $product){
$query->where('product_id', $product->id);
$query->where('catalog_id', $key);
})->get();
Laravel's wheres use an and condition by default:
$products = Product::where('this','=','that')->where('something','=','hello')->get();
is somewhat equivalent to:
SELECT * FROM products WHERE this = 'that' AND something = 'hello';
You simply chain the ->where() methods together. No need for an array.
If you want to use an or condition:
$products = Product::where('this','=','that')->orWhere('something','=','hello')->get();

Zend Framework - join query

I build a function
public function getBannedByLogin($commentId)
{
$sql = $this->getDbAdapter()->select()
->from(array('comments' => 'comments'), array())
->join(array('users' => 'qengine_users'),
'comments.bannedBy = users.userId',
array())
->where('commentId = ?', $commentId)
;
$row = $this->fetchRow($sql);
return $row['login'];
}
And there are problems, that does'nt work! :D
Let's I explain you. Column 'bannedBy' from comments returns id of user, who give a ban. I need to join this with table users to load a login field. Where i have mistakes?
I assume the code works in the sense of not throwing an exception. If so, your code is OK, you just specifically tell Zend_Db not to select any columns.
public function getBannedByLogin($commentId)
{
$sql = $this->getDbAdapter()->select()
->from(array('comments' => 'comments'))
->join(array('users' => 'qengine_users'),
'comments.bannedBy = users.userId')
->where('commentId = ?', $commentId)
;
$row = $this->fetchRow($sql);
return $row['login'];
}
The last argument to from() and join() functions is an array of columns you wish to select. If you pass in an empty array, no columns are selected. No argument = select everything. You can, of course, specify only the columns you need too.

CakePHP: How can I use a "HAVING" operation when building queries with find method?

I'm trying to use the "HAVING" clause in a SQL query using the CakePHP paginate() method.
After some searching around it looks like this can't be achieved through Cake's paginate()/find() methods.
The code I have looks something like this:
$this->paginate = array(
'fields' => $fields,
'conditions' => $conditions,
'recursive' => 1,
'limit' => 10,
'order' => $order,
'group' => 'Venue.id');
One of the $fields is an alias "distance". I want to add a query for when distance < 25 (e.g. HAVING distance < 25).
I have seen two workarounds so far, unfortunately neither suit my needs. The two I've seen are:
1) Adding the HAVING clause in the "group" option. e.g. 'group' => 'Venue.id HAVING distance < 25'. This doesn't seem to work when used in conjunction with pagination as it messes up the initial count query that is performed. (ie tries to SELECT distinct(Venue.id HAVING distance < 25) which is obviously invalid syntax.
2) Adding the HAVING clause after the WHERE condition (e.g. WHERE 1 = 1 HAVING field > 25) This doesn't work as it seems the HAVING clause must come after the group statement which Cake is placing after the WHERE condition in the query it generates.
Does anyone know of a way to do this with CakePHP's find() method? I don't want to use query() as that would involve a lot of rework and also mean I'd need to implement my own pagination logic!
Thanks in advance
You have to put it with the group conditions. like this
$this->find('all', array(
'conditions' => array(
'Post.length >=' => 100
),
'fields' => array(
'Author.id', 'COUNT(*) as Total'
),
'group' => array(
'Total HAVING Total > 10'
)
));
Hope it helps you
I used the following trick to add my own HAVING clause at the end of my WHERE clause. The "dbo->expression()" method is mentioned in the cake sub-query documentation.
function addHaving(array $existingConditions, $havingClause) {
$model = 'User';
$db = $this->$model->getDataSource();
// Two fun things at play here,
// 1 - mysql doesn't allow you to use aliases in WHERE clause
// 2 - Cake doesn't allow a HAVING clause separate from a GROUP BY
// This expression should go last in the WHERE clause (following the last AND)
$taut = count($existingConditions) > 0 ? '1 = 1' : '';
$having = $db->expression("$taut HAVING $havingClause");
$existingConditions[] = $having;
return $existingConditions;
}
As per the manual, CakePHP/2 supports having at last. It was added as find array parameter on version 2.10.0, released on 22nd July 2017.
From the 2.10 Migration Guide:
Model::find() now supports having and lock options that enable you to
add HAVING and FOR UPDATE locking clauses to your find operations.
Just had the same problem. I know, one is not supposed to modify the internal code but if you open the PaginatorComponent and you modify line 188:
$count = $object->find('count', array_merge($parameters, $extra));
to this:
$count = $object->find(
'count',
array_merge(array("fields" => $fields),$parameters, $extra)
);
Everything will be fixed. You will be able to add your HAVING clause to the 'group' and the COUNT(*) won't be a problem.
Or, make line:
$count = $object->paginateCount($conditions, $recursive, $extra);
to include the $fields:
$count = $object->paginateCount($fields,$conditions, $recursive, $extra);
After that, you can "override" the method on the Model and make sure to include the $fields in the find() and that's it!, =P
Here is another idea that doesn't solve the pagination issue, but it is clean since it just overrides the find command in AppModel. Just add a group and having element to your query and this will convert to a HAVING clause.
public function find($type = 'first', $query = array()) {
if (!empty($query['having']) && is_array($query['having']) && !empty($query['group'])) {
if ($type == 'all') {
if (!is_array($query['group'])) {
$query['group'] = array($query['group']);
}
$ds = $this->getDataSource();
$having = $ds->conditions($query['having'], true, false);
$query['group'][count($query['group']) - 1] .= " HAVING $having";
CakeLog::write('debug', 'Model->find: out query=' . print_r($query, true));
} else {
unset($query['having']);
}
}
return parent::find($type, $query);
}
Found it here
https://groups.google.com/forum/?fromgroups=#!topic/tickets-cakephp/EYFxihwb55I
Using 'having' in find did not work for me. Instead I put into one string with the group
" group => product_id, color_id having sum(quantity) > 2000 " and works like a charm.
Using CakePHP 2.9