Doctrine、お気に入りのORMフレームワークの機能を拡張します! パート1.a(I18n、翻訳可能な属性へのクイックアクセス)

DoctrineはPHPにとって最も強力で便利なORMの1つであることに多くの人が同意していると思いますが、最近では十分ではなくなりました。 まず、フィルタリング条件との関連付け、「マジック」検索、I18nによる翻訳を考慮に入れることなどは不可能です。

Doctrineの機能を試すあらゆる可能な方法で、私は必要な拡張と不必要な拡張の束を書き、それを「世界に」持ち込むことにしました。 したがって、私は人生を簡素化するあらゆる種類のホイッスルの実用的な執筆に捧げられた一連の記事を始めます。 その過程で、開発の方法論も開示するようにします。これにより、記事全体で相互に排他的な段落が存在する場合がありますが、最終的には解決されます。

まずは最も簡単なものから始めます-多言語のDoctrine_Template_I18nの拡張機能を使用します。 私はすぐに予約をします、多くのテキストだけでなく、多くの混oticとした技術情報があります

記事で使用するクラス名に関するいくつかの言葉:問題のアドオンを実装する独自のフレームワークがあり、クラス名はOfネームスペースに分類されます。教義)。 私はDoctrineと同じ命名規則を使用します(実際には、ZendFrameworkのように、何とか何とか...それらの多くは、ほとんど標準です)

国際化プラグイン



最も困惑しているのは、サイトのリクエスト時に言語が最初に選択または受信された場合、翻訳可能な属性に直接アクセスできないことです。たとえば、

echo $record->my_translatable_attribute; //
$record->my_translatable_attribute = 'something' ; //


$ record ['Translation'] [$ lang] ['title']のような標準構造を記述する必要があるたびに(知らない人は、配列表記を介してレコードにアクセスでき、HYDRATION_ARRAYもパフォーマンスを向上させるためによく使用されます。 link )、選択した言語の値をテンプレートに転送する必要がありますが、これはしばしば不便です。

この機能を追加します。このために、actAsの独自のテンプレートと、デフォルトの翻訳言語を決定する静的メソッドを記述する必要があります。

翻訳可能な属性への高速アクセス



最初に、さらなる作業のために記録クラスと環境を作成します。

class Product extends Doctrine_Record {

public function setTableDefinition() {
//....
$ this ->hasColumn( 'name' , 'string' , 255);
$ this ->hasColumn( 'description' , 'string' );
//....
}

public function setUp() {

$ this ->actAs( 'I18n' , array(
'fields' =>array( 'name' , 'description' )
));

}

}



require_once 'Doctrine.php' ;
spl_autoload_register(array( 'Doctrine' , 'autoload' ));
$connection = Doctrine_Manager::connection( 'mysql://user:passw@host/dbname' );



ご覧のとおり、2つの翻訳可能なフィールド名と説明を含む単純なモデルを作成しました。これは、アクセサーを介して利用できます。

$record->Translation[$language]->name;
$record->Translation[$language]->description;




テンプレートを作成し(空のまま)、モデルに接続します

class Of_ExtDoctrine_I18n_Template extends Doctrine_Template {

//

}

class Product extends Doctrine_Record {

public function setTableDefinition() {
//....
$ this ->hasColumn( 'name' , 'string' , 255);
$ this ->hasColumn( 'description' , 'string' );
//....
}

public function setUp() {

$ this ->actAs( 'I18n' , array(
'fields' =>array( 'name' , 'description' )
));

//
$ this ->actsAs( new Of_ExtDoctrine_I18n_Template());

}

}



なぜなら 定義済み言語の翻訳アクセスインターフェイスを実装する場合は、この値を保存する場所を決定する必要があります。 テンプレートで言語を設定およびアクセスするためのメソッドを定義します。



また、モデルから現在の言語を直接決定するための 'lang'オプションを追加する自由を自分に与えます。 原則として、それは必要ではありませんが、突然いつか必要になるでしょう:)

class Of_ExtDoctrine_I18n_Template extends Doctrine_Template {

protected $_options = array(
'lang' => null ,
);

/**
* Holds default language for all behaviors
*
* @var string
*/
static protected $_defaultLanguage;

/**
* Holds language for current model behavior
*
* @var string
*/
protected $_language;

public function setUp() {

if ($language = $ this ->getOption( 'lang' )) $ this ->setLanguage();

}

/**
* Returns default language for all behaviors
*
* @return string
*/
static public function getDefaultLanguage() {
return ( string )self::$_defaultLanguage;
}

/**
* Sets default language for all behaviors
*
* @param string $language
* @return void
*/
static public function setDefaultLanguage($language) {
self::$_defaultLanguage = $language;
}

/**
* Returns current behavior language
*
* @return void
*/
public function getLanguage() {
if ($ this ->_language === null ) return self::getDefaultLanguage();
else return ( string )$ this ->_language;
}

/**
* Sets current behavior language
*
* @param $language
* @return string
*/
public function setLanguage($language) {
$ this ->_language = $language;
}

}



原則として、すべてが単純です。現在の言語がなければ、デフォルトの言語を返します。 ゲッターの変換(文字列)は、常に文字列を返すように設定されています。 文字列の代わりに、Zend_Localeのインスタンスを言語にスローしたので、翻訳パーサーはこれを理解していませんでしたが、後で詳しく説明します。

