Skip to content

Commit 45b92dd

Browse files
author
dFayet
committed
Add new Form WeekType
1 parent 19811b8 commit 45b92dd

File tree

10 files changed

+329
-11
lines changed

10 files changed

+329
-11
lines changed

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

+11
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,17 @@
255255
{{ block('form_widget_simple') }}
256256
{%- endblock color_widget -%}
257257

258+
{%- block week_widget -%}
259+
{%- if widget == 'single_text' -%}
260+
{{ block('form_widget_simple') }}
261+
{%- else -%}
262+
{%- set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} -%}
263+
<div {{ block('widget_container_attributes') }}>
264+
{{ form_widget(form.year, vars) }}-{{ form_widget(form.week, vars) }}
265+
</div>
266+
{%- endif -%}
267+
{%- endblock week_widget -%}
268+
258269
{# Labels #}
259270

260271
{%- block form_label -%}

src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php

+15
Original file line numberDiff line numberDiff line change
@@ -2719,6 +2719,21 @@ public function testColor()
27192719
[@name="name"]
27202720
[@class="my&class form-control"]
27212721
[@value="#0000ff"]
2722+
'
2723+
);
2724+
}
2725+
2726+
public function testWeek()
2727+
{
2728+
$form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01');
2729+
2730+
$this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']],
2731+
'/input
2732+
[@type="week"]
2733+
[@name="holidays"]
2734+
[@class="my&class form-control"]
2735+
[@value="1970-W01"]
2736+
[not(@maxlength)]
27222737
'
27232738
);
27242739
}

src/Symfony/Bridge/Twig/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"symfony/asset": "^3.4|^4.0|^5.0",
2525
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
2626
"symfony/finder": "^3.4|^4.0|^5.0",
27-
"symfony/form": "^4.3|^5.0",
27+
"symfony/form": "^4.4|^5.0",
2828
"symfony/http-foundation": "^4.3|^5.0",
2929
"symfony/http-kernel": "^3.4|^4.0|^5.0",
3030
"symfony/mime": "^4.3|^5.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?php echo $view['form']->block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'week']);

src/Symfony/Component/Form/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
CHANGELOG
22
=========
33

4+
4.3.0
5+
-----
6+
* add new `WeekType`
7+
48
4.3.0
59
-----
610

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

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ protected function loadTypes()
8383
new Type\CurrencyType(),
8484
new Type\TelType(),
8585
new Type\ColorType(),
86+
new Type\WeekType(),
8687
];
8788
}
8889

src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToArrayTransformer.php

