1. 程式人生 > >雲客Drupal8原始碼分析之實體Entity(五)內容實體基類

雲客Drupal8原始碼分析之實體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


本節就到這裡,但內容實體基類遠未講完,由於他集中了太多功能,後續系列講逐步講解,你將會反覆檢視該基類的實現,通過本節的介紹您在理解時應該不是難事了

我是雲客,【雲遊天下,做客四方】,微訊號:PHP-world,歡迎轉載,但須註明出處,討論請加qq群203286137


作者語:
當一個人站在懸崖的頂端欣賞美景時,下面站著一個同樣想領略此美景的人,懸崖很高,他想要攀登上去,可是懸崖上沒有路,連抓手的地方也沒有,這個時候下面的人會很苦惱,甚至絕望,這時候他該怎麼辦呢?很多人說drupal難學,這是因為在學習過程中就會遇到這種情況,雲客將其稱為drupal的學習懸崖,在學習到內容實體時將尤為如此,這個時候請不要懷疑自己,既然有人登上去了就一定是有路的,人與人之間的差別是有限的,只要多花時間就能明白,出現懸崖下的那種絕望是因為只看到了結果,沒有看到過程,可能在懸崖的背面有一條平坦的路,上面的人根本不是從懸崖爬上去的。內容實體就是這樣,drupal發展至今你只會看到一個龐大複雜的系統,實體最初只是一個很簡單的系統(見本系列的實體介紹),隨後逐步豐滿起來,需要你慢慢消化,理解通向懸崖頂端的小路,不要被學習懸崖嚇到。這裡分享個故事,在數學界有個大名鼎鼎的難題叫做“費馬大定理 ”,許多人敗落在正面懸崖的攀爬上,直到三百多年後有個叫懷爾斯的人在研究圓錐曲線時意外證明了它,如此順理成章,大家可以搜尋下這個故事,路不是沒有,只是需要尋找和萬分的堅持。

我是雲客,【雲遊天下,做客四方】,微訊號:PHP-world,歡迎轉載,但須註明出處,討論請加qq群203286137