Yii::createObject work-around in Yii3

In yii2 we were able to create new Object and inject all dependencies via Yii::createObject and I still not get what the best practice for Yii3 is.

Imagine you have a FormModel Car that contains Wheels and those wheels require certain Screws.
So you have 3 Classes
Car -> Wheel -> Screw. And you have a post request that looks somehow like

[
    'car' => [
        'model'  => 'x5',
        'wheels' => [
            [
                'position' => 1,
                'type'     => 'XYZ',
                'screws'   => [
                    ['type' => 'A'],
                    ['type' => 'A'],
                ]
            ],
            [
                'position' => 2,
                'type'     => 'XYZ',
                'screws'   => [
                    ['type' => 'B'],
                    ['type' => 'B'],
                ]
            ],
            [
                'position' => 3,
                'type'     => 'XYZ',
                'screws'   => [
                    ['type' => 'C'],
                    ['type' => 'C'],
                ]
            ]
        ]
    ]
];

In Yii2 I usually did something like this in my Car class (this is just an example)

public function setWheels($wheels)
{
    $this->wheels = [];
    foreach ($wheels as $wheel){
        if(is_array($wheel)){
            $wheel['__class'] = Wheel::class;
            $wheel = Yii::createObject($wheel);
        }
        $this->wheels[] = $wheel;
    }
}

So I could load my post request (the array) and create objects of those again. The Wheel class on the other hand has the same so I was able to resolve all dependencies.
Imagine the constructor of our Screw looks like this.

public function __construct(ScrewService $screwService)
{
    $this->screwService = $screwService;
}

In that case we would either need to pass that ScrewService all the way down from the Car, to the Wheel and then to the Screw or do some nasty clones

public class Car
{
    public function __construct(Wheel $basicWheel)
    {
        $this->basicWheel = $basicWheel;
    }

    public function setWheels($wheels)
    {
        $this->wheels = [];
        foreach ($wheels as $wheel){
            if(is_array($wheel)){
                $newWheel = clone $this->basicWheel;
                $newWheel->load($wheel);
                $wheel = $newWheel;
            }
            $this->wheels[] = $newWheel;
        }
    }
}

So we have one “basic” Model with all resolved dependencies and due to DI and always clone that. It feels rather ugly too.
My question is: what is best practice here in that case?
Could someone please provide a simple example how to deal with deep nested dependencies that were easily resolved via Yii::createObject in the past?

In Yii 2 you can always configure the ScrewService beforehand in the Container. I’m not sure if anything changed in Yii 3 regarding this. Your example is quite similar to what I would like to introduce in Yii 2, please take a look.

I saw that and already upvoted for it. But since the container/yii create Object will be kinda removed from normal classes as a static call I’m still not sure what your feature request has to do with my question

It makes no sense to use createObject analogue there since there are no dependencies to use that are ready and stored in DI container. Instead it would be something like this:

final class CarFactory
{
    public function create(ServerRequest $request)
    {
        $carData = $request->getParsedBody()['car'] ?? null;
        if ($carData === null) {
            throw new InvalidArgumentException('No car data provided.');
        }

        $wheels = [];
        foreach ($carData['wheels'] as $wheelData) {
            $wheels[] = $this->createWheel($wheelData);
        }
        return new Car($carData['model'], $wheels);
    }

    private function createWheel(array $data): Wheel
    {
        if (!array_key_exists('type', $data)) {
            throw new \InvalidArgumentException('Invalid wheel data. Type is missing.');
        }

        if (!array_key_exists('screws', $data)) {
            throw new \InvalidArgumentException('Invalid wheel data. Screws data is missing.');
        }

        $screws = [];
        foreach ($data['screws'] as $screwData) {
            $screws[] = $this->createScrew($screwData);
        }

        return new Wheel($data['type'], $screws, $data['position']);
    }

    private function createScrew(array $data): Screw
    {
        if (!array_key_exists('type', $data)) {
            throw new \InvalidArgumentException('Invalid screw data. Type is missing.');
        }
        return new Screw($data['type']);
    }
}

