我在使用Symfony2 3.3定义一种特殊形式时遇到问题。
以灵活,干净的方式存储邮政地址数据,减少重复是一项艰巨的任务,因为有很多地址组件的组合,如街道,街道号,邮政编码,地点,县,国家等。没有标准化的国际地址格式,地址构成'各国之间有所不同。除此之外,用户输入的数据通常包含输入错误。
示例:
Platzl 9, 80331 Munic, Germany
221B Baker Street, London, United Kingdom
3-1 Kōkyogaien, Chiyoda-ku, Tōkyō-to 100-0002, Japan
由于几乎所有国家/地区之间的地址成分各不相同(例如,在德国,'县'没有实际意义,而在其他国家/地区需要用于邮寄目的),我不想使用表单,每个地址组件都有单独的字段。如果存在涵盖所有国家的标准地址表格,生活会更容易; - )
由于没有,我想要实现一个只有一个地址字符串的表单' Google地图等文字字段地址搜索字段。这应该为用户提供更大的灵活性,并使他更容易输入他的(国际)地址。
我想使用数据转换器将地址字符串转换为地址对象,因此可以验证其值(例如,检查已存在的匹配地址)并将其保存到数据库中。我的地址表目前有5列(街道,街道号,邮政编码,地区和国家),其他列可能会在以后添加到模型中。为了从地址字符串中获取组件,我使用基于Web的地理数据地址API'。这样,地址组件以标准化的形式出现,因此 - 作为额外奖励 - 我可以轻松地检查重复项。好处是地址字符串中的拼写或输入错误的变化不会导致地址重复,因为API返回标准化的'解决删除输入错误的组件值。
- >请参阅文件的注释' AddressEntityToAddressStringTransformer.php'在下面的来源中了解更多详情
首先,我不确定使用数据转换器是否是实现此类解决方案的正确方法,因为在我在文档和其他教程中找到的示例中,只有已经存在的数据才会被转换进入另一种(视觉)表示#39;对于我的解决方案,还必须创建新的地址对象(尚未存在于数据库表中)(+验证并保留)。
根据我在这篇文章中添加的来源,我在提交新地址时会得到以下结果(请查看调试工具栏的附件截图):
Screenshot of web debug toolbar
表单对象(在AddressController.php中转储)有一个名为' address'的子表单。 (屏幕截图中的上方红框)包含标准化值为' modelData' (截图中的绿色框),但' modelData'实际地址表单(类型='地址',屏幕截图中的下方红框)为空。无论我尝试了什么,我都没有将转换后的数据转化为' modelData' $ form对象的对象。结果,一个新的但空洞的地址'实体被持久化到数据库表,因此表列包含NULL值。
我想我仍然缺少一些东西'但我不知道是什么。这就是我需要你帮助的原因; - )
如果数据转换器不适合实现我需要的东西:是否有另一种Symfony方式'解决我的问题?
以下是我到目前为止的PHP资源:
src / AppBundle / Controller / AddressController.php(这里没什么特别的......)
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\Address;
//use ...
/**
* Address controller.
*
* @Route("/{_locale}/address")
*/
class AddressController extends Controller
{
// ...
/**
* Creates a new address entity.
*
* @Route("/new", name="address_new")
* @Method({"GET", "POST"})
*/
public function newAction(Request $request)
{
$address = new Address();
$form = $this->createForm('AppBundle\Form\Type\AddressType', $address);
$form->handleRequest($request);
dump($form);
if ($form->isSubmitted() && $form->isValid()) {
dump($address);
$em = $this->getDoctrine()->getManager();
$em->persist($address);
$em->flush();
return $this->redirectToRoute('address_show', array('id' => $address->getId()));
}
return $this->render('address/new.html.twig', array(
'address' => $address,
'form' => $form->createView(),
));
}
// ...
}
的src /的appbundle /实体/ Address.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use AppBundle\Validator\Constraints as AddressAssert;
/**
* Address
*
* @ORM\Table(name="address")
* @ORM\Entity
' @AddressAssert\AddressUniqueConstraint()
*/
class Address
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer", precision=0, scale=0, nullable=false, unique=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="street", type="string", length=255, precision=0, scale=0, nullable=true, unique=false)
*/
private $street;
/**
* @var string
*
* @ORM\Column(name="housenumber", type="string", length=255, precision=0, scale=0, nullable=true, unique=false)
*/
private $housenumber;
/**
* @var string
*
* @ORM\Column(name="zipcode", type="string", length=255, precision=0, scale=0, nullable=true, unique=false)
*/
private $zipcode;
/**
* @var string
*
* @ORM\Column(name="locality", type="string", length=255, precision=0, scale=0, nullable=true, unique=false)
*/
private $locality;
/**
* @var string
*
* @ORM\Column(name="country", type="string", length=255, precision=0, scale=0, nullable=true, unique=false)
*/
private $country;
// getter and setter methods
}
应用程序/配置/ services.yml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
address_type:
class: AppBundle\Form\Type\AddressType
tags:
- { name: form.type }
address_string_type:
class: AppBundle\Form\Type\AddressStringType
tags:
- { name: form.type }
的src /的appbundle /窗体/类型/ AddressType.php
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Doctrine\ORM\EntityManagerInterface;
class AddressType extends AbstractType
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('address', AddressStringType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'mapped' => false,
));
}
public function getBlockPrefix()
{
return 'appbundle_address';
}
}
的src /的appbundle /窗体/类型/ AddressStringType.php
<?php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\Form\DataTransformer\AddressEntityToAddressStringTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class AddressStringType extends AbstractType
{
private $transformer;
public function __construct(AddressEntityToAddressStringTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addViewTransformer($this->transformer)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'invalid_message' => 'Error :-(',
'mapped' => false,
));
}
public function getParent()
{
return TextType::class;
}
}
的src /的appbundle /窗体/ DataTransformer / AddressEntityToAddressStringTransformer.php
<?php
namespace AppBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\ORM\EntityManagerInterface;
use AppBundle\Entity\Address;
use AppBundle\Validator\AddressValidator;
class AddressEntityToAddressStringTransformer implements DataTransformerInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Transforms a non-standardized address string into a standardized address string
*
* @param string $adrObjStandardized
* @return string|null
*/
public function transform($adrObjStandardized)
{
if ($adrObjStandardized === null)
{
return null;
}
$street_housenumber_str = trim($adrObjStandardized->getStreet().' '.$adrObjStandardized->getHousenumber());
$zipcode_locality_str = trim($adrObjStandardized->getZipcode().' '.$adrObjStandardized->getLocality());
$country_str = trim($adrObjStandardized->getCountry());
$tmp_arr = array($street_housenumber_str, $zipcode_locality_str, $country_str);
$tmp_arr = array_diff($tmp_arr, array(''));
$adrStrStandardized = implode(', ', $tmp_arr);
//$adrStrStandardized = 'Platzl 9, 80331 Munic, Germany';
return $adrStrStandardized;
}
/**
* Transforms an address string to an address object
* If a matching address already exists in the address table, return it. When creating a new entry, AddressUniqueConstraint will later tell us 'Address already exists.'
* If no address in the address table matches, return a new address object to be persisted into the address table
*
* @param string $adrStrNotStandardized
* @return object
* @throws TransformationFailedException (if address could not be standardized by address validation service (via an API))
*/
public function reverseTransform($adrStrNotStandardized)
{
// TODO: Implement workflow as follows:
// 1. Try to standardize data by sending the address string to an address validation service (API)
// 1.1. If standardizing was successful
// 1.1.1. Look for an already existing record in the database table 'address'
// 1.1.1.1. If address in table 'address' matches, return it
// 1.1.1.2. If NO address in table 'address' matches, create new Address object containing standardized data and return it
// 1.2. If standardizing was NOT successful
// 1.2.1. Throw TransformationFailedException
$validator = new AddressValidator();
try
{
// 1. Try to standardize data by sending the address string to an address validation service (API)
$standardized_data = $validator->validateAddressFromString($adrStrNotStandardized);
$addr_data = array(
'street' => $standardized_data['street'],
'housenumber' => $standardized_data['housenumber'],
'zipcode' => $standardized_data['zipcode'],
'locality' => $standardized_data['locality'],
'country' => $standardized_data['country'],
);
}
catch (\Exception $e) // 1.2. Standardizing was NOT successful
{
// 1.2.1. Throw TransformationFailedException
throw new TransformationFailedException(
'Die Adress-Daten konnten nicht standardisiert werden.'
);
}
// 1.1. We are still here, so standardizing was successful
// 1.1.1. Look for an already existing record in the database table 'address'
$address = $this->em
->getRepository(Address::class)
->findOneBy($addr_data)
;
if ($address === NULL)
{
// 1.1.1.2. If NO address in table 'address' matches, create new Address object containing standardized data and return it
$new_addr_obj = new Address();
$new_addr_obj->setStreet($addr_data['street']);
$new_addr_obj->setHousenumber($addr_data['housenumber']);
$new_addr_obj->setZipcode($addr_data['zipcode']);
$new_addr_obj->setLocality($addr_data['locality']);
$new_addr_obj->setCountry($addr_data['country']);
return $new_addr_obj;
}
else
{
// 1.1.1.1. If address in table 'address' matches, return it
return $address;
}
}
}
的src /的appbundle /验证/约束/ AddressUniqueConstraint.php
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class AddressUniqueConstraint extends Constraint
{
public $message_already_exists = 'Address already exists.';
public $message_invalid_form_data = 'Invalid form data.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
/**
* @return string
*/
public function validatedBy()
{
return AddressUniqueConstraintValidator::class;
}
}
的src /的appbundle /验证/约束/ AddressUniqueConstraintValidator.php
<?php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Doctrine\ORM\EntityManagerInterface;
use AppBundle\Entity\Address;
class AddressUniqueConstraintValidator extends ConstraintValidator
{
/**
* @var EntityManagerInterface
*/
protected $em;
/**
* Construct
* @param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
public function validate($value, Constraint $constraint)
{
if (!$value instanceof Address)
{
$this->context->buildViolation($constraint->message_invalid_form_data)
->addViolation();
}
$search = array('street' => $value->getStreet(), 'housenumber' => $value->getHousenumber(), 'zipcode' => $value->getZipcode(), 'locality' => $value->getLocality(), 'country' => $value->getCountry());
$adresses = $this->em->getRepository('AppBundle:Address')->findBy($search);
if ($adresses && count($adresses) > 0)
{
$this->context->buildViolation($constraint->message_already_exists)
->addViolation();
}
}
}