webimpress / doctrine-zend-hydrator
Doctrine hydrators for Zend Framework applications
Requires
- php: ^7.1
- ext-ctype: *
- doctrine/common: ^2.8
- zendframework/zend-hydrator: ^2.3
- zendframework/zend-stdlib: ^3.1
Requires (Dev)
- phpunit/phpunit: ^7.5.2
- zendframework/zend-coding-standard: ~1.0.0
- zendframework/zend-filter: ^2.8
- zendframework/zend-servicemanager: ^3.3
This package is auto-updated.
Last update: 2020-09-01 20:43:12 UTC
README
废弃
此包已不再维护。请使用 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()方法是否允许空值,并据此操作。以下描述接收空值时发生的流程
- 如果setCity()方法不允许空值,即
function setCity(City $city)
,则空值会被静默忽略,并且不会被hydrated。 - 如果setCity()方法允许空值,即
function setCity(City $city = null)
,则空值将被hydrated。
集合策略
默认情况下,每个集合关联都会在hydrating和extracting阶段附加一个特殊策略。所有这些策略都扩展自类Doctrine\Zend\Hydrator\Strategy\AbstractCollectionStrategy
。
库提供了四种内置策略
Doctrine\Zend\Hydrator\Strategy\AllowRemoveByValue
:这是默认策略,它会移除新集合中不存在的老元素。Doctrine\Zend\Hydrator\Strategy\AllowRemoveByReference
:这是默认策略(如果设置为byReference),它会移除新集合中不存在的老元素。Doctrine\Zend\Hydrator\Strategy\DisallowRemoveByValue
:此策略不会移除新集合中不存在的老元素。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模块),我们将看到有一个请求被用来获取用户城市关系的数据,因此我们得到了一个完全无用的数据库调用,因为这个信息没有在表单中渲染。
您可能会问,“为什么?”是的,我们设置了验证组,但问题是发生在提取阶段。下面是如何工作的:当一个对象被绑定到表单时,这个表单会遍历其所有字段,并尝试从绑定的对象中提取数据。在我们的例子中,这是如何工作的
- 它首先到达UserFieldset。输入是"名字"(这是一个字符串字段),以及一个"城市",它是一个字段集(在我们的User实体中,这是与其他实体的一个一对一关系)。hydrator会提取名字和城市(这将是一个Doctrine 2 Proxy对象)。
- 因为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关系,它将不会被提取!
作为一条经验法则,尝试删除任何不必要的字段集关系,并始终查看哪些数据库调用被做出。