8000 [Form] Add a DateInterval form type · symfony/symfony@3586785 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3586785

Browse files
committed
[Form] Add a DateInterval form type
Also add dateinterval widget to twig templates.
1 parent 582f475 commit 3586785

File tree

12 files changed

+1401
-0
lines changed

12 files changed

+1401
-0
lines changed

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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 18 additions & 0 deletions
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/Bundle/FrameworkBundle/Resources/config/form.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
<service id="form.type.datetime" class="Symfony\Component\Form\Extension\Core\Type\DateTimeType">
7474
<tag name="form.type" />
7575
</service>
76+
<service id="form.type.dateinterval" class="Symfony\Component\Form\Extension\Core\Type\DateIntervalType">
77+
<tag name="form.type" />
78+
</service>
7679
<service id="form.type.email" class="Symfony\Component\Form\Extension\Core\Type\EmailType">
7780
<tag name="form.type" />
7881
</service>

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

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

0 commit comments

Comments
 (0)
0