⬆️ ⬇️

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

In the last article, I looked at one of the ways to quickly access translatable attributes. In order to understand what is being discussed at all, it is strongly recommended to read the above article before this one) For those who have already read, let me remind you that the main cymes consisted in artificial heteras and seters via hasAccessorMutator (), which in turn were overriden through __call ( ) -function template. The obvious disadvantage of this approach is the impossibility of using __call () in other templates, which is not good. There is a more beautiful and effective way to implement such access, and it was originally provided for in Doctrine almost from the very first versions - these are attribute filters. And again under the cut a lot of code and text.



Quick access to translatable attributes through filters



How are the filters used in Doctrine_Record? Without going into details, the approximate scheme looks like this:



// Doctrine_Record::_get()

foreach ($ this ->_table->getFilters() as $filter) {

try {

$ value = $filter->filterGet($ this , $fieldName);

$success = true ;

} catch (Doctrine_Exception $e) {}

}

if ($success) {

return $ value ;

} else {

throw $e;

}


As you can see, for any attribute (including the one that does not exist), a call is made to filterGet () for each filter instance assigned to write!



What does this mean to us? And the fact that we can use the native attribute access method through an external filter class. By default, only 1 filter is installed in Doctrine_Record - Doctrine_Record_Filter_Compound, which tracks the presence of a given attribute, and if it does not exist, throws an exception. This exception is caught in Doctrine_Record and either proceeds to the next filter, or re-calls it if the filters have run out. Everything is quite simple and our task is to implement our filter, which will intercept the call of attributes that we specify in initialization and return the values ​​of the corresponding fields of the Translation association for our record, and for others to throw an exception. As can be seen from the insertion of the code (see above), the record calls up the filters in order and catches their exception or the result in order to go to the next filter. Let's try to implement such a filter.

')

Filter



First, to create a filter, you need to create a class inherited from Doctrine_Record_Filter and implement 2 abstract methods Doctrine_Record_Filter :: filterGet () and Doctrine_Record_Filter :: filterSet (). As can be seen from the approximate scheme of the work of getters and setters (see above), the record causes filters in order, and in case of some errors we have to throw an exception.



This time I will not go into the chain of reasoning how to write classes, but I’ll give you a ready-made filter code, and then let's analyze what and why.

/**

* EasyAccess package filter. Implements access to record's properties as for translated in I18n.

*

* Can be used as a part of Of_ExtDoctrine_I18n_Helper system, or as stand-alone filter both.

*

* @author OmeZ

* @version 1.0

* @license www.opensource.org/licenses/lgpl-license.php LGPL

* @package Of_ExtDoctrine_I18n_EasyAccess

*/

