oasis / doctrine-addon
Doctrine 扩展组件
Requires
- doctrine/orm: ^2.5
- oasis/logging: ^1.1
Requires (Dev)
- phpunit/phpunit: ^5.3
This package is auto-updated.
Last update: 2024-09-04 18:45:29 UTC
README
Doctrine 是 PHP 数据库组件最流行的供应商。该名称下的核心项目包括一个 对象关系映射器 (ORM) 和它所构建的 数据库抽象层 (DBAL)。
oasis/doctrine-addon 组件提供了一些有用的功能,以扩展 doctrine/orm 组件
- 一个 trait 以简化自动生成 id 字段的声明
- 一种 trait/机制,使缓存失效更加灵活,在对象删除阶段
安装
使用以下命令安装最新版本
$ composer require oasis/doctrine-addon
AutoIdTrait
AutoIdTrait
定义了一个 id
属性和获取器方法 getId()
。它可以用于任何使用 id
作为其主索引的实体类。
级联删除解决方案
当缓存用于 ORM 功能的应用程序时,缓存失效始终是一个需要处理的有趣话题。ORM 中最常见的一个异常是尝试访问包含过时实体的集合。这个问题通常被称为 级联删除问题。例如如下所示
你有一个 Team 实体,它包含一个成员集合,这些成员是 User 实体。当你删除一个 User 实体时,如果没有适当的失效过程,访问引用该 User 的 Team 将会抛出异常。
默认情况下,doctrine/orm 提供了两种方法来解决此问题
- 每次删除时手动失效
- 使用
cascade={"remove"}
注解
然而,这两种解决方案都不是最优的。不用说,手动删除很复杂,当存在引用链时将成为噩梦。另一方面,使用 cascade
注解在开发效率上非常高效,但在性能上非常慢,尤其是在关系映射复杂和数据集大的时候。
oasis/doctrine-addon 通过提供 CascadeRemoveTrait
解决级联删除问题,引入了另一种解决方案
- 声明为
ORM\HasLifecycleCallbacks
注解 - 实现
CascadeRemovableInterface
- 使用
CascadeRemoveTrait
- 使用数据库提供的模式来删除 强相关实体
实现 CascadeRemovableInterface
还需要实现以下两个方法
getCascadeRemoveableEntities()
,它不接受任何参数,并返回一个 强相关实体 的数组getDirtyEntitiesOnInvalidation()
,它不接受任何参数,并返回一个 弱相关实体 的数组
一个 强相关实体 是一个在当前实体被删除时也应该被删除的实体。
一个 弱相关实体 是一个持有当前实体引用的实体,无论是直接(一对一关系)还是通过集合(多对多关系)。当当前实体被删除时,这个引用应该被失效。
通过数据库约束实现 强相关实体 的实际删除,该约束必须是 ON DELETE CASCADE
。
代码示例(一个非常简单的 CMS)
想象我们有一个简单的 CMS,它有 3 种类型的实体:Categroy、Article 和 Tag。该系统必须满足以下要求
- 文章可以属于一个分类
- 一篇文章可以拥有多个标签
- 不同的文章可以共享标签
以下为具体实现
<?php use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Oasis\Mlib\Doctrine\AutoIdTrait; use Oasis\Mlib\Doctrine\CascadeRemovableInterface; use Oasis\Mlib\Doctrine\CascadeRemoveTrait; /** * Class Article * * @ORM\Entity() * @ORM\Table(name="articles") * * @ORM\HasLifecycleCallbacks() */ class Article implements CascadeRemovableInterface { use CascadeRemoveTrait; use AutoIdTrait; /** * @var Category * @ORM\ManyToOne(targetEntity="Category", inversedBy="articles") * @ORM\JoinColumn(onDelete="CASCADE") */ protected $category; /** * @var ArrayCollection * @ORM\ManyToMany(targetEntity="Tag", inversedBy="articles") * @ORM\JoinTable(name="article_tags", * inverseJoinColumns={@ORM\JoinColumn(name="tag", referencedColumnName="id", onDelete="CASCADE")}, * joinColumns={@ORM\JoinColumn(name="`article`", referencedColumnName="id", onDelete="CASCADE")}) */ protected $tags; public function __construct() { $this->tags = new ArrayCollection(); } function __toString() { return sprintf("Article #%s", $this->getId()); } public function addTag(Tag $tag) { if (!$this->tags->contains($tag)) { $this->tags->add($tag); /** @noinspection PhpInternalEntityUsedInspection */ $tag->addArticle($this); } } /** * @return array an array of entities which will also be removed when the calling entity is remvoed */ public function getCascadeRemoveableEntities() { return []; } /** * @return array an array of entities asscociated to the calling entity, which should be detached when calling * entity is removed. */ public function getDirtyEntitiesOnInvalidation() { return $this->tags->toArray(); } /** * @return Category */ public function getCategory() { return $this->category; } /** * @param Category $category */ public function setCategory($category) { if ($this->category == $category) { return; } if ($this->category) { /** @noinspection PhpInternalEntityUsedInspection */ $this->category->removeArticle($this); } $this->category = $category; if ($category) { /** @noinspection PhpInternalEntityUsedInspection */ $category->addArticle($this); } } /** * @return ArrayCollection */ public function getTags() { return $this->tags; } }
<?php use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Oasis\Mlib\Doctrine\AutoIdTrait; use Oasis\Mlib\Doctrine\CascadeRemovableInterface; use Oasis\Mlib\Doctrine\CascadeRemoveTrait; /** * Class Category * * @ORM\Entity() * @ORM\Table(name="categories") * * @ORM\HasLifecycleCallbacks() */ class Category implements CascadeRemovableInterface { use CascadeRemoveTrait; use AutoIdTrait; /** * @var ArrayCollection * @ORM\OneToMany(targetEntity="Article", mappedBy="category") */ protected $articles; /** * @var Category * @ORM\ManyToOne(targetEntity="Category", inversedBy="children") * @ORM\JoinColumn(onDelete="SET NULL"); */ protected $parent; /** * @var ArrayCollection * @ORM\OneToMany(targetEntity="Category", mappedBy="parent") */ protected $children; public function __construct() { $this->articles = new ArrayCollection(); $this->children = new ArrayCollection(); } function __toString() { return '111'; } /** * @param Article $article * * @internal */ public function addArticle($article) { if (!$this->articles->contains($article)) { $this->articles->add($article); } } /** * @param $child * * @internal */ public function addChild($child) { if (!$this->children->contains($child)) { $this->children->add($child); } } /** * @param Article $article * * @internal */ public function removeArticle($article) { if ($this->articles->contains($article)) { $this->articles->remove($article); } } /** * @param $child * * @internal */ public function removeChild($child) { if ($this->children->contains($child)) { $this->children->remove($child); } } /** * @return array an array of entities which will also be removed when the calling entity is remvoed */ public function getCascadeRemoveableEntities() { return $this->articles->toArray(); } /** * @return array an array of entities asscociated to the calling entity, which should be detached when calling * entity is removed. */ public function getDirtyEntitiesOnInvalidation() { return []; } /** * @return ArrayCollection */ public function getChildren() { return $this->children; } /** * @return Category */ public function getParent() { return $this->parent; } /** * @param Category $parent */ public function setParent($parent) { if ($this->parent) { $this->parent->removeChild($this); } $this->parent = $parent; if ($parent) { $parent->addChild($this); } } }
<?php use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Oasis\Mlib\Doctrine\AutoIdTrait; use Oasis\Mlib\Doctrine\CascadeRemovableInterface; use Oasis\Mlib\Doctrine\CascadeRemoveTrait; /** * Class Tag * * @ORM\Entity() * * @ORM\HasLifecycleCallbacks() */ class Tag implements CascadeRemovableInterface { use CascadeRemoveTrait; use AutoIdTrait; /** * @var ArrayCollection * * @ORM\ManyToMany(targetEntity="Article", mappedBy="tags") */ protected $articles; public function __construct() { $this->articles = new ArrayCollection(); } function __toString() { return sprintf("Tag #%s", $this->getId()); } /** * @param Article $article * @internal */ public function addArticle(Article $article) { if (!$this->articles->contains($article)) { $this->articles->add($article); } } /** * @return ArrayCollection */ public function getArticles() { return $this->articles; } /** * @return array an array of entities which will also be removed when the calling entity is remvoed */ public function getCascadeRemoveableEntities() { return []; } /** * @return array an array of entities asscociated to the calling entity, which should be detached when calling * entity is removed. */ public function getDirtyEntitiesOnInvalidation() { return $this->articles->toArray(); } }
注意:双向关系中总有一方且仅有一方可以被外部用户访问。在此示例中,您在文章上调用
setCategory()
,而您永远不会直接在类别上调用addArticle()
。为了进一步确保这种行为并让IDE帮助我们定位潜在的错误,我们应该声明 隐藏 方法为@internal
,这样每次从外部调用都会触发警告。
作业:文章上只有一个
addTag()
方法。您可以尝试编写removeTag()
方法的实现,以进一步熟悉ORM工具。