Pass a complete object to translate function so it's attributes can be used in placeholders


(Leo) #1

A convenient way to translate dynamic text in applications is to use placeholders, like so:

$message = 'Hello I am {firstname}';
$translation = Yii::t($message, array('{firstname}', 'John'));

But I want it to be some more dynamicly and just pass a complete person object (just for example) and the user of the application is free to use whatever he feels comfortable of the person object.

So, given the following object:

$customer = new tstCustomer();
$customer->firstname = 'testfirst';
$customer->lastname = 'testlast';
$customer->email = 'testemail';
class tstCustomer
{
	public $firstname;
	public $lastname;
	public $email;
	
	public function __construct(){}
}

I want my user to be able to create a translatable string like this:

$message = '{greetings} I am {customer->firstname} {customer->lastname} and my email address is {customer->email} {customer->notexisting}.';

I only have to pass the $customer object now, instead of all kinds of separate properties (and their possible placeholders). This solution also gives the possibility of object traversing (but is it a problem?).

echo translate($message, array('{greetings}' => 'Hello World', '{customer}' => $customer));
// Output:
// Hello World I am testfirst testlast and my email address is testemail . 

I’ve written the following function to accomplish above:

function translation($message, $placeholders = array())
{
    // catch all notices/warnings/errors due to non existent attributes
	$backupHandler = set_error_handler('strtrErrorHandler');
	    	
	foreach($placeholders as $placeholder => $value)
	{
		if(gettype($value) === 'string')
		{
			$message = strtr($message, array($placeholder => $value));
			continue;
		}
		
		if(gettype($value) !== 'object')
			continue;
		
		$trimmed_placeholder = trim($placeholder, '{}');

		$regex = '/\{'.$trimmed_placeholder.'[\-\>[\w]*]*\}/';
		$matches = array();
		preg_match_all($regex, $message, $matches);
		// $matches should look like:
		// array(1) { [0]=> array(3) { [0]=> string(21) "{customer->firstname}" [1]=> string(20) "{customer->lastname}" [2]=> string(17) "{customer->email}" } } 
		if(!isset($matches[0]))
			continue;
		
		foreach($matches[0] as $match)
		{
			$stringpath = trim($match, '{}');
			$traversal = substr($stringpath, strlen($trimmed_placeholder)+2); 
			try
			{
				$message = strtr($message, array($match => $value->{$traversal}));
			}
			catch(Exception $e)
			{
				// replace the placeholder with empty string
				$message = strtr($message, array($match => ''));
			}
			
		}
	} // foreach $placeholders
	
	set_error_handler($backupHandler);
	
	return $message;
}
// catch all and convert to a catchable Exception 
function strtrErrorHandler($severity, $message, $filename, $lineno) {
	Yii::log($message, 'error', 'backend.test.index');
	throw new Exception('');
}

Now, my questions:

  1. How safe will this be? (I hope object traversing is limited like this)
  2. Can this be accomplished better in terms of safety?
  3. Can this be accomplished better in terms of performance?
  4. Any other thoughts?

My original post on SO which is getting very low number of views: https://stackoverflow.com/questions/55635119/convert-placeholders-in-a-string-to-the-values-of-objects-property-how-safe-is


(Alexander Makarov) #2
  1. Less safe than pre-composing data array. It’s likely that at some point important object data could be leaked.
  2. Yes. As it is now.
  3. Yes. As it is now.

(Leo) #3

Thanks for your answer Alexander.

Basicly the ‘best way’ to accomplish my needs is write a function that will convert one or more objects to a ‘placeholder array’ with all their properties available, before passing it to the translation function.
So, solve it a step earlier in the chain.


(Alexander Makarov) #4

Yes, I think so.