oasis/doctrine-addon

Doctrine 扩展组件

v2.0.2 2017-02-04 06:23 UTC

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工具。