53. 实体Entity(五)内容实体基类

源码分析重点在于在自己的大脑中重现开发者的思维过程,内容实体基类是drupal中很大的一个类,她要处理众多的问题,内容实体的大多数功能都集中在这里,开发者有许多的考虑,要弄清楚她的所有细节,学习者可能会觉得有些困难,这时需要明白任何复杂庞大的事物都是一步步累积发展起来的,初遇的学习者只看到了她的结果,没有看到她的演化历程,所以有这样的感觉很正常,开发者也不是一步到位的,而是从简单到复杂、反复迭代加校正而来,我们由简入深来介绍她。

内容实体基类完成了内容实体的主要功能,具体的内容实体继承她后只需要写少量代码即可使用,不论内容实体类型是否为可版本化的,内容实体类只接受某个版本或默认版本的全部翻译数据,包括源语言,关键词是“一个版本”“全部翻译”,该基类可以为不同语言克隆自己,这些克隆出的实体又关联在一起,这用到了以下设计模式。

设计模式:

为了处理翻译问题,内容实体基类使用了一种少见的设计模式,这是该类的主要运作骨架,明白了这个模式就好理解该基类了,为此云客写了段简化代码用以学习者研究该模式,可以称这个模式叫做多实例共享属性模式、连体模式、分身模式等等,名字是次要的,原理才是主要的,代码见下:

<?php
class yunke
{
    public $values = []; //用于存放变量
    public $activeLangcode = "en";  //当前语言
    public $defaultLangcode = "en"; //建立对象时的源语言
    public $translations; //各个翻译对象

    public function __construct($langcode = NULL)
    {
        if (empty($langcode)) {
            $langcode = "en";
        }
        $this->activeLangcode = $langcode;  //设置当前语言
        $this->defaultLangcode = $langcode; //标记源语言
    }

    public function get($key)
    {
        if (isset($this->values[$key][$this->activeLangcode])) {
            return $this->values[$key][$this->activeLangcode];
        }
        return isset($this->values[$key][$this->defaultLangcode]) ? $this->values[$key][$this->defaultLangcode] : NULL;
    }

    public function set($key, $value, $isTranslatable = true)
    {
        if ($isTranslatable) {
            $this->values[$key][$this->activeLangcode] = $value;
            if (!isset($this->values[$key][$this->defaultLangcode])) {
                $this->values[$key][$this->defaultLangcode] = $value;
            }
        } else {
            $this->values[$key][$this->defaultLangcode] = $value;
        }
        return $this;
    }

    public function getTranslation($langcode)
    {
        if (isset($this->translations[$langcode])) {
            return $this->translations[$langcode];
        }
        if ($langcode == $this->activeLangcode) {
            $this->translations[$langcode] = $this;
            return $this;
        }
        return $this->initializeTranslation($langcode);
    }

    public function initializeTranslation($langcode)
    {
        $translation = clone $this;
        $translation->values = &$this->values;
        $translation->translations = &$this->translations;
        $translation->activeLangcode = $langcode;
        $translation->defaultLangcode = &$this->defaultLangcode;
        $this->translations[$langcode] = $translation;
        return $translation;
    }

    public function getLangcode()
    {
        return $this->activeLangcode;
    }

    public function getOriginalLangcode()
    {
        return $this->defaultLangcode;
    }
}

$en = new yunke("en");
$en->set("msg", " English"); //首次建立后使用的语言产生的内容
echo $en->get("msg");

$zh_hans = $en->getTranslation("zh-hans"); //建立翻译对象
echo $zh_hans->get("msg");  //未进行人工翻译时默认使用原始语言的内容
$zh_hans->set("msg", "简体中文"); //对某语言进行人工翻译
echo $zh_hans->get("msg");
echo $en->getTranslation("zh-hans")->get("msg");
echo $zh_hans->getTranslation("en")->get("msg"); //可以通过任何一种语言得到其他语言

$zh_hans->set("untranslatableMsg", 1234, false); //在任意语言下均可设置不可翻译内容 
echo $en->get("untranslatableMsg"); //在任意语言下均可使用不可翻译内容

