Skip to content

Commit

Permalink
feature #16809 [Form][FrameworkBundle][Bridge] Add a DateInterval for…
Browse files Browse the repository at this point in the history
…m type (MisatoTremor)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[Form][FrameworkBundle][Bridge] Add a DateInterval form type

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #13389
| License       | MIT
| Doc PR        | symfony/symfony-docs#4817

Replaces #15030

Commits
-------

f7669be [Form] Add a DateInterval form type Also add dateinterval widget to twig templates.
  • Loading branch information
fabpot committed Jun 22, 2016
2 parents 24e08e9 + f7669be commit 1298ce5
Show file tree
Hide file tree
Showing 11 changed files with 1,363 additions and 0 deletions.
Expand Up @@ -87,6 +87,25 @@
{% endif %}
{%- endblock time_widget %}

{% block dateinterval_widget %}
{% if widget == 'single_text' %}
{{- block('form_widget_simple') -}}
{% else %}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) %}
<div {{ block('widget_container_attributes') }}>
{{ form_errors(form) }}
{% if with_years %}{{ form_widget(form.years) }}{% endif %}
{% if with_months %}{{ form_widget(form.months) }}{% endif %}
{% if with_weeks %}{{ form_widget(form.weeks) }}{% endif %}
{% if with_days %}{{ form_widget(form.days) }}{% endif %}
{% if with_hours %}{{ form_widget(form.hours) }}{% endif %}
{% if with_minutes %}{{ form_widget(form.minutes) }}{% endif %}
{% if with_seconds %}{{ form_widget(form.seconds) }}{% endif %}
{% if with_invert %}{{ form_widget(form.invert) }}{% endif %}
</div>
{% endif %}
{% endblock dateinterval_widget %}

{% block choice_widget_collapsed -%}
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %}
{{- parent() -}}
Expand Down
Expand Up @@ -131,6 +131,24 @@
{%- endif -%}
{%- endblock time_widget -%}

{% block dateinterval_widget %}
{% if widget == 'single_text' %}
{{- block('form_widget_simple') -}}
{% else %}
<div {{ block('widget_container_attributes') }}>
{{ form_errors(form) }}
{% if with_years %}{{ form_widget(form.years) }}{% endif %}
{% if with_months %}{{ form_widget(form.months) }}{% endif %}
{% if with_weeks %}{{ form_widget(form.weeks) }}{% endif %}
{% if with_days %}{{ form_widget(form.days) }}{% endif %}
{% if with_hours %}{{ form_widget(form.hours) }}{% endif %}
{% if with_minutes %}{{ form_widget(form.minutes) }}{% endif %}
{% if with_seconds %}{{ form_widget(form.seconds) }}{% endif %}
{% if with_invert %}{{ form_widget(form.invert) }}{% endif %}
</div>
{% endif %}
{% endblock dateinterval_widget %}

{%- block number_widget -%}
{# type="number" doesn't work with floats #}
{%- set type = type|default('text') -%}
Expand Down
Expand Up @@ -51,6 +51,7 @@ protected function loadTypes()
new Type\ChoiceType($this->choiceListFactory),
new Type\CollectionType(),
new Type\CountryType(),
new Type\DateIntervalType(),
new Type\DateType(),
new Type\DateTimeType(),
new Type\EmailType(),
Expand Down
@@ -0,0 +1,173 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\Core\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;

/**
* Transforms between a normalized date interval and an interval string/array.
*
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
*/
class DateIntervalToArrayTransformer implements DataTransformerInterface
{
const YEARS = 'years';
const MONTHS = 'months';
const DAYS = 'days';
const HOURS = 'hours';
const MINUTES = 'minutes';
const SECONDS = 'seconds';
const INVERT = 'invert';

private static $availableFields = array(
self::YEARS => 'y',
self::MONTHS => 'm',
self::DAYS => 'd',
self::HOURS => 'h',
self::MINUTES => 'i',
self::SECONDS => 's',
self::INVERT => 'r',
);
private $fields;

/**
* @param string[] $fields The date fields
* @param bool $pad Whether to use padding
*/
public function __construct(array $fields = null, $pad = false)
{
if (null === $fields) {
$fields = array('years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert');
}
$this->fields = $fields;
$this->pad = (bool) $pad;
}

/**
* Transforms a normalized date interval into an interval array.
*
* @param \DateInterval $dateInterval Normalized date interval.
*
* @return array Interval array.
*
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
*/
public function transform($dateInterval)
{
if (null === $dateInterval) {
return array_intersect_key(
array(
'years' => '',
'months' => '',
'weeks' => '',
'days' => '',
'hours' => '',
'minutes' => '',
'seconds' => '',
'invert' => false,
),
array_flip($this->fields)
);
}
if (!$dateInterval instanceof \DateInterval) {
throw new UnexpectedTypeException($dateInterval, '\DateInterval');
}
$result = array();
foreach (self::$availableFields as $field => $char) {
$result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char));
}
if (in_array('weeks', $this->fields, true)) {
$result['weeks'] = 0;
if (isset($result['days']) && (int) $result['days'] >= 7) {
$result['weeks'] = (string) floor($result['days'] / 7);
$result['days'] = (string) ($result['days'] % 7);
}
}
$result['invert'] = '-' === $result['invert'];
$result = array_intersect_key($result, array_flip($this->fields));

return $result;
}

