1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Database\Expression;
16:
17: use Cake\Database\Exception;
18: use Cake\Database\ExpressionInterface;
19: use Cake\Database\Query;
20: use Cake\Database\TypeMapTrait;
21: use Cake\Database\Type\ExpressionTypeCasterTrait;
22: use Cake\Database\ValueBinder;
23:
24: /**
25: * An expression object to contain values being inserted.
26: *
27: * Helps generate SQL with the correct number of placeholders and bind
28: * values correctly into the statement.
29: */
30: class ValuesExpression implements ExpressionInterface
31: {
32: use ExpressionTypeCasterTrait;
33: use TypeMapTrait;
34:
35: /**
36: * Array of values to insert.
37: *
38: * @var array
39: */
40: protected $_values = [];
41:
42: /**
43: * List of columns to ensure are part of the insert.
44: *
45: * @var array
46: */
47: protected $_columns = [];
48:
49: /**
50: * The Query object to use as a values expression
51: *
52: * @var \Cake\Database\Query|null
53: */
54: protected $_query;
55:
56: /**
57: * Whether or not values have been casted to expressions
58: * already.
59: *
60: * @var bool
61: */
62: protected $_castedExpressions = false;
63:
64: /**
65: * Constructor
66: *
67: * @param array $columns The list of columns that are going to be part of the values.
68: * @param \Cake\Database\TypeMap $typeMap A dictionary of column -> type names
69: */
70: public function __construct(array $columns, $typeMap)
71: {
72: $this->_columns = $columns;
73: $this->setTypeMap($typeMap);
74: }
75:
76: /**
77: * Add a row of data to be inserted.
78: *
79: * @param array|\Cake\Database\Query $data Array of data to append into the insert, or
80: * a query for doing INSERT INTO .. SELECT style commands
81: * @return void
82: * @throws \Cake\Database\Exception When mixing array + Query data types.
83: */
84: public function add($data)
85: {
86: if ((count($this->_values) && $data instanceof Query) ||
87: ($this->_query && is_array($data))
88: ) {
89: throw new Exception(
90: 'You cannot mix subqueries and array data in inserts.'
91: );
92: }
93: if ($data instanceof Query) {
94: $this->setQuery($data);
95:
96: return;
97: }
98: $this->_values[] = $data;
99: $this->_castedExpressions = false;
100: }
101:
102: /**
103: * Sets the columns to be inserted.
104: *
105: * @param array $cols Array with columns to be inserted.
106: * @return $this
107: */
108: public function setColumns($cols)
109: {
110: $this->_columns = $cols;
111: $this->_castedExpressions = false;
112:
113: return $this;
114: }
115:
116: /**
117: * Gets the columns to be inserted.
118: *
119: * @return array
120: */
121: public function getColumns()
122: {
123: return $this->_columns;
124: }
125:
126: /**
127: * Sets the columns to be inserted. If no params are passed, then it returns
128: * the currently stored columns.
129: *
130: * @deprecated 3.4.0 Use setColumns()/getColumns() instead.
131: * @param array|null $cols Array with columns to be inserted.
132: * @return array|$this
133: */
134: public function columns($cols = null)
135: {
136: deprecationWarning(
137: 'ValuesExpression::columns() is deprecated. ' .
138: 'Use ValuesExpression::setColumns()/getColumns() instead.'
139: );
140: if ($cols !== null) {
141: return $this->setColumns($cols);
142: }
143:
144: return $this->getColumns();
145: }
146:
147: /**
148: * Get the bare column names.
149: *
150: * Because column names could be identifier quoted, we
151: * need to strip the identifiers off of the columns.
152: *
153: * @return array
154: */
155: protected function _columnNames()
156: {
157: $columns = [];
158: foreach ($this->_columns as $col) {
159: if (is_string($col)) {
160: $col = trim($col, '`[]"');
161: }
162: $columns[] = $col;
163: }
164:
165: return $columns;
166: }
167:
168: /**
169: * Sets the values to be inserted.
170: *
171: * @param array $values Array with values to be inserted.
172: * @return $this
173: */
174: public function setValues($values)
175: {
176: $this->_values = $values;
177: $this->_castedExpressions = false;
178:
179: return $this;
180: }
181:
182: /**
183: * Gets the values to be inserted.
184: *
185: * @return array
186: */
187: public function getValues()
188: {
189: if (!$this->_castedExpressions) {
190: $this->_processExpressions();
191: }
192:
193: return $this->_values;
194: }
195:
196: /**
197: * Sets the values to be inserted. If no params are passed, then it returns
198: * the currently stored values
199: *
200: * @deprecated 3.4.0 Use setValues()/getValues() instead.
201: * @param array|null $values Array with values to be inserted.
202: * @return array|$this
203: */
204: public function values($values = null)
205: {
206: deprecationWarning(
207: 'ValuesExpression::values() is deprecated. ' .
208: 'Use ValuesExpression::setValues()/getValues() instead.'
209: );
210: if ($values !== null) {
211: return $this->setValues($values);
212: }
213:
214: return $this->getValues();
215: }
216:
217: /**
218: * Sets the query object to be used as the values expression to be evaluated
219: * to insert records in the table.
220: *
221: * @param \Cake\Database\Query $query The query to set
222: * @return $this
223: */
224: public function setQuery(Query $query)
225: {
226: $this->_query = $query;
227:
228: return $this;
229: }
230:
231: /**
232: * Gets the query object to be used as the values expression to be evaluated
233: * to insert records in the table.
234: *
235: * @return \Cake\Database\Query|null
236: */
237: public function getQuery()
238: {
239: return $this->_query;
240: }
241:
242: /**
243: * Sets the query object to be used as the values expression to be evaluated
244: * to insert records in the table. If no params are passed, then it returns
245: * the currently stored query
246: *
247: * @deprecated 3.4.0 Use setQuery()/getQuery() instead.
248: * @param \Cake\Database\Query|null $query The query to set
249: * @return \Cake\Database\Query|null|$this
250: */
251: public function query(Query $query = null)
252: {
253: deprecationWarning(
254: 'ValuesExpression::query() is deprecated. ' .
255: 'Use ValuesExpression::setQuery()/getQuery() instead.'
256: );
257: if ($query !== null) {
258: return $this->setQuery($query);
259: }
260:
261: return $this->getQuery();
262: }
263:
264: /**
265: * Convert the values into a SQL string with placeholders.
266: *
267: * @param \Cake\Database\ValueBinder $generator Placeholder generator object
268: * @return string
269: */
270: public function sql(ValueBinder $generator)
271: {
272: if (empty($this->_values) && empty($this->_query)) {
273: return '';
274: }
275:
276: if (!$this->_castedExpressions) {
277: $this->_processExpressions();
278: }
279:
280: $columns = $this->_columnNames();
281: $defaults = array_fill_keys($columns, null);
282: $placeholders = [];
283:
284: $types = [];
285: $typeMap = $this->getTypeMap();
286: foreach ($defaults as $col => $v) {
287: $types[$col] = $typeMap->type($col);
288: }
289:
290: foreach ($this->_values as $row) {
291: $row += $defaults;
292: $rowPlaceholders = [];
293:
294: foreach ($columns as $column) {
295: $value = $row[$column];
296:
297: if ($value instanceof ExpressionInterface) {
298: $rowPlaceholders[] = '(' . $value->sql($generator) . ')';
299: continue;
300: }
301:
302: $placeholder = $generator->placeholder('c');
303: $rowPlaceholders[] = $placeholder;
304: $generator->bind($placeholder, $value, $types[$column]);
305: }
306:
307: $placeholders[] = implode(', ', $rowPlaceholders);
308: }
309:
310: if ($this->getQuery()) {
311: return ' ' . $this->getQuery()->sql($generator);
312: }
313:
314: return sprintf(' VALUES (%s)', implode('), (', $placeholders));
315: }
316:
317: /**
318: * Traverse the values expression.
319: *
320: * This method will also traverse any queries that are to be used in the INSERT
321: * values.
322: *
323: * @param callable $visitor The visitor to traverse the expression with.
324: * @return void
325: */
326: public function traverse(callable $visitor)
327: {
328: if ($this->_query) {
329: return;
330: }
331:
332: if (!$this->_castedExpressions) {
333: $this->_processExpressions();
334: }
335:
336: foreach ($this->_values as $v) {
337: if ($v instanceof ExpressionInterface) {
338: $v->traverse($visitor);
339: }
340: if (!is_array($v)) {
341: continue;
342: }
343: foreach ($v as $column => $field) {
344: if ($field instanceof ExpressionInterface) {
345: $visitor($field);
346: $field->traverse($visitor);
347: }
348: }
349: }
350: }
351:
352: /**
353: * Converts values that need to be casted to expressions
354: *
355: * @return void
356: */
357: protected function _processExpressions()
358: {
359: $types = [];
360: $typeMap = $this->getTypeMap();
361:
362: $columns = $this->_columnNames();
363: foreach ($columns as $c) {
364: if (!is_scalar($c)) {
365: continue;
366: }
367: $types[$c] = $typeMap->type($c);
368: }
369:
370: $types = $this->_requiresToExpressionCasting($types);
371:
372: if (empty($types)) {
373: return;
374: }
375:
376: foreach ($this->_values as $row => $values) {
377: foreach ($types as $col => $type) {
378: /* @var \Cake\Database\Type\ExpressionTypeInterface $type */
379: $this->_values[$row][$col] = $type->toExpression($values[$col]);
380: }
381: }
382: $this->_castedExpressions = true;
383: }
384: }
385: