CaseStatementExpression.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 4.3.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Database\Expression;
  17. use Cake\Database\ExpressionInterface;
  18. use Cake\Database\Type\ExpressionTypeCasterTrait;
  19. use Cake\Database\TypedResultInterface;
  20. use Cake\Database\TypeMapTrait;
  21. use Cake\Database\ValueBinder;
  22. use Closure;
  23. use InvalidArgumentException;
  24. use LogicException;
  25. /**
  26. * Represents a SQL case statement with a fluid API
  27. */
  28. class CaseStatementExpression implements ExpressionInterface, TypedResultInterface
  29. {
  30. use CaseExpressionTrait;
  31. use ExpressionTypeCasterTrait;
  32. use TypeMapTrait;
  33. /**
  34. * The names of the clauses that are valid for use with the
  35. * `clause()` method.
  36. *
  37. * @var array<string>
  38. */
  39. protected array $validClauseNames = [
  40. 'value',
  41. 'when',
  42. 'else',
  43. ];
  44. /**
  45. * Whether this is a simple case expression.
  46. *
  47. * @var bool
  48. */
  49. protected bool $isSimpleVariant = false;
  50. /**
  51. * The case value.
  52. *
  53. * @var \Cake\Database\ExpressionInterface|object|scalar|null
  54. */
  55. protected mixed $value = null;
  56. /**
  57. * The case value type.
  58. *
  59. * @var string|null
  60. */
  61. protected ?string $valueType = null;
  62. /**
  63. * The `WHEN ... THEN ...` expressions.
  64. *
  65. * @var array<\Cake\Database\Expression\WhenThenExpression>
  66. */
  67. protected array $when = [];
  68. /**
  69. * Buffer that holds values and types for use with `then()`.
  70. *
  71. * @var array|null
  72. */
  73. protected ?array $whenBuffer = null;
  74. /**
  75. * The else part result value.
  76. *
  77. * @var \Cake\Database\ExpressionInterface|object|scalar|null
  78. */
  79. protected mixed $else = null;
  80. /**
  81. * The else part result type.
  82. *
  83. * @var string|null
  84. */
  85. protected ?string $elseType = null;
  86. /**
  87. * The return type.
  88. *
  89. * @var string|null
  90. */
  91. protected ?string $returnType = null;
  92. /**
  93. * Constructor.
  94. *
  95. * When a value is set, the syntax generated is
  96. * `CASE case_value WHEN when_value ... END` (simple case),
  97. * where the `when_value`'s are compared against the
  98. * `case_value`.
  99. *
  100. * When no value is set, the syntax generated is
  101. * `CASE WHEN when_conditions ... END` (searched case),
  102. * where the conditions hold the comparisons.
  103. *
  104. * Note that `null` is a valid case value, and thus should
  105. * only be passed if you actually want to create the simple
  106. * case expression variant!
  107. *
  108. * @param \Cake\Database\ExpressionInterface|object|scalar|null $value The case value.
  109. * @param string|null $type The case value type. If no type is provided, the type will be tried to be inferred
  110. * from the value.
  111. */
  112. public function __construct(mixed $value = null, ?string $type = null)
  113. {
  114. if (func_num_args() > 0) {
  115. if (
  116. $value !== null &&
  117. !is_scalar($value) &&
  118. !(is_object($value) && !($value instanceof Closure))
  119. ) {
  120. throw new InvalidArgumentException(sprintf(
  121. 'The `$value` argument must be either `null`, a scalar value, an object, ' .
  122. 'or an instance of `\%s`, `%s` given.',
  123. ExpressionInterface::class,
  124. get_debug_type($value)
  125. ));
  126. }
  127. $this->value = $value;
  128. if (
  129. $value !== null &&
  130. $type === null &&
  131. !($value instanceof ExpressionInterface)
  132. ) {
  133. $type = $this->inferType($value);
  134. }
  135. $this->valueType = $type;
  136. $this->isSimpleVariant = true;
  137. }
  138. }
  139. /**
  140. * Sets the `WHEN` value for a `WHEN ... THEN ...` expression, or a
  141. * self-contained expression that holds both the value for `WHEN`
  142. * and the value for `THEN`.
  143. *
  144. * ### Order based syntax
  145. *
  146. * When passing a value other than a self-contained
  147. * `\Cake\Database\Expression\WhenThenExpression`,
  148. * instance, the `WHEN ... THEN ...` statement must be closed off with
  149. * a call to `then()` before invoking `when()` again or `else()`:
  150. *
  151. * ```
  152. * $queryExpression
  153. * ->case($query->identifier('Table.column'))
  154. * ->when(true)
  155. * ->then('Yes')
  156. * ->when(false)
  157. * ->then('No')
  158. * ->else('Maybe');
  159. * ```
  160. *
  161. * ### Self-contained expressions
  162. *
  163. * When passing an instance of `\Cake\Database\Expression\WhenThenExpression`,
  164. * being it directly, or via a callable, then there is no need to close
  165. * using `then()` on this object, instead the statement will be closed
  166. * on the `\Cake\Database\Expression\WhenThenExpression`
  167. * object using
  168. * `\Cake\Database\Expression\WhenThenExpression::then()`.
  169. *
  170. * Callables will receive an instance of `\Cake\Database\Expression\WhenThenExpression`,
  171. * and must return one, being it the same object, or a custom one:
  172. *
  173. * ```
  174. * $queryExpression
  175. * ->case()
  176. * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
  177. * return $whenThen
  178. * ->when(['Table.column' => true])
  179. * ->then('Yes');
  180. * })
  181. * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
  182. * return $whenThen
  183. * ->when(['Table.column' => false])
  184. * ->then('No');
  185. * })
  186. * ->else('Maybe');
  187. * ```
  188. *
  189. * ### Type handling
  190. *
  191. * The types provided via the `$type` argument will be merged with the
  192. * type map set for this expression. When using callables for `$when`,
  193. * the `\Cake\Database\Expression\WhenThenExpression`
  194. * instance received by the callables will inherit that type map, however
  195. * the types passed here will _not_ be merged in case of using callables,
  196. * instead the types must be passed in
  197. * `\Cake\Database\Expression\WhenThenExpression::when()`:
  198. *
  199. * ```
  200. * $queryExpression
  201. * ->case()
  202. * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
  203. * return $whenThen
  204. * ->when(['unmapped_column' => true], ['unmapped_column' => 'bool'])
  205. * ->then('Yes');
  206. * })
  207. * ->when(function (\Cake\Database\Expression\WhenThenExpression $whenThen) {
  208. * return $whenThen
  209. * ->when(['unmapped_column' => false], ['unmapped_column' => 'bool'])
  210. * ->then('No');
  211. * })
  212. * ->else('Maybe');
  213. * ```
  214. *
  215. * ### User data safety
  216. *
  217. * When passing user data, be aware that allowing a user defined array
  218. * to be passed, is a potential SQL injection vulnerability, as it
  219. * allows for raw SQL to slip in!
  220. *
  221. * The following is _unsafe_ usage that must be avoided:
  222. *
  223. * ```
  224. * $case
  225. * ->when($userData)
  226. * ```
  227. *
  228. * A safe variant for the above would be to define a single type for
  229. * the value:
  230. *
  231. * ```
  232. * $case
  233. * ->when($userData, 'integer')
  234. * ```
  235. *
  236. * This way an exception would be triggered when an array is passed for
  237. * the value, thus preventing raw SQL from slipping in, and all other
  238. * types of values would be forced to be bound as an integer.
  239. *
  240. * Another way to safely pass user data is when using a conditions
  241. * array, and passing user data only on the value side of the array
  242. * entries, which will cause them to be bound:
  243. *
  244. * ```
  245. * $case
  246. * ->when([
  247. * 'Table.column' => $userData,
  248. * ])
  249. * ```
  250. *
  251. * Lastly, data can also be bound manually:
  252. *
  253. * ```
  254. * $query
  255. * ->select([
  256. * 'val' => $query->newExpr()
  257. * ->case()
  258. * ->when($query->newExpr(':userData'))
  259. * ->then(123)
  260. * ])
  261. * ->bind(':userData', $userData, 'integer')
  262. * ```
  263. *
  264. * @param \Cake\Database\ExpressionInterface|\Closure|object|array|scalar $when The `WHEN` value. When using an
  265. * array of conditions, it must be compatible with `\Cake\Database\Query::where()`. Note that this argument is
  266. * _not_ completely safe for use with user data, as a user supplied array would allow for raw SQL to slip in! If
  267. * you plan to use user data, either pass a single type for the `$type` argument (which forces the `$when` value to
  268. * be a non-array, and then always binds the data), use a conditions array where the user data is only passed on
  269. * the value side of the array entries, or custom bindings!
  270. * @param array<string, string>|string|null $type The when value type. Either an associative array when using array style
  271. * conditions, or else a string. If no type is provided, the type will be tried to be inferred from the value.
  272. * @return $this
  273. * @throws \LogicException In case this a closing `then()` call is required before calling this method.
  274. * @throws \LogicException In case the callable doesn't return an instance of
  275. * `\Cake\Database\Expression\WhenThenExpression`.
  276. */
  277. public function when(mixed $when, array|string|null $type = null)
  278. {
  279. if ($this->whenBuffer !== null) {
  280. throw new LogicException('Cannot call `when()` between `when()` and `then()`.');
  281. }
  282. if ($when instanceof Closure) {
  283. $when = $when(new WhenThenExpression($this->getTypeMap()));
  284. if (!($when instanceof WhenThenExpression)) {
  285. throw new LogicException(sprintf(
  286. '`when()` callables must return an instance of `\%s`, `%s` given.',
  287. WhenThenExpression::class,
  288. get_debug_type($when)
  289. ));
  290. }
  291. }
  292. if ($when instanceof WhenThenExpression) {
  293. $this->when[] = $when;
  294. } else {
  295. $this->whenBuffer = ['when' => $when, 'type' => $type];
  296. }
  297. return $this;
  298. }
  299. /**
  300. * Sets the `THEN` result value for the last `WHEN ... THEN ...`
  301. * statement that was opened using `when()`.
  302. *
  303. * ### Order based syntax
  304. *
  305. * This method can only be invoked in case `when()` was previously
  306. * used with a value other than a closure or an instance of
  307. * `\Cake\Database\Expression\WhenThenExpression`:
  308. *
  309. * ```
  310. * $case
  311. * ->when(['Table.column' => true])
  312. * ->then('Yes')
  313. * ->when(['Table.column' => false])
  314. * ->then('No')
  315. * ->else('Maybe');
  316. * ```
  317. *
  318. * The following would all fail with an exception:
  319. *
  320. * ```
  321. * $case
  322. * ->when(['Table.column' => true])
  323. * ->when(['Table.column' => false])
  324. * // ...
  325. * ```
  326. *
  327. * ```
  328. * $case
  329. * ->when(['Table.column' => true])
  330. * ->else('Maybe')
  331. * // ...
  332. * ```
  333. *
  334. * ```
  335. * $case
  336. * ->then('Yes')
  337. * // ...
  338. * ```
  339. *
  340. * ```
  341. * $case
  342. * ->when(['Table.column' => true])
  343. * ->then('Yes')
  344. * ->then('No')
  345. * // ...
  346. * ```
  347. *
  348. * @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
  349. * @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
  350. * value.
  351. * @return $this
  352. * @throws \LogicException In case `when()` wasn't previously called with a value other than a closure or an
  353. * instance of `\Cake\Database\Expression\WhenThenExpression`.
  354. */
  355. public function then(mixed $result, ?string $type = null)
  356. {
  357. if ($this->whenBuffer === null) {
  358. throw new LogicException('Cannot call `then()` before `when()`.');
  359. }
  360. $whenThen = (new WhenThenExpression($this->getTypeMap()))
  361. ->when($this->whenBuffer['when'], $this->whenBuffer['type'])
  362. ->then($result, $type);
  363. $this->whenBuffer = null;
  364. $this->when[] = $whenThen;
  365. return $this;
  366. }
  367. /**
  368. * Sets the `ELSE` result value.
  369. *
  370. * @param \Cake\Database\ExpressionInterface|object|scalar|null $result The result value.
  371. * @param string|null $type The result type. If no type is provided, the type will be tried to be inferred from the
  372. * value.
  373. * @return $this
  374. * @throws \LogicException In case a closing `then()` call is required before calling this method.
  375. * @throws \InvalidArgumentException In case the `$result` argument is neither a scalar value, nor an object, an
  376. * instance of `\Cake\Database\ExpressionInterface`, or `null`.
  377. */
  378. public function else(mixed $result, ?string $type = null)
  379. {
  380. if ($this->whenBuffer !== null) {
  381. throw new LogicException('Cannot call `else()` between `when()` and `then()`.');
  382. }
  383. if (
  384. $result !== null &&
  385. !is_scalar($result) &&
  386. !(is_object($result) && !($result instanceof Closure))
  387. ) {
  388. throw new InvalidArgumentException(sprintf(
  389. 'The `$result` argument must be either `null`, a scalar value, an object, ' .
  390. 'or an instance of `\%s`, `%s` given.',
  391. ExpressionInterface::class,
  392. get_debug_type($result)
  393. ));
  394. }
  395. $type ??= $this->inferType($result);
  396. $this->else = $result;
  397. $this->elseType = $type;
  398. return $this;
  399. }
  400. /**
  401. * Returns the abstract type that this expression will return.
  402. *
  403. * If no type has been explicitly set via `setReturnType()`, this
  404. * method will try to obtain the type from the result types of the
  405. * `then()` and `else() `calls. All types must be identical in order
  406. * for this to work, otherwise the type will default to `string`.
  407. *
  408. * @return string
  409. * @see CaseStatementExpression::then()
  410. */
  411. public function getReturnType(): string
  412. {
  413. if ($this->returnType !== null) {
  414. return $this->returnType;
  415. }
  416. $types = [];
  417. foreach ($this->when as $when) {
  418. $type = $when->getResultType();
  419. if ($type !== null) {
  420. $types[] = $type;
  421. }
  422. }
  423. if ($this->elseType !== null) {
  424. $types[] = $this->elseType;
  425. }
  426. $types = array_unique($types);
  427. if (count($types) === 1) {
  428. return $types[0];
  429. }
  430. return 'string';
  431. }
  432. /**
  433. * Sets the abstract type that this expression will return.
  434. *
  435. * If no type is being explicitly set via this method, then the
  436. * `getReturnType()` method will try to infer the type from the
  437. * result types of the `then()` and `else() `calls.
  438. *
  439. * @param string $type The type name to use.
  440. * @return $this
  441. */
  442. public function setReturnType(string $type)
  443. {
  444. $this->returnType = $type;
  445. return $this;
  446. }
  447. /**
  448. * Returns the available data for the given clause.
  449. *
  450. * ### Available clauses
  451. *
  452. * The following clause names are available:
  453. *
  454. * * `value`: The case value for a `CASE case_value WHEN ...` expression.
  455. * * `when`: An array of `WHEN ... THEN ...` expressions.
  456. * * `else`: The `ELSE` result value.
  457. *
  458. * @param string $clause The name of the clause to obtain.
  459. * @return \Cake\Database\ExpressionInterface|object|array<\Cake\Database\Expression\WhenThenExpression>|scalar|null
  460. * @throws \InvalidArgumentException In case the given clause name is invalid.
  461. */
  462. public function clause(string $clause): mixed
  463. {
  464. if (!in_array($clause, $this->validClauseNames, true)) {
  465. throw new InvalidArgumentException(
  466. sprintf(
  467. 'The `$clause` argument must be one of `%s`, the given value `%s` is invalid.',
  468. implode('`, `', $this->validClauseNames),
  469. $clause
  470. )
  471. );
  472. }
  473. return $this->{$clause};
  474. }
  475. /**
  476. * @inheritDoc
  477. */
  478. public function sql(ValueBinder $binder): string
  479. {
  480. if ($this->whenBuffer !== null) {
  481. throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
  482. }
  483. if (!$this->when) {
  484. throw new LogicException('Case expression must have at least one when statement.');
  485. }
  486. $value = '';
  487. if ($this->isSimpleVariant) {
  488. $value = $this->compileNullableValue($binder, $this->value, $this->valueType) . ' ';
  489. }
  490. $whenThenExpressions = [];
  491. foreach ($this->when as $whenThen) {
  492. $whenThenExpressions[] = $whenThen->sql($binder);
  493. }
  494. $whenThen = implode(' ', $whenThenExpressions);
  495. $else = $this->compileNullableValue($binder, $this->else, $this->elseType);
  496. return "CASE {$value}{$whenThen} ELSE $else END";
  497. }
  498. /**
  499. * @inheritDoc
  500. */
  501. public function traverse(Closure $callback)
  502. {
  503. if ($this->whenBuffer !== null) {
  504. throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
  505. }
  506. if ($this->value instanceof ExpressionInterface) {
  507. $callback($this->value);
  508. $this->value->traverse($callback);
  509. }
  510. foreach ($this->when as $when) {
  511. $callback($when);
  512. $when->traverse($callback);
  513. }
  514. if ($this->else instanceof ExpressionInterface) {
  515. $callback($this->else);
  516. $this->else->traverse($callback);
  517. }
  518. return $this;
  519. }
  520. /**
  521. * Clones the inner expression objects.
  522. *
  523. * @return void
  524. */
  525. public function __clone()
  526. {
  527. if ($this->whenBuffer !== null) {
  528. throw new LogicException('Case expression has incomplete when clause. Missing `then()` after `when()`.');
  529. }
  530. if ($this->value instanceof ExpressionInterface) {
  531. $this->value = clone $this->value;
  532. }
  533. foreach ($this->when as $key => $when) {
  534. $this->when[$key] = clone $this->when[$key];
  535. }
  536. if ($this->else instanceof ExpressionInterface) {
  537. $this->else = clone $this->else;
  538. }
  539. }
  540. }