次に、いくつかの読み取り/書き込みメソッドを特定のフィールドに追加する必要があります。これは将来使用します。

class Of_ExtDoctrine_I18n_Template extends Doctrine_Template {

//...

/**
* Returns translatable attribute
*
* @param string $field - name of attribute/field
* @param string $language - custom language of translation
* @return mixed|null
*/
public function getTranslatableAttribute($field, $language = null ) {
if ($language === null ) $language = $ this ->getLanguage();
$translation = $ this ->getTranslationObject();
if ($translation->contains($language)) {
return $translation[$language][$field];
} else return null ;
}

/**
* Sets translatable attribute
*
* @param string $field - attribute/field name
* @param mixed $value - value to set
* @param string $language - custom language of translation
* @return void
*/
public function setTranslatableAttribute($field, $ value , $language = null ) {
if ($language === null ) $language = $ this ->getLanguage();
$translation = $ this ->getTranslationObject();
$translation[$language][$field] = $ value ;
}

}



ここでも複雑なことはありません。 属性を読み取るときに、言語の翻訳があるかどうかを確認し、ない場合はnullを返します。 これは、不要な翻訳を作成しないようにするためです。 存在しないエントリ$ translation [$ lang]にアクセスすると、新しいインスタンスが自動的に作成されます。 記録時には、このチェックは必要ありません。 新しい翻訳を作成できる可能性があると理解されています。

言語がわかったので、レコードから属性へのアクセスの実装を開始します。 レコードのアクセサー/ミューテーターのペアを介してこれを行うのが最も便利です。したがって、言語を考慮して、翻訳された属性にアクセスするためのメソッドが必要です。そのため、各属性の独自のゲッター/セッターを生成する必要があります。 解決策は明らかです-テンプレートで__call()メソッドを使用して、呼び出されたメソッドの名前を解析し、タスクに応じて既に作成されたgetTranslatedAttribute()/ setTranslatedAttribute()にリダイレクトします。 一般的に、Doctrine_Recordで行われているように、__ get()と__set()を介してこれを行うことはより論理的ですが、残念ながら不可能です。 これらのメソッドによる翻訳は、actAsテンプレートでは機能しません。

class Of_ExtDoctrine_I18n_Template extends Doctrine_Template {

protected $_options = array(
'lang' => null ,
'fields' => null , //
);

public function setUp() {

//...

// accessor/mutator
$fields = $ this ->getOption( 'fields' );
if ($fields) {
foreach ($fields as $field) {
$ this ->getInvoker()->hasAccessorMutator($field, 'getTranslatableAttribute' .$field, 'setTranslatableAttribute' .$field);
}
}

}

//...

/**
* Covers calls of functions [getTranslatableAttribute|setTranslatableAttribute][fieldname]
*
* @param string $method
* @param array $arguments
* @return mixed
*/
public function __call($method, $arguments) {
$methodAction = substr($method,0,24);
if ($methodAction == 'getTranslatableAttribute' ) {
return call_user_func_array(array($ this , $methodAction), array(substr($method, 24)));
} elseif($methodAction == 'setTranslatableAttribute' ) {
return call_user_func_array(array($ this , $methodAction), array(substr($method, 24),$arguments[0]));
}
}

}



ご覧のとおり、I18nテンプレートと同じように、新しいオプション「フィールド」を追加しました。 クイックアクセスに使用するフィールドを決定します。 このオプションに従って製品クラスを変更します。

class Product extends Doctrine_Record {

public function setTableDefinition() {
//....
$ this ->hasColumn( 'name' , 'string' , 255);
$ this ->hasColumn( 'description' , 'string' );
//....
}

public function setUp() {

$ this ->actAs( 'I18n' , array(
'fields' =>array( 'name' , 'description' )
));

$ this ->actAs( new Of_ExtDoctrine_I18n_Template(array(
'fields' =>array( 'name' , 'description' ) //
)));

}

}



それだけです、私たちはチェックします:

$record = new Product();

$record->Translation[ 'en' ]->name = 'notebook' ; // -

Of_ExtDoctrine_I18n_Template::setDefaultLanguage( 'en' );

echo $record->name; // 'nodebook' -

$record->name = 'nuclear weapon' ; // mutator

echo $record->name; // 'nuclear weapon' - !



これらはすべて可能なテストではありませんが、システムは動作します。 もちろん、単体テストで発行する価値はありましたが、率直に言って怠です。

翻訳可能な属性へのクイックアクセスのこの特定の実装に関しては、いくつかの微妙な違いがあります。

これらは物事であり、私はあなたが多かれ少なかれ何が起こっていたかを明確にしたことを願っています。 次の部分では、このアクセスエンジンを変更してリソースをより最適に使用し、検索結果のさまざまな表現(ハイドレーションモード)との統合機能を拡張し、すべてのdqlクエリでの結合を介して翻訳を自動的にロードし、タスクのコンテキストでDoctrine_Record_Listenerコンポーネントを使用するプラクティスを紹介します。

この記事では、 ソースコードハイライターを使用してコードを強調表示しました。

Source: https://habr.com/ru/post/J79878/


All Articles