doctrine / doctrine-zend-hydrator
Doctrine hydrator 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-01-24 01:21:42 UTC
README
此库为Zend Framework应用程序提供Doctrine Hydrator。
安装
运行以下命令安装此库
$ composer require doctrine/doctrine-zend-hydrator
用法
Hydrator将数据数组转换为对象(这称为“初始化”)并将对象转换回数组(这称为“提取”)。Hydrator主要用于表单的上下文中,与Zend Framework的绑定功能,但也可用于任何初始化/提取上下文(例如,可以在RESTful上下文中使用)。如果您对hydrator不熟悉,请先阅读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通过访问实体(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在初始化过程中自动将时间戳转换为DateTime对象,因此我们可以在实体中拥有一个良好的API和正确类型提示。
示例 2 : OneToOne/ManyToOne 关联
Doctrine Hydrator在处理关联(OneToOne、OneToMany、ManyToOne)时特别有用,并且与表单/Fieldset逻辑很好地集成(了解更多信息)。
让我们用一个简单的BlogPost和User实体示例来展示OneToOne关联
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; } }
然后是具有ManyToOne关联的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; } }
在OneToOne关联使用时,可能会出现两种用例:一对一实体(在这种情况下,是用户)可能已经存在(这通常会在用户和博客文章的例子中发生),或者可以创建。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可以在关联中持久化新的实体(注意OneToMany关联上的级联选项)
namespace Application\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class BlogPost { /** .. */ /** * @ORM\ManyToOne(targetEntity="Application\Entity\User", cascade={"persist"}) */ protected $user; /** … */ }
还可以为用户数据使用嵌套字段集。Hydrator将使用映射数据来确定一对一关系的标识符,并尝试找到现有的记录或实例化一个新目标实例,该实例在传递给BlogPost实体之前将被填充。
注意:你真的不希望用户通过博客文章来添加,对吧?
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:OneToMany 关联
Doctrine Hydrator还处理OneToMany关系(当使用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; } }
请注意BlogPost实体中的一些有趣之处。我们定义了两个函数: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可以在关联中持久化新的实体(注意OneToMany关联上的级联选项)
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, ];
数据填充器会检查实体的setCity()方法是否允许空值,并根据情况执行操作。以下描述了接收到空值时的处理过程
- 如果setCity()方法不允许空值,即
function setCity(City $city)
,则空值会被静默忽略,并且不会被数据填充。 - 如果setCity()方法允许空值,即
function setCity(City $city = null)
,则空值将被数据填充。
集合策略
默认情况下,每个集合关联都有一个特殊策略与之关联,该策略在填充和提取阶段被调用。所有这些策略都扩展自类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按值操作。这意味着数据填充器将通过实体的公共API(即getters和setters)访问和修改您的属性。然而,您可以覆盖此行为以按引用工作(即数据填充器将通过反射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(); } /** ... */ }
现在让我们使用默认方法,按值使用数据填充器
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
正如我们所看到的,数据填充器使用了公共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 Framework的表单组件中的。我们将使用一个简单的例子,再次使用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 Framework中,创建一个字段集用于每个实体是一个好习惯,以便在许多表单中重用它们。
这是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 Framework表单,您可能会为每个实体创建一个字段集,以便在实体和字段集之间有一个完美的映射。以下是User和City实体的字段集。
如果您不熟悉Fieldsets及其工作方式,请参阅Zend Framework文档的这部分内容。
首先,是用户Fieldset
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, ], ]; } }
然后是城市Fieldset
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。输入是"名字"(这是一个字符串字段),以及一个"城市",这是一个另一个Fieldset(在我们的User实体中,这是到另一个实体的OneToOne关系)。Hydrator将提取名字和城市(这将是一个Doctrine 2 Proxy对象)。
- 因为UserFieldset包含对另一个Fieldset的引用(在我们的情况下,是CityFieldset),所以它将转而尝试提取城市的值以填充CityFieldset的值。问题是:City是一个Proxy,因此由于Hydrator尝试提取其值(名称和邮编字段),Doctrine会自动从数据库中获取对象以满足Hydrator。
这是完全正常的,这是ZF表单的工作方式,这也是它们几乎神奇的原因之一,但在这个特定的情况下,它可能导致灾难性的后果。当您有非常复杂的实体和大量的OneToMany集合时,想象一下会有多少不必要的调用(实际上,在发现这个问题后,我意识到我的应用程序做了10次不必要的数据库调用)。
实际上,修复方法非常简单:如果您不需要表单中的特定Fieldset,请删除它们。这里是修复后的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关系,它就不会被提取了!
作为经验法则,尝试删除任何不必要的Fieldset关系,并始终查看哪些数据库调用被调用。