class Of_ExtDoctrine_I18n_EasyAccess_Filter extends Doctrine_Record_Filter {



/**

* Fields

*

* @var array

*/

protected $_fields = array();



/**

* Language

*

* @var string

*/

protected $_language;



/**

* @var Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface

*/

protected $_owner;



/**

* Constructs new filter with options

*

* @param array $options

* @return void

*/

public function __construct(array $options) {

if (isset($options[ 'fields' ])) $ this ->setFields($options[ 'fields' ]);

if (isset($options[ 'language' ])) $ this ->setLanguage($options[ 'language' ]);

if (isset($options[ 'owner' ])) $ this ->setOwner($options[ 'owner' ]);

}



/**

* Returns owner of filter

*

* @return Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface

*/

public function getOwner() {

return $ this ->_owner;

}



/**

* Sets owner for filter

*

* @param Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface $owner

* @return Of_ExtDoctrine_I18n_EasyAccess_Filter

*/

public function setOwner(Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface $owner) {

$ this ->_owner = $owner;

return $ this ;

}



/**

* Returns fields aliases for filter

*

* @return array

*/

public function getFields() {

return $ this ->_fields;

}



/**

* Sets fields aliases for filter

*

* @param $fields

* @return Of_ExtDoctrine_I18n_EasyAccess_Filter

*/

public function setFields($fields) {

$ this ->_fields = (array)$fields;

return $ this ;

}



/**

* Returns default language for filter

*

* @return string

*/

public function getLanguage() {

if ($ this ->_language !== null ) {

return $ this ->_language;

} elseif ($ this ->_owner) {

return $ this ->_owner->getLanguage();

} else {

require_once 'Of/ExtDoctrine/I18n/EasyAccess/Exception.php' ;

throw new Of_ExtDoctrine_I18n_EasyAccess_Exception( 'Impossible to detect language' );

}

}



/**

* Sets language to filter

*

* @return Of_ExtDoctrine_I18n_EasyAccess_Filter

*/

public function setLanguage($language) {

$ this ->_language = $language;

return $ this ;

}



/**

* Returns value of translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @return mixed

*/

public function filterGet(Doctrine_Record $record, $name) {

return $ this ->getTranslation($record, $name, $ this ->getLanguage());

}



/**

* Sets value to translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @param mixed $value

* @return void

*/

public function filterSet(Doctrine_Record $record, $name, $ value ) {

return $ this ->setTranslation($record, $name, $ value , $ this ->getLanguage());

}



/**

* Language dependent getter to translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @param string $language

* @param boolean $return_first_found

* @return mixed

*/

public function getTranslation(Doctrine_Record $record, $name, $language = null , $return_first_found = true ) {

if (in_array($name, $ this ->_fields)) {

$language = empty($language)?( string )$ this ->getLanguage():( string )$language;

if ($record->hasRelation( 'Translation' )) {

if ($record->Translation->contains($language)) {

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

} elseif ($return_first_found && $record->Translation->count()) {

foreach ($record->Translation as $translation)

if (!empty($translation[$name]))

return $translation[$name];

return null ;

} else return null ;

} else return null ;

} else {

require_once 'Of/ExtDoctrine/I18n/EasyAccess/NotTranslatableException.php' ;

throw new Of_ExtDoctrine_I18n_EasyAccess_NotTranslatableException( "Field {$name} is not marked as easy getter to tranlsatable attribute in " .get_class($record));

}

}



/**

* Language dependent setter to translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @param mixed $value

* @param string $language

* @return void

*/

public function setTranslation(Doctrine_Record $record, $name, $ value , $language = null ) {

if (in_array($name, $ this ->_fields)) {

$language = empty($language)?( string )$ this ->getLanguage():( string )$language;

if ($record->hasRelation( 'Translation' )) {

$record->Translation[$language][$name] = $ value ;

}

} else {

require_once 'Of/ExtDoctrine/I18n/EasyAccess/NotTranslatableException.php' ;

throw new Of_ExtDoctrine_I18n_EasyAccess_NotTranslatableException( "Field {$name} is not marked as easy setter to tranlsatable attribute in " .get_class($record));

}

}



}





Analysis time!



Constructor


/**

* Constructs new filter with options

*

* @param array $options

* @return void

*/

public function __construct(array $options) {

if (isset($options[ 'fields' ])) $ this ->setFields($options[ 'fields' ]);

if (isset($options[ 'language' ])) $ this ->setLanguage($options[ 'language' ]);

if (isset($options[ 'owner' ])) $ this ->setOwner($options[ 'owner' ]);

}


Here we initialize our filter, and give it a list of options that contain information about the fields, language, and the mysterious owner of the filter. Let's take a closer look at the options (interface similar to the one in the previous article ):





Installation Methods and Options


This is a set of methods (get / set) Fields (), (get / set) Language (), (get / set) Owner (). In principle, there is nothing nontrivial, then I will not consider everything in detail. I will stop only getLanguage (), since the mysterious owner reappears in it:

/**

* Returns default language for filter

*

* @return string

*/

public function getLanguage() {

if ($ this ->_language !== null ) {

return $ this ->_language;

} elseif ($ this ->_owner) {

return $ this ->_owner->getLanguage();

} else {

require_once 'Of/ExtDoctrine/I18n/EasyAccess/Exception.php' ;

throw new Of_ExtDoctrine_I18n_EasyAccess_Exception( 'Impossible to detect language' );

}

}