$fr = $zh_hans->getTranslation("fr");
echo $fr->get("untranslatableMsg"); //在任意语言下均可使用不可翻译内容
?>

请读者运行该代码,彻底弄清楚这个模式后再去学习内容实体基类,下面来看看这个基类。

内容实体基类:

内容实体和配置实体是drupal的两大实体类型,和配置实体基类一样,内容实体基类是系统定义的一个用于内容实体的抽象基类,继承自实体基类,完成了内容实体的大部分通用功能,具体的内容实体往往会继承它,这样写少量代码即可,类定义如下:
Drupal\Core\Entity\ContentEntityBase
面向对象oop开发都应该是面向接口的,接口提供了软件对外的调用特征,是先定的,使用层面不必关注内部实现,了解接口很重要,和配置实体相比内容实体稍复杂些,该基类实现了如下多个接口:
\IteratorAggregate
聚合迭代器接口,php原生提供,允许将内容实体当做数组一样来进行遍历操作,见补充说明。
Drupal\Core\TypedData\TranslationStatusInterface
翻译状态接口,定义系统必要的常量,提供方法用于检测实体的翻译在某语言代码下的翻译状态
Drupal\Core\Entity\ContentEntityInterface
内容实体接口,专用于内容实体,是一个综合接口,描述了内容实体,继承自以下接口:
Drupal\Core\Entity\FieldableEntityInterface
大多数内容实体是基于字段对象的,后者充当实体的属性,(注意此处讲的字段并非数据库表中的列,一个字段对象可以包括很多个列)字段对象可以分解存放在数据库中,该接口提供相关功能,这和配置实体的储存原理不同,可字段化储存的内容实体建立在核心提供的字段组件之上
Drupal\Core\Entity\RevisionableInterface
默认所有内容实体均可支持版本化,但也可以关闭,该接口提供版本化功能
Drupal\Core\TypedData\TranslatableInterface
在配置实体中通过配置覆写来实现翻译相关的转换,但内容实体不一样,数据量庞大的多,需要采用另外的机制,实现该接口以处理翻译工作
\Traversable
可遍历抽象接口,php原生提供,指明内容实体是可遍历的