+30-9
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function transform($dateTime)
5858
return array_intersect_key([
5959
'year' => '',
6060
'month' => '',
61+
'week' => '',
6162
'day' => '',
6263
'hour' => '',
6364
'minute' => '',
@@ -80,6 +81,7 @@ public function transform($dateTime)
8081
$result = array_intersect_key([
8182
'year' => $dateTime->format('Y'),
8283
'month' => $dateTime->format('m'),
84+
'week' => $dateTime->format('W'),
8385
'day' => $dateTime->format('d'),
8486
'hour' => $dateTime->format('H'),
8587
'minute' => $dateTime->format('i'),
@@ -146,6 +148,16 @@ public function reverseTransform($value)
146148
throw new TransformationFailedException('This year is invalid');
147149
}
148150

151+
if (isset($value['week'])) {
152+
if (0 !== strpos($value['week'], 'W') || !ctype_digit($weekNumber = ltrim($value['week'], 'W'))) {
153+
throw new TransformationFailedException('This week is invalid');
154+
}
155+
156+
if (date('W', strtotime('28th December '. $value['year'] ?? '1970')) < $weekNumber) {
157+
throw new TransformationFailedException(sprintf('Week No. %d does not exists for year %d', $weekNumber, $value['year'] ?? '1970'));
158+
}
159+
}
160+
149161
if (!empty($value['month']) && !empty($value['day']) && !empty($value['year']) && false === checkdate($value['month'], $value['day'], $value['year'])) {
150162
throw new TransformationFailedException('This is an invalid date');
151163
}
@@ -163,15 +175,24 @@ public function reverseTransform($value)
163175
}
164176

165177
try {
166-
$dateTime = new \DateTime(sprintf(
167-
'%s-%s-%s %s:%s:%s',
168-
empty($value['year']) ? '1970' : $value['year'],
169-
empty($value['month']) ? '1' : $value['month'],
170-
empty($value['day']) ? '1' : $value['day'],
171-
empty($value['hour']) ? '0' : $value['hour'],
172-
empty($value['minute']) ? '0' : $value['minute'],
173-
empty($value['second']) ? '0' : $value['second']
174-
),
178+
if (in_array('week', $this->fields)) {
179+
$date = sprintf(
180+
'%s-%s',
181+
empty($value['year']) ? '1970' : $value['year'],
182+
empty($value['week']) ? 'W01' : $value['week']
183+
);
184+
} else {
185+
$date = sprintf(
186+
'%s-%s-%s %s:%s:%s',
187+
empty($value['year']) ? '1970' : $value['year'],
188+
empty($value['month']) ? '1' : $value['month'],
189+
empty($value['day']) ? '1' : $value['day'],
190+
empty($value['hour']) ? '0' : $value['hour'],
191+
empty($value['minute']) ? '0' : $value['minute'],
192+
empty($value['second']) ? '0' : $value['second']
193+
);
194+
}
195+
$dateTime = new \DateTime($date,
175196
new \DateTimeZone($this->outputTimezone)
176197
);
177198

src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php

+22-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,28 @@ public function reverseTransform($value)
117117
}
118118

119119
$outputTz = new \DateTimeZone($this->outputTimezone);
120-
$dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);
120+
121+
if ('Y-\\WW|' === $this->parseFormat) {
122+
if (0 === preg_match('/^\d{4}\-W\d{2}$/', $value)) {
123+
throw new TransformationFailedException('Given data does not follow the date format "Y-\WW"');
124+
}
125+
126+
$weekData = explode('-W', $value);
127+
128+
// The 28th December is always in the last week of the year
129+
if (date('W', strtotime('28th December '. $weekData[0])) < $weekData[1]) {
130+
throw new TransformationFailedException(sprintf('Week No. %d does not exists for year %d', $weekData[1], $weekData[0]));
131+
}
132+
133+
$dateTime = (new \DateTime())->setISODate(...$weekData);
134+
135+
// For when the first day of the first week is in the previous year
136+
if ($dateTime->format('Y') < $weekData[0]) {
137+
$dateTime->setDate($weekData[0], '01', '01');
138+
}
139+
} else {
140+
$dateTime = \DateTime::createFromFormat($this->parseFormat, $value, $outputTz);
141+
}
121142

122143
$lastErrors = \DateTime::getLastErrors();
123144

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
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\Type;
13+
14+
use Symfony\Component\Form\AbstractType;
15+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeImmutableToDateTimeTransformer;
16+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
17+
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
18+
use Symfony\Component\Form\FormBuilderInterface;
19+
use Symfony\Component\Form\FormInterface;
20+
use Symfony\Component\Form\FormView;
21+
use Symfony\Component\Form\ReversedTransformer;
22+
use Symfony\Component\OptionsResolver\Options;
23+
use Symfony\Component\OptionsResolver\OptionsResolver;
24+
25+
class WeekType extends AbstractType
26+
{
27+
private static $widgets = [
28+
'text' => TextType::class,
29+
'choice' => ChoiceType::class,
30+
];
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function buildForm(FormBuilderInterface $builder, array $options)
36+
{
37+
$parts = ['year', 'week'];
38+
39+
$format = 'Y-\WW';
40+
41+
if ('single_text' === $options['widget']) {
42+
$builder->addViewTransformer(new DateTimeToStringTransformer($options['model_timezone'], $options['view_timezone'], $format));
43+
} else {
44+
$yearOptions = $weekOptions = [
45+
'error_bubbling' => true,
46+
'empty_data' => '',
47+
];
48+
// when the form is compound the entries of the array are ignored in favor of children data
49+
// so we need to handle the cascade setting here
50+
$emptyData = $builder->getEmptyData() ?: [];
51+
52+
if (isset($options['invalid_message'])) {
53+
$yearOptions['invalid_message'] = $options['invalid_message'];
54+
$weekOptions['invalid_message'] = $options['invalid_message'];
55+
}
56+
57+
if (isset($options['invalid_message_parameters'])) {
58+
$yearOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
59+
$weekOptions['invalid_message_parameters'] = $options['invalid_message_parameters'];
60+
}
61+
62+
if ('choice' === $options['widget']) {
63+
$years = $weeks = [];
64+
65+
foreach ($options['years'] as $year) {
66+
$years[str_pad($year, 2, '0', STR_PAD_LEFT)] = $year;
67+
}
68+
69+
foreach ($options['weeks'] as $week) {
70+
$weeks[str_pad($week, 2, '0', STR_PAD_LEFT)] = $week;
71+
}
72+
73+
// Only pass a subset of the options to children
74+
$yearOptions['choices'] = $years;
75+
$yearOptions['placeholder'] = $options['placeholder']['year'];
76+
$yearOptions['choice_translation_domain'] = $options['choice_translation_domain']['year'];
77+
78+
$weekOptions['choices'] = $weeks;
79+
$weekOptions['placeholder'] = $options['placeholder']['week'];
80+
$weekOptions['choice_translation_domain'] = $options['choice_translation_domain']['week'];
81+
82+
// Append generic carry-along options
83+
foreach (['required', 'translation_domain'] as $passOpt) {
84+
$yearOptions[$passOpt] = $options[$passOpt];
85+
86+
$weekOptions[$passOpt] = $options[$passOpt];
87+
}
88+
}
89+
90+
$builder->add('year', self::$widgets[$options['widget']], $yearOptions);
91+
92+
$builder->add('week', self::$widgets[$options['widget']], $weekOptions);
93+
94+
if (isset($emptyData['week'])) {
95+
$weekOptions['empty_data'] = $emptyData['week'];
96+
}
97+
$builder->add('week', self::$widgets[$options['widget']], $weekOptions);
98+
99+
$builder->addViewTransformer(new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts, 'text' === $options['widget']));
100+
}
101+
102+
if ('datetime_immutable' === $options['input']) {
103+
$builder->addModelTransformer(new DateTimeImmutableToDateTimeTransformer());
104+
} elseif ('string' === $options['input']) {
105+
$builder->addModelTransformer(new ReversedTransformer(
106+
new DateTimeToStringTransformer($options['model_timezone'], $options['model_timezone'], $format)
107+
));
108+
} elseif ('array' === $options['input']) {
109+
$builder->addModelTransformer(new ReversedTransformer(
110+
new DateTimeToArrayTransformer($options['model_timezone'], $options['model_timezone'], $parts)
111+
));
112+
}
113+
}
114+
115+
/**
116+
* {@inheritdoc}
117+
*/
118+
public function buildView(FormView $view, FormInterface $form, array $options)
119+
{
120+
$view->vars = array_replace($view->vars, [
121+
'widget' => $options['widget'],
122+
]);
123+
124+
if ($options['html5'] && 'single_text' === $options['widget']) {
125+
$view->vars['type'] = 'week';
126+
}
127+
}
128+
129+
/**
130+
* {@inheritdoc}
131+
*/
132+
public function configureOptions(OptionsResolver $resolver)
133+
{
134+
$compound = function (Options $options) {
135+
return 'single_text' !== $options['widget'];
136+
};
137+
138+
$placeholderDefault = function (Options $options) {
139+
return $options['required'] ? null : '';
140+
};
141+
142+
$placeholderNormalizer = function (Options $options, $placeholder) use ($placeholderDefault) {
143+
if (\is_array($placeholder)) {
144+
$default = $placeholderDefault($options);
145+
146+
return array_merge(
147+
['year' => $default, 'week' => $default],
148+
$placeholder
149+
);
150+
}
151+
152+
return [
153+
'year' => $placeholder,
154+
'week' => $placeholder,
155+
];
156+
};
157+
158+
$choiceTranslationDomainNormalizer = function (Options $options, $choiceTranslationDomain) {
159+
if (\is_array($choiceTranslationDomain)) {
160+
$default = false;
161+
162+
return array_replace(
163+
['year' => $default, 'week' => $default,],
164+
$choiceTranslationDomain
165+
);
166+
}
167+
168+
return [
169+
'year' => $choiceTranslationDomain,
170+
'week' => $choiceTranslationDomain,
171+
];
172+
};
173+
174+
$weeksNumbers = array_map(function ($item) {
175+
return sprintf('W%02d', $item);
176+
}, range(1, 53));
177+
178+
$resolver->setDefaults([
179+
'years' => range(date('Y') - 5, date('Y') + 5),
180+
'weeks' => array_combine($weeksNumbers, $weeksNumbers),
181+
'widget' => 'choice',
182+
'input' => 'datetime',
183+
'model_timezone' => null,
184+
'view_timezone' => null,
185+
'placeholder' => $placeholderDefault,
186+
'html5' => true,
187+
// Don't modify \DateTime classes by reference, we treat
188+
// them like immutable value objects
189+
'by_reference' => false,
190+
'error_bubbling' => false,
191+
// If initialized with a \DateTime object, FormType initializes
192+
// this option to "\DateTime". Since the internal, normalized
193+
// representation is not \DateTime, but an array, we need to unset
194+
// this option.
195+
'data_class' => null,
196+
'empty_data' => function (Options $options) {
197+
return $options['compound'] ? [] : '';
198+
},
199+
'compound' => $compound,
200+
'choice_translation_domain' => false,
201+
]);
202+
203+
$resolver->setNormalizer('placeholder', $placeholderNormalizer);
204+
$resolver->setNormalizer('choice_translation_domain', $choiceTranslationDomainNormalizer);
205+
206+
$resolver->setAllowedValues('input', [
207+
'datetime',
208+
'datetime_immutable',
209+
'string',
210+
'array',
211+
]);
212+
213+
$resolver->setAllowedValues('widget', [
214+
'single_text',
215+
'text',
216+
'choice',
217+
]);
218+
219+
$resolver->setAllowedTypes('years', 'array');
220+
$resolver->setAllowedTypes('weeks', 'array');
221+
}
222+
223+
/**
224+
* {@inheritdoc}
225+
*/
226+
public function getBlockPrefix()
227+
{
228+
return 'week';
229+
}
230+
}

0 commit comments

Comments
 (0)