*/ protected $validClauseNames = [ 'value', 'when', 'else', ]; /** * Whether this is a simple case expression. * * @var bool */ protected $isSimpleVariant = false; /** * The case value. * * @var \Cake\Database\ExpressionInterface|object|scalar|null */ protected $value = null; /** * The case value type. * * @var string|null */ protected $valueType = null; /** * The `WHEN ... THEN ...` expressions. * * @var array<\Cake\Database\Expression\WhenThenExpression> */ protected $when = []; /** * Buffer that holds values and types for use with `then()`. * * @var array|null */ protected $whenBuffer = null; /** * The else part result value. * * @var \Cake\Database\ExpressionInterface|object|scalar|null */ protected $else = null; /** * The else part result type. * * @var string|null */ protected $elseType = null; /** * The return type. * * @var string|null */ protected $returnType = null; /** * Constructor. * * When a value is set, the syntax generated is * `CASE case_value WHEN when_value ... END` (simple case), * where the `when_value`'s are compared against the * `case_value`. * * When no value is set, the syntax generated is * `CASE WHEN when_conditions ... END` (searched case), * where the conditions hold the comparisons. * * Note that `null` is a valid case value, and thus should * only be passed if you actually want to create the simple * case expression variant! * * @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value. * @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred * from the value. */ public function __construct($value = null, ?string $type = null) { if (func_num_args() > 0) { if ( $value !== null && !is_scalar($value) && !(is_object($value) && !($value instanceof Closure)) ) { throw new InvalidArgumentException(sprintf( 'The `$value` argument must be either `null`, a scalar value, an object, ' . 'or an instance of `\%s`, `%s` given.', ExpressionInterface::class, getTypeName($value) )); } $this->value = $value; if ( $value !== null && $type === null && !($value instanceof ExpressionInterface) ) { $type = $this->inferType($value); } $this->valueType = $type; $this->isSimpleVariant = true; } } /** * Sets the `WHEN` value for a `WHEN ... THEN ...` expression, or a * self-contained expression that holds both the value for `WHEN` * and the value for `THEN`. * * ### Order based syntax * * When passing a value other than a self-contained * `\Cake\Database\Expression\WhenThenExpression`, * instance, the `WHEN ... THEN ...` statement must be closed off with * a call to `then()` before invoking `when()` again or `else()`: * * ``` * $queryExpression * ->case($query->identifier('Table.column')) * ->when(true) * ->then('Yes') * ->when(false) * ->then('No') * ->else('Maybe'); * ``` * * ### Self-contained expressions * * When passing an instance of `\Cake\Database\Expression\WhenThenExpression`, * being it directly, or via a callable, then there is no need to close * using `then()` on this object, instead the statement will be closed * on the `\Cake\Database\Expression\WhenThenExpression` * object using * `\Cake\Database\Expression\WhenThenExpression::then()`. * * Callables will receive an instance of `\Cake\Database\Expression\WhenThenExpression`, * and must return one, being it the same object, or a custom one: * * ``` * $queryExpression * ->case() * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { * return $whenThen * ->when(['Table.column' => true]) * ->then('Yes'); * }) * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { * return $whenThen * ->when(['Table.column' => false]) * ->then('No'); * }) * ->else('Maybe'); * ``` * * ### Type handling * * The types provided via the `$type` argument will be merged with the * type map set for this expression. When using callables for `$when`, * the `\Cake\Database\Expression\WhenThenExpression` * instance received by the callables will inherit that type map, however * the types passed here will _not_ be merged in case of using callables, * instead the types must be passed in * `\Cake\Database\Expression\WhenThenExpression::when()`: * * ``` * $queryExpression * ->case() * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { * return $whenThen * ->when(['unmapped_column' => true], ['unmapped_column' => 'bool']) * ->then('Yes'); * }) * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) { * return $whenThen * ->when(['unmapped_column' => false], ['unmapped_column' => 'bool']) * ->then('No'); * }) * ->else('Maybe'); * ``` * * ### User data safety * * When passing user data, be aware that allowing a user defined array * to be passed, is a potential SQL injection vulnerability, as it * allows for raw SQL to slip in! * * The following is _unsafe_ usage that must be avoided: * * ``` * $case * ->when($userData) * ``` * * A safe variant for the above would be to define a single type for * the value: * * ``` * $case * ->when($userData, 'integer') * ``` * * This way an exception would be triggered when an array is passed for * the value, thus preventing raw SQL from slipping in, and all other * types of values would be forced to be bound as an integer. * * Another way to safely pass user data is when using a conditions * array, and passing user data only on the value side of the array * entries, which will cause them to be bound: * * ``` * $case * ->when([ * 'Table.column' => $userData, * ]) * ``` * * Lastly, data can also be bound manually: * * ``` * $query * ->select([ * 'val' => $query->newExpr() * ->case() * ->when($query->newExpr(':userData')) * ->then(123) * ]) * ->bind(':userData', $userData, 'integer') * ``` * * @param \Cake\Database\ExpressionInterface|\Closure|object|array|scalar $when The `WHEN` value. When using an * array of conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is * _not_ completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If * you plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to * be a non-array, and then always binds the data), use a conditions array where the user data is only passed on * the value side of the array entries, or custom bindings! * @param array|string|null $type The when value type. Either an associative array when using array style * conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value. * @return $this * @throws \LogicException In case this a closing `then()` call is required before calling this method. * @throws \LogicException In case the callable doesn't return an instance of * `\Cake\Database\Expression\WhenThenExpression`. */ public function when($when, $type = null) { if ($this->whenBuffer !== null) { throw new LogicException('Cannot call `when()` between `when()` and `then()`.'); } if ($when instanceof Closure) { $when = $when(new WhenThenExpression($this->getTypeMap())); if (!($when instanceof WhenThenExpression)) { throw new LogicException(sprintf( '`when()` callables must return an instance of `\%s`, `%s` given.', WhenThenExpression::class, getTypeName($when) )); } } if ($when instanceof WhenThenExpression) { $this->when[] = $when; } else { $this->whenBuffer = ['when' => $when, 'type' => $type]; } return $this; } /** * Sets the `THEN` result value for the last `WHEN ... THEN ...` * statement that was opened using `when()`. * * ### Order based syntax * * This method can only be invoked in case `when()` was previously * used with a value other than a closure or an instance of * `\Cake\Database\Expression\WhenThenExpression`: * * ``` * $case * ->when(['Table.column' => true]) * ->then('Yes') * ->when(['Table.column' => false]) * ->then('No') * ->else('Maybe'); * ``` * * The following would all fail with an exception: * * ``` * $case * ->when(['Table.column' => true]) * ->when(['Table.column' => false]) * // ... * ``` * * ``` * $case * ->when(['Table.column' => true]) * ->else('Maybe') * // ... * ``` * * ``` * $case * ->then('Yes') * // ... * ``` * * ``` * $case * ->when(['Table.column' => true]) * ->then('Yes') * ->then('No') * // ... * ``` * * @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value. * @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the * value. * @return $this * @throws \LogicException In case `when()` wasn't previously called with a value other than a closure or an * instance of `\Cake\Database\Expression\WhenThenExpression`. */ public function then($result, ?string $type = null) { if ($this->whenBuffer === null) { throw new LogicException('Cannot call `then()` before `when()`.'); } $whenThen = (new WhenThenExpression($this->getTypeMap())) ->when($this->whenBuffer['when'], $this->whenBuffer['type']) ->then($result, $type); $this->whenBuffer = null; $this->when[] = $whenThen; return $this; } /** * Sets the `ELSE` result value. * * @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value. * @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the * value. * @return $this * @throws \LogicException In case a closing `then()` call is required before calling this method. * @throws \InvalidArgumentException In case the `$result` argument is neither a scalar value, nor an object, an * instance of `\Cake\Database\ExpressionInterface`, or `null`. */ public function else($result, ?string $type = null) { if ($this->whenBuffer !== null) { throw new LogicException('Cannot call `else()` between `when()` and `then()`.'); } if ( $result !== null && !is_scalar($result) && !(is_object($result) && !($result instanceof Closure)) ) { throw new InvalidArgumentException(sprintf( 'The `$result` argument must be either `null`, a scalar value, an object, ' . 'or an instance of `\%s`, `%s` given.', ExpressionInterface::class, getTypeName($result) )); } if ($type === null) { $type = $this->inferType($result); } $this->else = $result; $this->elseType = $type; return $this; } /** * Returns the abstract type that this expression will return. * * If no type has been explicitly set via `setReturnType()`, this * method will try to obtain the type from the result types of the * `then()` and `else() `calls. All types must be identical in order * for this to work, otherwise the type will default to `string`. * * @return string * @see CaseStatementExpression::then() */ public function getReturnType(): string { if ($this->returnType !== null) { return $this->returnType; } $types = []; foreach ($this->when as $when) { $type = $when->getResultType(); if ($type !== null) { $types[] = $type; } } if ($this->elseType !== null) { $types[] = $this->elseType; } $types = array_unique($types); if (count($types) === 1) { return $types[0]; } return 'string'; } /** * Sets the abstract type that this expression will return. * * If no type is being explicitly set via this method, then the * `getReturnType()` method will try to infer the type from the * result types of the `then()` and `else() `calls. * * @param string $type The type name to use. * @return $this */ public function setReturnType(string $type) { $this->returnType = $type; return $this; } /** * Returns the available data for the given clause. * * ### Available clauses * * The following clause names are available: * * * `value`: The case value for a `CASE case_value WHEN ...` expression. * * `when`: An array of `WHEN ... THEN ...` expressions. * * `else`: The `ELSE` result value. * * @param string $clause The name of the clause to obtain. * @return \Cake\Database\ExpressionInterface|object|array<\Cake\Database\Expression\WhenThenExpression>|scalar|null * @throws \InvalidArgumentException In case the given clause name is invalid. */ public function clause(string $clause) { if (!in_array($clause, $this->validClauseNames, true)) { throw new InvalidArgumentException( sprintf( 'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.', implode('`, `', $this->validClauseNames), $clause ) ); } return $this->{$clause}; } /** * @inheritDoc */ public function sql(ValueBinder $binder): string { if ($this->whenBuffer !== null) { throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.'); } if (empty($this->when)) { throw new LogicException('Case expression must have at least one when statement.'); } $value = ''; if ($this->isSimpleVariant) { $value = $this->compileNullableValue($binder, $this->value, $this->valueType) . ' '; } $whenThenExpressions = []; foreach ($this->when as $whenThen) { $whenThenExpressions[] = $whenThen->sql($binder); } $whenThen = implode(' ', $whenThenExpressions); $else = $this->compileNullableValue($binder, $this->else, $this->elseType); return "CASE {$value}{$whenThen} ELSE $else END"; } /** * @inheritDoc */ public function traverse(Closure $callback) { if ($this->whenBuffer !== null) { throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.'); } if ($this->value instanceof ExpressionInterface) { $callback($this->value); $this->value->traverse($callback); } foreach ($this->when as $when) { $callback($when); $when->traverse($callback); } if ($this->else instanceof ExpressionInterface) { $callback($this->else); $this->else->traverse($callback); } return $this; } /** * Clones the inner expression objects. * * @return void */ public function __clone() { if ($this->whenBuffer !== null) { throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.'); } if ($this->value instanceof ExpressionInterface) { $this->value = clone $this->value; } foreach ($this->when as $key => $when) { $this->when[$key] = clone $this->when[$key]; } if ($this->else instanceof ExpressionInterface) { $this->else = clone $this->else; } } }