1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14:
15: namespace Cake\View\Widget;
16:
17: use Cake\View\Form\ContextInterface;
18: use Cake\View\StringTemplate;
19: use DateTime;
20: use Exception;
21: use RuntimeException;
22:
23: 24: 25: 26: 27: 28:
29: class DateTimeWidget implements WidgetInterface
30: {
31:
32: 33: 34: 35: 36:
37: protected $_select;
38:
39: 40: 41: 42: 43:
44: protected $_selects = [
45: 'year',
46: 'month',
47: 'day',
48: 'hour',
49: 'minute',
50: 'second',
51: 'meridian',
52: ];
53:
54: 55: 56: 57: 58:
59: protected $_templates;
60:
61: 62: 63: 64: 65: 66:
67: public function __construct(StringTemplate $templates, SelectBoxWidget $selectBox)
68: {
69: $this->_select = $selectBox;
70: $this->_templates = $templates;
71: }
72:
73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123:
124: public function render(array $data, ContextInterface $context)
125: {
126: $data = $this->_normalizeData($data);
127:
128: $selected = $this->_deconstructDate($data['val'], $data);
129:
130: $templateOptions = ['templateVars' => $data['templateVars']];
131: foreach ($this->_selects as $select) {
132: if ($data[$select] === false || $data[$select] === null) {
133: $templateOptions[$select] = '';
134: unset($data[$select]);
135: continue;
136: }
137: if (!is_array($data[$select])) {
138: throw new RuntimeException(sprintf(
139: 'Options for "%s" must be an array|false|null',
140: $select
141: ));
142: }
143: $method = "_{$select}Select";
144: $data[$select]['name'] = $data['name'] . '[' . $select . ']';
145: $data[$select]['val'] = $selected[$select];
146:
147: if (!isset($data[$select]['empty'])) {
148: $data[$select]['empty'] = $data['empty'];
149: }
150: if (!isset($data[$select]['disabled'])) {
151: $data[$select]['disabled'] = $data['disabled'];
152: }
153: if (isset($data[$select]['templateVars']) && $templateOptions['templateVars']) {
154: $data[$select]['templateVars'] = array_merge(
155: $templateOptions['templateVars'],
156: $data[$select]['templateVars']
157: );
158: }
159: if (!isset($data[$select]['templateVars'])) {
160: $data[$select]['templateVars'] = $templateOptions['templateVars'];
161: }
162: $templateOptions[$select] = $this->{$method}($data[$select], $context);
163: unset($data[$select]);
164: }
165: unset($data['name'], $data['empty'], $data['disabled'], $data['val']);
166: $templateOptions['attrs'] = $this->_templates->formatAttributes($data);
167:
168: return $this->_templates->format('dateWidget', $templateOptions);
169: }
170:
171: 172: 173: 174: 175: 176:
177: protected function _normalizeData($data)
178: {
179: $data += [
180: 'name' => '',
181: 'empty' => false,
182: 'disabled' => null,
183: 'val' => null,
184: 'year' => [],
185: 'month' => [],
186: 'day' => [],
187: 'hour' => [],
188: 'minute' => [],
189: 'second' => [],
190: 'meridian' => null,
191: 'templateVars' => [],
192: ];
193:
194: $timeFormat = isset($data['hour']['format']) ? $data['hour']['format'] : null;
195: if ($timeFormat === 12 && !isset($data['meridian'])) {
196: $data['meridian'] = [];
197: }
198: if ($timeFormat === 24) {
199: $data['meridian'] = false;
200: }
201:
202: return $data;
203: }
204:
205: 206: 207: 208: 209: 210: 211:
212: protected function _deconstructDate($value, $options)
213: {
214: if ($value === '' || $value === null) {
215: return [
216: 'year' => '', 'month' => '', 'day' => '',
217: 'hour' => '', 'minute' => '', 'second' => '',
218: 'meridian' => '',
219: ];
220: }
221: try {
222: if (is_string($value) && !is_numeric($value)) {
223: $date = new DateTime($value);
224: } elseif (is_bool($value)) {
225: $date = new DateTime();
226: } elseif (is_int($value) || is_numeric($value)) {
227: $date = new DateTime('@' . $value);
228: } elseif (is_array($value)) {
229: $dateArray = [
230: 'year' => '', 'month' => '', 'day' => '',
231: 'hour' => '', 'minute' => '', 'second' => '',
232: 'meridian' => '',
233: ];
234: $validDate = false;
235: foreach ($dateArray as $key => $dateValue) {
236: $exists = isset($value[$key]);
237: if ($exists) {
238: $validDate = true;
239: }
240: if ($exists && $value[$key] !== '') {
241: $dateArray[$key] = str_pad($value[$key], 2, '0', STR_PAD_LEFT);
242: }
243: }
244: if ($validDate) {
245: if (!isset($dateArray['second'])) {
246: $dateArray['second'] = 0;
247: }
248: if (!empty($value['meridian'])) {
249: $isAm = strtolower($dateArray['meridian']) === 'am';
250: $dateArray['hour'] = $isAm ? $dateArray['hour'] : $dateArray['hour'] + 12;
251: }
252: if (!empty($dateArray['minute']) && isset($options['minute']['interval'])) {
253: $dateArray['minute'] += $this->_adjustValue($dateArray['minute'], $options['minute']);
254: $dateArray['minute'] = str_pad((string)$dateArray['minute'], 2, '0', STR_PAD_LEFT);
255: }
256:
257: return $dateArray;
258: }
259:
260: $date = new DateTime();
261: } else {
262:
263: $date = clone $value;
264: }
265: } catch (Exception $e) {
266: $date = new DateTime();
267: }
268:
269: if (isset($options['minute']['interval'])) {
270: $change = $this->_adjustValue((int)$date->format('i'), $options['minute']);
271: $date->modify($change > 0 ? "+$change minutes" : "$change minutes");
272: }
273:
274: return [
275: 'year' => $date->format('Y'),
276: 'month' => $date->format('m'),
277: 'day' => $date->format('d'),
278: 'hour' => $date->format('H'),
279: 'minute' => $date->format('i'),
280: 'second' => $date->format('s'),
281: 'meridian' => $date->format('a'),
282: ];
283: }
284:
285: 286: 287: 288: 289: 290: 291:
292: protected function _adjustValue($value, $options)
293: {
294: $options += ['interval' => 1, 'round' => null];
295: $changeValue = $value * (1 / $options['interval']);
296: switch ($options['round']) {
297: case 'up':
298: $changeValue = ceil($changeValue);
299: break;
300: case 'down':
301: $changeValue = floor($changeValue);
302: break;
303: default:
304: $changeValue = round($changeValue);
305: }
306:
307: return ($changeValue * $options['interval']) - $value;
308: }
309:
310: 311: 312: 313: 314: 315: 316:
317: protected function _yearSelect($options, $context)
318: {
319: $options += [
320: 'name' => '',
321: 'val' => null,
322: 'start' => date('Y', strtotime('-5 years')),
323: 'end' => date('Y', strtotime('+5 years')),
324: 'order' => 'desc',
325: 'templateVars' => [],
326: 'options' => []
327: ];
328:
329: if (!empty($options['val'])) {
330: $options['start'] = min($options['val'], $options['start']);
331: $options['end'] = max($options['val'], $options['end']);
332: }
333: if (empty($options['options'])) {
334: $options['options'] = $this->_generateNumbers($options['start'], $options['end']);
335: }
336: if ($options['order'] === 'desc') {
337: $options['options'] = array_reverse($options['options'], true);
338: }
339: unset($options['start'], $options['end'], $options['order']);
340:
341: return $this->_select->render($options, $context);
342: }
343:
344: 345: 346: 347: 348: 349: 350:
351: protected function _monthSelect($options, $context)
352: {
353: $options += [
354: 'name' => '',
355: 'names' => false,
356: 'val' => null,
357: 'leadingZeroKey' => true,
358: 'leadingZeroValue' => false,
359: 'templateVars' => [],
360: ];
361:
362: if (empty($options['options'])) {
363: if ($options['names'] === true) {
364: $options['options'] = $this->_getMonthNames($options['leadingZeroKey']);
365: } elseif (is_array($options['names'])) {
366: $options['options'] = $options['names'];
367: } else {
368: $options['options'] = $this->_generateNumbers(1, 12, $options);
369: }
370: }
371:
372: unset($options['leadingZeroKey'], $options['leadingZeroValue'], $options['names']);
373:
374: return $this->_select->render($options, $context);
375: }
376:
377: 378: 379: 380: 381: 382: 383:
384: protected function _daySelect($options, $context)
385: {
386: $options += [
387: 'name' => '',
388: 'val' => null,
389: 'leadingZeroKey' => true,
390: 'leadingZeroValue' => false,
391: 'templateVars' => [],
392: ];
393: $options['options'] = $this->_generateNumbers(1, 31, $options);
394:
395: unset($options['names'], $options['leadingZeroKey'], $options['leadingZeroValue']);
396:
397: return $this->_select->render($options, $context);
398: }
399:
400: 401: 402: 403: 404: 405: 406:
407: protected function _hourSelect($options, $context)
408: {
409: $options += [
410: 'name' => '',
411: 'val' => null,
412: 'format' => 24,
413: 'start' => null,
414: 'end' => null,
415: 'leadingZeroKey' => true,
416: 'leadingZeroValue' => false,
417: 'templateVars' => [],
418: ];
419: $is24 = $options['format'] == 24;
420:
421: $defaultStart = $is24 ? 0 : 1;
422: $defaultEnd = $is24 ? 23 : 12;
423: $options['start'] = max($defaultStart, $options['start']);
424:
425: $options['end'] = min($defaultEnd, $options['end']);
426: if ($options['end'] === null) {
427: $options['end'] = $defaultEnd;
428: }
429:
430: if (!$is24 && $options['val'] > 12) {
431: $options['val'] = sprintf('%02d', $options['val'] - 12);
432: }
433: if (!$is24 && in_array($options['val'], ['00', '0', 0], true)) {
434: $options['val'] = 12;
435: }
436:
437: if (empty($options['options'])) {
438: $options['options'] = $this->_generateNumbers(
439: $options['start'],
440: $options['end'],
441: $options
442: );
443: }
444:
445: unset(
446: $options['end'],
447: $options['start'],
448: $options['format'],
449: $options['leadingZeroKey'],
450: $options['leadingZeroValue']
451: );
452:
453: return $this->_select->render($options, $context);
454: }
455:
456: 457: 458: 459: 460: 461: 462:
463: protected function _minuteSelect($options, $context)
464: {
465: $options += [
466: 'name' => '',
467: 'val' => null,
468: 'interval' => 1,
469: 'round' => 'up',
470: 'leadingZeroKey' => true,
471: 'leadingZeroValue' => true,
472: 'templateVars' => [],
473: ];
474: $options['interval'] = max($options['interval'], 1);
475: if (empty($options['options'])) {
476: $options['options'] = $this->_generateNumbers(0, 59, $options);
477: }
478:
479: unset(
480: $options['leadingZeroKey'],
481: $options['leadingZeroValue'],
482: $options['interval'],
483: $options['round']
484: );
485:
486: return $this->_select->render($options, $context);
487: }
488:
489: 490: 491: 492: 493: 494: 495:
496: protected function _secondSelect($options, $context)
497: {
498: $options += [
499: 'name' => '',
500: 'val' => null,
501: 'leadingZeroKey' => true,
502: 'leadingZeroValue' => true,
503: 'options' => $this->_generateNumbers(0, 59),
504: 'templateVars' => [],
505: ];
506:
507: unset($options['leadingZeroKey'], $options['leadingZeroValue']);
508:
509: return $this->_select->render($options, $context);
510: }
511:
512: 513: 514: 515: 516: 517: 518:
519: protected function _meridianSelect($options, $context)
520: {
521: $options += [
522: 'name' => '',
523: 'val' => null,
524: 'options' => ['am' => 'am', 'pm' => 'pm'],
525: 'templateVars' => [],
526: ];
527:
528: return $this->_select->render($options, $context);
529: }
530:
531: 532: 533: 534: 535: 536:
537: protected function _getMonthNames($leadingZero = false)
538: {
539: $months = [
540: '01' => __d('cake', 'January'),
541: '02' => __d('cake', 'February'),
542: '03' => __d('cake', 'March'),
543: '04' => __d('cake', 'April'),
544: '05' => __d('cake', 'May'),
545: '06' => __d('cake', 'June'),
546: '07' => __d('cake', 'July'),
547: '08' => __d('cake', 'August'),
548: '09' => __d('cake', 'September'),
549: '10' => __d('cake', 'October'),
550: '11' => __d('cake', 'November'),
551: '12' => __d('cake', 'December'),
552: ];
553:
554: if ($leadingZero === false) {
555: $i = 1;
556: foreach ($months as $key => $name) {
557: unset($months[$key]);
558: $months[$i++] = $name;
559: }
560: }
561:
562: return $months;
563: }
564:
565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578:
579: protected function _generateNumbers($start, $end, $options = [])
580: {
581: $options += [
582: 'leadingZeroKey' => true,
583: 'leadingZeroValue' => true,
584: 'interval' => 1
585: ];
586:
587: $numbers = [];
588: $i = $start;
589: while ($i <= $end) {
590: $key = (string)$i;
591: $value = (string)$i;
592: if ($options['leadingZeroKey'] === true) {
593: $key = sprintf('%02d', $key);
594: }
595: if ($options['leadingZeroValue'] === true) {
596: $value = sprintf('%02d', $value);
597: }
598: $numbers[$key] = $value;
599: $i += $options['interval'];
600: }
601:
602: return $numbers;
603: }
604:
605: 606: 607: 608: 609: 610: 611: 612: 613:
614: public function secureFields(array $data)
615: {
616: $data = $this->_normalizeData($data);
617:
618: $fields = [];
619: foreach ($this->_selects as $select) {
620: if ($data[$select] === false || $data[$select] === null) {
621: continue;
622: }
623:
624: $fields[] = $data['name'] . '[' . $select . ']';
625: }
626:
627: return $fields;
628: }
629: }
630: