|
|
@@ -18,13 +18,14 @@ namespace Cake\Database\Expression;
|
|
|
|
|
|
use Cake\Database\ExpressionInterface;
|
|
|
use Cake\Database\Type\ExpressionTypeCasterTrait;
|
|
|
+use Cake\Database\TypedResultInterface;
|
|
|
use Cake\Database\TypeMapTrait;
|
|
|
use Cake\Database\ValueBinder;
|
|
|
use Closure;
|
|
|
use InvalidArgumentException;
|
|
|
use LogicException;
|
|
|
|
|
|
-class CaseStatementExpression implements CaseExpressionInterface
|
|
|
+class CaseStatementExpression implements ExpressionInterface, TypedResultInterface
|
|
|
{
|
|
|
use CaseExpressionTrait;
|
|
|
use ExpressionTypeCasterTrait;
|
|
|
@@ -66,7 +67,7 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
/**
|
|
|
* The `WHEN ... THEN ...` expressions.
|
|
|
*
|
|
|
- * @var array<\Cake\Database\Expression\WhenThenExpressionInterface>
|
|
|
+ * @var array<\Cake\Database\Expression\WhenThenExpression>
|
|
|
*/
|
|
|
protected $when = [];
|
|
|
|
|
|
@@ -150,29 +151,161 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @inheritDoc
|
|
|
+ * 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|array|object|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 add new `WHEN` value while an open `when()` buffer is present, ' .
|
|
|
- 'it must first be closed using `then()`.'
|
|
|
- );
|
|
|
+ throw new LogicException('Cannot call `when()` between `when()` and `then()`.');
|
|
|
}
|
|
|
|
|
|
if ($when instanceof Closure) {
|
|
|
$when = $when(new WhenThenExpression($this->getTypeMap()));
|
|
|
- if (!($when instanceof WhenThenExpressionInterface)) {
|
|
|
+ if (!($when instanceof WhenThenExpression)) {
|
|
|
throw new LogicException(sprintf(
|
|
|
'`when()` callables must return an instance of `\%s`, `%s` given.',
|
|
|
- WhenThenExpressionInterface::class,
|
|
|
+ WhenThenExpression::class,
|
|
|
get_debug_type($when)
|
|
|
));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if ($when instanceof WhenThenExpressionInterface) {
|
|
|
+ if ($when instanceof WhenThenExpression) {
|
|
|
$this->when[] = $when;
|
|
|
} else {
|
|
|
$this->whenBuffer = ['when' => $when, 'type' => $type];
|
|
|
@@ -182,15 +315,65 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @inheritDoc
|
|
|
+ * 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(
|
|
|
- 'There is no `when()` buffer present, ' .
|
|
|
- 'you must first open one before calling `then()`.'
|
|
|
- );
|
|
|
+ throw new LogicException('Cannot call `then()` before `when()`.');
|
|
|
}
|
|
|
|
|
|
$whenThen = (new WhenThenExpression($this->getTypeMap()))
|
|
|
@@ -205,15 +388,20 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @inheritDoc
|
|
|
+ * 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 set `ELSE` value when an open `when()` buffer is present, ' .
|
|
|
- 'it must first be closed using `then()`.'
|
|
|
- );
|
|
|
+ throw new LogicException('Cannot call `else()` between `when()` and `then()`.');
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
@@ -240,7 +428,15 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @inheritDoc
|
|
|
+ * 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
|
|
|
{
|
|
|
@@ -269,7 +465,14 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @inheritDoc
|
|
|
+ * 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)
|
|
|
{
|
|
|
@@ -279,27 +482,48 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @inheritDoc
|
|
|
+ * Returns the available data for the given clause.
|
|
|
+ *
|
|
|
+ * ### Available clauses
|
|
|
+ *
|
|
|
+ * The following clause names are available:
|
|
|
+ *
|
|
|
+ * * `value` (`\Cake\Database\ExpressionInterface|object|scalar|null`): The case value for a
|
|
|
+ * `CASE case_value WHEN ...` expression.
|
|
|
+ * * `when (`array<\Cake\Database\Expression\WhenThenExpression>`)`: An array of self-contained
|
|
|
+ * `WHEN ... THEN ...` expressions.
|
|
|
+ * * `else` (`\Cake\Database\ExpressionInterface|object|scalar|null`): The `ELSE` result value.
|
|
|
+ *
|
|
|
+ * @param string $clause The name of the clause to obtain.
|
|
|
+ * @return array<\Cake\Database\Expression\WhenThenExpression>|\Cake\Database\ExpressionInterface|object|scalar|null
|
|
|
+ * @throws \InvalidArgumentException In case the given clause name is invalid.
|
|
|
*/
|
|
|
- public function sql(ValueBinder $binder): string
|
|
|
+ public function clause(string $clause)
|
|
|
{
|
|
|
- if ($this->whenBuffer !== null) {
|
|
|
- throw new LogicException(
|
|
|
+ if (!in_array($clause, $this->validClauseNames, true)) {
|
|
|
+ throw new InvalidArgumentException(
|
|
|
sprintf(
|
|
|
- 'Cannot compile incomplete `\%s` expression, there is an open `when()` buffer present ' .
|
|
|
- 'that must be closed using `then()`.',
|
|
|
- CaseExpressionInterface::class
|
|
|
+ '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(
|
|
|
- sprintf(
|
|
|
- 'Cannot compile incomplete `\%s` expression, there are no `WHEN ... THEN ...` statements.',
|
|
|
- CaseExpressionInterface::class
|
|
|
- )
|
|
|
- );
|
|
|
+ throw new LogicException('Case expression must have at least one when statement.');
|
|
|
}
|
|
|
|
|
|
$value = '';
|
|
|
@@ -324,13 +548,7 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
public function traverse(Closure $callback)
|
|
|
{
|
|
|
if ($this->whenBuffer !== null) {
|
|
|
- throw new LogicException(
|
|
|
- sprintf(
|
|
|
- 'Cannot traverse incomplete `\%s` expression, there is an open `when()` buffer present ' .
|
|
|
- 'that must be closed using `then()`.',
|
|
|
- CaseExpressionInterface::class
|
|
|
- )
|
|
|
- );
|
|
|
+ throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
|
|
|
}
|
|
|
|
|
|
if ($this->value instanceof ExpressionInterface) {
|
|
|
@@ -359,13 +577,7 @@ class CaseStatementExpression implements CaseExpressionInterface
|
|
|
public function __clone()
|
|
|
{
|
|
|
if ($this->whenBuffer !== null) {
|
|
|
- throw new LogicException(
|
|
|
- sprintf(
|
|
|
- 'Cannot clone incomplete `\%s` expression, there is an open `when()` buffer present ' .
|
|
|
- 'that must be closed using `then()`.',
|
|
|
- CaseExpressionInterface::class
|
|
|
- )
|
|
|
- );
|
|
|
+ throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
|
|
|
}
|
|
|
|
|
|
if ($this->value instanceof ExpressionInterface) {
|