Thanks for your answer. But what if screws had a Dependency? What would you do in that case?

I guess your answer would be to create all objects in one class instead of several different one like I currently do it?

final class CarFactory
{
    private CacheInterface $cache;

    public function __construct(CacheInterface $cache)
    {
        $this->cache = $cache;
    }

    public function create(ServerRequest $request)
    {
        $carData = $request->getParsedBody()['car'] ?? null;
        if ($carData === null) {
            throw new InvalidArgumentException('No car data provided.');
        }

        $wheels = [];
        foreach ($carData['wheels'] as $wheelData) {
            $wheels[] = $this->createWheel($wheelData);
        }
        return new Car($carData['model'], $wheels);
    }

    private function createWheel(array $data): Wheel
    {
        if (!array_key_exists('type', $data)) {
            throw new \InvalidArgumentException('Invalid wheel data. Type is missing.');
        }

        if (!array_key_exists('screws', $data)) {
            throw new \InvalidArgumentException('Invalid wheel data. Screws data is missing.');
        }

        $screws = [];
        foreach ($data['screws'] as $screwData) {
            $screws[] = $this->createScrew($screwData);
        }

        return new Wheel($data['type'], $screws, $data['position']);
    }

    private function createScrew(array $data): Screw
    {
        if (!array_key_exists('type', $data)) {
            throw new \InvalidArgumentException('Invalid screw data. Type is missing.');
        }
        return new Screw($data['type'], $this->cache);
    }
}

You can have multiple factories if it makes sense to create entities separately.

I really appreciate your help, it’s just a little bit difficult for me to understand it :see_no_evil: after 5 years with yii2 and the service locator + static calls.
What’s the benefit of the factory class when there is still no access to Yii::createObject

This exact case isn’t related to Yii::createObject. Yii 2 was using all-public properties all the time making it easy to create objects from arrays but violating data encapsulation in the worst way possible.

The benefit isn’t in the process of creating an object. You got the validation in my variant but, obviously, it’s more complicated than it was in Yii 2. The benefit in having proper encapsulation. That results in having way less weird errors in the code and predictable behavior.

I totally see that and I understand it’s worth. I just don’t know how to be able to get all required services in my screw class without heavy heavy effort.
Let’s say you need the View component or the DB Connection in a screw.
I was able to add it as a construct param in yii2 and everything was good cause createObject injected it automatically.

Now with yii3 if my constructor changes it’s so much effort to change all calls to that class and add the additional parameter because I would need to pass down all the required components everywhere as construction parameters.

It feels like I need to pass nearly everything everywhere just in order to not get screwed when some deep level nesting class requires an additional service.
Maybe it’s worth mentioning that I’m using Craft CMS and my classes are very often called from somewhere else. I’m currently trying to archive the yii3 approach to my new projects and fail all the way.

Well, there’s a shortcut for that case in Yii 3 as well:

$screw = (new Injector($container))->make(Screw::class, ['__construct()' => ['type' => $type]]);

Thank you very much for your patience.
Where do I get the container from when I’m in the class? I thought the static yii call with the container will be removed?

In the factory constructor you can type-hint against Injector:

final class CarFactory
{
    private Injector $injector;

    public function __construct(Injector $injector)
    {
        $this->injector = $injector;
    }

Then in the controller:

final class CarController
{
    public functio actionCreate(ServerRequestInterface $request, CarFactory $factory): Response
    {
        $car = $factory->create($request->...);
    }
}

Alright. So I can do the same with the container and pass it everywhere via constructor and achieve the same as yii::createObject just now its $this->container->get.
Got it. Feels kinda odd because that’s kinda like the static call and a service locator, but I’ll be fine with it. Thanks your your help and explanation.

You can but you should not. That’s another anti-pattern. It is acceptable in some cases but if you know dependencies right away, it is preferrable to specify dependencies explicitly in constructor.

1 Like