在使用Symfony DataTransformer

时间:2017-12-30 12:33:34

标签: php forms symfony

我在使用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'在下面的来源中了解更多详情

  • 检查空地址字符串值,并在值为空时添加表单错误
  • 地址字符串将发送到API
  • 如果出现技术问题(例如超时)或解析错误,则必须在表单中添加错误消息
  • 如果API返回地址对象作为结果,请检查数据库表中是否已存在具有相同数据的记录
  • 如果数据已存在,请向表单添加错误。然后,地址字符串字段中显示的值应该是返回的标准化值的组合,因此删除了输入错误
  • 如果数据库中不存在数据,请创建一个新的地址实体对象并保留它

问题:

首先,我不确定使用数据转换器是否是实现此类解决方案的正确方法,因为在我在文档和其他教程中找到的示例中,只有已经存在的数据才会被转换进入另一种(视觉)表示#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();
        }
    }
}

0 个答案:

没有答案