webimpress / doctrine-zend-hydrator

此包已废弃,不再维护。作者建议使用 doctrine/doctrine-zend-hydrator 包。

Doctrine hydrators for Zend Framework applications

dev-master / 1.0.x-dev 2019-07-10 21:26 UTC

This package is auto-updated.

Last update: 2020-09-01 20:43:12 UTC


README

Build Status Coverage Status

废弃

此包已不再维护。请使用 doctrine/doctrine-zend-hydrator 代替。

此库为 Zend Framework 应用提供了 Doctrine Hydrators。

安装

运行以下命令以安装此库

$ composer require webimpress/doctrine-zend-hydrator

使用方法

Hydrators 将数据数组转换为对象(称为 "hydrating"),并将对象转换回数组(称为 "extracting")。Hydrators 主要用于表单的上下文,与 Zend Framework 的绑定功能,但也可以用于任何 hydrating/extracting 上下文(例如,可以用于 RESTful 上下文)。如果您不熟悉 hydrators,请先阅读 Zend Framework hydrator 的文档

基本使用

该库附带一个功能强大的 hydrator,几乎适用于任何使用场景。

创建 hydrator

要创建 Doctrine Hydrator,您只需要一个对象管理器(在 Doctrine ORM 中也称为实体管理器,在 Doctrine ODM 中称为文档管理器)

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($objectManager);

hydrator 构造函数还允许第二个参数,byValue,默认为 true。我们稍后会回来讨论这个区别,但简而言之,它允许 hydrator 通过访问实体的公共 API(getter/setter)或直接通过反射获取/设置数据,从而绕过任何自定义逻辑。

示例 1:无关联的简单实体

让我们从一个简单的例子开始

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class City
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=48)
     */
    protected $name;

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}

现在,让我们使用 Doctrine hydrator

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($entityManager);
$city = new City();
$data = [
    'name' => 'Paris',
];

$city = $hydrator->hydrate($data, $city);

echo $city->getName(); // prints "Paris"

$dataArray = $hydrator->extract($city);
echo $dataArray['name']; // prints "Paris"

如示例所示,在简单情况下,Doctrine Hydrator 与简单的 hydrator(如 "ClassMethods")几乎没有任何优势。然而,即使在那些情况下,我也建议您使用它,因为它执行类型之间的自动转换。例如,它可以转换时间戳为 DateTime(这是 Doctrine 表示日期使用的类型)

namespace Application\Entity;

use DateTime;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Appointment
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="datetime")
     */
    protected $time;

    public function getId()
    {
        return $this->id;
    }

    public function setTime(DateTime $time)
    {
        $this->time = $time;
    }

    public function getTime()
    {
        return $this->time;
    }
}

让我们使用 hydrator

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($entityManager);
$appointment = new Appointment();
$data = [
    'time' => '1357057334',
];

$appointment = $hydrator->hydrate($data, $appointment);

echo get_class($appointment->getTime()); // prints "DateTime"

如你所见,hydrator 在 hydrating 过程中将时间戳自动转换为 DateTime 对象,从而允许我们在实体中拥有一个类型正确的良好 API。

示例 2:一对一/多对一关联

Doctrine Hydrator 在处理关联(一对一、一对多、多对一)时特别有用,并且可以很好地与表单/字段集逻辑集成(了解更多信息请点击此处)。

让我们用一个简单的例子来说明一对一关联:一个博客帖子实体和一个用户实体。

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class User
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=48)
     */
    protected $username;

    /**
     * @ORM\Column(type="string")
     */
    protected $password;

    public function getId()
    {
        return $this->id;
    }

    public function setUsername($username)
    {
        $this->username = $username;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function setPassword($password)
    {
        $this->password = $password;
    }

    public function getPassword()
    {
        return $this->password;
    }
}

下面是具有多对一关联的博客帖子实体。

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class BlogPost
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Application\Entity\User")
     */
    protected $user;

    /**
     * @ORM\Column(type="string")
     */
    protected $title;

    public function getId()
    {
        return $this->id;
    }

    public function setUser(User $user)
    {
        $this->user = $user;
    }

    public function getUser()
    {
        return $this->user;
    }

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }
}

