| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594 |
- <?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 4.3.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\TypedResultInterface;
- use Cake\Database\TypeMapTrait;
- use Cake\Database\ValueBinder;
- use Closure;
- use InvalidArgumentException;
- use LogicException;
- /**
- * Represents a SQL case statement with a fluid API
- */
- class CaseStatementExpression implements ExpressionInterface, TypedResultInterface
- {
- use CaseExpressionTrait;
- use ExpressionTypeCasterTrait;
- use TypeMapTrait;
- /**
- * The names of the clauses that are valid for use with the
- * `clause()` method.
- *
- * @var array<string>
- */
- protected array $validClauseNames = [
- 'value',
- 'when',
- 'else',
- ];
- /**
- * Whether this is a simple case expression.
- *
- * @var bool
- */
- protected bool $isSimpleVariant = false;
- /**
- * The case value.
- *
- * @var \Cake\Database\ExpressionInterface|object|scalar|null
- */
- protected mixed $value = null;
- /**
- * The case value type.
- *
- * @var string|null
- */
- protected ?string $valueType = null;
- /**
- * The `WHEN ... THEN ...` expressions.
- *
- * @var array<\Cake\Database\Expression\WhenThenExpression>
- */
- protected array $when = [];
- /**
- * Buffer that holds values and types for use with `then()`.
- *
- * @var array|null
- */
- protected ?array $whenBuffer = null;
- /**
- * The else part result value.
- *
- * @var \Cake\Database\ExpressionInterface|object|scalar|null
- */
- protected mixed $else = null;
- /**
- * The else part result type.
- *
- * @var string|null
- */
- protected ?string $elseType = null;
- /**
- * The return type.
- *
- * @var string|null
- */
- protected ?string $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(mixed $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,
- get_debug_type($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, string>|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(mixed $when, array|string|null $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,
- get_debug_type($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(mixed $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(mixed $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,
- get_debug_type($result)
- ));
- }
- $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): mixed
- {
- 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 (!$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;
- }
- }
- }
|