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