From d93051762403caf94fb6d35c66af04a59bbbac14 Mon Sep 17 00:00:00 2001 From: Alejandro Barrabino Delgado Date: Fri, 30 Sep 2016 20:01:46 +0200 Subject: [PATCH] Added Symfony Form support #66 --- composer.json | 2 + .../translations/addressLabels.en_US.yml | 37 +++ .../translations/addressLabels.es_ES.yml | 37 +++ src/Address.php | 5 + src/AddressFormat/AddressFormatRepository.php | 1 + .../GenerateAddressFieldsSubscriber.php | 236 ++++++++++++++++++ ...nerateAddressFieldsSubscriberInterface.php | 25 ++ src/Form/Type/AddressType.php | 51 ++++ src/Translator/labelTranslator.php | 48 ++++ src/Translator/labelTranslatorInterface.php | 21 ++ .../GenerateAddressFieldSubscriberTest.php | 134 ++++++++++ tests/Form/Type/AddressTypeTest.php | 57 +++++ tests/Translator/labelTranslatorTest.php | 49 ++++ 13 files changed, 703 insertions(+) create mode 100644 resources/translations/addressLabels.en_US.yml create mode 100644 resources/translations/addressLabels.es_ES.yml create mode 100644 src/Form/EventListener/GenerateAddressFieldsSubscriber.php create mode 100644 src/Form/EventListener/GenerateAddressFieldsSubscriberInterface.php create mode 100644 src/Form/Type/AddressType.php create mode 100644 src/Translator/labelTranslator.php create mode 100644 src/Translator/labelTranslatorInterface.php create mode 100644 tests/Form/EventListener/GenerateAddressFieldSubscriberTest.php create mode 100644 tests/Form/Type/AddressTypeTest.php create mode 100644 tests/Translator/labelTranslatorTest.php diff --git a/composer.json b/composer.json index d72e69f0..25de39c6 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ "suggest": { "commerceguys/intl": "to use it as the source of country data", "symfony/intl": "to use it as the source of country data", + "symfony/form": "to generate symfony forms", + "symfony/translation": "to translate labels", "symfony/validator": "to validate addresses" }, "autoload": { diff --git a/resources/translations/addressLabels.en_US.yml b/resources/translations/addressLabels.en_US.yml new file mode 100644 index 00000000..561da846 --- /dev/null +++ b/resources/translations/addressLabels.en_US.yml @@ -0,0 +1,37 @@ +area: 'Area' +county: 'County' +department: 'Department' +district: 'District' +do_si: 'Do' +emirate: 'Emirate' +island: 'Island' +oblast: 'Oblast' +parish: 'Parish' +prefecture: 'Prefecture' +province: 'Province' +state: 'State' + +city: 'City' +district: 'District' +post_town: 'Post Town' +suburb: 'Suburb' +locality: 'Locality' + +neighborhood: 'Neighborhood' +village_township: 'Village / Township' +townland: '' + +postal: 'Postal Code' +zip: 'ZIP code' +pin: 'PIN code' + +addressLine1: 'Street Address' +addressLine2: '' + +organization: 'Company' +givenName: 'Contact Name' +additionalName: '' +familyName: '' + +sortingCode: 'Cedex' +postalCode: 'Postal Code' \ No newline at end of file diff --git a/resources/translations/addressLabels.es_ES.yml b/resources/translations/addressLabels.es_ES.yml new file mode 100644 index 00000000..7f2f4f06 --- /dev/null +++ b/resources/translations/addressLabels.es_ES.yml @@ -0,0 +1,37 @@ +area: 'Área' +county: 'Condado' +department: 'Departamento' +district: 'Distrito' +do_si: 'Do' +emirate: 'Emirato' +island: 'Isla' +oblast: 'Área' +parish: 'Municipio' +prefecture: 'Prefectura' +province: 'Provincia' +state: 'Estado' + +city: 'Ciudad' +district: 'Distrito' +post_town: 'Post Town' +suburb: 'Suburbio' +locality: 'Localidad' + +neighborhood: 'Barrio' +village_township: 'Pueblo / Aldea' +townland: 'Localidad' + +postal: 'Código Postal' +zip: 'Código ZIP' +pin: 'Código PIN' + +addressLine1: 'Dirección Postal' +addressLine2: '' + +organization: 'Compañía' +givenName: 'Nombre' +additionalName: '' +familyName: 'Apellidos' + +sortingCode: 'Cedex' +postalCode: 'Código Postal' \ No newline at end of file diff --git a/src/Address.php b/src/Address.php index 3ad72f80..6c49e515 100644 --- a/src/Address.php +++ b/src/Address.php @@ -393,4 +393,9 @@ public function withLocale($locale) return $new; } + + public function __set($property, $value) + { + $this->$property = $value; + } } diff --git a/src/AddressFormat/AddressFormatRepository.php b/src/AddressFormat/AddressFormatRepository.php index 11c869e2..ead4c4e6 100644 --- a/src/AddressFormat/AddressFormatRepository.php +++ b/src/AddressFormat/AddressFormatRepository.php @@ -410,6 +410,7 @@ protected function getDefinitions() ], 'ES' => [ 'format' => "%givenName %familyName\n%organization\n%addressLine1\n%addressLine2\n%postalCode %locality %administrativeArea", + 'locale' => 'es_ES', 'required_fields' => [ 'addressLine1', 'locality', 'administrativeArea', 'postalCode', ], diff --git a/src/Form/EventListener/GenerateAddressFieldsSubscriber.php b/src/Form/EventListener/GenerateAddressFieldsSubscriber.php new file mode 100644 index 00000000..2b9a25fb --- /dev/null +++ b/src/Form/EventListener/GenerateAddressFieldsSubscriber.php @@ -0,0 +1,236 @@ +addressFormatRepository = $addressFormatRepository; + $this->subdivisionRepository = $subdivisionRepository; + } + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return [ + FormEvents::PRE_SET_DATA => 'preSetData', + FormEvents::PRE_SUBMIT => 'preSubmit', + ]; + } + + public function preSetData(FormEvent $event) + { + $address = $event->getData(); + + $form = $event->getForm(); + if (null === $address) { + return; + } + $countryCode = $address->getCountryCode(); + $administrativeArea = $address->getAdministrativeArea(); + $locality = $address->getLocality(); + $this->buildForm($form, $countryCode, $administrativeArea, $locality); + } + + public function preSubmit(FormEvent $event) + { + $data = $event->getData(); + $form = $event->getForm(); + + $countryCode = array_key_exists('countryCode', $data) ? $data['countryCode'] : null; + $administrativeArea = array_key_exists('administrativeArea', $data) ? $data['administrativeArea'] : null; + $locality = array_key_exists('locality', $data) ? $data['locality'] : null; + $this->buildForm($form, $countryCode, $administrativeArea, $locality); + } + + /** + * Builds the address form for the provided country code. + * + * @param FormInterface $form + * @param string $countryCode The country code. + * @param string $administrativeArea The administrative area. + * @param string $locality The locality. + * @param labelTranslatorInterface $translator + */ + protected function buildForm(FormInterface $form, $countryCode, $administrativeArea, $locality, labelTranslatorInterface $translator = null) + { + $addressFormat = $this->addressFormatRepository->get($countryCode); + + if(empty($translator)) { + $translator = new labelTranslator($addressFormat->getLocale()); + } + + // A list of needed subdivisions and their parent ids. + $subdivisions = [ + AddressField::ADMINISTRATIVE_AREA => 0, + ]; + if (!empty($administrativeArea)) { + $subdivisions[AddressField::LOCALITY] = $administrativeArea; + } + if (!empty($locality)) { + $subdivisions[AddressField::DEPENDENT_LOCALITY] = $locality; + } + $fields = $this->getFormFields($addressFormat, $subdivisions, $translator); + foreach ($fields as $field => $fieldOptions) { + $type = isset($fieldOptions['choices']) ? ChoiceType::class : TextType::class; + $form->add($field, $type, $fieldOptions); + } + } + + /** + * Gets a list of form fields for the provided address format. + * + * @param AddressFormat $addressFormat + * @param array $subdivisions An array of needed subdivisions. + * + * @param labelTranslatorInterface $translator + * @return array An array in the $field => $formOptions format. + */ + protected function getFormFields(AddressFormat $addressFormat, $subdivisions, labelTranslatorInterface $translator) + { + $fields = []; + $labels = $this->getFieldLabels($addressFormat, $translator); + $requiredFields = $addressFormat->getRequiredFields(); + $format = $addressFormat->getFormat(); + $groupedFields = AddressFormatHelper::getGroupedFields($format); + foreach ($groupedFields as $lineFields) { + foreach ($lineFields as $field) { + $fields[$field] = [ + 'label' => $labels[$field], + 'required' => in_array($field, $requiredFields), + ]; + } + } + // Add choices for predefined subdivisions. + foreach ($subdivisions as $field => $parentId) { + // @todo Pass the form locale to get the translated values. + $children = $this->subdivisionRepository->getList(array($addressFormat->getCountryCode())); + if ($children) { + $fields[$field]['choices'] = $children; + } + } + return $fields; + } + + /** + * Gets the labels for the provided address format's fields. + * + * @param AddressFormat $addressFormat + * + * @param labelTranslatorInterface $translator + * @return array An array of labels keyed by field constants. + */ + protected function getFieldLabels($addressFormat, labelTranslatorInterface $translator) + { + // All possible subdivision labels. + $subdivisionLabels = [ + AdministrativeAreaType::AREA => $translator->translate(AdministrativeAreaType::AREA), + AdministrativeAreaType::COUNTY => $translator->translate(AdministrativeAreaType::COUNTY), + AdministrativeAreaType::DEPARTMENT => $translator->translate(AdministrativeAreaType::DEPARTMENT), + AdministrativeAreaType::DISTRICT => $translator->translate(AdministrativeAreaType::DISTRICT), + AdministrativeAreaType::DO_SI => $translator->translate(AdministrativeAreaType::DO_SI), + AdministrativeAreaType::EMIRATE => $translator->translate(AdministrativeAreaType::EMIRATE), + AdministrativeAreaType::ISLAND => $translator->translate(AdministrativeAreaType::ISLAND), + AdministrativeAreaType::OBLAST => $translator->translate(AdministrativeAreaType::OBLAST), + AdministrativeAreaType::PARISH => $translator->translate(AdministrativeAreaType::PARISH), + AdministrativeAreaType::PREFECTURE => $translator->translate(AdministrativeAreaType::PREFECTURE), + AdministrativeAreaType::PROVINCE => $translator->translate(AdministrativeAreaType::PROVINCE), + AdministrativeAreaType::STATE => $translator->translate(AdministrativeAreaType::STATE), + LocalityType::CITY => $translator->translate(LocalityType::CITY), + LocalityType::DISTRICT => $translator->translate(LocalityType::DISTRICT), + LocalityType::POST_TOWN => $translator->translate(LocalityType::POST_TOWN), + DependentLocalityType::DISTRICT => $translator->translate(DependentLocalityType::DISTRICT), + DependentLocalityType::NEIGHBORHOOD => $translator->translate(DependentLocalityType::NEIGHBORHOOD), + DependentLocalityType::VILLAGE_TOWNSHIP => $translator->translate(DependentLocalityType::VILLAGE_TOWNSHIP), + DependentLocalityType::SUBURB => $translator->translate(DependentLocalityType::SUBURB), + PostalCodeType::POSTAL => $translator->translate(PostalCodeType::POSTAL), + PostalCodeType::ZIP => $translator->translate(PostalCodeType::ZIP), + PostalCodeType::PIN => $translator->translate(PostalCodeType::PIN), + ]; + + // Determine the correct administrative area label. + $administrativeAreaType = $addressFormat->getAdministrativeAreaType(); + $administrativeAreaLabel = ''; + if (isset($subdivisionLabels[$administrativeAreaType])) { + $administrativeAreaLabel = $subdivisionLabels[$administrativeAreaType]; + } + // Determine the correct locality label. + $localityType = $addressFormat->getLocalityType(); + $localityLabel = ''; + if (isset($subdivisionLabels[$localityType])) { + $localityLabel = $subdivisionLabels[$localityType]; + } + // Determine the correct dependent locality label. + $dependentLocalityType = $addressFormat->getDependentLocalityType(); + $dependentLocalityLabel = ''; + if (isset($subdivisionLabels[$dependentLocalityType])) { + $dependentLocalityLabel = $subdivisionLabels[$dependentLocalityType]; + } + // Determine the correct postal code label. + $postalCodeType = $addressFormat->getPostalCodeType(); + $postalCodeLabel = $subdivisionLabels[PostalCodeType::POSTAL]; + if (isset($subdivisionLabels[$postalCodeType])) { + $postalCodeLabel = $subdivisionLabels[$postalCodeType]; + } + // Assemble the final set of labels. + $labels = [ + AddressField::ADMINISTRATIVE_AREA => $translator->translate($administrativeAreaLabel), + AddressField::LOCALITY => $translator->translate($localityLabel), + AddressField::DEPENDENT_LOCALITY => $translator->translate($dependentLocalityLabel), + AddressField::ADDRESS_LINE1 => $translator->translate(AddressField::ADDRESS_LINE1), + AddressField::ADDRESS_LINE2 => $translator->translate(AddressField::ADDRESS_LINE2), + AddressField::ORGANIZATION => $translator->translate(AddressField::ORGANIZATION), + AddressField::GIVEN_NAME => $translator->translate(AddressField::GIVEN_NAME), + AddressField::ADDITIONAL_NAME => $translator->translate(AddressField::ADDITIONAL_NAME), + AddressField::FAMILY_NAME => $translator->translate(AddressField::FAMILY_NAME), + // Google's libaddressinput provides no label for this field type, + // Google wallet calls it "CEDEX" for every country that uses it. + AddressField::SORTING_CODE => $translator->translate(AddressField::SORTING_CODE), + AddressField::POSTAL_CODE => $translator->translate($postalCodeLabel), + ]; + return $labels; + } +} \ No newline at end of file diff --git a/src/Form/EventListener/GenerateAddressFieldsSubscriberInterface.php b/src/Form/EventListener/GenerateAddressFieldsSubscriberInterface.php new file mode 100644 index 00000000..6846d94b --- /dev/null +++ b/src/Form/EventListener/GenerateAddressFieldsSubscriberInterface.php @@ -0,0 +1,25 @@ +add('countryCode', ChoiceType::class, [ + 'choices' => array_flip($options['countryRepository']->getList()), + 'required' => true, + 'constraints' => array(new CountryConstraint(array('groups' => array('Default')))), + + ]); + $builder->addEventSubscriber(new GenerateAddressFieldsSubscriber($options['addressFormatRepository'], $options['subdivisionRepository'], $options['labelTranslator'])); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => 'CommerceGuys\Addressing\Address', + 'addressFormatRepository' => new AddressFormatRepository(), + 'subdivisionRepository' => new SubdivisionRepository(), + 'countryRepository' => new CountryRepository(), + 'labelTranslator' => null, + 'validation_groups' => 'Default', + 'constraints' => array( + new AddressFormatConstraint(array('groups' => array('Default'))), + ), + ]); + } + public function getName() + { + return 'address'; + } +} \ No newline at end of file diff --git a/src/Translator/labelTranslator.php b/src/Translator/labelTranslator.php new file mode 100644 index 00000000..1ee01b05 --- /dev/null +++ b/src/Translator/labelTranslator.php @@ -0,0 +1,48 @@ +translator = new \Symfony\Component\Translation\Translator($locale); + $this->loadTranslations(); + } + } + + /** + * loads Translations file + */ + protected function loadTranslations() + { + $this->translator->addLoader('yaml', new \Symfony\Component\Translation\Loader\YamlFileLoader()); + $this->translator->addResource('yaml', __DIR__ . '/../../resources/translations/addressLabels.en_US.yml', 'en_US', 'addressing'); + $this->translator->addResource('yaml', __DIR__ . '/../../resources/translations/addressLabels.es_ES.yml', 'es_ES', 'addressing'); + + $this->translator->setFallbackLocales(array('en_US')); + } + + /** + * {@inheritdoc} + */ + public function translate($key, $locale = null) + { + if(empty($this->translator)) { + return $key; + } + + return $this->translator->trans($key, array(), 'addressing', $locale); + } +} \ No newline at end of file diff --git a/src/Translator/labelTranslatorInterface.php b/src/Translator/labelTranslatorInterface.php new file mode 100644 index 00000000..46aae5ef --- /dev/null +++ b/src/Translator/labelTranslatorInterface.php @@ -0,0 +1,21 @@ +addressFormatRepository = new AddressFormatRepository(); + $this->countryRepository = new CountryRepository(); + $this->subdivisionRepository = new SubdivisionRepository(); + $this->formFactory = Forms::createFormFactory(); + } + + /** + * @covers ::__construct + */ + public function testConstructor() + { + $subscriber = new GenerateAddressFieldsSubscriber($this->addressFormatRepository, $this->subdivisionRepository); + $this->assertEquals($this->addressFormatRepository, $this->getObjectAttribute($subscriber, 'addressFormatRepository')); + $this->assertEquals($this->subdivisionRepository, $this->getObjectAttribute($subscriber, 'subdivisionRepository')); + } + + /** + * @covers ::preSetData + * @covers ::buildForm + * @covers ::getFormFields + * @covers ::getFieldLabels + */ + public function testOnPreSetData() + { + $form = $this->formFactory->create(AddressType::class, null, array( + 'addressFormatRepository' => $this->addressFormatRepository, + 'countryRepository' => $this->countryRepository, + 'subdivisionRepository' => $this->subdivisionRepository, + )); + + // If no address, the form should contain just the country field + $view = $form->createView(); + $children = $view->children; + $this->assertEquals(count($children), 1); + + // If country is set, generate the rest of the fields + $address = new Address('ES'); + $event = new FormEvent($form, $address); + $subscriber = new GenerateAddressFieldsSubscriber($this->addressFormatRepository, $this->subdivisionRepository); + $subscriber->preSetData($event); + $view = $form->createView(); + $children = $view->children; + $this->assertEquals(count($children), 9); + } + + /** + * @covers ::preSubmit + * @covers ::buildForm + * @covers ::getFormFields + * @covers ::getFieldLabels + */ + public function testOnPreSubmit() + { + $formData = array( + 'countryCode' => 'ES' + ); + + $form = $this->formFactory->create(AddressType::class, null, array( + 'addressFormatRepository' => $this->addressFormatRepository, + 'countryRepository' => $this->countryRepository, + 'subdivisionRepository' => $this->subdivisionRepository, + )); + + // If no address, the form should contain just the country field + $view = $form->createView(); + $children = $view->children; + $this->assertEquals(count($children), 1); + + // If country was entered, generate the rest of the fields + $event = new FormEvent($form, $formData); + $subscriber = new GenerateAddressFieldsSubscriber($this->addressFormatRepository, $this->subdivisionRepository); + $subscriber->preSubmit($event); + + $view = $form->createView(); + $children = $view->children; + $this->assertEquals(count($children), 9); + } +} \ No newline at end of file diff --git a/tests/Form/Type/AddressTypeTest.php b/tests/Form/Type/AddressTypeTest.php new file mode 100644 index 00000000..f15f71f1 --- /dev/null +++ b/tests/Form/Type/AddressTypeTest.php @@ -0,0 +1,57 @@ + 'ES', + 'administrativeArea' => 'Madrid', + 'locality' => 'Madrid', + 'givenName' => 'Test' + ); + + $address = new Address($formData['countryCode']); + + $formFactory = Forms::createFormFactory(); + $form = $formFactory->create(AddressType::class, $address, $options); + + // submit the data to the form directly + $form->get('countryCode')->submit('ES'); + $form->get('administrativeArea')->submit('Madrid'); + $form->get('locality')->submit('Madrid'); + $form->get('givenName')->submit('Test'); + + + $this->assertTrue($form->isSynchronized()); + $this->assertEquals($address, $form->getData()); + + $view = $form->createView(); + $children = $view->children; + + foreach (array_keys($formData) as $key) { + $this->assertArrayHasKey($key, $children); + } + } +} \ No newline at end of file diff --git a/tests/Translator/labelTranslatorTest.php b/tests/Translator/labelTranslatorTest.php new file mode 100644 index 00000000..7b031b9c --- /dev/null +++ b/tests/Translator/labelTranslatorTest.php @@ -0,0 +1,49 @@ +locale = 'es_ES'; + $this->translator = new labelTranslator($this->locale); + } + + /** + * @covers ::__construct + */ + public function testConstructor() + { + if(class_exists('\Symfony\Component\Translation\Translator')) { + $this->assertInstanceOf('\Symfony\Component\Translation\Translator', $this->getObjectAttribute($this->translator, 'translator')); + } + } + + /** + * @covers ::translate + */ + public function testTranslate() + { + if(class_exists('\Symfony\Component\Translation\Translator')) { + $this->assertEquals($this->translator->translate('city'),'Ciudad'); + } else { + $this->assertEquals($this->translator->translate('city'),'city'); + } + } +} \ No newline at end of file