Laminas: Get instance of DB Adapter in an Class (Injection?)

1.8k views Asked by At

I'm relative new to Laminas and some things still doesn't make sense to me - in terms of - it looks to me very complicated how things have to be done in laminas. in my case now I need the instance of the DB adapter.

The project is like this:

I have an IndexController (and a Factory) that creates (in case of an Post Request) an instance of an Mail Class and that Mail class is supposed to add Data in the MailQueueTable. But I don't know how to get the DB Adapter in the MailQueueTable

The Source code is as follows:

IndexControllerFactory.php

<?php

declare(strict_types=1);

namespace Test\Controller\Factory;

use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;
use Test\Controller\IndexController;


class IndexControllerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestName, array $options = null)
    {
        return new IndexController(
            $container->get('ApplicationConfig')
        );
    }
}

IndexController.php

<?php

declare(strict_types=1);

namespace Test\Controller;

use Laminas\Config\Config;
use Laminas\Mvc\Controller\AbstractActionController;
use Laminas\View\Model\ViewModel;
use Mail\Model\Mail;


class IndexController extends AbstractActionController
{
    private $config;

    public function __construct(array $config)
    {
        $this->config = $config;
    }


    public function indexAction()
    {
        $request = $this->getRequest();
     
        if ($request->isPost()) {     
                $Mail = new Mail();
                $Mail->send();
            }
        }
    }
}

Mail.php

<?php

namespace Mail\Model;

use Laminas\View\Model\ViewModel;
use Mail\Model\Table\MailQueueTable;


class Mail {

    public function send()
    {
        $MailQueueTable = new MailQueueTable();
        $MailQueueTable->add();
    }

}

MailQueueTable.php

<?php

declare(strict_types=1);

namespace Mail\Model\Table;

use Laminas\Db\Adapter\Adapter;
use Mail\Model\Mail;

class MailQueueTable extends AbstractTableGateway
{
    protected $adapter;
    protected $table = 'mail_queue';


    public function __construct(Adapter $adapter)
    {
        // Here starts the problem...
        // As far as I understand, I have to inject
        // the DB Adapter in the Construct of the
        // AbstractTableGateway Class...
        // But no idea how to get it here...

        $this->adapter = $adapter;
        $this->initialize();
    }


    public function add()
    {
        // SQL Insert Statement would be here
    }
}

The MailQueue Table Code is in terms of constructor etc. based on the tutorials I have read. as you can see the construct needs the Adapter. But I have no idea how to get the Adapter at this point.

As far as I have read until now, I need to inject the DB Adapter in the Index Controller Factory, then from the Action in the Index Controller to the new created Mail Instance and from there I have to inject it to the MailQueue Table ?

I don't feel that this is the right solution - before using Laminas i could just write

global $DB;

and I had my DB available.

2

There are 2 answers

0
Ermenegildo On

It is quite complicated at the beginning, but when you get used to Factories (and DI), it will become easier :)

The problem is inside your controller:

public function indexAction()
{
    $request = $this->getRequest();
    
    if ($request->isPost()) {
            // !! wrong !!
            $Mail = new Mail();
            // !! wrong !!

            $Mail->send();
        }
    }
}

You are creating an instance of Mail, but mail has few dependencies which must be resolved, but your controller just can't do it. To solve this problem, you should obtain the Mail instance from your DI (the ContainerInterface).

Inside IndexControllerFactory.php

public function __invoke(ContainerInterface $container, $requestName, array $options = null)
{
    $applicationConfig = $container->get('ApplicationConfig');
    $mail = $container->get(\Mail\Model\Mail::class);
    return new IndexController($applicationConfig, $mail);
}

This way, dependencies for IndexController will be resolved by its factory. Now you must adapt the controller to accept the new instance in the constructor:

class IndexController extends AbstractActionController
{
    private $config;

    private $mail;

    public function __construct(array $config, Mail $mail)
    {
        $this->config = $config;
        $this->mail = $mail;
    }


    public function indexAction()
    {
        $request = $this->getRequest();
     
        if ($request->isPost()) {     
                $this->mail->send();
            }
        }
    }
}

But then, you will have the same problem for Mail (since it requires an instance of MailQueuqTable) and for MailQueueTable (since it requires an instance of Adapter).

Therefore, you should supply those dependencies when constructing the object, and adding factories Mail\Model\Mail:

class Mail {
    private $mailQueueTable;

    public function __construct(MailQueueTable $mailQueueTable)
    {
        $this->mailQueueTable = $mailQueueTable;
    }

    public function send()
    {
        $this->mailQueueTable->add();
    }
}

its factory Mail\Model\MailFactory:

class MailFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestName, array $options = null)
    {
        return new Mail(
            $container->get(\Mail\Model\Table\MailQueueTable::class)
        );
    }
}

Mail\Model\Table\MailQueueTable is actually correct, since the constructor already requires an instance of Adapter. You must create its factory, Mail\Model\Table\MailQueueTableFactory:

class MailQueueTableFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestName, array $options = null)
    {
        // Create the DB adapter
        $dbConfig = [
            'driver' => 'PDO', 
            'pdodriver' => '...',
            'dbname' => '...',
            'host' => '...',
            'username' => '...',
            'password' => '...'
        ];
        $adapter = new Adapter($dbConfig);
        return new MailQueueTable($adapter);
    }
}

Finally, you must tell your DI where to find those factories, so in the module configuration file:

'service_manager' => [
    'factories' => [
        \Mail\Model\Mail::class => Mail\Model\MailFactory::class,
        \Mail\Model\Table\MailQueueTable::class => Mail\Model\Table\MailQueueTableFactory::class
    ]
]
0
jan7007 On

Thank you so much for your great answer.

So that means, that for every Class that "i might need" / that "i might create an instace of" in my controllers, i have to declare it in the ControllerFactory ?

Wow. Laminas requires really a lot of code for this "simple" example.

Even though your answer is great it leads to new problems:

  1. if i obtain the Mail Instance inside IndexControllerFactory.php this would lead to the problem, that i cant pass variables to the constructor of Mail Class (i.e. email adress, content of the email etc., which are being created within the IndexController) because the constructor of the Mail Class would be:
    public function __construct(MailQueueTable $mailQueueTable)
    {
        $this->mailQueueTable = $mailQueueTable;
    }

do i see that correct ?

  1. I was following the laminas youtube tutorials from waxmud studios - the DB tutorial is here: https://www.youtube.com/watch?v=07h3OBIF9kc

It saves the DB credidentials, DB Name etc. in the config files. how can i access them instead of putting them again in the MailQueueTableFactory as you have mentioned ? Getting instance of the DB Adapter from the IndexControllerFactory pass it to the IndexController and from there i pass it to the Mail?

  1. The curious thing which I still haven't figured out yet is, that if i inject the MailQueueTable in the IndexControllerFactory, i.e:
class IndexControllerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestName, array $options = null)
    {
        return new IndexController(
            $container->get(MailQueue::class)
        );
    }
}

I don't have to create a MailQueueTableFactory and don't have to pass the DB Credentials etc. Somehow the MailQueueTableseems like it gets the Adapter... ?!