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