netbull/doctrine-behaviors

Doctrine2行为特性

v1.6.3 2019-11-28 08:47 UTC

README

Doctrine2 Behaviors

Build Status

  • 分支 master 包含下一个 v2.0 版本
  • 分支 v1 包含当前 v1.x 版本

这是一个PHP >=7.0 库,包含一系列特性接口,用于向Doctrine2实体和仓库添加行为。

目前它处理以下行为:

此项目正在寻找维护者

我们意识到我们再也没有那么多时间来维护这个项目了,因此我们正在寻找维护者。如果你希望继续工作,请创建一个问题。

注意

一些行为(可翻译、可时间戳、软删除、责任归属、可地理编码)需要Doctrine订阅者才能工作。请确保通过阅读订阅者部分来激活它们。

安装

composer require knplabs/doctrine-behaviors:~1.1

配置

默认情况下,当与Symfony集成时,所有订阅者都是启用的(如果你没有为该包指定任何配置)。但是,你可以通过白名单方式启用所需的行为

knp_doctrine_behaviors:
    blameable:      false
    geocodable:     ~     # Here null is converted to false
    loggable:       ~
    sluggable:      true
    soft_deletable: true
    # All others behaviors are disabled

订阅者

如果你使用symfony2,你可以轻松地在以下位置注册它们:

  • 推荐方法

添加到AppKernel

class AppKernel
{
    function registerBundles()
    {
        $bundles = array(
            //...
            new Knp\DoctrineBehaviors\Bundle\DoctrineBehaviorsBundle(),
            //...
        );

        //...

        return $bundles;
    }
}
  • 已弃用方法:导入服务定义文件
    # app/config/config.yml
    imports:
        - { resource: ../../vendor/knplabs/doctrine-behaviors/config/orm-services.yml }

你也可以使用doctrine2 api来注册它们

<?php

$em->getEventManager()->addEventSubscriber(new \Knp\DoctrineBehaviors\ORM\Translatable\TranslatableSubscriber);
// register more if needed

使用方法

你所要做的就是定义一个Doctrine2实体并使用特性

<?php

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;

/**
 * @ORM\Entity(repositoryClass="CategoryRepository")
 */
class Category implements ORMBehaviors\Tree\NodeInterface, \ArrayAccess
{
    use ORMBehaviors\Blameable\Blameable,
        ORMBehaviors\Geocodable\Geocodable,
        ORMBehaviors\Loggable\Loggable,
        ORMBehaviors\Sluggable\Sluggable,
        ORMBehaviors\SoftDeletable\SoftDeletable,
        ORMBehaviors\Sortable\Sortable,
        ORMBehaviors\Timestampable\Timestampable,
        ORMBehaviors\Translatable\Translatable,
        ORMBehaviors\Tree\Node
    ;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="NONE")
     */
    protected $id;
}

对于一些像树这样的行为,你可以使用仓库特性

<?php

use Doctrine\ORM\EntityRepository;
use Knp\DoctrineBehaviors\ORM as ORMBehaviors;

class CategoryRepository extends EntityRepository
{
    use ORMBehaviors\Tree\Tree,
}

就是这样!

现在你有一个可以工作的Category,它的行为就像:

tree(树状结构)

<?php

    $category = new Category;
    $category->setId(1); // tree nodes need an id to construct path.
    $child = new Category;
    $child->setId(2);

    $child->setChildNodeOf($category);

    $em->persist($child);
    $em->persist($category);
    $em->flush();

    $root = $em->getRepository('Category')->getTree();

    $root->getParentNode(); // null
    $root->getChildNodes(); // ArrayCollection
    $root[0][1]; // node or null
    $root->isLeafNode(); // boolean
    $root->isRootNode(); // boolean

你可以使用除id以外的另一个标识符,只需覆盖getNodeId并返回你的自定义标识符(与Sluggable结合使用效果很好)

translatable(可翻译)

如果你正在处理一个Category实体,默认情况下Translatable行为期望在Category实体同一文件夹中有一个CategoryTranslation实体。