As you can see, we need the owner only to request a language if its own value is not specified. This is done to allow this filter to integrate into another structure. In my case, this is a modified template Of_ExtDoctrine_I18n_EasyAccess_Helper, an analogue of which I discussed in a previous article . Owner must follow the Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface interface, which contains only one public method getLanguage ()

interface Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface {



/**

* Returns language for inherited components

*

* @return string

*/

public function getLanguage();



}




Filter methods


In the end, we got to the methods themselves, which implement the necessary filter functionality.

/**

* Returns value of translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @return mixed

*/

public function filterGet(Doctrine_Record $record, $name) {

return $ this ->getTranslation($record, $name, $ this ->getLanguage());

}



/**

* Sets value to translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @param mixed $value

* @return void

*/

public function filterSet(Doctrine_Record $record, $name, $ value ) {

return $ this ->setTranslation($record, $name, $ value , $ this ->getLanguage());

}



/**

* Language dependent getter to translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @param string $language

* @param boolean $return_first_found

* @return mixed

*/

public function getTranslation(Doctrine_Record $record, $name, $language = null , $return_first_found = true ) {

if (in_array($name, $ this ->_fields)) {

$language = empty($language)?( string )$ this ->getLanguage():( string )$language;

if ($record->hasRelation( 'Translation' )) {

if ($record->Translation->contains($language)) {

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

} elseif ($return_first_found && $record->Translation->count()) {

foreach ($record->Translation as $translation)

if (!empty($translation[$name]))

return $translation[$name];

return null ;

} else return null ;

} else return null ;

} else {

require_once 'Of/ExtDoctrine/I18n/EasyAccess/NotTranslatableException.php' ;

throw new Of_ExtDoctrine_I18n_EasyAccess_NotTranslatableException( "Field {$name} is not marked as easy getter to tranlsatable attribute in " .get_class($record));

}

}



/**

* Language dependent setter to translatable attribute

*

* @param Doctrine_Record $record

* @param string $name

* @param mixed $value

* @param string $language

* @return void

*/

public function setTranslation(Doctrine_Record $record, $name, $ value , $language = null ) {

if (in_array($name, $ this ->_fields)) {

$language = empty($language)?( string )$ this ->getLanguage():( string )$language;

if ($record->hasRelation( 'Translation' )) {

$record->Translation[$language][$name] = $ value ;

}

} else {

require_once 'Of/ExtDoctrine/I18n/EasyAccess/NotTranslatableException.php' ;

throw new Of_ExtDoctrine_I18n_EasyAccess_NotTranslatableException( "Field {$name} is not marked as easy setter to tranlsatable attribute in " .get_class($record));

}

}


It can be seen that they practically do not differ from accessors and mutators from the previous part and consist of 2 filtering methods and 2 artificial getters and setters. I made them because I would like to leave the opportunity to get the value of the translation for an arbitrary language on occasion.



Also in the getter, it is possible to disable or enable the ability to search for the first translated value in the case when the translation for the current language is empty. By default, it is enabled, but it will be necessary to somehow bring it into an additional option.



Filter connection



Create a record similar to the one in the first part and connect the filter.

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

));



$i18nFilter = new Of_ExtDoctrine_I18n_EasyAccess_Filter(array(

'language' => 'en' ,

'fields' =>array( 'name' , 'description' ) //

));

$ this ->getTable()->unshiftFilter($i18nFilter);

}



}


$record = new Product();

$record->description = 'my description' ; // $record->Translation['en']->description



echo $record->description; // echo $record->Translation[ 'en' ]->description


When accessing the “description” attribute, the filter will work and the value for the English language will return, but if you access the unspecified attribute, the filter will throw an exception, and the Doctrine_Record_Filter_Compound filter that follows will tell us that there is no such attribute.

