325 lines
9.2 KiB
PHP
325 lines
9.2 KiB
PHP
|
<?php
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
/**
|
||
|
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||
|
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||
|
*
|
||
|
* Licensed under The MIT License
|
||
|
* For full copyright and license information, please see the LICENSE.txt
|
||
|
* Redistributions of files must retain the above copyright notice.
|
||
|
*
|
||
|
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||
|
* @link https://cakephp.org CakePHP(tm) Project
|
||
|
* @since 3.0.0
|
||
|
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||
|
*/
|
||
|
namespace Cake\Database\Expression;
|
||
|
|
||
|
use Cake\Database\Exception\DatabaseException;
|
||
|
use Cake\Database\ExpressionInterface;
|
||
|
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
||
|
use Cake\Database\ValueBinder;
|
||
|
use Closure;
|
||
|
|
||
|
/**
|
||
|
* A Comparison is a type of query expression that represents an operation
|
||
|
* involving a field an operator and a value. In its most common form the
|
||
|
* string representation of a comparison is `field = value`
|
||
|
*/
|
||
|
class ComparisonExpression implements ExpressionInterface, FieldInterface
|
||
|
{
|
||
|
use ExpressionTypeCasterTrait;
|
||
|
use FieldTrait;
|
||
|
|
||
|
/**
|
||
|
* The value to be used in the right hand side of the operation
|
||
|
*
|
||
|
* @var mixed
|
||
|
*/
|
||
|
protected $_value;
|
||
|
|
||
|
/**
|
||
|
* The type to be used for casting the value to a database representation
|
||
|
*
|
||
|
* @var string|null
|
||
|
*/
|
||
|
protected $_type;
|
||
|
|
||
|
/**
|
||
|
* The operator used for comparing field and value
|
||
|
*
|
||
|
* @var string
|
||
|
*/
|
||
|
protected $_operator = '=';
|
||
|
|
||
|
/**
|
||
|
* Whether the value in this expression is a traversable
|
||
|
*
|
||
|
* @var bool
|
||
|
*/
|
||
|
protected $_isMultiple = false;
|
||
|
|
||
|
/**
|
||
|
* A cached list of ExpressionInterface objects that were
|
||
|
* found in the value for this expression.
|
||
|
*
|
||
|
* @var array<\Cake\Database\ExpressionInterface>
|
||
|
*/
|
||
|
protected $_valueExpressions = [];
|
||
|
|
||
|
/**
|
||
|
* Constructor
|
||
|
*
|
||
|
* @param \Cake\Database\ExpressionInterface|string $field the field name to compare to a value
|
||
|
* @param mixed $value The value to be used in comparison
|
||
|
* @param string|null $type the type name used to cast the value
|
||
|
* @param string $operator the operator used for comparing field and value
|
||
|
*/
|
||
|
public function __construct($field, $value, ?string $type = null, string $operator = '=')
|
||
|
{
|
||
|
$this->_type = $type;
|
||
|
$this->setField($field);
|
||
|
$this->setValue($value);
|
||
|
$this->_operator = $operator;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the value
|
||
|
*
|
||
|
* @param mixed $value The value to compare
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setValue($value): void
|
||
|
{
|
||
|
$value = $this->_castToExpression($value, $this->_type);
|
||
|
|
||
|
$isMultiple = $this->_type && strpos($this->_type, '[]') !== false;
|
||
|
if ($isMultiple) {
|
||
|
[$value, $this->_valueExpressions] = $this->_collectExpressions($value);
|
||
|
}
|
||
|
|
||
|
$this->_isMultiple = $isMultiple;
|
||
|
$this->_value = $value;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the value used for comparison
|
||
|
*
|
||
|
* @return mixed
|
||
|
*/
|
||
|
public function getValue()
|
||
|
{
|
||
|
return $this->_value;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the operator to use for the comparison
|
||
|
*
|
||
|
* @param string $operator The operator to be used for the comparison.
|
||
|
* @return void
|
||
|
*/
|
||
|
public function setOperator(string $operator): void
|
||
|
{
|
||
|
$this->_operator = $operator;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the operator used for comparison
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getOperator(): string
|
||
|
{
|
||
|
return $this->_operator;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @inheritDoc
|
||
|
*/
|
||
|
public function sql(ValueBinder $binder): string
|
||
|
{
|
||
|
/** @var \Cake\Database\ExpressionInterface|string $field */
|
||
|
$field = $this->_field;
|
||
|
|
||
|
if ($field instanceof ExpressionInterface) {
|
||
|
$field = $field->sql($binder);
|
||
|
}
|
||
|
|
||
|
if ($this->_value instanceof IdentifierExpression) {
|
||
|
$template = '%s %s %s';
|
||
|
$value = $this->_value->sql($binder);
|
||
|
} elseif ($this->_value instanceof ExpressionInterface) {
|
||
|
$template = '%s %s (%s)';
|
||
|
$value = $this->_value->sql($binder);
|
||
|
} else {
|
||
|
[$template, $value] = $this->_stringExpression($binder);
|
||
|
}
|
||
|
|
||
|
return sprintf($template, $field, $this->_operator, $value);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @inheritDoc
|
||
|
*/
|
||
|
public function traverse(Closure $callback)
|
||
|
{
|
||
|
if ($this->_field instanceof ExpressionInterface) {
|
||
|
$callback($this->_field);
|
||
|
$this->_field->traverse($callback);
|
||
|
}
|
||
|
|
||
|
if ($this->_value instanceof ExpressionInterface) {
|
||
|
$callback($this->_value);
|
||
|
$this->_value->traverse($callback);
|
||
|
}
|
||
|
|
||
|
foreach ($this->_valueExpressions as $v) {
|
||
|
$callback($v);
|
||
|
$v->traverse($callback);
|
||
|
}
|
||
|
|
||
|
return $this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a deep clone.
|
||
|
*
|
||
|
* Clones the field and value if they are expression objects.
|
||
|
*
|
||
|
* @return void
|
||
|
*/
|
||
|
public function __clone()
|
||
|
{
|
||
|
foreach (['_value', '_field'] as $prop) {
|
||
|
if ($this->{$prop} instanceof ExpressionInterface) {
|
||
|
$this->{$prop} = clone $this->{$prop};
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a template and a placeholder for the value after registering it
|
||
|
* with the placeholder $binder
|
||
|
*
|
||
|
* @param \Cake\Database\ValueBinder $binder The value binder to use.
|
||
|
* @return array First position containing the template and the second a placeholder
|
||
|
*/
|
||
|
protected function _stringExpression(ValueBinder $binder): array
|
||
|
{
|
||
|
$template = '%s ';
|
||
|
|
||
|
if ($this->_field instanceof ExpressionInterface && !$this->_field instanceof IdentifierExpression) {
|
||
|
$template = '(%s) ';
|
||
|
}
|
||
|
|
||
|
if ($this->_isMultiple) {
|
||
|
$template .= '%s (%s)';
|
||
|
$type = $this->_type;
|
||
|
if ($type !== null) {
|
||
|
$type = str_replace('[]', '', $type);
|
||
|
}
|
||
|
$value = $this->_flattenValue($this->_value, $binder, $type);
|
||
|
|
||
|
// To avoid SQL errors when comparing a field to a list of empty values,
|
||
|
// better just throw an exception here
|
||
|
if ($value === '') {
|
||
|
$field = $this->_field instanceof ExpressionInterface ? $this->_field->sql($binder) : $this->_field;
|
||
|
/** @psalm-suppress PossiblyInvalidCast */
|
||
|
throw new DatabaseException(
|
||
|
"Impossible to generate condition with empty list of values for field ($field)"
|
||
|
);
|
||
|
}
|
||
|
} else {
|
||
|
$template .= '%s %s';
|
||
|
$value = $this->_bindValue($this->_value, $binder, $this->_type);
|
||
|
}
|
||
|
|
||
|
return [$template, $value];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Registers a value in the placeholder generator and returns the generated placeholder
|
||
|
*
|
||
|
* @param mixed $value The value to bind
|
||
|
* @param \Cake\Database\ValueBinder $binder The value binder to use
|
||
|
* @param string|null $type The type of $value
|
||
|
* @return string generated placeholder
|
||
|
*/
|
||
|
protected function _bindValue($value, ValueBinder $binder, ?string $type = null): string
|
||
|
{
|
||
|
$placeholder = $binder->placeholder('c');
|
||
|
$binder->bind($placeholder, $value, $type);
|
||
|
|
||
|
return $placeholder;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Converts a traversable value into a set of placeholders generated by
|
||
|
* $binder and separated by `,`
|
||
|
*
|
||
|
* @param iterable $value the value to flatten
|
||
|
* @param \Cake\Database\ValueBinder $binder The value binder to use
|
||
|
* @param string|null $type the type to cast values to
|
||
|
* @return string
|
||
|
*/
|
||
|
protected function _flattenValue(iterable $value, ValueBinder $binder, ?string $type = null): string
|
||
|
{
|
||
|
$parts = [];
|
||
|
if (is_array($value)) {
|
||
|
foreach ($this->_valueExpressions as $k => $v) {
|
||
|
$parts[$k] = $v->sql($binder);
|
||
|
unset($value[$k]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!empty($value)) {
|
||
|
$parts += $binder->generateManyNamed($value, $type);
|
||
|
}
|
||
|
|
||
|
return implode(',', $parts);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns an array with the original $values in the first position
|
||
|
* and all ExpressionInterface objects that could be found in the second
|
||
|
* position.
|
||
|
*
|
||
|
* @param \Cake\Database\ExpressionInterface|iterable $values The rows to insert
|
||
|
* @return array
|
||
|
*/
|
||
|
protected function _collectExpressions($values): array
|
||
|
{
|
||
|
if ($values instanceof ExpressionInterface) {
|
||
|
return [$values, []];
|
||
|
}
|
||
|
|
||
|
$expressions = $result = [];
|
||
|
$isArray = is_array($values);
|
||
|
|
||
|
if ($isArray) {
|
||
|
/** @var array $result */
|
||
|
$result = $values;
|
||
|
}
|
||
|
|
||
|
foreach ($values as $k => $v) {
|
||
|
if ($v instanceof ExpressionInterface) {
|
||
|
$expressions[$k] = $v;
|
||
|
}
|
||
|
|
||
|
if ($isArray) {
|
||
|
$result[$k] = $v;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return [$result, $expressions];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// phpcs:disable
|
||
|
class_alias(
|
||
|
'Cake\Database\Expression\ComparisonExpression',
|
||
|
'Cake\Database\Expression\Comparison'
|
||
|
);
|
||
|
// phpcs:enable
|