默认命名约定(或通过特性方法进行自定义)可以避免你手动处理实体关联。它将由TranslationSubscriber自动处理。

为了使用可翻译特性,你必须创建这个CategoryTranslation实体。

<?php

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;

/**
 * @ORM\Entity
 */
class CategoryTranslation
{
    use ORMBehaviors\Translatable\Translation;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $name;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $description;

    /**
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @param  string
     * @return null
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }

    /**
     * @param  string
     * @return null
     */
    public function setDescription($description)
    {
        $this->description = $description;
    }
}

相应的Category实体需要use ORMBehaviors\Translatable\Translatable;并且应该只包含不需要翻译的字段。

<?php

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;

/**
 * @ORM\Entity
 */
class Category
{
    use ORMBehaviors\Translatable\Translatable;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $someFieldYouDoNotNeedToTranslate;
}

更新数据库后,例如使用./console doctrine:schema:update --force,你现在可以使用translategetTranslations方法进行翻译工作。

<?php

    $category = new Category;
    $category->translate('fr')->setName('Chaussures');
    $category->translate('en')->setName('Shoes');
    $em->persist($category);

    // In order to persist new translations, call mergeNewTranslations method, before flush
    $category->mergeNewTranslations();

    $category->translate('en')->getName();

覆盖

如果你更喜欢为翻译实体使用不同的类名,或者想使用单独的命名空间,你有两种方法

如果你想在全局范围内定义一个自定义翻译实体类名:覆盖特性Translatable及其方法getTranslationEntityClass和特性Translation及其方法getTranslatableEntityClass在翻译实体中。如果你覆盖了一个,你也需要覆盖另一个以返回相反的类。

示例:假设你想创建一个子命名空间AppBundle\Entity\Translation来存储翻译类,然后在那个文件夹中放置覆盖的特性。

<?php
namespace AppBundle\Entity\Translation;

use Knp\DoctrineBehaviors\Model\Translatable\Translatable;
use Symfony\Component\PropertyAccess\PropertyAccess;

trait TranslatableTrait
{
    use Translatable;

    /**
     * @inheritdoc
     */
    public static function getTranslationEntityClass()
    {
        $explodedNamespace = explode('\\', __CLASS__);
        $entityClass = array_pop($explodedNamespace);
        return '\\'.implode('\\', $explodedNamespace).'\\Translation\\'.$entityClass.'Translation';
    }
}
<?php
namespace AppBundle\Entity\Translation;

use Knp\DoctrineBehaviors\Model\Translatable\Translation;

trait TranslationTrait
{
    use Translation;

    /**
     * @inheritdoc
     */
    public static function getTranslatableEntityClass()
    {
        $explodedNamespace = explode('\\', __CLASS__);
        $entityClass = array_pop($explodedNamespace);
        // Remove Translation namespace
        array_pop($explodedNamespace);
        return '\\'.implode('\\', $explodedNamespace).'\\'.substr($entityClass, 0, -11);
    }
}

如果你使用这种方法,请确保覆盖DoctrineBehaviors的特性参数

parameters:
    knp.doctrine_behaviors.translatable_subscriber.translatable_trait: AppBundle\Entity\Translation\TranslatableTrait
    knp.doctrine_behaviors.translatable_subscriber.translation_trait: AppBundle\Entity\Translation\TranslationTrait

如果您只想为单个可翻译类定义自定义翻译实体类名:请覆盖可翻译实体中的 getTranslationEntityClass 特性方法和翻译实体中的 getTranslatableEntityClass 方法。如果您覆盖了一个,也需要覆盖另一个以返回逆类。

猜测当前区域设置

您可以通过将其第一个参数设置为可调用对象来配置订阅者猜测当前区域设置的方式。此库提供了一个可调用对象(Knp\DoctrineBehaviors\ORM\Translatable\CurrentLocaleCallable),它使用 Symfony2 返回当前区域设置。

代理翻译

一个额外功能允许您代理可翻译实体的翻译字段。

您可以在您可翻译实体的 __call 魔法方法中使用它,这样当您尝试调用 getName(例如)时,它将返回当前区域设置的名称的翻译值。

