Skip to content

Commit f7669be

Browse files
committed
[Form] Add a DateInterval form type
Also add dateinterval widget to twig templates.
1 parent 24e08e9 commit f7669be

11 files changed

+1363
-0
lines changed

src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig

+19
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,25 @@
8787
{% endif %}
8888
{%- endblock time_widget %}
8989

90+
{% block dateinterval_widget %}
91+
{% if widget == 'single_text' %}
92+
{{- block('form_widget_simple') -}}
93+
{% else %}
94+
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-inline')|trim}) %}
95+
<div {{ block('widget_container_attributes') }}>
96+
{{ form_errors(form) }}
97+
{% if with_years %}{{ form_widget(form.years) }}{% endif %}
98+
{% if with_months %}{{ form_widget(form.months) }}{% endif %}
99+
{% if with_weeks %}{{ form_widget(form.weeks) }}{% endif %}
100+
{% if with_days %}{{ form_widget(form.days) }}{% endif %}
101+
{% if with_hours %}{{ form_widget(form.hours) }}{% endif %}
102+
{% if with_minutes %}{{ form_widget(form.minutes) }}{% endif %}
103+
{% if with_seconds %}{{ form_widget(form.seconds) }}{% endif %}
104+
{% if with_invert %}{{ form_widget(form.invert) }}{% endif %}
105+
</div>
106+
{% endif %}
107+
{% endblock dateinterval_widget %}
108+
90109
{% block choice_widget_collapsed -%}
91110
{% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %}
92111
{{- parent() -}}

src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig

+18
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,24 @@
131131
{%- endif -%}
132132
{%- endblock time_widget -%}
133133