使用一对一关联时可能会出现两种情况:在关联中,单一实体(在本例中是用户)可能已经存在(这通常是在用户和博客帖子示例中),或者可以创建。DoctrineHydrator 原生支持这两种情况。

关联中的现有实体

当关联的实体已经存在时,您只需简单地提供关联的标识符即可。

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($entityManager);
$blogPost = new BlogPost();
$data = [
    'title' => 'The best blog post in the world!',
    'user'  => [
        'id' => 2, // Written by user 2
    ],
];

$blogPost = $hydrator->hydrate($data, $blogPost);

echo $blogPost->getTitle(); // prints "The best blog post in the world!"
echo $blogPost->getUser()->getId(); // prints 2

注意:当使用主键不是复合的关联时,您可以更简洁地重写以下内容

$data = [
    'title' => 'The best blog post in the world!',
    'user'  => [
        'id' => 2, // Written by user 2
    ],
];

$data = [
    'title' => 'The best blog post in the world!',
    'user'  => 2,
];
关联中的非现有实体

如果关联的实体不存在,您只需提供对象即可。

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($entityManager);
$blogPost = new BlogPost();
$user = new User();
$user->setUsername('bakura');
$user->setPassword('p@$$w0rd');

$data = [
    'title' => 'The best blog post in the world!',
    'user'  => $user,
];

$blogPost = $hydrator->hydrate($data, $blogPost);

echo $blogPost->getTitle(); // prints "The best blog post in the world!"
echo $blogPost->getUser()->getId(); // prints 2

为了使这可行,您还必须稍微更改您的映射,以便 Doctrine 可以在关联上持久化新实体(注意一对多关联上的级联选项)。

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class BlogPost
{
    /** .. */

    /**
     * @ORM\ManyToOne(targetEntity="Application\Entity\User", cascade={"persist"})
     */
    protected $user;

    /** … */
}

还可以为用户数据使用嵌套字段集。hydrator 将使用映射数据来确定一对一关系的标识符,然后尝试找到现有记录或实例化一个新的目标实例,该实例将在传递给博客帖子实体之前进行填充。

注意:您真的允许通过博客帖子添加用户吗?

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($entityManager, 'Application\Entity\BlogPost');
$blogPost = new BlogPost();

$data = [
    'title' => 'Art thou mad?',
    'user' => [
        'id' => '',
        'username' => 'willshakes',
        'password' => '2BorN0t2B',
    ],
];

$blogPost = $hydrator->hydrate($data, $blogPost);

echo $blogPost->getUser()->getUsername(); // prints willshakes
echo $blogPost->getUser()->getPassword(); // prints 2BorN0t2B

示例 3:一对多关联

Doctrine Hydrator 还处理一对多关系(当使用 Zend\Form\Element\Collection 元素时)。请参考官方的Zend 框架文档来了解有关集合的更多信息。

注意:在内部,对于给定的集合,如果数组包含标识符,hydrator 将自动通过 Doctrine 的 find 函数获取对象。但是,这可能会导致问题,如果集合中的一个值是空字符串 ''(因为 find 很可能失败)。为了解决这个问题,在填充阶段简单地忽略空字符串标识符。因此,如果您的数据库中包含空字符串主键值,hydrator 可能无法正常工作(避免这种情况的最简单方法是不要有空字符串主键,如果您使用自动递增主键,则通常不会发生这种情况)。

让我们再次举一个简单的例子:博客帖子实体和标签实体。

namespace Application\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class BlogPost
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\OneToMany(targetEntity="Application\Entity\Tag", mappedBy="blogPost")
     */
    protected $tags;

    /**
     * Never forget to initialize your collections!
     */
    public function __construct()
    {
        $this->tags = new ArrayCollection();
    }

    public function getId()
    {
        return $this->id;
    }

    public function addTags(Collection $tags)
    {
        foreach ($tags as $tag) {
            $tag->setBlogPost($this);
            $this->tags->add($tag);
        }
    }

    public function removeTags(Collection $tags)
    {
        foreach ($tags as $tag) {
            $tag->setBlogPost(null);
            $this->tags->removeElement($tag);
        }
    }

    public function getTags()
    {
        return $this->tags;
    }
}