<?php

    public function __call($method, $arguments)
    {
        return $this->proxyCurrentLocaleTranslation($method, $arguments);
    }

    // or do it with PropertyAccessor that ships with Symfony SE
    // if your methods don't take any required arguments
    public function __call($method, $arguments)
    {
        return \Symfony\Component\PropertyAccess\PropertyAccess::createPropertyAccessor()->getValue($this->translate(), $method);
    }

软删除

<?php

    $category = new Category;
    $em->persist($category);
    $em->flush();

    // get id
    $id = $category->getId();

    // now remove it
    $em->remove($category);
    $em->flush();

    // hey, I'm still here:
    $category = $em->getRepository('Category')->findOneById($id);

    // but I'm "deleted"
    $category->isDeleted(); // === true

    // restore me
    $category->restore();

    //look ma, I am back
    $category->isDeleted(); // === false

    //do not forget to call flush method to apply the change
    $em->flush();
<?php

    $category = new Category;
    $em->persist($category);
    $em->flush();

    // I'll delete you tomorrow
    $category->setDeletedAt((new \DateTime())->modify('+1 day'));

    // OK, I'm here
    $category->isDeleted(); // === false

    /*
     *  24 hours later...
     */

    // OK, I'm deleted
    $category->isDeleted(); // === true

timestampable(可时间戳)

<?php

    $category = new Category;
    $em->persist($category);
    $em->flush();

    $id = $category->getId();
    $category = $em->getRepository('Category')->findOneById($id);

    $category->getCreatedAt();
    $category->getUpdatedAt();

如果您希望更改用于时间戳模型的数据库字段的数据类型,可以设置以下参数,如下所示:

parameters:
    knp.doctrine_behaviors.timestampable_subscriber.db_field_type: datetimetz

datetimetz 如果您正在使用 PostgreSQL 数据库,这是一个很有用的选项,否则您可能会遇到一些时区问题。有关更多信息,请参阅:[http://doctrine-dbal.readthedocs.org/en/latest/reference/known-vendor-issues.html#datetime-datetimetz-and-time-types](http://doctrine-dbal.readthedocs.org/en/latest/reference/known-vendor-issues.html#datetime-datetimetz-and-time-types)

默认类型是 datetime

blameable(责任归属)

可追究责任的功能能够追踪给定实体的创建者和更新者。一个可追究责任的 可调用对象 用于从您的应用程序中获取当前用户。

如果您使用 Doctrine 实体来表示您的用户,您可以配置订阅者来自动管理此用户实体与您的实体之间的关联。

使用 symfony2,您只需要配置名为 %knp.doctrine_behaviors.blameable_subscriber.user_entity% 的 DI 参数,例如

# app/config/config.yml
parameters:
    knp.doctrine_behaviors.blameable_subscriber.user_entity: AppBundle\Entity\User

然后,您可以使用它,如下所示

<?php

    $category = new Category;
    $em->persist($category);

    // instances of %knp.doctrine_behaviors.blameable_subscriber.user_entity%
    $creator = $category->getCreatedBy();
    $updater = $category->getUpdatedBy();

loggable(可记录日志)

可记录的功能能够追踪生命周期修改并使用任何第三方日志系统记录它们。一个可记录的 可调用对象 用于从任何您想要的地方获取记录器。

<?php

/**
 * @ORM\Entity
 */
class Category
{
    use ORMBehaviors\Loggable\Loggable;

    // you can override the default log messages defined in trait:
    public function getUpdateLogMessage(array $changeSets = [])
    {
        return 'Changed: '.print_r($changeSets, true);
    }

    public function getRemoveLogMessage()
    {
        return 'removed!';
    }
}

然后,将这些消息传递到配置的可调用对象。您可以通过向 LoggableSubscriber 传递另一个可调用对象来定义自己的。

<?php

$em->getEventManager()->addEventSubscriber(
    new \Knp\DoctrineBehaviors\ORM\Loggable\LoggableSubscriber(
        new ClassAnalyzer,
        function($message) {
            // do stuff with message
        }
    )
);

如果您使用 symfony,您也可以配置要使用哪个可调用对象

// app/config/config.yml
parameters:
    knp.doctrine_behaviors.loggable_subscriber.logger_callable.class: Your\InvokableClass

geocodable(可地理编码)

可地理编码的功能为 PostgreSQL 平台提供了扩展,以便使用 cube 和 earthdistance 扩展。

它允许您基于地理坐标查询实体。它还提供了一个简单的方法来使用第三方库,如出色的 geocoder 将地址转换为纬度和经度。

<?php

    $geocoder = new \Geocoder\Geocoder;
    // register geocoder providers

    // $subscriber instanceof GeocodableSubscriber (add "knp.doctrine_behaviors.geocodable_subscriber" into your services.yml)
    $subscriber->setGeolocationCallable(function($entity) use($geocoder) {
        $location = $geocoder->geocode($entity->getAddress());
        return new Point(
            $location->getLatitude(),
            $location->getLongitude()
        ));
    });

    $category = new Category;
    $em->persist($category);

    $location = $category->getLocation(); // instanceof Point

    // find cities in a circle of 500 km around point 47 lon., 7 lat.
    $nearCities = $repository->findByDistance(new Point(47, 7), 500);