134+
{% block dateinterval_widget %}
135+
{% if widget == 'single_text' %}
136+
{{- block('form_widget_simple') -}}
137+
{% else %}
138+
<div {{ block('widget_container_attributes') }}>
139+
{{ form_errors(form) }}
140+
{% if with_years %}{{ form_widget(form.years) }}{% endif %}
141+
{% if with_months %}{{ form_widget(form.months) }}{% endif %}
142+
{% if with_weeks %}{{ form_widget(form.weeks) }}{% endif %}
143+
{% if with_days %}{{ form_widget(form.days) }}{% endif %}
144+
{% if with_hours %}{{ form_widget(form.hours) }}{% endif %}
145+
{% if with_minutes %}{{ form_widget(form.minutes) }}{% endif %}
146+
{% if with_seconds %}{{ form_widget(form.seconds) }}{% endif %}
147+
{% if with_invert %}{{ form_widget(form.invert) }}{% endif %}
148+
</div>
149+
{% endif %}
150+
{% endblock dateinterval_widget %}
151+
134152
{%- block number_widget -%}
135153
{# type="number" doesn't work with floats #}
136154
{%- set type = type|default('text') -%}

src/Symfony/Component/Form/Extension/Core/CoreExtension.php

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ protected function loadTypes()
5151
new Type\ChoiceType($this->choiceListFactory),
5252
new Type\CollectionType(),
5353
new Type\CountryType(),
54+
new Type\DateIntervalType(),
5455
new Type\DateType(),
5556
new Type\DateTimeType(),
5657
new Type\EmailType(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
13+
14+
use Symfony\Component\Form\DataTransformerInterface;
15+
use Symfony\Component\Form\Exception\TransformationFailedException;
16+
use Symfony\Component\Form\Exception\UnexpectedTypeException;
17+
18+
/**
19+
* Transforms between a normalized date interval and an interval string/array.
20+
*
21+
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
22+
*/
23+
class DateIntervalToArrayTransformer implements DataTransformerInterface
24+
{
25+
const YEARS = 'years';
26+
const MONTHS = 'months';
27+
const DAYS = 'days';
28+
const HOURS = 'hours';
29+
const MINUTES = 'minutes';
30+
const SECONDS = 'seconds';
31+
const INVERT = 'invert';
32+
33+
private static $availableFields = array(
34+
self::YEARS => 'y',
35+
self::MONTHS => 'm',
36+
self::DAYS => 'd',
37+
self::HOURS => 'h',
38+
self::MINUTES => 'i',
39+
self::SECONDS => 's',
40+
self::INVERT => 'r',
41+
);
42+
private $fields;
43+
44+
/**
45+
* @param string[] $fields The date fields
46+
* @param bool $pad Whether to use padding
47+
*/
48+
public function __construct(array $fields = null, $pad = false)
49+
{
50+
if (null === $fields) {
51+
$fields = array('years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert');
52+
}
53+
$this->fields = $fields;
54+
$this->pad = (bool) $pad;
55+
}
56+
57+
/**
58+
* Transforms a normalized date interval into an interval array.
59+
*
60+
* @param \DateInterval $dateInterval Normalized date interval.
61+
*
62+
* @return array Interval array.
63+
*
64+
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
65+
*/
66+
public function transform($dateInterval)
67+
{
68+
if (null === $dateInterval) {
69+
return array_intersect_key(
70+
array(
71+
'years' => '',
72+
'months' => '',
73+
'weeks' => '',
74+
'days' => '',
75+
'hours' => '',
76+
'minutes' => '',
77+
'seconds' => '',
78+
'invert' => false,
79+
),
80+
array_flip($this->fields)
81+
);
82+
}
83+
if (!$dateInterval instanceof \DateInterval) {
84+
throw new UnexpectedTypeException($dateInterval, '\DateInterval');
85+
}
86+
$result = array();
87+
foreach (self::$availableFields as $field => $char) {
88+
$result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char));
89+
}
90+
if (in_array('weeks', $this->fields, true)) {
91+
$result['weeks'] = 0;
92+
if (isset($result['days']) && (int) $result['days'] >= 7) {
93+
$result['weeks'] = (string) floor($result['days'] / 7);
94+
$result['days'] = (string) ($result['days'] % 7);
95+
}
96+
}
97+
$result['invert'] = '-' === $result['invert'];
98+
$result = array_intersect_key($result, array_flip($this->fields));
99+
100+
return $result;
101+
}
102+
103+
/**
104+
* Transforms an interval array into a normalized date interval.
105+
*
106+
* @param array $value Interval array
107+
*
108+
* @return \DateInterval Normalized date interval
109+
*
110+
* @throws UnexpectedTypeException If the given value is not an array.
111+
* @throws TransformationFailedException If the value could not be transformed.
112+
*/
113+
public function reverseTransform($value)
114+
{
115+
if (null === $value) {
116+
return;
117+
}
118+
if (!is_array($value)) {
119+
throw new UnexpectedTypeException($value, 'array');
120+
}
121+
if ('' === implode('', $value)) {
122+
return;
123+
}
124+
$emptyFields = array();
125+
foreach ($this->fields as $field) {
126+
if (!isset($value[$field])) {
127+
$emptyFields[] = $field;
128+
}
129+
}
130+
if (count($emptyFields) > 0) {
131+
throw new TransformationFailedException(sprintf('The fields "%s" should not be empty', implode('", "', $emptyFields)));
132+
}
133+
if (isset($value['invert']) && !is_bool($value['invert'])) {
134+
throw new TransformationFailedException('The value of "invert" must be boolean');
135+
}
136+
foreach (self::$availableFields as $field => $char) {
137+
if ($field !== 'invert' && isset($value[$field]) && !ctype_digit((string) $value[$field])) {
138+
throw new TransformationFailedException(sprintf('This amount of "%s" is invalid', $field));
139+
}
140+
}
141+
try {
142+
if (!empty($value['weeks'])) {
143+
$interval = sprintf(
144+
'P%sY%sM%sWT%sH%sM%sS',
145+
empty($value['years']) ? '0' : $value['years'],
146+
empty($value['months']) ? '0' : $value['months'],
147+
empty($value['weeks']) ? '0' : $value['weeks'],
148+
empty($value['hours']) ? '0' : $value['hours'],
149+
empty($value['minutes']) ? '0' : $value['minutes'],
150+
empty($value['seconds']) ? '0' : $value['seconds']
151+
);
152+
} else {
153+
$interval = sprintf(
154+
'P%sY%sM%sDT%sH%sM%sS',
155+
empty($value['years']) ? '0' : $value['years'],
156+
empty($value['months']) ? '0' : $value['months'],
157+
empty($value['days']) ? '0' : $value['days'],
158+
empty($value['hours']) ? '0' : $value['hours'],
159+
empty($value['minutes']) ? '0' : $value['minutes'],
160+
empty($value['seconds']) ? '0' : $value['seconds']
161+
);
162+
}
163+
$dateInterval = new \DateInterval($interval);
164+
if (isset($value['invert'])) {
165+
$dateInterval->invert = $value['invert'] ? 1 : 0;
166+
}
167+
} catch (\Exception $e) {
168+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
169+
}
170+
171+
return $dateInterval;
172+
}
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Form\Extension\Core\DataTransformer;
13+
14+
use Symfony\Component\Form\DataTransformerInterface;
15+
use Symfony\Component\Form\Exception\TransformationFailedException;
16+
use Symfony\Component\Form\Exception\UnexpectedTypeException;
17+
18+
/**
19+
* Transforms between a date string and a DateInterval object.
20+
*
21+
* @author Steffen Roßkamp <steffen.rosskamp@gimmickmedia.de>
22+
*/
23+
class DateIntervalToStringTransformer implements DataTransformerInterface
24+
{
25+
private $format;
26+
private $parseSigned;
27+
28+
/**
29+
* Transforms a \DateInterval instance to a string.
30+
*
31+
* @see \DateInterval::format() for supported formats
32+
*
33+
* @param string $format The date format
34+
* @param bool $parseSigned Whether to parse as a signed interval
35+
*/
36+
public function __construct($format = 'P%yY%mM%dDT%hH%iM%sS', $parseSigned = false)
37+
{
38+
$this->format = $format;
39+
$this->parseSigned = $parseSigned;
40+
}
41+
42+
/**
43+
* Transforms a DateInterval object into a date string with the configured format.
44+
*
45+
* @param \DateInterval $value A DateInterval object
46+
*
47+
* @return string An ISO 8601 or relative date string like date interval presentation
48+
*
49+
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
50+
*/
51+
public function transform($value)
52+
{
53+
if (null === $value) {
54+
return '';
55+
}
56+
if (!$value instanceof \DateInterval) {
57+
throw new UnexpectedTypeException($value, '\DateInterval');
58+
}
59+
60+
return $value->format($this->format);
61+
}
62+
63+
/**
64+
* Transforms a date string in the configured format into a DateInterval object.
65+
*
66+
* @param string $value An ISO 8601 or date string like date interval presentation
67+
*
68+
* @return \DateInterval An instance of \DateInterval
69+
*
70+
* @throws UnexpectedTypeException If the given value is not a string.
71+
* @throws TransformationFailedException If the date interval could not be parsed.
72+
*/
73+
public function reverseTransform($value)
74+
{
75+
if (null === $value) {
76+
return;
77+
}
78+
if (!is_string($value)) {
79+
throw new UnexpectedTypeException($value, 'string');
80+
}
81+
if ('' === $value) {
82+
return;
83+
}
84+
if (!$this->isISO8601($value)) {
85+
throw new TransformationFailedException('Non ISO 8601 date strings are not supported yet');
86+
}
87+
$valuePattern = '/^'.preg_replace('/%([yYmMdDhHiIsSwW])(\w)/', '(?P<$1>\d+)$2', $this->format).'$/';
88+
if (!preg_match($valuePattern, $value)) {
89+
throw new TransformationFailedException(sprintf('Value "%s" contains intervals not accepted by format "%s".', $value, $this->format));
90+
}
91+
try {
92+
$dateInterval = new \DateInterval($value);
93+
} catch (\Exception $e) {
94+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
95+
}
96+
97+
return $dateInterval;
98+
}
99+
100+
private function isISO8601($string)
101+
{
102+
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);
103+
}
104+
}

0 commit comments

Comments
 (0)