下面是标签实体。

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Tag
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Application\Entity\BlogPost", inversedBy="tags")
     */
    protected $blogPost;

    /**
     * @ORM\Column(type="string")
     */
    protected $name;

    public function getId()
    {
        return $this->id;
    }

    /**
     * Allow null to remove association
     */
    public function setBlogPost(BlogPost $blogPost = null)
    {
        $this->blogPost = $blogPost;
    }

    public function getBlogPost()
    {
        return $this->blogPost;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}

请注意博客帖子实体中的一些有趣之处。我们定义了两个函数:addTags 和 removeTags。这些函数必须始终定义,并且当处理集合时,Doctrine hydrator 会自动调用它们。您可能会认为这有点过度,并问为什么不能只定义一个 setTags 函数来替换旧的集合

public function setTags(Collection $tags)
{
    $this->tags = $tags;
}

但这是非常不好的,因为 Doctrine 集合不应该被交换,主要是因为集合由 ObjectManager 管理,因此不应替换为新实例。

同样,可能会出现两种情况:标签已存在或不存在。

关联中的现有实体

当关联实体已经存在时,您只需要提供实体的标识符即可。

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($entityManager);
$blogPost = new BlogPost();
$data = [
    'title' => 'The best blog post in the world!',
    'tags'  => [
        ['id' => 3], // add tag whose id is 3
        ['id' => 8], // also add tag whose id is 8
    ],
];

$blogPost = $hydrator->hydrate($data, $blogPost);

echo $blogPost->getTitle(); // prints "The best blog post in the world!"
echo count($blogPost->getTags()); // prints 2

注意:再次提醒,这

$data = [
    'title' => 'The best blog post in the world!',
    'tags'  => [
        ['id' => 3], // add tag whose id is 3
        ['id' => 8], // also add tag whose id is 8
    ],
];

可以写成

$data = [
    'title' => 'The best blog post in the world!',
    'tags'  => [3, 8],
];
关联中不存在的实体

如果关联的实体不存在,您只需提供对象即可。

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($entityManager);
$blogPost = new BlogPost();

$tags = [];

$tag1 = new Tag();
$tag1->setName('PHP');
$tags[] = $tag1;

$tag2 = new Tag();
$tag2->setName('STL');
$tags[] = $tag2;

$data = [
    'title' => 'The best blog post in the world!',
    'tags'  => $tags, // Note that you can mix integers and entities without any problem
];

$blogPost = $hydrator->hydrate($data, $blogPost);

echo $blogPost->getTitle(); // prints "The best blog post in the world!"
echo count($blogPost->getTags()); // prints 2

为了使这可行,您还必须稍微更改您的映射,以便 Doctrine 可以在关联上持久化新实体(注意一对多关联上的级联选项)。

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class BlogPost
{
    /** .. */

    /**
     * @ORM\OneToMany(targetEntity="Application\Entity\Tag", mappedBy="blogPost", cascade={"persist"})
     */
    protected $tags;

    /** … */
}
处理空值

当将空值传递给OneToOne或ManyToOne字段时,例如;

$data = [
    'city' => null,
];

hydrator会检查Entity上的setCity()方法是否允许空值,并据此操作。以下描述接收空值时发生的流程

  1. 如果setCity()方法不允许空值,即function setCity(City $city),则空值会被静默忽略,并且不会被hydrated。
  2. 如果setCity()方法允许空值,即function setCity(City $city = null),则空值将被hydrated。

集合策略

默认情况下,每个集合关联都会在hydrating和extracting阶段附加一个特殊策略。所有这些策略都扩展自类Doctrine\Zend\Hydrator\Strategy\AbstractCollectionStrategy