源语言Original language:
默认情况下内容实体是支持可翻译可版本化的,以节点为列,每个节点内容都可以有很多个版本,每个版本可以有很多个翻译,但这些翻译中仅有一个作为源语言,在程序中称为默认语言,其他翻译都是基于该源语言的;在内容类型的语言配置设置中,我们可以设置新建节点时源语言的默认值和是否显示语言选择表单项,当新建节点的第一个版本时,在允许显示语言选择器的情况下,内容编辑页面可以指定源语言是什么,该版本添加的翻译均是基于该源语言的,添加翻译时内容编辑页面无法更改语言选项,被限定为被添加翻译的语言(该限制可能将来会取消,见https://www.drupal.org/node/2443989),在源语言所在内容的编辑页面源语言可以修改,但不可修改为已添加翻译的语言;在该版本任意一个翻译上均可以新建版本,新建版本的源语言默认为前一个版本的源语言,但这可以修改,修改规则依然是不能为该版本已经存在的翻译,在该版本上对源语言的修改不影响前一个版本的源语言设置,在数据库中用布尔值 default_langcode来指示本行信息的语言是否为源语言,用布尔值revision_translation_affected 指示新建的版本中某个语言是否已经进行过人工翻译,用 content_translation_source 来指示本行信息翻译自那种语言,一般均是本版本的源语言,当源语言被修改后,该值保持不变,不会随之修改;在某个版本上删除非源语言的翻译时,仅删除该版本的该翻译,以往版本的翻译不受影响;新建版本时所有翻译均会新建版本,内容从前一个版本中该翻译的内容复制,而不是新建版本时所在的那个语言。
数据库表node保存节点的当前版本(也是最新版本),其中的langcode保存该版本的源语言;可以从节点版本表node_revision中的langcode看到每个版本的源语言
有两种特殊的语言代码:“und”表示未指定,“zxx”表示不适用
在程序中源语言并不以其语言代码表示,而以常量Drupal\Core\Language\LanguageInterface::LANGCODE_DEFAULT表示,其值为“x-default”


内容实体构造函数参数解释:
$values:该参数是一个数组,保存着实体某个版本(只能是一个)的全部数据,包括被翻译的数据,键名为字段名,下一级键名为语言代码,随后值根据字段在共享表或专用表中结构有差异(见内容实体储存处理器主题)
在共享表中储存的字段没有多值,所以没有值下标,语言代码的下一级键名为属性名:
$values[$field_name][$langcode][$property_name] =$value;
如果该字段仅有一个属性,那么进一步省略属性名:
$values[$field_name][$langcode] = $value;
在专用表中一律为:
$values[$field_name][$langcode][$item][$property_name] = $value;
$item为值下标,仅有一个值那么为0
如果$langcode为源语言,那么语言代码用LanguageInterface::LANGCODE_DEFAULT表示,而不用源语言的语言代码表示

$translations:为一个由函数array_keys产生的包含语言代码的数组(如array_keys($translations[$id])),用以指示该版本实体有哪些语言有翻译,其中默认语言用LanguageInterface::LANGCODE_DEFAULT表示

 

属性说明:

$values结构:
$values[$key][$langcode]=$valus;实体值,按语言存放,源语言的语言代码用LanguageInterface::LANGCODE_DEFAULT表示,而不是源语言的语言代码,见上。

$translations结构:
 是一个数组,键名为语言代码,键值可以是有三个元素的数组,如下
$translations[$langcode][“status”]表明在这些语言代码上的翻译状态:新建、存在、已移除
$translations[$langcode][“entity”]是一个由本对象克隆出来的共享基本属性的翻译对象
$translations[$langcode]['status_existed']布尔值,当实体对象中存在的翻译被删除时用于指示数据库是否需要进行删除动作

$translationInitialize
表明实体是否正在执行翻译初始化,也就是真正克隆翻译对象,设置该变量的作用是防止克隆翻译对象时执行__clone()函数

$langcodeKey
实体标准化键名langcode对应的字段名,定义在释文的entity_keys中,往往是langcode,该字段在数据库中储存实体对应的语言代码

$defaultLangcodeKey
实体标准化键名default_langcode对应的字段名,定义在释文的entity_keys中,往往是default_langcode,在数据库中该字段储存一个布尔值,表明实体储存中本条数据对应的语言是否作为实体源语言,在该语言下删除实体那么所有对应的翻译均会被删除。

$activeLangcode
当前实体对象代表的语言代码,如果一个实体是可翻译的(具备多个语言的),那么当前实体必代表着其中一种语言,这就是该值指定的语言,如果该语言是源语言则该值用常量LanguageInterface::LANGCODE_DEFAULT表示,该常量的值为:'x-default',实体实例化后即代表源语言,其他语言实体对象通过初始化翻译克隆得到。

$defaultLangcode
保存当前实体的源语言代码,也就是 LanguageInterface::LANGCODE_DEFAULT对应语言的代码,在实体进行翻译克隆时,$activeLangcode被设置为翻译的语言代码,而$defaultLangcode保持不变,这允许追踪某语言是从哪个语言翻译过来的。在实例化实体时源语言的数据是以LanguageInterface::LANGCODE_DEFAULT代替语言代码作为键名传入的,方法setDefaultLangcode()用于设置$defaultLangcode的值,在构造函数执行阶段注意:
$this->translatableEntityKeys['langcode'][$this->activeLangcode]
的值就是源语言代码,因为此时$this->activeLangcode的值为LanguageInterface::LANGCODE_DEFAULT

$entityKeys
保存释文entity_keys键中的不可翻译的属性,是一个数组,键名为实体标准键名,便于使用,值为数据库值,

$translatableEntityKeys
保存释文entity_keys键中可翻译的属性,值为一个数组,键名为实体标准键名,下级键名为语言代码,值为该语言代码下对应的值

$isDefaultRevision
指明当前实体是否是默认版本,默认版本总是最新的版本(版本回退操作是从要回退到的版本复制数据形成新的版本),该值并不以明显方式保存在数据库中,而是由储存处理器判断得出,并在参数$values中设置(由此可见该参数值并非全部来自数据库),构造函数会为该属性赋值

 

数据库相关字段说明:

revision_translation_affected数据库中该列储存一个布尔值,指明本语言的翻译是否已经随着新建的版本而更新,新建版本时所在的那个语言会被设置为真,默认情况下新建版本后,其他语言翻译是复制前一个版本该翻译中的内容,如果没有经过人工翻译该值为假,因为新建了版本但本翻译还没有更新,说明新建版本的内容差异没有体现在本翻译中,如果经过翻译更新该值为真。

克隆实体__clone:
php语言中该魔术方法在克隆后的新对象上调用,进行浅复制,里面的$this指新对象;在内容实体基类中该方法里面有很多类似如下的代码:

    $values = $this->values;
    $this->values = &$values;

这是因为翻译对象上这些属性保存的是到源语言对象上该属性的引用,尽管她们的值不是一个对象,但克隆时还是以引用传递,利用值复制打断引用复制,这样处理后能保证不是引用传递,请研究以下代码:

class yunke
{
    public $var = 555;

    public function __clone()
    {
        $var = $this->var;
        $this->var =& $var; //该处是否加“&”结果不一样,加了为555,不加为666
    }

    public function getme()
    {
        $copy = clone $this;
        $copy->var =& $this->var;
        return $copy;
    }
}

$a = new yunke();
$b = $a->getme();
$c = clone $b;
$a->var = 666;
print_r($c);

在对象克隆中执行严格的浅复制,引用将直接复制为引用,为了说明这点请再看以下代码:

class yunke{
    public  $var=7;
}
$a=new yunke();
$b=new yunke();
$c=new yunke();

$b->var=&$a->var;
$c->var=$b->var; //后者虽是一个引用,但执行值复制
$d=clone $b; //后者中的var属性执行引用复制,
$a->var=8;
echo $c->var; //输出7
echo $d->var; //输出8

补充说明:
1、    聚合迭代器接口,见php官网文档:
http://php.net/manual/zh/class.iteratoraggregate.php


本节就到这里,但内容实体基类远未讲完,由于他集中了太多功能,后续系列讲逐步讲解,你将会反复查看该基类的实现,通过本节的介绍您在理解时应该不是难事了

作者语:

当一个人站在悬崖的顶端欣赏美景时,下面站着一个同样想领略此美景的人,悬崖很高,他想要攀登上去,可是悬崖上没有路,连抓手的地方也没有,这个时候下面的人会很苦恼,甚至绝望,这时候他该怎么办呢?很多人说drupal难学,这是因为在学习过程中就会遇到这种情况,云客将其称为drupal的学习悬崖,在学习到内容实体时将尤为如此,这个时候请不要怀疑自己,既然有人登上去了就一定是有路的,人与人之间的差别是有限的,只要多花时间就能明白,出现悬崖下的那种绝望是因为只看到了结果,没有看到过程,可能在悬崖的背面有一条平坦的路,上面的人根本不是从悬崖爬上去的。内容实体就是这样,drupal发展至今你只会看到一个庞大复杂的系统,实体最初只是一个很简单的系统(见本系列的实体介绍),随后逐步丰满起来,需要你慢慢消化,理解通向悬崖顶端的小路,不要被学习悬崖吓到。这里分享个故事,在数学界有个大名鼎鼎的难题叫做“费马大定理 ”,许多人败落在正面悬崖的攀爬上,直到三百多年后有个叫怀尔斯的人在研究圆锥曲线时意外证明了它,如此顺理成章,大家可以搜索下这个故事,路不是没有,只是需要寻找和万分的坚持。

本书共94小节:

评论 (写第一个评论)