doctrine/doctrine-zend-hydrator

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

Doctrine hydrator for Zend Framework applications

1.0.0 2019-07-10 21:38 UTC

This package is auto-updated.

Last update: 2020-01-24 01:21:42 UTC


README

Build Status Coverage Status

此库为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()方法是否允许空值,并根据情况执行操作。以下描述了接收到空值时的处理过程

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

集合策略

默认情况下,每个集合关联都有一个特殊策略与之关联,该策略在填充和提取阶段被调用。所有这些策略都扩展自类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按值操作。这意味着数据填充器将通过实体的公共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模块),我们会看到有一个请求用于获取用户的“城市”关系数据,因此我们有一个完全无用的数据库调用,因为这个信息没有被表单渲染。

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

  1. 它首先到达UserFieldset。输入是"名字"(这是一个字符串字段),以及一个"城市",这是一个另一个Fieldset(在我们的User实体中,这是到另一个实体的OneToOne关系)。Hydrator将提取名字和城市(这将是一个Doctrine 2 Proxy对象)。
  2. 因为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关系,并始终查看哪些数据库调用被调用。