1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Contract\Core\Entity\Readable;
6: use Salient\Contract\Core\Chainable;
7: use Salient\Core\Concern\HasChainableMethods;
8: use Salient\Core\Concern\HasReadableProperties;
9: use LogicException;
10:
11: /**
12: * A simple representation of a SQL query
13: *
14: * @property-read array<string,mixed> $Values Parameter name => value
15: */
16: final class SqlQuery implements Chainable, Readable
17: {
18: use HasChainableMethods;
19: use HasReadableProperties;
20:
21: public const AND = 'AND';
22: public const OR = 'OR';
23:
24: /**
25: * A list of optionally nested WHERE conditions
26: *
27: * To join a list of conditions with an explicit operator:
28: *
29: * ```php
30: * <?php
31: * [
32: * '__' => SqlQuery::AND,
33: * 'Id = ?',
34: * 'Deleted IS NULL',
35: * ]
36: * ```
37: *
38: * To use nested conditions:
39: *
40: * ```php
41: * <?php
42: * [
43: * '__' => SqlQuery::AND,
44: * 'ItemKey = ?',
45: * [
46: * '__' => SqlQuery::OR,
47: * 'Expiry IS NULL',
48: * 'Expiry > ?',
49: * ],
50: * ]
51: * ```
52: *
53: * @var array<int|string,string|mixed[]>
54: */
55: public $Where = [];
56:
57: /**
58: * Parameter name => value
59: *
60: * @var array<string,mixed>
61: */
62: protected $Values = [];
63:
64: /** @var callable(string): string */
65: protected $ParamCallback;
66:
67: /**
68: * @inheritDoc
69: */
70: public static function getReadableProperties(): array
71: {
72: return ['Values'];
73: }
74:
75: /**
76: * Creates a new SqlQuery object
77: *
78: * @param callable(string): string $paramCallback Applied to the name of
79: * each parameter added to the query.
80: */
81: public function __construct(callable $paramCallback)
82: {
83: $this->ParamCallback = $paramCallback;
84: }
85:
86: /**
87: * Add a parameter and assign its query placeholder to a variable
88: *
89: * @param mixed $value
90: * @return $this
91: */
92: public function addParam(string $name, $value, ?string &$placeholder)
93: {
94: if (array_key_exists($name, $this->Values)) {
95: throw new LogicException(sprintf('Parameter already added: %s', $name));
96: }
97:
98: $placeholder = ($this->ParamCallback)($name);
99: $this->Values[$name] = $value;
100:
101: return $this;
102: }
103:
104: /**
105: * Add a WHERE condition
106: *
107: * @see SqlQuery::$Where
108: *
109: * @param (callable(): (string|mixed[]))|string|mixed[] $condition
110: * @return $this
111: */
112: public function where($condition)
113: {
114: $this->Where[] = is_callable($condition) ? $condition() : $condition;
115:
116: return $this;
117: }
118:
119: /**
120: * Add a list of values as a WHERE condition ("<name> IN (<value>...)")
121: * unless the list is empty
122: *
123: * @param mixed ...$value
124: * @return $this
125: */
126: public function whereValueInList(string $name, ...$value)
127: {
128: if (!$value) {
129: return $this;
130: }
131:
132: foreach ($value as $val) {
133: $expr[] = $this->addNextParam($val);
134: }
135: $this->Where[] = "$name IN (" . implode(',', $expr) . ')';
136:
137: return $this;
138: }
139:
140: /**
141: * Prepare a WHERE condition for use in a SQL statement
142: *
143: * @param array<string,mixed>|null $values
144: */
145: public function getWhere(?array &$values = null): ?string
146: {
147: $values = $this->Values;
148: $where = $this->buildWhere($this->Where);
149:
150: return $where === ''
151: ? null
152: : $where;
153: }
154:
155: /**
156: * @param mixed $value
157: */
158: private function addNextParam($value): string
159: {
160: $this->addParam('param_' . count($this->Values), $value, $param);
161:
162: return $param;
163: }
164:
165: /**
166: * @param array<int|string,string|mixed[]> $where
167: */
168: private function buildWhere(array $where): string
169: {
170: $glue = $where['__'] ?? self::AND;
171: unset($where['__']);
172: foreach ($where as $i => $condition) {
173: if (is_array($condition)) {
174: $condition = $this->buildWhere($condition);
175: if ($condition === '') {
176: unset($where[$i]);
177: continue;
178: }
179: $where[$i] = "($condition)";
180: }
181: }
182:
183: /** @var string[] $where */
184: return implode(" $glue ", $where);
185: }
186: }
187: