DateTime.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://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. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\View\Widget;
  16. use Cake\Utility\Time;
  17. use Cake\View\Form\ContextInterface;
  18. use Cake\View\StringTemplate;
  19. use Cake\View\Widget\WidgetInterface;
  20. /**
  21. * Input widget class for generating a date time input widget.
  22. *
  23. * This class is intended as an internal implementation detail
  24. * of Cake\View\Helper\FormHelper and is not intended for direct use.
  25. */
  26. class DateTime implements WidgetInterface {
  27. /**
  28. * Select box widget.
  29. *
  30. * @var \Cake\View\Widget\SelectBox
  31. */
  32. protected $_select;
  33. /**
  34. * List of inputs that can be rendered
  35. *
  36. * @var array
  37. */
  38. protected $_selects = [
  39. 'year',
  40. 'month',
  41. 'day',
  42. 'hour',
  43. 'minute',
  44. 'second',
  45. 'meridian',
  46. ];
  47. /**
  48. * Template instance.
  49. *
  50. * @var \Cake\View\StringTemplate
  51. */
  52. protected $_templates;
  53. /**
  54. * Constructor
  55. *
  56. * @param \Cake\View\StringTemplate $templates Templates list.
  57. * @param \Cake\View\Widget\SelectBox $selectBox Selectbox widget instance.
  58. */
  59. public function __construct($templates, $selectBox) {
  60. $this->_select = $selectBox;
  61. $this->_templates = $templates;
  62. }
  63. /**
  64. * Renders a date time widget
  65. *
  66. * - `name` - Set the input name.
  67. * - `disabled` - Either true or an array of options to disable.
  68. * - `val` - A date time string, integer or DateTime object
  69. * - `empty` - Set to true to add an empty option at the top of the
  70. * option elements. Set to a string to define the display value of the
  71. * empty option.
  72. *
  73. * In addtion to the above options, the following options allow you to control
  74. * which input elements are generated. By setting any option to false you can disable
  75. * that input picker. In addition each picker allows you to set additional options
  76. * that are set as HTML properties on the picker.
  77. *
  78. * - `year` - Array of options for the year select box.
  79. * - `month` - Array of options for the month select box.
  80. * - `day` - Array of options for the day select box.
  81. * - `hour` - Array of options for the hour select box.
  82. * - `minute` - Array of options for the minute select box.
  83. * - `second` - Set to true to enable the seconds input. Defaults to false.
  84. * - `meridian` - Set to true to enable the meridian input. Defaults to false.
  85. * The meridian will be enabled automatically if you chose a 12 hour format.
  86. *
  87. * The `year` option accepts the `start` and `end` options. These let you control
  88. * the year range that is generated. It defaults to +-5 years from today.
  89. *
  90. * The `month` option accepts the `name` option which allows you to get month
  91. * names instead of month numbers.
  92. *
  93. * The `hour` option allows you to set the following options:
  94. *
  95. * - `format` option which accepts 12 or 24, allowing
  96. * you to indicate which hour format you want.
  97. * - `start` The hour to start the options at.
  98. * - `end` The hour to stop the options at.
  99. *
  100. * The start and end options are dependent on the format used. If the
  101. * value is out of the start/end range it will not be included.
  102. *
  103. * The `minute` option allows you to define the following options:
  104. *
  105. * - `interval` The interval to round options to.
  106. * - `round` Accepts `up` or `down`. Defines which direction the current value
  107. * should be rounded to match the select options.
  108. *
  109. * @param array $data Data to render with.
  110. * @param \Cake\View\Form\ContextInterface $context The current form context.
  111. * @return string A generated select box.
  112. * @throws \RuntimeException When option data is invalid.
  113. */
  114. public function render(array $data, ContextInterface $context) {
  115. $data += [
  116. 'name' => '',
  117. 'empty' => false,
  118. 'disabled' => null,
  119. 'val' => null,
  120. 'year' => [],
  121. 'month' => [],
  122. 'day' => [],
  123. 'hour' => [],
  124. 'minute' => [],
  125. 'second' => [],
  126. 'meridian' => null,
  127. ];
  128. $selected = $this->_deconstructDate($data['val'], $data);
  129. $timeFormat = isset($data['hour']['format']) ? $data['hour']['format'] : null;
  130. if ($timeFormat === 12 && !isset($data['meridian'])) {
  131. $data['meridian'] = [];
  132. }
  133. if ($timeFormat === 24) {
  134. $data['meridian'] = false;
  135. }
  136. $templateOptions = [];
  137. foreach ($this->_selects as $select) {
  138. if ($data[$select] === false || $data[$select] === null) {
  139. $templateOptions[$select] = '';
  140. unset($data[$select]);
  141. continue;
  142. }
  143. if (!is_array($data[$select])) {
  144. throw \RuntimeException(sprintf(
  145. 'Options for "%s" must be an array|false|null',
  146. $select
  147. ));
  148. }
  149. $method = "_{$select}Select";
  150. $data[$select]['name'] = $data['name'] . "[" . $select . "]";
  151. $data[$select]['val'] = $selected[$select];
  152. if (!isset($data[$select]['empty'])) {
  153. $data[$select]['empty'] = $data['empty'];
  154. }
  155. if (!isset($data[$select]['disabled'])) {
  156. $data[$select]['disabled'] = $data['disabled'];
  157. }
  158. $templateOptions[$select] = $this->{$method}($data[$select], $context);
  159. unset($data[$select]);
  160. }
  161. unset($data['name'], $data['empty'], $data['disabled'], $data['val']);
  162. $templateOptions['attrs'] = $this->_templates->formatAttributes($data);
  163. return $this->_templates->format('dateWidget', $templateOptions);
  164. }
  165. /**
  166. * Deconstructs the passed date value into all time units
  167. *
  168. * @param string|int|array|DateTime $value Value to deconstruct.
  169. * @param array $options Options for conversion.
  170. * @return array
  171. */
  172. protected function _deconstructDate($value, $options) {
  173. if (empty($value)) {
  174. return [
  175. 'year' => '', 'month' => '', 'day' => '',
  176. 'hour' => '', 'minute' => '', 'second' => '',
  177. 'meridian' => '',
  178. ];
  179. }
  180. try {
  181. if (is_string($value)) {
  182. $date = new \DateTime($value);
  183. } elseif (is_bool($value) || $value === null) {
  184. $date = new \DateTime();
  185. } elseif (is_int($value)) {
  186. $date = new \DateTime('@' . $value);
  187. } elseif (is_array($value)) {
  188. $date = new \DateTime();
  189. if (isset($value['year'], $value['month'], $value['day'])) {
  190. $date->setDate($value['year'], $value['month'], $value['day']);
  191. }
  192. if (!isset($value['second'])) {
  193. $value['second'] = 0;
  194. }
  195. if (isset($value['meridian'])) {
  196. $isAm = strtolower($value['meridian']) === 'am';
  197. $value['hour'] = $isAm ? $value['hour'] : $value['hour'] + 12;
  198. }
  199. if (isset($value['hour'], $value['minute'], $value['second'])) {
  200. $date->setTime($value['hour'], $value['minute'], $value['second']);
  201. }
  202. } else {
  203. $date = clone $value;
  204. }
  205. } catch (\Exception $e) {
  206. $date = new \DateTime();
  207. }
  208. if (isset($options['minute']['interval'])) {
  209. $change = $this->_adjustValue($date->format('i'), $options['minute']);
  210. $date->modify($change > 0 ? "+$change minutes" : "$change minutes");
  211. }
  212. return [
  213. 'year' => $date->format('Y'),
  214. 'month' => $date->format('m'),
  215. 'day' => $date->format('d'),
  216. 'hour' => $date->format('H'),
  217. 'minute' => $date->format('i'),
  218. 'second' => $date->format('s'),
  219. 'meridian' => $date->format('a'),
  220. ];
  221. }
  222. /**
  223. * Adjust $value based on rounding settings.
  224. *
  225. * @param int $value The value to adjust.
  226. * @param array $options The options containing interval and possibly round.
  227. * @return int The amount to adjust $value by.
  228. */
  229. protected function _adjustValue($value, $options) {
  230. $options += ['interval' => 1, 'round' => null];
  231. $changeValue = $value * (1 / $options['interval']);
  232. switch ($options['round']) {
  233. case 'up':
  234. $changeValue = ceil($changeValue);
  235. break;
  236. case 'down':
  237. $changeValue = floor($changeValue);
  238. break;
  239. default:
  240. $changeValue = round($changeValue);
  241. }
  242. return ($changeValue * $options['interval']) - $value;
  243. }
  244. /**
  245. * Generates a year select
  246. *
  247. * @param array $options Options list.
  248. * @param \Cake\View\Form\ContextInterface $context The current form context.
  249. * @return string
  250. */
  251. protected function _yearSelect($options, $context) {
  252. $options += [
  253. 'name' => '',
  254. 'val' => null,
  255. 'start' => date('Y', strtotime('-5 years')),
  256. 'end' => date('Y', strtotime('+5 years')),
  257. 'order' => 'desc',
  258. 'options' => []
  259. ];
  260. if (!empty($options['val'])) {
  261. $options['start'] = min($options['val'], $options['start']);
  262. $options['end'] = max($options['val'], $options['end']);
  263. }
  264. if (empty($options['options'])) {
  265. $options['options'] = $this->_generateNumbers($options['start'], $options['end']);
  266. }
  267. if ($options['order'] === 'desc') {
  268. $options['options'] = array_reverse($options['options'], true);
  269. }
  270. unset($options['start'], $options['end'], $options['order']);
  271. return $this->_select->render($options, $context);
  272. }
  273. /**
  274. * Generates a month select
  275. *
  276. * @param array $options The options to build the month select with
  277. * @param \Cake\View\Form\ContextInterface $context The current form context.
  278. * @return string
  279. */
  280. protected function _monthSelect($options, $context) {
  281. $options += [
  282. 'name' => '',
  283. 'names' => false,
  284. 'val' => null,
  285. 'leadingZeroKey' => true,
  286. 'leadingZeroValue' => false
  287. ];
  288. if (empty($options['options'])) {
  289. if ($options['names'] === true) {
  290. $options['options'] = $this->_getMonthNames($options['leadingZeroKey']);
  291. } elseif (is_array($options['names'])) {
  292. $options['options'] = $options['names'];
  293. } else {
  294. $options['options'] = $this->_generateNumbers(1, 12, $options);
  295. }
  296. }
  297. unset($options['leadingZeroKey'], $options['leadingZeroValue'], $options['names']);
  298. return $this->_select->render($options, $context);
  299. }
  300. /**
  301. * Generates a day select
  302. *
  303. * @param array $options The options to generate a day select with.
  304. * @param \Cake\View\Form\ContextInterface $context The current form context.
  305. * @return string
  306. */
  307. protected function _daySelect($options, $context) {
  308. $options += [
  309. 'name' => '',
  310. 'val' => null,
  311. 'leadingZeroKey' => true,
  312. 'leadingZeroValue' => false,
  313. ];
  314. $options['options'] = $this->_generateNumbers(1, 31, $options);
  315. unset($options['names'], $options['leadingZeroKey'], $options['leadingZeroValue']);
  316. return $this->_select->render($options, $context);
  317. }
  318. /**
  319. * Generates a hour select
  320. *
  321. * @param array $options The options to generate an hour select with
  322. * @param \Cake\View\Form\ContextInterface $context The current form context.
  323. * @return string
  324. */
  325. protected function _hourSelect($options, $context) {
  326. $options += [
  327. 'name' => '',
  328. 'val' => null,
  329. 'format' => 24,
  330. 'start' => null,
  331. 'end' => null,
  332. 'leadingZeroKey' => true,
  333. 'leadingZeroValue' => false,
  334. ];
  335. $is24 = $options['format'] == 24;
  336. $defaultStart = $is24 ? 0 : 1;
  337. $defaultEnd = $is24 ? 23 : 12;
  338. $options['start'] = max($defaultStart, $options['start']);
  339. $options['end'] = min($defaultEnd, $options['end']);
  340. if ($options['end'] === null) {
  341. $options['end'] = $defaultEnd;
  342. }
  343. if (!$is24 && $options['val'] > 12) {
  344. $options['val'] = sprintf('%02d', $options['val'] - 12);
  345. }
  346. if (!$is24 && in_array($options['val'], ['00', '0', 0], true)) {
  347. $options['val'] = 12;
  348. }
  349. if (empty($options['options'])) {
  350. $options['options'] = $this->_generateNumbers(
  351. $options['start'],
  352. $options['end'],
  353. $options
  354. );
  355. }
  356. unset(
  357. $options['end'], $options['start'],
  358. $options['format'], $options['leadingZeroKey'],
  359. $options['leadingZeroValue']
  360. );
  361. return $this->_select->render($options, $context);
  362. }
  363. /**
  364. * Generates a minute select
  365. *
  366. * @param array $options The options to generate a minute select with.
  367. * @param \Cake\View\Form\ContextInterface $context The current form context.
  368. * @return string
  369. */
  370. protected function _minuteSelect($options, $context) {
  371. $options += [
  372. 'name' => '',
  373. 'val' => null,
  374. 'interval' => 1,
  375. 'round' => 'up',
  376. 'leadingZeroKey' => true,
  377. 'leadingZeroValue' => true,
  378. ];
  379. $options['interval'] = max($options['interval'], 1);
  380. if (empty($options['options'])) {
  381. $options['options'] = $this->_generateNumbers(0, 59, $options);
  382. }
  383. unset(
  384. $options['leadingZeroKey'],
  385. $options['leadingZeroValue'],
  386. $options['interval'],
  387. $options['round']
  388. );
  389. return $this->_select->render($options, $context);
  390. }
  391. /**
  392. * Generates a second select
  393. *
  394. * @param array $options The options to generate a second select with
  395. * @param \Cake\View\Form\ContextInterface $context The current form context.
  396. * @return string
  397. */
  398. protected function _secondSelect($options, $context) {
  399. $options += [
  400. 'name' => '',
  401. 'val' => null,
  402. 'leadingZeroKey' => true,
  403. 'leadingZeroValue' => true,
  404. 'options' => $this->_generateNumbers(1, 60)
  405. ];
  406. unset($options['leadingZeroKey'], $options['leadingZeroValue']);
  407. return $this->_select->render($options, $context);
  408. }
  409. /**
  410. * Generates a meridian select
  411. *
  412. * @param array $options The options to generate a meridian select with.
  413. * @param \Cake\View\Form\ContextInterface $context The current form context.
  414. * @return string
  415. */
  416. protected function _meridianSelect($options, $context) {
  417. $options += [
  418. 'name' => '',
  419. 'val' => null,
  420. 'options' => ['am' => 'am', 'pm' => 'pm']
  421. ];
  422. return $this->_select->render($options, $context);
  423. }
  424. /**
  425. * Returns a translated list of month names
  426. *
  427. * @param bool $leadingZero Whether to generate month keys with leading zero.
  428. * @return array
  429. */
  430. protected function _getMonthNames($leadingZero = false) {
  431. $months = [
  432. '01' => __d('cake', 'January'),
  433. '02' => __d('cake', 'February'),
  434. '03' => __d('cake', 'March'),
  435. '04' => __d('cake', 'April'),
  436. '05' => __d('cake', 'May'),
  437. '06' => __d('cake', 'June'),
  438. '07' => __d('cake', 'July'),
  439. '08' => __d('cake', 'August'),
  440. '09' => __d('cake', 'September'),
  441. '10' => __d('cake', 'October'),
  442. '11' => __d('cake', 'November'),
  443. '12' => __d('cake', 'December'),
  444. ];
  445. if ($leadingZero === false) {
  446. $i = 1;
  447. foreach ($months as $key => $name) {
  448. $months[$i++] = $name;
  449. unset($months[$key]);
  450. }
  451. }
  452. return $months;
  453. }
  454. /**
  455. * Generates a range of numbers
  456. *
  457. * ### Options
  458. *
  459. * - leadingZeroKey - Set to true to add a leading 0 to single digit keys.
  460. * - leadingZeroValue - Set to true to add a leading 0 to single digit values.
  461. * - interval - The interval to generate numbers for. Defaults to 1.
  462. *
  463. * @param int $start Start of the range of numbers to generate
  464. * @param int $end End of the range of numbers to generate
  465. * @param array $options Options list.
  466. * @return array
  467. */
  468. protected function _generateNumbers($start, $end, $options = []) {
  469. $options += [
  470. 'leadingZeroKey' => true,
  471. 'leadingZeroValue' => true,
  472. 'interval' => 1
  473. ];
  474. $numbers = [];
  475. $i = $start;
  476. while ($i <= $end) {
  477. $key = (string)$i;
  478. $value = (string)$i;
  479. if ($options['leadingZeroKey'] === true) {
  480. $key = sprintf('%02d', $key);
  481. }
  482. if ($options['leadingZeroValue'] === true) {
  483. $value = sprintf('%02d', $value);
  484. }
  485. $numbers[$key] = $value;
  486. $i += $options['interval'];
  487. }
  488. return $numbers;
  489. }
  490. /**
  491. * {@inheritDoc}
  492. */
  493. public function secureFields(array $data) {
  494. $fields = [];
  495. foreach ($this->_selects as $type) {
  496. if ($data[$type] !== false) {
  497. $fields[] = $data['name'] . '[' . $type . ']';
  498. }
  499. }
  500. return $fields;
  501. }
  502. }