库提供了四种内置策略

  1. Doctrine\Zend\Hydrator\Strategy\AllowRemoveByValue:这是默认策略,它会移除新集合中不存在的老元素。
  2. Doctrine\Zend\Hydrator\Strategy\AllowRemoveByReference:这是默认策略(如果设置为byReference),它会移除新集合中不存在的老元素。
  3. Doctrine\Zend\Hydrator\Strategy\DisallowRemoveByValue:此策略不会移除新集合中不存在的老元素。
  4. Doctrine\Zend\Hydrator\Strategy\DisallowRemoveByReference:此策略不会移除新集合中不存在的老元素。

因此,当使用AllowRemove*时,您需要定义添加器(例如addTags)和移除器(例如removeTags)。另一方面,当使用DisallowRemove*策略时,您必须始终定义至少添加器,但移除器是可选的(因为元素永远不会被移除)。

以下表格说明了两种策略之间的区别

策略 初始集合 提交的集合 结果
AllowRemove* A, B B, C B, C
DisallowRemove* A, B B, C A, B, C

值和引用的区别在于,当使用以ByReference结尾的策略时,它不会使用您的实体(添加器和移除器)的公共API - 您甚至不需要定义它们 - 它将直接从集合中添加和移除元素。

更改策略

更改集合的策略非常简单。

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Doctrine\Zend\Hydrator\Strategy;

$hydrator = new DoctrineHydrator($entityManager);
$hydrator->addStrategy('tags', new Strategy\DisallowRemoveByValue());

请注意,您还可以将策略添加到简单字段。

值和引用

默认情况下,Doctrine Hydrator按值工作。这意味着hydrator将通过实体的公共API(即getters和setters)访问和修改您的属性。然而,您可以覆盖此行为以按引用工作(即hydrator将通过Reflection API访问属性,从而绕过您可能在setters/getters中包含的任何逻辑)。

要更改行为,只需将构造函数的第二个参数设置为false

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($objectManager, false);

为了说明这两者的区别,让我们对给定的实体进行提取

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class SimpleEntity
{
    /**
     * @ORM\Column(type="string")
     */
    protected $foo;

    public function getFoo()
    {
        die();
    }

    /** ... */
}

现在让我们使用默认方法,按值使用hydrator

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($objectManager);
$object   = new SimpleEntity();
$object->setFoo('bar');

$data = $hydrator->extract($object);

echo $data['foo']; // never executed, because the script was killed when getter was accessed

如我们所见,hydrator使用了公共API(这里的getFoo)来检索值。

然而,如果我们按引用使用它

use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;

$hydrator = new DoctrineHydrator($objectManager, false);
$object   = new SimpleEntity();
$object->setFoo('bar');

$data = $hydrator->extract($object);

echo $data['foo']; // prints 'bar'

现在它只打印出"bar",这清楚地表明没有调用getter。

使用 Zend\Form 的完整示例

现在我们了解了 hydrator 的工作原理,让我们看看它是如何集成到 Zend 框架的表单组件中的。我们将使用一个简单的示例,再次使用 BlogPost 和 Tag 实体。我们将看到如何创建博客文章,并且能够编辑它。

实体

首先,让我们定义(简化版)实体,从 BlogPost 实体开始。

namespace Application\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class BlogPost
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\OneToMany(targetEntity="Application\Entity\Tag", mappedBy="blogPost", cascade={"persist"})
     */
    protected $tags;

    /**
     * Never forget to initialize your collections!
     */
    public function __construct()
    {
        $this->tags = new ArrayCollection();
    }

    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param Collection $tags
     */
    public function addTags(Collection $tags)
    {
        foreach ($tags as $tag) {
            $tag->setBlogPost($this);
            $this->tags->add($tag);
        }
    }

    /**
     * @param Collection $tags
     */
    public function removeTags(Collection $tags)
    {
        foreach ($tags as $tag) {
            $tag->setBlogPost(null);
            $this->tags->removeElement($tag);
        }
    }

    /**
     * @return Collection
     */
    public function getTags()
    {
        return $this->tags;
    }
}

然后是 Tag 实体。

namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Tag
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Application\Entity\BlogPost", inversedBy="tags")
     */
    protected $blogPost;

    /**
     * @ORM\Column(type="string")
     */
    protected $name;

    /**
     * Get the id
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Allow null to remove association
     *
     * @param BlogPost $blogPost
     */
    public function setBlogPost(BlogPost $blogPost = null)
    {
        $this->blogPost = $blogPost;
    }

    /**
     * @return BlogPost
     */
    public function getBlogPost()
    {
        return $this->blogPost;
    }

    /**
     * @param string $name
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }
}

字段集

现在我们需要创建两个字段集来映射这些实体。在 Zend 框架中,创建每个实体的一个字段集是一个好习惯,以便在多个表单中重用它们。

以下是 Tag 的字段集。注意,在这个例子中,我添加了一个名为 "id" 的隐藏输入。这在编辑时是必需的。大多数情况下,当你第一次创建博客文章时,标签并不存在。因此,id 将为空。然而,当你编辑博客文章时,所有标签已经存在于数据库中(它们已经被持久化并且有一个 id),因此隐藏的 "id" 输入将有一个值。这允许你通过修改现有的 Tag 实体来修改标签名称,而无需创建一个新的标签(并删除旧的标签)。

namespace Application\Form;

use Application\Entity\Tag;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;

class TagFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('tag');

        $this->setHydrator(new DoctrineHydrator($objectManager))
             ->setObject(new Tag());

        $this->add([
            'type' => 'Zend\Form\Element\Hidden',
            'name' => 'id',
        ]);

        $this->add([
            'type'    => 'Zend\Form\Element\Text',
            'name'    => 'name',
            'options' => [
                'label' => 'Tag',
            ],
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            'id' => [
                'required' => false,
            ],
            'name' => [
                'required' => true,
            ],
        ];
    }
}

以及 BlogPost 字段集。

namespace Application\Form;

use Application\Entity\BlogPost;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;

class BlogPostFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('blog-post');

        $this->setHydrator(new DoctrineHydrator($objectManager))
             ->setObject(new BlogPost());

        $this->add([
            'type' => 'Zend\Form\Element\Text',
            'name' => 'title',
        ]);

        $tagFieldset = new TagFieldset($objectManager);
        $this->add([
            'type'    => 'Zend\Form\Element\Collection',
            'name'    => 'tags',
            'options' => [
                'count'          => 2,
                'target_element' => $tagFieldset,
            ],
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            'title' => [
                'required' => true,
            ],
        ];
    }
}

简单直接。博客文章只是一个简单的字段集,元素类型为 Zend\Form\Element\Collection,它表示多对一关联。

表单

现在我们已经创建了字段集,我们将创建两个表单:一个用于创建,一个用于更新。表单的目的是成为字段集之间的粘合剂。在这个简单的示例中,这两个表单是完全相同的,但在实际应用中,你可能想通过更改验证组(例如,你可能想禁止用户在更新时修改博客文章的标题)来更改此行为。

以下是创建表单。

namespace Application\Form;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Form;

class CreateBlogPostForm extends Form
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('create-blog-post-form');

        // The form will hydrate an object of type "BlogPost"
        $this->setHydrator(new DoctrineHydrator($objectManager));

        // Add the BlogPost fieldset, and set it as the base fieldset
        $blogPostFieldset = new BlogPostFieldset($objectManager);
        $blogPostFieldset->setUseAsBaseFieldset(true);
        $this->add($blogPostFieldset);

        // … add CSRF and submit elements …

        // Optionally set your validation group here
    }
}

以及更新表单。

namespace Application\Form;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Form;

class UpdateBlogPostForm extends Form
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('update-blog-post-form');

        // The form will hydrate an object of type "BlogPost"
        $this->setHydrator(new DoctrineHydrator($objectManager));

        // Add the BlogPost fieldset, and set it as the base fieldset
        $blogPostFieldset = new BlogPostFieldset($objectManager);
        $blogPostFieldset->setUseAsBaseFieldset(true);
        $this->add($blogPostFieldset);

        // … add CSRF and submit elements …

        // Optionally set your validation group here
    }
}

控制器

我们现在有了所有这些。让我们创建控制器。

创建

在 createAction 中,我们将创建一个新的 BlogPost 和所有相关联的标签。因此,标签的隐藏 ids 将为空(因为它们尚未被持久化)。

以下是创建新博客文章的动作。

public function createAction()
{
    // Get your ObjectManager from the ServiceManager
    $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');

    // Create the form and inject the ObjectManager
    $form = new CreateBlogPostForm($objectManager);

    // Create a new, empty entity and bind it to the form
    $blogPost = new BlogPost();
    $form->bind($blogPost);

    if ($this->request->isPost()) {
        $form->setData($this->request->getPost());

        if ($form->isValid()) {
            $objectManager->persist($blogPost);
            $objectManager->flush();
        }
    }

    return ['form' => $form];
}

更新表单类似,但我们从数据库中获取博客文章,而不是创建一个新的空博客文章。

public function editAction()
{
    // Get your ObjectManager from the ServiceManager
    $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');

    // Create the form and inject the ObjectManager
    $form = new UpdateBlogPostForm($objectManager);

    // Fetch the existing BlogPost from storage and bind it to the form.
    // This will pre-fill form field values
    $blogPost = $this->userService->get($this->params('blogPost_id'));
    $form->bind($blogPost);

    if ($this->request->isPost()) {
        $form->setData($this->request->getPost());

        if ($form->isValid()) {
            // Save the changes
            $objectManager->flush();
        }
    }

    return ['form' => $form];
}

性能考虑

尽管使用 hydrator 很像魔法一样,因为它抽象了大部分繁琐的任务,但你必须意识到在某些情况下它可能导致性能问题。请仔细阅读以下段落,以了解如何解决(并避免!)这些问题。

不希望的副作用

当使用 Doctrine Hydrator 与包含大量关联的复杂实体时,你必须非常小心,如果你没有完全意识到底层发生了什么,可能会进行大量的不必要的数据库调用。为了解释这个问题,让我们举一个例子。

想象以下实体:

namespace Application\Entity;

/**
 * @ORM\Entity
 * @ORM\Table(name="Students")
 */
