timeType = IntlDateFormatter::LONG; } /** * Invoke helper as function * * Proxies to {@link render()}. * * @template T as null|ElementInterface * @psalm-param T $element * @psalm-return (T is null ? self : string) * @return string|self */ public function __invoke( ?ElementInterface $element = null, int $dateType = IntlDateFormatter::LONG, int $timeType = IntlDateFormatter::LONG, ?string $locale = null ) { if (! $element) { return $this; } $this->setDateType($dateType); $this->setTimeType($timeType); if ($locale !== null) { $this->setLocale($locale); } return $this->render($element); } /** * Render a date element that is composed of six selects * * @throws Exception\InvalidArgumentException * @throws Exception\DomainException */ public function render(ElementInterface $element): string { if (! $element instanceof DateTimeSelectElement) { throw new Exception\InvalidArgumentException(sprintf( '%s requires that the element is of type Laminas\Form\Element\DateTimeSelect', __METHOD__ )); } $name = $element->getName(); if ($name === null || $name === '') { throw new Exception\DomainException(sprintf( '%s requires that the element has an assigned name; none discovered', __METHOD__ )); } $shouldRenderDelimiters = $element->shouldRenderDelimiters(); $selectHelper = $this->getSelectElementHelper(); $pattern = $this->parsePattern($shouldRenderDelimiters); $daysOptions = $this->getDaysOptions($pattern['day']); $monthsOptions = $this->getMonthsOptions($pattern['month']); $yearOptions = $this->getYearsOptions($element->getMinYear(), $element->getMaxYear()); $hourOptions = $this->getHoursOptions($pattern['hour']); $minuteOptions = $this->getMinutesOptions($pattern['minute']); $dayElement = $element->getDayElement()->setValueOptions($daysOptions); $monthElement = $element->getMonthElement()->setValueOptions($monthsOptions); $yearElement = $element->getYearElement()->setValueOptions($yearOptions); $hourElement = $element->getHourElement()->setValueOptions($hourOptions); $minuteElement = $element->getMinuteElement()->setValueOptions($minuteOptions); if ($element->shouldCreateEmptyOption()) { $dayElement->setEmptyOption(''); $yearElement->setEmptyOption(''); $monthElement->setEmptyOption(''); $hourElement->setEmptyOption(''); $minuteElement->setEmptyOption(''); } $data = []; $data[$pattern['day']] = $selectHelper->render($dayElement); $data[$pattern['month']] = $selectHelper->render($monthElement); $data[$pattern['year']] = $selectHelper->render($yearElement); $data[$pattern['hour']] = $selectHelper->render($hourElement); $data[$pattern['minute']] = $selectHelper->render($minuteElement); if ($element->shouldShowSeconds() && array_key_exists('second', $pattern)) { $secondOptions = $this->getSecondsOptions($pattern['second']); $secondElement = $element->getSecondElement()->setValueOptions($secondOptions); if ($element->shouldCreateEmptyOption()) { $secondElement->setEmptyOption(''); } $data[$pattern['second']] = $selectHelper->render($secondElement); } else { unset($pattern['second']); if ($shouldRenderDelimiters) { unset($pattern[4]); } } $markup = ''; foreach ($pattern as $key => $value) { // Delimiter if (is_numeric($key)) { $markup .= $value; } else { $markup .= $data[$value]; } } return trim($markup); } /** * @return $this */ public function setTimeType(int $timeType) { // The FULL format uses values that are not used if ($timeType === IntlDateFormatter::FULL) { $timeType = IntlDateFormatter::LONG; } if ($this->timeType !== $timeType) { $this->pattern = null; } $this->timeType = $timeType; return $this; } public function getTimeType(): int { return $this->timeType; } /** * Override to also get time part */ public function getPattern(): string { if ($this->pattern === null) { $intl = new IntlDateFormatter($this->getLocale(), $this->dateType, $this->timeType); // remove time zone format character $pattern = rtrim($intl->getPattern(), ' z'); $this->pattern = $pattern; } return $this->pattern; } /** * Parse the pattern * * @return array */ protected function parsePattern(bool $renderDelimiters = true): array { $pattern = $this->getPattern(); // are there any non-latin characters in the pattern? $count = preg_match_all('/[^\p{Latin}\s\-,.:\/\\\'\[\]\(\)]+/u', $pattern, $matches); if ($count) { // put single quotes around these characters foreach ($matches[0] as $match) { $pattern = str_replace($match, "'$match'", $pattern); } // remove double quotes, if there were already quotes around them $pattern = str_replace("''", "'", $pattern); } $pregResult = preg_split( "/([ \-,.:\/]*'.*?'[ \-,.:\/]*)|([ \-,.:\/]+)/", $pattern, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); $result = []; foreach ($pregResult as $value) { $noDelimiter = str_contains($value, "'") === false; if ($noDelimiter && stripos($value, 'd') !== false) { $result['day'] = $value; } elseif ($noDelimiter && str_contains($value, 'M')) { $result['month'] = $value; } elseif ($noDelimiter && stripos($value, 'y') !== false) { $result['year'] = $value; } elseif ($noDelimiter && stripos($value, 'h') !== false) { $result['hour'] = $value; } elseif ($noDelimiter && str_contains($value, 'm')) { $result['minute'] = $value; } elseif ($noDelimiter && str_contains($value, 's')) { $result['second'] = $value; } elseif ($noDelimiter && stripos($value, 'a') !== false) { // ignore ante/post meridiem marker continue; } elseif ($renderDelimiters) { $result[] = str_replace("'", '', $value); } } return $result; } /** * Create a key => value options for days * * @param string $pattern Pattern to use for days * @return array */ protected function getDaysOptions(string $pattern): array { $valueFormatter = new IntlDateFormatter( $this->getLocale(), IntlDateFormatter::NONE, IntlDateFormatter::NONE, null, null, $pattern ); $date = new DateTime('1970-01-01'); $result = []; for ($day = 1; $day <= 31; $day++) { $key = $date->format('d'); $value = $valueFormatter->format($date); $result[$key] = $value; $date->modify('+1 day'); } return $result; } /** * Create a key => value options for hours * * @param string $pattern Pattern to use for hours * @return array */ protected function getHoursOptions(string $pattern): array { $valueFormatter = new IntlDateFormatter( $this->getLocale(), IntlDateFormatter::NONE, IntlDateFormatter::NONE, null, null, $pattern ); $date = new DateTime('1970-01-01 00:00:00'); $result = []; for ($hour = 0; $hour <= 23; $hour++) { $key = $date->format('H'); $value = $valueFormatter->format($date); $result[$key] = $value; $date->modify('+1 hour'); } return $result; } /** * Create a key => value options for minutes * * @param string $pattern Pattern to use for minutes * @return array */ protected function getMinutesOptions(string $pattern): array { $valueFormatter = new IntlDateFormatter( $this->getLocale(), IntlDateFormatter::NONE, IntlDateFormatter::NONE, null, null, $pattern ); $date = new DateTime('1970-01-01 00:00:00'); $result = []; for ($min = 0; $min <= 59; $min++) { $key = $date->format('i'); $value = $valueFormatter->format($date); $result[$key] = $value; $date->modify('+1 minute'); } return $result; } /** * Create a key => value options for seconds * * @param string $pattern Pattern to use for seconds * @return array */ protected function getSecondsOptions(string $pattern): array { $valueFormatter = new IntlDateFormatter( $this->getLocale(), IntlDateFormatter::NONE, IntlDateFormatter::NONE, null, null, $pattern ); $date = new DateTime('1970-01-01 00:00:00'); $result = []; for ($sec = 0; $sec <= 59; $sec++) { $key = $date->format('s'); $value = $valueFormatter->format($date); $result[$key] = $value; $date->modify('+1 second'); } return $result; } }