webimpress / zend-doctrine-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: 2019-07-10 21:44:06 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中也称为Entity Manager,在Doctrine ODM中也称为Document Manager)
use Doctrine\Zend\Hydrator\DoctrineObject as DoctrineHydrator; $hydrator = new DoctrineHydrator($objectManager);
hydrator构造函数还允许第二个参数 byValue
,默认值为true。我们稍后会回过头来讨论这种区别,但简而言之,它允许hydrator通过访问您的实体(getters/setters)的公共API或直接通过反射获取/设置数据来更改其获取/设置数据的方式,从而绕过任何自定义逻辑。
示例 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对象,因此我们可以拥有一个具有正确类型提示的实体中漂亮的应用程序接口。
示例 2:一对一/多对一关联
当处理关联(一对一、多对一、多对多)时,Doctrine hydrator特别有用,并且与Form/Fieldset逻辑(了解更多信息)集成良好。
让我们用一个BlogPost实体和一个User实体为例,说明一对一关联
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; } }
BlogPost实体,具有多对一关联
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; /** … */ }
空值处理
当传递空值到一对一或多对一字段时,例如;
$data = [ 'city' => null, ];
水化器将检查Entity的setCity()方法是否允许null值,并相应地执行。以下描述了接收到null值时发生的流程
- 如果setCity()方法不允许null值,即
function setCity(City $city)
,则null值将被静默忽略,不会进行水化。 - 如果setCity()方法允许null值,即
function setCity(City $city = null)
,则null值将进行水化。
集合策略
默认情况下,每个集合关联都有一个特殊的策略附加在其上,该策略在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 |
ByValue和ByReference的区别在于,当使用以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(即,使用getter和setter)访问和修改您的属性。但是,您可以覆盖此行为以按引用工作(即,hydrator将通过Reflection API访问属性,从而绕过您可能包含在setter/getter中的任何逻辑)。
要更改行为,只需将构造函数的第二个参数设置为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和所有相关的标签。因此,标签的隐藏id将是空的(因为它们尚未被持久化)。
这是创建新博客文章的操作。
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框架表单,你将很可能为每个实体创建一个字段集,这样你就有了一个完美的实体与字段集之间的映射。以下是User和City实体的字段集。
如果您不熟悉Fieldsets及其工作方式,请参阅Zend Framework 文档的这部分内容。
首先,是用户字段集
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。输入是"name"(这是一个字符串字段),以及一个"city",这是一个另一个字段集(在我们的User实体中,这是一个到另一个实体的一对一关系)。hydrator将提取名称和城市(将是一个Doctrine 2代理对象)。
- 因为UserFieldset包含对另一个Fieldset的引用(在我们的例子中,是CityFieldset),它将转而尝试提取城市的值以填充CityFieldset的值。问题是:City是一个代理,因此因为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关系,它将不会被提取!
作为一个经验法则,尽量删除任何不必要的字段集关系,并始终查看哪些数据库调用被做出。