<?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\ExpressionInterface;
use Cake\Database\Type\ExpressionTypeCasterTrait;
use Cake\Database\ValueBinder;
use Closure;
/**
* This class represents a SQL Case statement
*/
class CaseExpression implements ExpressionInterface
{
use ExpressionTypeCasterTrait;
/**
* A list of strings or other expression objects that represent the conditions of
* the case statement. For example one key of the array might look like "sum > :value"
*
* @var array
*/
protected $_conditions = [];
/**
* Values that are associated with the conditions in the $_conditions array.
* Each value represents the 'true' value for the condition with the corresponding key.
*
* @var array
*/
protected $_values = [];
/**
* The `ELSE` value for the case statement. If null then no `ELSE` will be included.
*
* @var string|\Cake\Database\ExpressionInterface|array|null
*/
protected $_elseValue;
/**
* Constructs the case expression
*
* @param array|\Cake\Database\ExpressionInterface $conditions The conditions to test. Must be a ExpressionInterface
* instance, or an array of ExpressionInterface instances.
* @param array|\Cake\Database\ExpressionInterface $values associative array of values to be associated with the
* conditions passed in $conditions. If there are more $values than $conditions,
* the last $value is used as the `ELSE` value.
* @param array $types associative array of types to be associated with the values
* passed in $values
*/
public function __construct($conditions = [], $values = [], $types = [])
{
if (!empty($conditions)) {
$this->add($conditions, $values, $types);
}
if (is_array($conditions) && is_array($values) && count($values) > count($conditions)) {
end($values);
$key = key($values);
$this->elseValue($values[$key], $types[$key] ?? null);
}
}
/**
* Adds one or more conditions and their respective true values to the case object.
* Conditions must be a one dimensional array or a QueryExpression.
* The trueValues must be a similar structure, but may contain a string value.
*
* @param array|\Cake\Database\ExpressionInterface $conditions Must be a ExpressionInterface instance,
* or an array of ExpressionInterface instances.
* @param array|\Cake\Database\ExpressionInterface $values associative array of values of each condition
* @param array $types associative array of types to be associated with the values
* @return $this
*/
public function add($conditions = [], $values = [], $types = [])
{
if (!is_array($conditions)) {
$conditions = [$conditions];
}
if (!is_array($values)) {
$values = [$values];
}
if (!is_array($types)) {
$types = [$types];
}
$this->_addExpressions($conditions, $values, $types);
return $this;
}
/**
* Iterates over the passed in conditions and ensures that there is a matching true value for each.
* If no matching true value, then it is defaulted to '1'.
*
* @param array $conditions Array of ExpressionInterface instances.
* @param array $values associative array of values of each condition
* @param array $types associative array of types to be associated with the values
* @return void
*/
protected function _addExpressions(array $conditions, array $values, array $types): void
{
$rawValues = array_values($values);
$keyValues = array_keys($values);
foreach ($conditions as $k => $c) {
$numericKey = is_numeric($k);
if ($numericKey && empty($c)) {
continue;
}
if (!$c instanceof ExpressionInterface) {
continue;
}
$this->_conditions[] = $c;
$value = $rawValues[$k] ?? 1;
if ($value === 'literal') {
$value = $keyValues[$k];
$this->_values[] = $value;
continue;
}
if ($value === 'identifier') {
$value = new IdentifierExpression($keyValues[$k]);
$this->_values[] = $value;
continue;
}
$type = $types[$k] ?? null;
if ($type !== null && !$value instanceof ExpressionInterface) {
$value = $this->_castToExpression($value, $type);
}
if ($value instanceof ExpressionInterface) {
$this->_values[] = $value;
continue;
}
$this->_values[] = ['value' => $value, 'type' => $type];
}
}
/**
* Sets the default value
*
* @param \Cake\Database\ExpressionInterface|string|array|null $value Value to set
* @param string|null $type Type of value
* @return void
*/
public function elseValue($value = null, ?string $type = null): void
{
if (is_array($value)) {
end($value);
$value = key($value);
}
if ($value !== null && !$value instanceof ExpressionInterface) {
$value = $this->_castToExpression($value, $type);
}
if (!$value instanceof ExpressionInterface) {
$value = ['value' => $value, 'type' => $type];
}
$this->_elseValue = $value;
}
/**
* Compiles the relevant parts into sql
*
* @param array|string|\Cake\Database\ExpressionInterface $part The part to compile
* @param \Cake\Database\ValueBinder $binder Sql generator
* @return string
*/
protected function _compile($part, ValueBinder $binder): string
{
if ($part instanceof ExpressionInterface) {
$part = $part->sql($binder);
} elseif (is_array($part)) {
$placeholder = $binder->placeholder('param');
$binder->bind($placeholder, $part['value'], $part['type']);
$part = $placeholder;
}
return $part;
}
/**
* Converts the Node into a SQL string fragment.
*
* @param \Cake\Database\ValueBinder $binder Placeholder generator object
* @return string
*/
public function sql(ValueBinder $binder): string
{
$parts = [];
$parts[] = 'CASE';
foreach ($this->_conditions as $k => $part) {
$value = $this->_values[$k];
$parts[] = 'WHEN ' . $this->_compile($part, $binder) . ' THEN ' . $this->_compile($value, $binder);
}
if ($this->_elseValue !== null) {
$parts[] = 'ELSE';
$parts[] = $this->_compile($this->_elseValue, $binder);
}
$parts[] = 'END';
return implode(' ', $parts);
}
/**
* @inheritDoc
*/
public function traverse(Closure $callback)
{
foreach (['_conditions', '_values'] as $part) {
foreach ($this->{$part} as $c) {
if ($c instanceof ExpressionInterface) {
$callback($c);
$c->traverse($callback);
}
}
}
if ($this->_elseValue instanceof ExpressionInterface) {
$callback($this->_elseValue);
$this->_elseValue->traverse($callback);
}
return $this;
}
}