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\View\Form;
16:
17: use Cake\Http\ServerRequest;
18: use Cake\Utility\Hash;
19: use Cake\Validation\Validator;
20:
21: /**
22: * Provides a basic array based context provider for FormHelper.
23: *
24: * This adapter is useful in testing or when you have forms backed by
25: * simple array data structures.
26: *
27: * Important keys:
28: *
29: * - `defaults` The default values for fields. These values
30: * will be used when there is no request data set. Data should be nested following
31: * the dot separated paths you access your fields with.
32: * - `required` A nested array of fields, relationships and boolean
33: * flags to indicate a field is required. The value can also be a string to be used
34: * as the required error message
35: * - `schema` An array of data that emulate the column structures that
36: * Cake\Database\Schema\Schema uses. This array allows you to control
37: * the inferred type for fields and allows auto generation of attributes
38: * like maxlength, step and other HTML attributes. If you want
39: * primary key/id detection to work. Make sure you have provided a `_constraints`
40: * array that contains `primary`. See below for an example.
41: * - `errors` An array of validation errors. Errors should be nested following
42: * the dot separated paths you access your fields with.
43: *
44: * ### Example
45: *
46: * ```
47: * $data = [
48: * 'schema' => [
49: * 'id' => ['type' => 'integer'],
50: * 'title' => ['type' => 'string', 'length' => 255],
51: * '_constraints' => [
52: * 'primary' => ['type' => 'primary', 'columns' => ['id']]
53: * ]
54: * ],
55: * 'defaults' => [
56: * 'id' => 1,
57: * 'title' => 'First post!',
58: * ],
59: * 'required' => [
60: * 'id' => true, // will use default required message
61: * 'title' => 'Please enter a title',
62: * 'body' => false,
63: * ],
64: * ];
65: * ```
66: */
67: class ArrayContext implements ContextInterface
68: {
69: /**
70: * The request object.
71: *
72: * @var \Cake\Http\ServerRequest
73: */
74: protected $_request;
75:
76: /**
77: * Context data for this object.
78: *
79: * @var array
80: */
81: protected $_context;
82:
83: /**
84: * Constructor.
85: *
86: * @param \Cake\Http\ServerRequest $request The request object.
87: * @param array $context Context info.
88: */
89: public function __construct(ServerRequest $request, array $context)
90: {
91: $this->_request = $request;
92: $context += [
93: 'schema' => [],
94: 'required' => [],
95: 'defaults' => [],
96: 'errors' => [],
97: ];
98: $this->_context = $context;
99: }
100:
101: /**
102: * Get the fields used in the context as a primary key.
103: *
104: * @return array
105: */
106: public function primaryKey()
107: {
108: if (empty($this->_context['schema']['_constraints']) ||
109: !is_array($this->_context['schema']['_constraints'])
110: ) {
111: return [];
112: }
113: foreach ($this->_context['schema']['_constraints'] as $data) {
114: if (isset($data['type']) && $data['type'] === 'primary') {
115: return isset($data['columns']) ? (array)$data['columns'] : [];
116: }
117: }
118:
119: return [];
120: }
121:
122: /**
123: * {@inheritDoc}
124: */
125: public function isPrimaryKey($field)
126: {
127: $primaryKey = $this->primaryKey();
128:
129: return in_array($field, $primaryKey);
130: }
131:
132: /**
133: * Returns whether or not this form is for a create operation.
134: *
135: * For this method to return true, both the primary key constraint
136: * must be defined in the 'schema' data, and the 'defaults' data must
137: * contain a value for all fields in the key.
138: *
139: * @return bool
140: */
141: public function isCreate()
142: {
143: $primary = $this->primaryKey();
144: foreach ($primary as $column) {
145: if (!empty($this->_context['defaults'][$column])) {
146: return false;
147: }
148: }
149:
150: return true;
151: }
152:
153: /**
154: * Get the current value for a given field.
155: *
156: * This method will coalesce the current request data and the 'defaults'
157: * array.
158: *
159: * @param string $field A dot separated path to the field a value
160: * is needed for.
161: * @param array $options Options:
162: * - `default`: Default value to return if no value found in request
163: * data or context record.
164: * - `schemaDefault`: Boolean indicating whether default value from
165: * context's schema should be used if it's not explicitly provided.
166: * @return mixed
167: */
168: public function val($field, $options = [])
169: {
170: $options += [
171: 'default' => null,
172: 'schemaDefault' => true
173: ];
174:
175: $val = $this->_request->getData($field);
176: if ($val !== null) {
177: return $val;
178: }
179: if ($options['default'] !== null || !$options['schemaDefault']) {
180: return $options['default'];
181: }
182: if (empty($this->_context['defaults']) || !is_array($this->_context['defaults'])) {
183: return null;
184: }
185:
186: // Using Hash::check here incase the default value is actually null
187: if (Hash::check($this->_context['defaults'], $field)) {
188: return Hash::get($this->_context['defaults'], $field);
189: }
190:
191: return Hash::get($this->_context['defaults'], $this->stripNesting($field));
192: }
193:
194: /**
195: * Check if a given field is 'required'.
196: *
197: * In this context class, this is simply defined by the 'required' array.
198: *
199: * @param string $field A dot separated path to check required-ness for.
200: * @return bool
201: */
202: public function isRequired($field)
203: {
204: return (bool)$this->getRequiredMessage($field);
205: }
206:
207: /**
208: * {@inheritDoc}
209: */
210: public function getRequiredMessage($field)
211: {
212: if (!is_array($this->_context['required'])) {
213: return null;
214: }
215: $required = Hash::get($this->_context['required'], $field);
216: if ($required === null) {
217: $required = Hash::get($this->_context['required'], $this->stripNesting($field));
218: }
219:
220: if ($required === false) {
221: return null;
222: }
223:
224: if ($required === true) {
225: $required = __d('cake', 'This field is required');
226: }
227:
228: return $required;
229: }
230:
231: /**
232: * Get field length from validation
233: *
234: * In this context class, this is simply defined by the 'length' array.
235: *
236: * @param string $field A dot separated path to check required-ness for.
237: * @return int|null
238: */
239: public function getMaxLength($field)
240: {
241: if (!is_array($this->_context['schema'])) {
242: return null;
243: }
244:
245: return Hash::get($this->_context['schema'], "$field.length");
246: }
247:
248: /**
249: * {@inheritDoc}
250: */
251: public function fieldNames()
252: {
253: $schema = $this->_context['schema'];
254: unset($schema['_constraints'], $schema['_indexes']);
255:
256: return array_keys($schema);
257: }
258:
259: /**
260: * Get the abstract field type for a given field name.
261: *
262: * @param string $field A dot separated path to get a schema type for.
263: * @return string|null An abstract data type or null.
264: * @see \Cake\Database\Type
265: */
266: public function type($field)
267: {
268: if (!is_array($this->_context['schema'])) {
269: return null;
270: }
271:
272: $schema = Hash::get($this->_context['schema'], $field);
273: if ($schema === null) {
274: $schema = Hash::get($this->_context['schema'], $this->stripNesting($field));
275: }
276:
277: return isset($schema['type']) ? $schema['type'] : null;
278: }
279:
280: /**
281: * Get an associative array of other attributes for a field name.
282: *
283: * @param string $field A dot separated path to get additional data on.
284: * @return array An array of data describing the additional attributes on a field.
285: */
286: public function attributes($field)
287: {
288: if (!is_array($this->_context['schema'])) {
289: return [];
290: }
291: $schema = Hash::get($this->_context['schema'], $field);
292: if ($schema === null) {
293: $schema = Hash::get($this->_context['schema'], $this->stripNesting($field));
294: }
295: $whitelist = ['length' => null, 'precision' => null];
296:
297: return array_intersect_key((array)$schema, $whitelist);
298: }
299:
300: /**
301: * Check whether or not a field has an error attached to it
302: *
303: * @param string $field A dot separated path to check errors on.
304: * @return bool Returns true if the errors for the field are not empty.
305: */
306: public function hasError($field)
307: {
308: if (empty($this->_context['errors'])) {
309: return false;
310: }
311:
312: return (bool)Hash::check($this->_context['errors'], $field);
313: }
314:
315: /**
316: * Get the errors for a given field
317: *
318: * @param string $field A dot separated path to check errors on.
319: * @return array An array of errors, an empty array will be returned when the
320: * context has no errors.
321: */
322: public function error($field)
323: {
324: if (empty($this->_context['errors'])) {
325: return [];
326: }
327:
328: return (array)Hash::get($this->_context['errors'], $field);
329: }
330:
331: /**
332: * Strips out any numeric nesting
333: *
334: * For example users.0.age will output as users.age
335: *
336: * @param string $field A dot separated path
337: * @return string A string with stripped numeric nesting
338: */
339: protected function stripNesting($field)
340: {
341: return preg_replace('/\.\d*\./', '.', $field);
342: }
343: }
344: