This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
12.

Translated Object Wrapper

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Alright, so now we have a way to store our translations. Let's dive into how we can make use of them. If you pull up our ObjectTranslator, we have this todo waiting for us.

There's a couple ways we can approach this. One way is to directly modify the properties on the object. But since this is a Doctrine entity, it'll be marked as modified. If you unintentionally flush, you'll overwrite your default locale translations on the entity itself. We definitely don't want that.

Another approach would be to first clone the object, then modify the properties. This is better... but if you accidentally try and persist this new object, it might save a new version to the database, which would be bad.

Let's explore my preferred option: creating a wrapper for the object, you can think of this as a view model. This wrapper will pass all method calls and property access to the underlying object. Then, when a property is accessed, it will first check if a translation exists for that property. If so, it'll return the translated value; otherwise, it will return the original value.

TranslatedObject Class

First, in your bundle's src directory, create a new class named TranslatedObject. Mark it as final and create a constructor. It'll accept a single parameter, private object $_inner:

// ... lines 1 - 4
final class TranslatedObject
{
public function __construct(
private object $_inner,
) {
}
// ... lines 11 - 25
}

The underscore is a convention to ensure there's no conflict when calling methods or properties on this wrapper.

Adding Generics for Better IDE Support

We're going to use PHP generics again here, but at a class level. Add a class doc block, and add @template T of object. Then, make this a mixin by adding @mixin T:

// ... lines 1 - 4
/**
* @template T of object
*
* @mixin T
*/
final class TranslatedObject
// ... lines 11 - 35

This essentially means that this object will have all the same methods and properties as the T template object.

Next, add a doc block to the constructor, and instead of @param object, replace with @param T:

// ... lines 1 - 9
final class TranslatedObject
{
/**
* @param T $_inner
*/
public function __construct(
private object $_inner,
) {
}
// ... lines 19 - 33
}

This lets PHPStorm know that we're injecting this class-level template object here.

Magic!

So, how do we forward method and property calls to the inner object? Magic methods! Override three methods: __get(), __isset(), and __call().

__call() is called when a method is used that doesn't exist on this object. Set the return type to mixed. Inside, write return $this->_inner->$name(...$arguments).

// ... lines 1 - 4
final class TranslatedObject
{
// ... lines 7 - 11
public function __call(string $name, array $arguments): mixed
{
return $this->_inner->$name(...$arguments);
}
// ... lines 16 - 25
}

This forwards any method calls to the inner object by using the $name variable as the method name. PHP is cool like this!

__get() is invoked when trying to access a property on this object that doesn't exist. Set the return type to mixed, and inside, forward to the inner object with return $this->_inner->$name.

// ... lines 1 - 4
final class TranslatedObject
{
// ... lines 7 - 16
public function __get(string $name): mixed
{
return $this->_inner->$name;
}
// ... lines 21 - 25
}

Again, using the $name variable as the property name.

Finally, __isset() is called when using isset() on a property that doesn't exist. Forward the call with return isset($this->_inner->$name):

// ... lines 1 - 4
final class TranslatedObject
{
// ... lines 7 - 21
public function __isset(string $name): bool
{
return isset($this->_inner->$name);
}
}

Now, if you've written this type of object before, you might be thinking: "Wait a minute, what about the __set() magic method?" Good question! We want this to be a read-only wrapper, so we're skipping this magic method.

Using TranslatedObject

Let's put this new class to work! Go to ObjectTranslator::translate() and remove the todo. Wrap this $object in new TranslatedObject():

// ... lines 1 - 6
final class ObjectTranslator
{
// ... lines 9 - 22
public function translate(object $object): object
{
// ... lines 25 - 32
return new TranslatedObject($object);
}
}

Remember, in our ArticleController::show() method, we're running the article object through this translator and passing it to Twig.

Time to try this out! At your browser, visit an article page. Ok, so far so good... But we're on the English version - our default locale. Remember, in ObjectTranslator::translate(), if we're on the default locale, we're just returning the object as is. This isn't using our new TranslatedObject class.

So, back in the browser, switch to the French version... Uh oh, an error: "Call to undefined method Article::title()". Hmm, this worked no problem with the real article object. What's going on here?

This is a Twig nuance we'll need to account for in our wrapper. We'll tackle this next, and with a unit test to boot!