├── .gitignore ├── Resources ├── docs │ └── example.png ├── translations │ ├── messages.sv.yml │ ├── messages.en.yml │ ├── validators.en.yml │ └── validators.sv.yml ├── config │ └── services.yml └── views │ └── Form │ └── fields.html.twig ├── .travis.yml ├── HappyrBirthdayBundle.php ├── README.md ├── DependencyInjection ├── Configuration.php └── HappyrBirthdayExtension.php ├── composer.json ├── phpunit.xml.dist └── From ├── Transformer └── BirthdayTransformer.php └── BirthdayType.php /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | phpunit.xml 3 | composer.lock 4 | vendor 5 | -------------------------------------------------------------------------------- /Resources/docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Happyr/BirthdayBundle/master/Resources/docs/example.png -------------------------------------------------------------------------------- /Resources/translations/messages.sv.yml: -------------------------------------------------------------------------------- 1 | happyr.birthday: 2 | form: 3 | year: 'År' 4 | month: 'Månad' 5 | day: 'Dag' -------------------------------------------------------------------------------- /Resources/translations/messages.en.yml: -------------------------------------------------------------------------------- 1 | happyr.birthday: 2 | form: 3 | year: 'Year' 4 | month: 'Month' 5 | day: 'Day' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | 9 | before_install: 10 | - composer self-update 11 | 12 | install: 13 | - composer update 14 | 15 | script: 16 | - phpunit -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | 3 | services: 4 | happyr.birthday.form.birthday: 5 | class: Happyr\BirthdayBundle\From\BirthdayType 6 | arguments: ["@translator"] 7 | tags: 8 | - { name: form.type, alias: happyr_birthday } -------------------------------------------------------------------------------- /HappyrBirthdayBundle.php: -------------------------------------------------------------------------------- 1 | 6 |
7 | {{ form_widget(form.year) }} 8 | {{ form_widget(form.month) }} 9 | {{ form_widget(form.day) }} 10 |
11 | 12 | {% endspaceless %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /Resources/translations/validators.en.yml: -------------------------------------------------------------------------------- 1 | happyr.birthday: 2 | form: 3 | future: 'The date you entered is in the future.' 4 | incomplete: 'Date is incomplete' 5 | unknown_error: 'The birthday field does not look right.' 6 | year: 7 | max_message: 'Oldest age allowed is %limit% years.' 8 | min_message: 'Youngest age allowed is %limit% years.' 9 | invalid: 'Invalid date' 10 | format_error: 'Enter a year with 4 digits.' 11 | month.invalid: 'Invalid date' 12 | day.invalid: 'Invalid date' -------------------------------------------------------------------------------- /Resources/translations/validators.sv.yml: -------------------------------------------------------------------------------- 1 | happyr.birthday: 2 | form: 3 | future: 'Du har angivit ett datum i framtiden. ' 4 | incomplete: 'Datumet är bara delvis ifyllt.' 5 | unknown_error: 'Det ser ut att vara något fel med födelsedags-fältet.' 6 | year: 7 | max_message: 'Hösta tillåtna ålder är %limit% år.' 8 | min_message: 'Minsta tillåtna ålder är %limit% år.' 9 | invalid: 'Felaktigt datum' 10 | format_error: 'Ange ett årtal med 4 siffror.' 11 | month.invalid: 'Felaktigt datum' 12 | day.invalid: 'Felaktigt datum' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Happyr Birthday Bundle 2 | ====================== 3 | 4 | Get a nice looking form for birthdays. Enter the year manually and then drop down for month and day. 5 | 6 | ![Example how it will look](/Resources/docs/example.png) 7 | 8 | 9 | ## Install 10 | 11 | ``` yml 12 | // app/config/config.yml 13 | twig: 14 | form: 15 | resources: 16 | - 'HappyrBirthdayBundle:Form:fields.html.twig' 17 | 18 | ``` 19 | 20 | ## Usage 21 | 22 | ``` php 23 | 24 | //WhateverFormType.php 25 | 26 | public function buildForm(FormBuilderInterface $builder, array $options) 27 | { 28 | $builder->add('birthday', 'happyr_birthday') 29 | } 30 | ``` -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('happyr_birthday'); 21 | 22 | return $treeBuilder; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happyr/birthday-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Happyr birthday bundle gives you a nicer way to render birthday widgets", 5 | "keywords": ["Birthday"], 6 | "homepage": "http://developer.happyr.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Tobias Nyholm", 11 | "email": "tobias@happyr.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.4", 16 | "symfony/framework-bundle": "^2.3 || ^3.0" 17 | }, 18 | "autoload": { 19 | "psr-0": { 20 | "Happyr\\BirthdayBundle\\": "" 21 | } 22 | }, 23 | "target-dir": "Happyr/BirthdayBundle" 24 | } 25 | 26 | -------------------------------------------------------------------------------- /DependencyInjection/HappyrBirthdayExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 26 | $loader->load('services.yml'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | ./Tests 20 | 21 | 22 | 23 | 24 | 25 | ./ 26 | 27 | vendor 28 | Tests 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /From/Transformer/BirthdayTransformer.php: -------------------------------------------------------------------------------- 1 | norm 16 | */ 17 | public function transform($date) 18 | { 19 | if ($date === null || !($date instanceof \DateTime)) { 20 | return null; 21 | } 22 | 23 | return array( 24 | 'year' => $date->format('Y'), 25 | 'month' => $date->format('n'), 26 | 'day' => $date->format('j'), 27 | ); 28 | } 29 | 30 | /** 31 | * @param mixed $data 32 | * 33 | * @return \DateTime|mixed 34 | */ 35 | public function reverseTransform($data) 36 | { 37 | if ($data['year'] === null && $data['month'] === null && $data['day'] === null) { 38 | //if left untouched 39 | return null; 40 | } elseif ($data['year'] === null || $data['month'] === null || $data['day'] === null) { 41 | //if just partially filled in 42 | return 'incomplete'; 43 | } elseif (!is_int($data['year']) || strlen($data['year']) != 4 || $data['year'] < 0) { 44 | return 'error'; 45 | } 46 | 47 | try { 48 | $date = new \DateTime(sprintf('%d-%d-%d', $data['year'], $data['month'], $data['day'])); 49 | } catch (\Exception $e) { 50 | return 'unknown_error'; 51 | } 52 | 53 | return $date; 54 | } 55 | } -------------------------------------------------------------------------------- /From/BirthdayType.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 52 | } 53 | 54 | public function buildForm(FormBuilderInterface $builder, array $options) 55 | { 56 | $timeFormat = \IntlDateFormatter::NONE; 57 | $calendar = \IntlDateFormatter::GREGORIAN; 58 | 59 | $yearOptions = $monthOptions = $dayOptions = array( 60 | 'error_bubbling' => true, 61 | ); 62 | 63 | $formatter = new \IntlDateFormatter( 64 | \Locale::getDefault(), 65 | $options['format'], 66 | $timeFormat, 67 | 'UTC', 68 | $calendar 69 | ); 70 | $formatter->setLenient(false); 71 | 72 | 73 | $currentYear=date('Y'); 74 | // Only pass a subset of the options to children 75 | $yearOptions['label'] =$yearOptions['attr']['placeholder'] = $options['placeholder']['year']; 76 | $yearOptions['attr']['min'] = $currentYear-$options['max_age']; 77 | $yearOptions['attr']['max'] = $currentYear-$options['min_age']; 78 | 79 | $monthOptions['choices'] = $this->formatTimestamps($formatter, '/[M|L]+/', $this->listMonths($options['months'])); 80 | $dayOptions['label'] = $monthOptions['placeholder'] = $this->translator->trans($options['placeholder']['month']); 81 | $monthOptions['choice_translation_domain'] = false; 82 | 83 | $dayOptions['choices'] = $this->formatTimestamps($formatter, '/d+/', $this->listDays($options['days'])); 84 | $dayOptions['label'] = $dayOptions['placeholder'] = $this->translator->trans($options['placeholder']['day']); 85 | $dayOptions['choice_translation_domain'] = false; 86 | 87 | 88 | // Append generic carry-along options 89 | foreach (array('required', 'translation_domain') as $passOpt) { 90 | $yearOptions[$passOpt] = $monthOptions[$passOpt] = $dayOptions[$passOpt] = $options[$passOpt]; 91 | } 92 | 93 | $this->doBuildForm($builder, $yearOptions, $monthOptions, $dayOptions, $formatter); 94 | $this->addValidation($builder, $options); 95 | } 96 | 97 | /** 98 | * Add validation 99 | * 100 | * @param FormBuilderInterface $builder 101 | * @param array $options 102 | */ 103 | protected function addValidation(FormBuilderInterface $builder, array $options) 104 | { 105 | $builder->addEventListener(FormEvents::SUBMIT, function(FormEvent $event) use ($options) { 106 | $form = $event->getForm(); 107 | $date=$event->getData(); 108 | 109 | if ($date === null) { 110 | return; 111 | } elseif ($date==='incomplete') { 112 | $form->addError(new FormError('happyr.birthday.form.incomplete')); 113 | $event->setData(null); 114 | return; 115 | } elseif ($date==='error') { 116 | $form->addError(new FormError('happyr.birthday.form.year.format_error')); 117 | $event->setData(null); 118 | return; 119 | } elseif ($date==='unknown_error') { 120 | $form->addError(new FormError('happyr.birthday.form.unknown_error')); 121 | $event->setData(null); 122 | return; 123 | } 124 | 125 | $yearField = $form->get('year'); 126 | 127 | /* 128 | * Verify date 129 | */ 130 | if ($date->format('Y') != $yearField->getData()) { 131 | $form->addError(new FormError('happyr.birthday.form.year.invalid')); 132 | return; 133 | } 134 | 135 | if ($date->format('n') != $form->get('month')->getData()) { 136 | $form->addError(new FormError('happyr.birthday.form.month.invalid')); 137 | return; 138 | } 139 | 140 | if ($date->format('j') != $form->get('day')->getData()) { 141 | $form->addError(new FormError('happyr.birthday.form.day.invalid')); 142 | return; 143 | } 144 | 145 | /* 146 | * Verify age 147 | */ 148 | $now = new \DateTime(); 149 | $age = $date->diff($now)->format('%r%y'); 150 | 151 | if ($now < $date) { 152 | $form->addError(new FormError('happyr.birthday.form.future')); 153 | } else if ($age > $options['max_age']) { 154 | $form->addError(new FormError('happyr.birthday.form.year.max_message', null, array('%limit%'=>$options['max_age']))); 155 | } else if ($age < $options['min_age']) { 156 | $form->addError(new FormError('happyr.birthday.form.year.min_message', null, array('%limit%'=>$options['min_age']))); 157 | } 158 | }); 159 | } 160 | 161 | public function configureOptions(OptionsResolver $resolver) 162 | { 163 | $resolver->setDefaults(array( 164 | 'max_age' => 120, 165 | 'min_age' => 0, 166 | 'compound'=>true, 167 | 'months' => range(1, 12), 168 | 'days' => range(1, 31), 169 | 'input' => 'datetime', 170 | 'placeholder' => array( 171 | 'year' => 'happyr.birthday.form.year', 172 | 'month' => 'happyr.birthday.form.month', 173 | 'day' => 'happyr.birthday.form.day' 174 | ), 175 | 'format' => self::DEFAULT_FORMAT, 176 | 'error_bubbling' => false, 177 | )); 178 | } 179 | 180 | 181 | private function formatTimestamps(\IntlDateFormatter $formatter, $regex, array $timestamps) 182 | { 183 | $pattern = $formatter->getPattern(); 184 | $timezone = $formatter->getTimezoneId(); 185 | 186 | if ($setTimeZone = method_exists($formatter, 'setTimeZone')) { 187 | $formatter->setTimeZone('UTC'); 188 | } else { 189 | $formatter->setTimeZoneId('UTC'); 190 | } 191 | 192 | if (preg_match($regex, $pattern, $matches)) { 193 | $formatter->setPattern($matches[0]); 194 | 195 | foreach ($timestamps as $key => $timestamp) { 196 | $timestamps[$key] = $formatter->format($timestamp); 197 | } 198 | 199 | // I'd like to clone the formatter above, but then we get a 200 | // segmentation fault, so let's restore the old state instead 201 | $formatter->setPattern($pattern); 202 | } 203 | 204 | if ($setTimeZone) { 205 | $formatter->setTimeZone($timezone); 206 | } else { 207 | $formatter->setTimeZoneId($timezone); 208 | } 209 | 210 | return array_flip($timestamps); 211 | } 212 | 213 | private function listMonths(array $months) 214 | { 215 | $result = array(); 216 | 217 | foreach ($months as $month) { 218 | $result[$month] = gmmktime(0, 0, 0, $month, 15); 219 | } 220 | 221 | return $result; 222 | } 223 | 224 | private function listDays(array $days) 225 | { 226 | $result = array(); 227 | 228 | foreach ($days as $day) { 229 | $result[$day] = gmmktime(0, 0, 0, 5, $day); 230 | } 231 | 232 | return $result; 233 | } 234 | 235 | /** 236 | * Returns the name of this type. 237 | * 238 | * @return string The name of this type 239 | */ 240 | public function getBlockPrefix() 241 | { 242 | return 'happyr_birthday'; 243 | } 244 | 245 | /** 246 | * @param FormBuilderInterface $builder 247 | * @param $yearOptions 248 | * @param $monthOptions 249 | * @param $dayOptions 250 | * @param $formatter 251 | * 252 | * @return \Symfony\Component\Form\FormConfigBuilderInterface 253 | */ 254 | protected function doBuildForm(FormBuilderInterface $builder, $yearOptions, $monthOptions, $dayOptions, $formatter) 255 | { 256 | return $builder 257 | ->add('year', IntegerType::class, $yearOptions) 258 | ->add('month', ChoiceType::class, $monthOptions) 259 | ->add('day', ChoiceType::class, $dayOptions) 260 | ->addViewTransformer(new BirthdayTransformer()) 261 | ->setAttribute('formatter', $formatter); 262 | } 263 | } --------------------------------------------------------------------------------