class User
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=48)
     */
    protected $name;

    /**
     * @ORM\OneToOne(targetEntity="City")
     */
    protected $city;

    // … getter and setters are defined …
}

这个简单的实体包含一个ID、一个字符串属性和一个一对一关系。如果您以正确的方式使用Zend框架表单,您可能会为每个实体创建一个字段集,以便在实体和字段集之间有完美的映射。以下是用户和城市实体的字段集。

如果您对字段集及其工作方式不熟悉,请参阅Zend框架文档的这部分

首先,是用户字段集

namespace Application\Form;

use Application\Entity\User;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;

class UserFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('user');

        $this->setHydrator(new DoctrineHydrator($objectManager))
             ->setObject(new User());

        $this->add([
            'type'    => 'Zend\Form\Element\Text',
            'name'    => 'name',
            'options' => [
                'label' => 'Your name',
            ],
            'attributes' => [
                'required' => 'required',
            ],
        ]);

        $cityFieldset = new CityFieldset($objectManager);
        $cityFieldset->setLabel('Your city');
        $cityFieldset->setName('city');
        $this->add($cityFieldset);
    }

    public function getInputFilterSpecification()
    {
        return [
            'name' => [
                'required' => true,
            ],
        ];
    }
}

然后是城市字段集

namespace Application\Form;

use Application\Entity\City;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;

class CityFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('city');

        $this->setHydrator(new DoctrineHydrator($objectManager))
             ->setObject(new City());

        $this->add([
            'type'    => 'Zend\Form\Element\Text',
            'name'    => 'name',
            'options' => [
                'label' => 'Name of your city',
            ],
            'attributes' => [
                'required' => 'required',
            ],
        ]);

        $this->add([
            'type'    => 'Zend\Form\Element\Text',
            'name'    => 'postCode',
            'options' => [
                'label' => 'Postcode of your city',
            ],
            'attributes' => [
                'required' => 'required',
            ],
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            'name' => [
                'required' => true,
            ],
            'postCode' => [
                'required' => true,
            ],
        ];
    }
}

现在,假设我们有一个表单,登录用户只能更改他的名字。这个特定的表单不允许用户更改这个城市,城市字段甚至没有在表单中渲染。天真地,这个表单会是这样

namespace Application\Form;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Form;

