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:
- setDefaultLanguage ($ language) - static - will be used to set the default language anywhere in our code. The method is static because the value of this language will be the same for all instances of the template.
- setLanguage ($ language) - not static - will be used for a specific language setting for models separately. Attention! Because the template is initialized for the entire model once and then used for each record the same, and if you store the value of the current language in any property of the template class, then this value is the same in all instances of the model
- getDefaultLanguage () and getLanguage () are respectively used to get the values of the language, and if a specific language is not specified for the model, the common language is used.
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:
- because If the __call () method is used directly in the template, then all unknown methods in the model will be redirected to this template, and if you suddenly want to add another one with a similar system, the handler will not work.
- No integration with HYDRATE_ARRAY, fast access will be unavailable. In principle, this problem has already been solved, and, running ahead, with a completely different access architecture. I will tell about it in the following part.
- quite slow engine. A bunch of if, jumping between methods, etc.
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 .