📜 ⬆️ ⬇️

Doctrine, extending the capabilities of your favorite ORM framework! Part 1.a (I18n, quick access to translatable attributes)

I think many people agree with me that Doctrine is one of the most powerful and convenient ORM for PHP, but more recently, I’ve lost the opportunity. To begin with, it is impossible to use associations with filtering conditions, a “magic” search with regard to translation via I18n and much more.

Experimenting in every possible way with the capabilities of Doctrine, I wrote a bunch of necessary and unnecessary extensions, which I decided to bring to the world. Thus, I begin a series of articles devoted to the practical writing of any whistles that simplify life. In the process, I will also try to reveal the development methodology, so that there may be mutually exclusive paragraphs during the article, but in the end they will be resolved.

I'll start with the easiest - with the Doctrine_Template_I18n multi-language extension. I will make a reservation right away, there will be a lot of text, as well as a lot of messy technical information.
')
A few words about the class names that I will use in the article: I have my own framework in which I implemented the considered additions and class names fall under my name Of, and for what I consider - under Of_ExtDoctrine (i.e. Extended Doctrine). I use the same naming conventions as in Doctrine (actually as in ZendFramework, blah blah blah ... a lot of them, almost standard)

I18n plugin



What confuses most is the impossibility of directly referring to translatable attributes, in cases where the language was originally selected or obtained when a request was made to a site, for example:

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


Every time you have to write standard constructions like $ record ['Translation'] [$ lang] ['title'] (who do not know, access to the records is possible via array-notation, and also often uses HYDRATION_ARRAY to increase performance. See the useful link ), which in turn requires the transfer of the value of the selected language to the template, which is often inconvenient.

Add this functionality, and for this you will have to write your own template for actAs, as well as a static method for determining the default translation language.

Fast access to translatable attributes



To begin, create a recording class and environment for further work.

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' );



As you can see, we created a simple model with 2 translatable fields name and descritpion, which will be accessible through accessors

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




It's time to create your template (empty yet) and connect it to our model.

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());

}

}



Because If you need to implement a translation access interface for a predefined language, then you need to determine where this value will be stored. Define methods for setting and accessing the language in the template:



Also allow myself the liberty to add the option 'lang' to determine the current language directly from the model. In principle, it is not necessary, but suddenly you will ever need it :)

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;
}

}



In principle, everything is simple, if there is no current language, we return the default language. Transformation (string) for getters would put that would always return a string, because It was the case that instead of a line I threw an instance of Zend_Locale into the language, and the Translation parser did not understand this, but more on that.

Now we need to add a couple of methods for reading / writing to certain fields, which we will use in the future:

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 ;
}

}



Here, too, nothing complicated. When reading the attribute, we check if there is a translation for the language and return it, if not, then null. This is done so as not to create unnecessary translations, since when accessing a non-existing $ translation [$ lang] entry, a new instance is automatically created. When recording this check is not needed, because implies that we can potentially create a new translation.

With languages ​​figured out, now proceed to implement access to the attributes of the record. It is most expedient to do this through a pair of Accessor / Mutator for records, therefore methods are needed for accessing the translated attributes taking into account the language, which in turn makes it necessary to generate its own getters / setters for each attribute. The solution is obvious - use the __call () method in the template in which to parse the name of the called method and redirect to the already created getTranslatedAttribute () / setTranslatedAttribute () depending on the task. Generally speaking, it would be more logical to do this through __get () and __set (), as is done in Doctrine_Record, but, alas, it is impossible, because translation through these methods does not work with the actAs templates.

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]));
}
}

}



As you can see, we have added a new option 'fields', the same as for the template I18n. It defines which fields we will use for quick access. Modify the Product class according to this option.

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' ) //
)));

}

}



Here, actually, and everything, we check:

$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' - !



These are not all possible tests, but the system works. Of course it was worth making it through unit tests, but frankly laziness.

There are some nuances regarding this particular implementation of quick access to translatable attributes:

There you have it, and I hope that it was more or less clear to you what was happening. In the next part, I will modify this access engine to more optimally use resources and expand integration with various representations of search results (hydration modes), download translations via join in all dql queries automatically, introduce Doctrine_Record_Listener components in the context of the task and much more.

In this article, the code was highlighted using Source Code Highlighter .

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


All Articles