try {

echo $record->lol; //

} catch (Of_ExtDoctrine_I18n_EasyAccess_NotTranslatableException $e) {

//...

}


It will be the same for writing to an attribute; I will not consider it.



Template Integration



I used to view access to attributes through the Of_ExtDoctrine_I18n_Template template. Let's try to change it in such a way that instead of hasAccessorMutator our filter is used. Along the way, I’ll change the name to form our system in one package Of_ExtDoctrine_I18n_EasyAccess. Immediately quote the source code of the class.

/**

* Temlate implements extrabehavior for standard Doctrine I18n template.

*

* @author OmeZ

* @version 1.7

* @license www.opensource.org/licenses/lgpl-license.php LGPL

* @package Of_ExtDoctrine_I18n_EasyAccess

*/

class Of_ExtDoctrine_I18n_EasyAccess_Helper extends Doctrine_Template implements Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface {



protected $_options = array(

'language' => null ,

'fields' => null ,

'disableFilter' => false ,

);



/**

* Holds default language for all behaviors

*

* @var string

*/

static protected $_defaultLanguage;



/**

* Holds language for current model behavior

*

* @var string

*/

protected $_language;



/**

* EasyAccess filter

*

* @var Of_ExtDoctrine_I18n_EasyAccess_Filter

*/

protected $_easyaccess_filter;



public function setUp() {



$language = $ this ->getOption( 'language' );

if ($language) $ this ->setLanguage($language);



// Adds filter for access to properties, this can be used as stand-alone plugin

if (!$ this ->getOption( 'disableFilter' )) {

require_once 'Of/ExtDoctrine/I18n/EasyAccess/Filter.php' ;

$ this ->_easyaccess_filter = new Of_ExtDoctrine_I18n_EasyAccess_Filter(array(

'owner' =>$ this ,

'language' => null , // this value will make filter access to template getLanguage() method

'fields' =>$ this ->getOption( 'fields' , array())

));

$ this ->_table->unshiftFilter($ this ->_easyaccess_filter);

}



// adds listener to manage Doctrine_Query hydrations. Will add translatable props as keys in

// array when HYDRATE_ARRAY, or mapped values in records. This can be used as stand-alone plugin

}



/**

* 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 language behavior

*

* @return void

*/

public function getLanguage($without_static = false ) {

if ($ this ->_language === null && !$without_static) return self::getDefaultLanguage();

else return ( string )$ this ->_language;

}



/**

* Sets current behavior language

*

* @param $language

* @return string

*/

public function setLanguage($language) {

$ this ->_language = $language;

}

}


Almost nothing has changed in the template, we just got rid of the read / write methods in Translation, because they are transferred to the filter, as well as the __call () method, because it is no longer needed for the same reasons.



Setting the filter to the table takes place in the setUp () method as before, added the option disableFilter if for some reason we need to disable the filter. As the owner of the filter, we set our template and pass an empty language value, which allows the template to control the current value of the filter language, Of_ExtDoctrine_I18n_EasyAccess_OwnerInterface :: getLanguage () method has already been implemented. All that remains is to connect the template in our demo and test it.

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_EasyAccess_Helper(array(

'fields' =>array( 'name' , 'description' ) //

)));



}



}


$record = new Product();



$record->description = 'my description' ; // $record->Translation['en']->description

echo $record->description; // echo $record->Translation['en']->description



$record->setLanguage( 'ru' ); //



echo $record->description; // 'my description', ..





Conclusion



Such an implementation of access is much more convenient, and, as it turned out, much faster than the method described earlier. The next step is to be able to access the attributes with different methods of hydration, for example, in Doctrine :: HYDRATE_ARRAY, since there are no objects created and we will still be forced to use the translation association (in this case, the nested array).



All classes in one archive can be found here.



Ps. An attentive reader might have noticed that the binding for the fields and language described in the template is repeated in the filter. This was used to enable the filter to be used as a stand-alone component without resorting to a template, which I consider to be a good tone for such systems.



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

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



All Articles