sluggable(可生成别名)

可缩略功能为实体生成缩略名(唯一性不可保证)。将在更新/持久化时自动生成(您可以通过覆盖 getRegenerateSlugOnUpdate 并返回 false 来禁用更新时的生成。您还可以通过覆盖 getSlugDelimiter 来覆盖默认的连字符作为缩略名分隔符。您可以通过覆盖 generateSlugValue 来更改缩略名生成算法。用例包括 SEO(例如,像 http://example.com/post/3/introduction-to-php 这样的 URL)

<?php

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;

/**
 * @ORM\Entity
 */
class BlogPost
{
    use ORMBehaviors\Sluggable\Sluggable;

    /**
     * @ORM\Column(type="string")
     */
    protected $title;

    public function getSluggableFields()
    {
        return [ 'title' ];
    }

    public function generateSlugValue($values)
    {
        return implode('-', $values);
    }
}

filterable(可过滤)

可筛选的可以在存储库级别使用

它允许我们简单地筛选结果

联合筛选示例

<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="ProductRepository")
 */
class ProductEntity
{

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", nullable=true)
     */
    private $name;

    /**
     * @ORM\Column(type="integer")
     */
    private $code;

    /**
     * @ORM\OneToMany(targetEntity="Order", mappedBy="product")
     */
    protected $orders;
}

和存储库

<?php

use Knp\DoctrineBehaviors\ORM\Filterable;
use Doctrine\ORM\EntityRepository;

class ProductRepository extends EntityRepository
{
    use Filterable\FilterableRepository;

    public function getLikeFilterColumns()
    {
        return ['e:name', 'o:code'];
    }

    public function getEqualFilterColumns()
    {
        return [];
    }

    protected function createFilterQueryBuilder()
    {
        return $this
            ->createQueryBuilder('e')
            ->leftJoin('e.orders', 'o');
    }
}

现在我们可以使用以下方法进行筛选

    $products = $em->getRepository('Product')->filterBy(['o:code' => '21']);

可调用对象

可调用对象被一些订阅者(如blameable和geocodable)使用,以基于第三方系统填充信息。

例如,blameable可调用对象可以是实现__invoke方法的任何symfony2服务或任何匿名函数,只要它们返回当前登录用户表示(这意味着一切,一个用户实体,一个字符串,用户名等)。关于被调用的DI服务的示例,请查看Knp\DoctrineBehaviors\ORM\Blameable\UserCallable类。

对于geocodable,您可以将它设置为任何实现__invoke的服务或返回Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point对象的匿名函数。

测试

阅读测试文档

维护者

KNPLabs正在寻找维护者(查看原因)。

如果您感兴趣,请随意提交一个PR请求以请求被添加为维护者。

我们很高兴听到您的消息 :)