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

Skip to content

Commit 39ac6de

Browse files
committed
[Form] Add a DateInterval form type
Also add dateinterval widget to twig templates.
1 parent 6f05632 commit 39ac6de

11 files changed

+1378
-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 %}
< 57A0 /td>
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/Component/Form/Extension/Core/CoreExtension.php

Lines changed: 1 addition & 0 deletions
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(),
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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+
* Constructor.
46+
*
47+
* @param array $fields The date fields
48+
* @param bool $pad Whether to use padding
49+
*/
50+
public function __construct(array $fields = null, $pad = false)
51+
{
52+
if (null === $fields) {
53+
$fields = array('years', 'months', 'days', 'hours', 'minutes', 'seconds', 'invert');
54+
}
55+
$this->fields = $fields;
56+
$this->pad = (bool) $pad;
57+
}
58+
59+
/**
60+
* Transforms a normalized date interval into an interval array.
61+
*
62+
* @param \DateInterval $dateInterval Normalized date interval.
63+
*
64+
* @return array Interval array.
65+
*
66+
* @throws UnexpectedTypeException If the given value is not a \DateInterval instance.
67+
*/
68+
public function transform($dateInterval)
69+
{
70+
if (null === $dateInterval) {
71+
return array_intersect_key(
72+
array(
73+
'years' => '',
74+
'months' => '',
75+
'weeks' => '',
76+
'days' => '',
77+
'hours' => '',
78+
'minutes' => '',
79+
'seconds' => '',
80+
'invert' => false,
81+
),
82+
array_flip($this->fields)
83+
);
84+
}
85+
if (!$dateInterval instanceof \DateInterval) {
86+
throw new UnexpectedTypeException($dateInterval, '\DateInterval');
87+
}
88+
$result = array();
89+
foreach (self::$availableFields as $field => $char) {
90+
$result[$field] = $dateInterval->format('%'.($this->pad ? strtoupper($char) : $char));
91+
}
92+
if (in_array('weeks', $this->fields, true)) {
93+
$result['weeks'] = 0;
94+
if (isset($result['days']) && (int) $result['days'] >= 7) {
95+
$result['weeks'] = (string) floor($result['days'] / 7);
96+
$result['days'] = (string) ($result['days'] % 7);
97+
}
98+
}
99+
$result['invert'] = '-' === $result['invert'];
100+
$result = array_intersect_key($result, array_flip($this->fields));
101+
102+
return $result;
103+
}
104+
105+
/**
106+
* Transforms an interval array into a normalized date interval.
107+
*
108+
* @param array $value Interval array
109+
*
110+
* @return \DateInterval Normalized date interval
111+
*
112+
* @throws UnexpectedTypeException If the given value is not an array.
113+
* @throws TransformationFailedException If the value could not be transformed.
114+
*/
115+
public function reverseTransform($value)
116+
{
117+
if (null === $value) {
118+
return;
119+
}
120+
if (!is_array($value)) {
121+
throw new UnexpectedTypeException($value, 'array');
122+
}
123+
if ('' === implode('', $value)) {
124+
return;
125+
}
126+
$emptyFields = array();
127+
foreach ($this->fields as $field) {
128+
if (!isset($value[$field])) {
129+
$emptyFields[] = $field;
130+
}
131+
}
132+
if (count($emptyFields) > 0) {
133+
throw new TransformationFailedException(sprintf('The fields "%s" should not be empty', implode('", "', $emptyFields)));
134+
}
135+
if (isset($value['invert']) && !is_bool($value['invert'])) {
136+
throw new UnexpectedTypeException($value['invert'], 'boolean');
137+
}
138+
foreach (self::$availableFields as $field => $char) {
139+
if ($field !== 'invert' && isset($value[$field]) && !ctype_digit((string) $value[$field])) {
140+
throw new TransformationFailedException(sprintf('This amount of "%s" is invalid', $field));
141+
}
142+
}
143+
try {
144+
if (!empty($value['weeks'])) {
145+
$interval = sprintf(
146+
'P%sY%sM%sWT%sH%sM%sS',
147+
empty($value['years']) ? '0' : $value['years'],
148+
empty($value['months']) ? '0' : $value['months'],
149+
empty($value['weeks']) ? '0' : $value['weeks'],
150+
empty($value['hours']) ? '0' : $value['hours'],
151+
empty($value['minutes']) ? '0' : $value['minutes'],
152+
empty($value['seconds']) ? '0' : $value['seconds']
153+
);
154+
} else {
155+
$interval = sprintf(
156+
'P%sY%sM%sDT%sH%sM%sS',
157+
empty($value['years']) ? '0' : $value['years'],
158+
empty($value['months']) ? '0' : $value['months'],
159+
empty($value['days']) ? '0' : $value['days'],
160+
empty($value['hours']) ? '0' : $value['hours'],
161+
empty($value['minutes']) ? '0' : $value['minutes'],
162+
empty($value['seconds']) ? '0' : $value['seconds']
163+
);
164+
}
165+
$dateInterval = new \DateInterval($interval);
166+
if (!empty($value['invert'])) {
167+
$dateInterval->invert = $value['invert'] ? 1 : 0;
168+
}
169+
} catch (\Exception $e) {
170+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
171+
}
172+
173+
return $dateInterval;
174+
}
175+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
/**
101+
* Checks if a string is a valid ISO 8601 duration string.
102+
*
103+
* @param string $string A string
104+
*
105+
* @return int
106+
*/
107+
private function isISO8601($string)
108+
{
109+
return preg_match(
110+
'/^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)?)?$/',
111+
$string
112+
);
113+
}
114+
}

0 commit comments

Comments
 (0)
0