class EditNameForm extends Form
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('edit-name-form');

        $this->setHydrator(new DoctrineHydrator($objectManager));

        // Add the user fieldset, and set it as the base fieldset
        $userFieldset = new UserFieldset($objectManager);
        $userFieldset->setName('user');
        $userFieldset->setUseAsBaseFieldset(true);
        $this->add($userFieldset);

        // … add CSRF and submit elements …

        // Set the validation group so that we don't care about city
        $this->setValidationGroup([
            'csrf', // assume we added a CSRF element
            'user' => [
                'name',
            ],
        ]);
    }
}

再次提醒,如果您不熟悉这里的概念,请阅读官方文档

在这里,我们创建了一个简单的表单,称为"EditSimpleForm"。因为我们设置了验证组,所以与城市相关的所有输入(邮编和城市名称)都不会被验证,这正是我们想要的。动作将类似于以下内容

public function editNameAction()
{
    // Get your ObjectManager from the ServiceManager
    $objectManager = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');

    // Create the form and inject the ObjectManager
    $form = new EditNameForm($objectManager);

    // Get the logged user (for more informations about userIdentity(), please read the Authentication doc)
    $loggedUser = $this->userIdentity();

    // We bind the logged user to the form, so that the name is pre-filled with previous data
    $form->bind($loggedUser);

    $request = $this->request;
    if ($request->isPost()) {
        // Set data from post
        $form->setData($request->getPost());

        if ($form->isValid()) {
            // You can now safely save $loggedUser
        }
    }
}

这看起来不错,不是吗?然而,如果我们检查所做的查询(例如,使用酷炫的ZendDeveloperTools模块),我们将看到有一个请求被用来获取用户城市关系的数据,因此我们得到了一个完全无用的数据库调用,因为这个信息没有在表单中渲染。

您可能会问,“为什么?”是的,我们设置了验证组,但问题是发生在提取阶段。下面是如何工作的:当一个对象被绑定到表单时,这个表单会遍历其所有字段,并尝试从绑定的对象中提取数据。在我们的例子中,这是如何工作的

  1. 它首先到达UserFieldset。输入是"名字"(这是一个字符串字段),以及一个"城市",它是一个字段集(在我们的User实体中,这是与其他实体的一个一对一关系)。hydrator会提取名字和城市(这将是一个Doctrine 2 Proxy对象)。
  2. 因为UserFieldset包含对另一个Fieldset的引用(在我们的情况下,是CityFieldset),所以它会试图提取城市(这将是一个代理对象)的值来填充CityFieldset的值。问题就在这里:城市是一个代理,因此由于hydrator试图提取它的值(名字和邮编字段),Doctrine将自动从数据库中获取对象以满足hydrator的要求。

这是完全正常的,这是ZF表单工作的方式,也是它们几乎神奇的原因,但在这种特定情况下,它可能会导致灾难性的后果。当您有非常复杂的实体和许多OneToMany集合时,想象一下会有多少不必要的调用(实际上,在发现这个问题后,我意识到我的应用程序做了10次不必要的数据库调用)。

事实上,解决方案非常简单:如果您在表单中不需要特定的字段集,请删除它们。以下是修复后的EditUserForm

namespace Application\Form;

use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator;
use Zend\Form\Form;

class EditNameForm extends Form
{
    public function __construct(ObjectManager $objectManager)
    {
        parent::__construct('edit-name-form');

        $this->setHydrator(new DoctrineHydrator($objectManager));

        // Add the user fieldset, and set it as the base fieldset
        $userFieldset = new UserFieldset($objectManager);
        $userFieldset->setName('user');
        $userFieldset->setUseAsBaseFieldset(true);

        // We don't want City relationship, so remove it!!
        $userFieldset->remove('city');

        $this->add($userFieldset);

        // … add CSRF and submit elements …

        // We don't even need the validation group as the City fieldset does not
        // exist anymore
    }
}

然后,砰!由于UserFieldset不再包含CityFieldset关系,它将不会被提取!

作为一条经验法则,尝试删除任何不必要的字段集关系,并始终查看哪些数据库调用被做出。