/**
* Transforms an interval array into a normalized date interval.
*
* @param array $value Interval array
*
* @return \DateInterval Normalized date interval
*
* @throws UnexpectedTypeException If the given value is not an array.
* @throws TransformationFailedException If the value could not be transformed.
*/
public function reverseTransform($value)
{
if (null === $value) {
return;
}
if (!is_array($value)) {
throw new UnexpectedTypeException($value, 'array');
}
if ('' === implode('', $value)) {
return;
}
$emptyFields = array();
foreach ($this->fields as $field) {
if (!isset($value[$field])) {
$emptyFields[] = $field;
}
}
if (count($emptyFields) > 0) {
throw new TransformationFailedException(sprintf('The fields "%s" should not be empty', implode('", "', $emptyFields)));
}
if (isset($value['invert']) && !is_bool($value['invert'])) {
throw new TransformationFailedException('The value of "invert" must be boolean');
}
foreach (self::$availableFields as $field => $char) {
if ($field !== 'invert' && isset($value[$field]) && !ctype_digit((string) $value[$field])) {
throw new TransformationFailedException(sprintf('This amount of "%s" is invalid', $field));
}
}
try {
if (!empty($value['weeks'])) {
$interval = sprintf(
'P%sY%sM%sWT%sH%sM%sS',
empty($value['years']) ? '0' : $value['years'],
empty($value['months']) ? '0' : $value['months'],
empty($value['weeks']) ? '0' : $value['weeks'],
empty($value['hours']) ? '0' : $value['hours'],
empty($value['minutes']) ? '0' : $value['minutes'],
empty($value['seconds']) ? '0' : $value['seconds']
);
} else {
$interval = sprintf(
'P%sY%sM%sDT%sH%sM%sS',
empty($value['years']) ? '0' : $value['years'],
empty($value['months']) ? '0' : $value['months'],
empty($value['days']) ? '0' : $value['days'],
empty($value['hours']) ? '0' : $value['hours'],
empty($value['minutes']) ? '0' : $value['minutes'],
empty($value['seconds']) ? '0' : $value['seconds']
);
}
$dateInterval = new \DateInterval($interval);
if (isset($value['invert'])) {
$dateInterval->invert = $value['invert'] ? 1 : 0;
}
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}

return $dateInterval;
}
}
@@ -0,0 +1,104 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Form\Extension\Core\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;

/**
* Transforms between a date string and a DateInterval object.
*
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
*/
class DateIntervalToStringTransformer implements DataTransformerInterface
{
private $format;
private $parseSigned;

/**
* Transforms a \DateInterval instance to a string.
*
* @see \DateInterval::format() for supported formats
*
* @param string $format The date format
* @param bool $parseSigned Whether to parse as a signed interval
*/
public function __construct($format = 'P%yY%mM%dDT%hH%iM%sS', $parseSigned = false)
{
$this->format = $format;
$this->parseSigned = $parseSigned;
}

/**
* Transforms a DateInterval object into a date string with the configured format.
*
* @param \DateInterval $value A DateInterval object
*
* @return string An ISO 8601 or relative date string like date interval presentation
*
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
*/
public function transform($value)
{
if (null === $value) {
return '';
}
if (!$value instanceof \DateInterval) {
throw new UnexpectedTypeException($value, '\DateInterval');
}

return $value->format($this->format);
}

/**
* Transforms a date string in the configured format into a DateInterval object.
*
* @param string $value An ISO 8601 or date string like date interval presentation
*
* @return \DateInterval An instance of \DateInterval
*
* @throws UnexpectedTypeException If the given value is not a string.
* @throws TransformationFailedException If the date interval could not be parsed.
*/
public function reverseTransform($value)
{
if (null === $value) {
return;
}
if (!is_string($value)) {
throw new UnexpectedTypeException($value, 'string');
}
if ('' === $value) {
return;
}
if (!$this->isISO8601($value)) {
throw new TransformationFailedException('Non ISO 8601 date strings are not supported yet');
}
$valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $this->format).'$/';
if (!preg_match($valuePattern, $value)) {
throw new TransformationFailedException(sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format));
}
try {
$dateInterval = new \DateInterval($value);
} catch (\Exception $e) {
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
}

return $dateInterval;
}

private function isISO8601($string)
{
return preg_match('/^P(?=\w*(?:\d|%\w))(?:\d+Y|%[yY]Y)?(?:\d+M|%[mM]M)?(?:(?:\d+D|%[dD]D)|(?:\d+W|%[wW]W))?(?:T(?:\d+H|[hH]H)?(?:\d+M|[iI]M)?(?:\d+S|[sS]S)?)?$/', $string);
}
}

0 comments on commit 1298ce5

Please sign in to comment.