Im trying to implement Dependency injection Using the Zend2 service manager. I want to inject a PDO instance into Service (Im not using the Zend Db).
Im following the tutorial here: https://framework.zend.com/manual/2.4/en/in-depth-guide/services-and-servicemanager.html
I have it working for another service, but the when injecting the PDO instance Im getting this error:
Catchable fatal error: Argument 1 passed to Application\Service\DataService::__construct() must be an instance of Application\Service\DbConnectorService, none given, called in /srv/www/shared-apps/approot/apps-dev/ktrist/SBSDash/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php on line 1077 and defined in /srv/www/shared-apps/approot/apps-dev/ktrist/SBSDash/module/Application/src/Application/Service/DataService.php on line 24
From the tutorial this seems to be related to my invokeables in the module.config. But I cannot work out what the issue is.
Any advice is appreciated.
Here is my code:
DataService:
class DataService {
protected $dbConnectorService;
public function __construct(DbConnectorService $dbConnectorService) {
$this->dbConnectorService = $dbConnectorService;
}
......
DataServiceFactory:
namespace Application\Factory;
use Application\Service\DataService;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class DataServiceFactory implements FactoryInterface {
function createService(ServiceLocatorInterface $serviceLocator) {
$realServiceLocator = $serviceLocator->getServiceLocator();
$dbService = $realServiceLocator->get('Application\Service\DbConnectorService');
return new DataService($dbService);
}
}
Module.Config:
'controllers' => array(
'factories' => array(
'Application\Controller\Index' => 'Application\Factory\IndexControllerFactory',
'Application\Service\DataService' => 'Application\Factory\DataServiceFactory',
)
),
'service_manager' => array(
'invokables' => array(
'Application\Service\DataServiceInterface' => 'Application\Service\DataService',
'Application\Service\DbConnectorService' => 'Application\Service\DbConnectorService',
)
),
You are trying to create the service as an 'invokable' class. ZF2 will treat this service as a class without dependencies (and not create it using the factory).
You should update your service configuration to register under the 'factories' key, pointing to the factory class name.
'service_manager' => [
'invokables' => [
'Application\\Service\\DbConnectorService'
=> 'Application\\Service\\DbConnectorService',
],
'factories' => [
'Application\\Service\\DataServiceInterface'
=> 'Application\\Factory\\DataServiceFactory',
],
],
You will need to make the same change for DbConnectorService if that also has a factory.
Related
I'm trying to register a bunch of custom DBAL types. When I run the migrations:diff I get the exception:
Fatal error: Class 'App\Persistence\Models\Types\Money' not found in D:\development\projects\project\vendor\doctrine\dbal\lib\Doctrine\DBAL\Types\Type.php on line 174
I've tried to do this either by registering it after all Doctrine settings and using a event subscriber:
class DoctrineCustomTypesEventSubscriber implements Subscriber {
public function getSubscribedEvents() {
return [Events::postConnect];
}
public function postConnect(ConnectionEventArgs $args) {
Type::addType('money', "App\Persistence\Models\Types\Money");
Type::addType('geopoint', "App\Persistence\Models\Types\Point");
Type::addType('geoarea', "App\Persistence\Models\Types\Area");
}
}
$doctrineCustomTypesSubscriber = new App\Persistence\DoctrineCustomTypesEventSubscriber();
$app['db.event_manager']->addEventSubscriber($doctrineCustomTypesSubscriber);
$app->register(new Dflydev\Provider\DoctrineOrm\DoctrineOrmServiceProvider, array(
'orm.proxies_dir' => $app['APP_ROOT_DIR'].'/app/persistence/proxies',
'orm.em.options' => array(
'mappings' => array(
array(
'type' => 'annotation',
'namespace' => 'App\Persistence\Models',
'path' => $app['APP_ROOT_DIR'].'/app/persistence/models',
'use_simple_annotation_reader' => false,
),
),
),
));
Update
Placing the registration before all orm settings doesn't work either:
use Doctrine\DBAL\Types\Type;
$app->register(new Silex\Provider\DoctrineServiceProvider(), array(
'db.options' => array('url' => $app['APP_DB_CONN_URL']),
));
Type::addType('money', "App\Persistence\Models\Types\Money");
Type::addType('geopoint', "App\Persistence\Models\Types\Point");
Type::addType('geoarea', "App\Persistence\Models\Types\Area");
What am I doing wrong here?
Also can you tell me where do I put these registrations:
$em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping(...)?
The type classes should exist and be registered in autoloader, so that they can be instantiated by FQCN associated with type.
in ZF2 it was possible to configurate multiple adapters like this in the module.config.php:
'db' => array(
'adapters'=>array(
'db1' => array(
'driver' => 'Pdo',
'dsn' => 'mysql:dbname=zf2;host=localhost',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
),
'username' => 'zf2',
'password' => 'zf2test',
),
'db2' => array(
'driver' => 'Pdo',
'dsn' => 'mysql:dbname=zf1;host=localhost',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
),
'username' => 'zf1',
'password' => 'zf1test',
),
)
),
In the controller factory I could get them via the ServiceManager:
class AlbumControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$albumTable = $serviceLocator->getServiceLocator()->get('Album\Model\AlbumTable');
$db1Adapter = $serviceLocator->getServiceLocator()->get('db1');
$db2Adapter = $serviceLocator->getServiceLocator()->get('db2');
return new AlbumController($albumTable, $db1Adapter, $db2Adapter);
}
}
Now I'm trying to do the same in Zend Framework 3 - but this nested array configuration doesn't work:
Fatal error: Uncaught exception 'Zend\Db\Adapter\Exception\InvalidArgumentException' with message 'createDriver expects a "driver" key to be present inside the parameters' in /var/www/USER/teckert/zf3/vendor/zendframework/zend-db/src/Adapter/Adapter.php:262
I think that in ZF 2 the adapters key are already handled when the dbAdapter is trying to create the driver - but this is not happening in ZF 3.
Any hints are warmly welcomed...
The manual of the zend-db with the adapters section wasn't clear enough for me
EDIT
According to this doc I've added the following snippet to the global config file:
'service_manager' => [
'abstract_factories' => [
\Zend\Db\Adapter\AdapterAbstractServiceFactory::class,
],
],
While trying to get the dbAdapter with $container->get('db1') in my AlbumTableFactory I get this error:
Unable to resolve service "db1 to a factory; are you certain you provided it during configuration?
Make sure you've added Zend\Db\Adapter\AdapterAbstractServiceFactory to the abstract_factories array of the ServiceManager configuration.
This abstract factory is responsible for instantiating the individual database adapters. Also, check whether your Album\Model\AlbumTable factory retrieves the database adapter with the correct name.
Okay I finally resolved the problem.
As mentioned by #Pieter I needed the following array content in my config.php:
'service_manager' => [
'abstract_factories' => [
\Zend\Db\Adapter\AdapterAbstractServiceFactory::class,
],
],
Additionally I had to change the process my classes and dependent factories are talking to each other.
AlbumControllerFactory is called and get's the dependent AlbumTable service ($container->get(AlbumTable::class);) which then triggers the AlbumTableFactory
AlbumTableFactory then is preparing the constructor injection for the AlbumTable with $tableGateway = $container->get(AlbumTableGateway::class);
Now I combined the logic from the AlbumTableFactory and the AlbumTableGatewayFactory into one AbstractFactoryInterface (I removed the AlbumTableGatewayFactory completely)
// AlbumTableFactory implements AbstractFactoryInterface
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$dbAdapter = $container->get('db1');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Album());
$tableGateway = new TableGateway('album', $dbAdapter, null, $resultSetPrototype);
return new AlbumTable($tableGateway);
}
I do not know how to configure Zend \ Log with Doctrine2. Only allows you to write directly to the database via a connection adapter or write to a file.
May be it's too late to answer this question but better late than never.
I've found a good post which explains how to create a basic SQL Logger for ZF2 and Doctrine.
The approach is pretty simple :
1. Creating Logger class : Create the following class in your Module/Application/Log folder :
<?php
namespace Application\Log;
use Zend\Log\Logger;
use Doctrine\DBAL\Logging\DebugStack;
class SqlLogger extends DebugStack
{
protected $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function stopQuery()
{
parent::stopQuery();
$q = $this->queries[$this->currentQuery];
$message = "Executed Query: " . print_r($q, true);
$this->logger->info($message);
}
}
The stopQuery() function which is called by Doctrine when it finiches sending the query to the database server, is
overrided so that it could write the current query to the Logger object.
2. Configuring the Logger : Add the following code in your config/autoload/global.php file, to make
the Logger accessible to the Service Manager using the name my_sql_logger :
'service_manager' => array(
'factories' => array(
'my_sql_logger' => function($sm) {
$log = new \Zend\Log\Logger();
$writer = new \Zend\Log\Writer\Stream('./data/logs/sql.log');
$log->addWriter($writer);
$sqllog = new \Application\Log\SqlLogger($log);
return $sqllog;
},
)
),
The Logger will write data to the data/logs/sql.log file. So, make sure that data/logs folder exists in your
application root directory.
3. Configuring Doctrine : Now you need to tell Doctrine to use the created Logger. Just add the following code
to your Doctrine configuration :
return array(
'doctrine' => array(
/*--------Add this code------------*/
'sql_logger_collector' => array(
'orm_default' => array(
'sql_logger' => 'my_sql_logger',
),
),
/*---------------------------------*/
'connection' => array(
'orm_default' => array(
'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
'params' => array(
'host' => 'localhost',
'port' => '3306',
'user' => 'username',
'password' => 'password',
'dbname' => 'dbname',
),
),
),
),
);
With the above configuration of Zend\Log and Doctrine2, you'll get all the query data logged in the
data/log/sql.log file.
Please see this Sql Logger for ZF2 and Doctrine for more details.
In my Zend\Form\Fieldset AddressFieldset it needs a Zend\Db\TableGateway\AbstractTableGateway BundeslandTable for a \Zend\Form\Element\Select().
So i implement \Zend\ServiceManager\ServiceManagerAwareInterface in this AddressFieldset and use the init() instead __construct().
And in module.config.php (not only in 'form_elements' tested, also in 'service_manager')
'form_elements' => array(
'factories' => array(
'MyFormway\Form\Fieldset\Address' => function($sm) {
$addressFieldset = new MyFormway\Form\Fieldset\AddressFieldset();
$addressFieldset->setServiceManager($sm);
return $addressFieldset;
}
),
),
In a \Zend\Form\Form's init():
$this->add(array(
'type' => 'MyFormway\Form\Fieldset\Address',
'name' => 'address',
));
this throws an error:
Zend\Form\FormElementManager::get was unable to fetch or create an instance for MyFormway\Form\Fieldset\Address
Why is zend unable to fetch an instance of this Fieldset?
edit-----------------------
'form_elements' => array(
'factories' => array(
'MyFormway\Form\Fieldset\Address' => function($formElementManager) {
die('inna form_elements config');
$addressFieldset = new \MyFormway\Form\Fieldset\AddressFieldset();
$addressFieldset->setServiceManager($formElementManager->getServiceLocator());
return $addressFieldset;
}
),
),
Because i have the Zend\Form\FormElementManager i fetch the ServiceLocator ...perhaps dont needed, because all XxxManager extends the Zend\ServiceManager\AbstractPluginManager and this extends ServiceManager.
In FormElementManager and also in AbstractPluginManager are no method getServiceManager().
But my problem: the die() is not called plus the error above. Is it a bug? ...i stand for a big wall :(
edit-----------------------
It works for a Form but not for a Fieldset!!!
Can you do a quick check if the \Invokable is called at all? Some professional die()-debugging will suffice.
Other than that a potential error source would be your injection of the ServiceManager. In the code you provide you're not actually injecting the ServiceLocator but rather the FormElementManager.
$addressFieldset->setServiceManager($sm->getServiceManager());
Doing it this way is considered Bad-Practice tho. You should only inject the stuff that you actually do need. Given you're injecting the whole manager i assume you're either working with Doctrine or you'll need access to some DB-Data. Do it like this:
'Foo' => function ($formElementManager) {
$sl = $formElementManager->getServiceManager();
$fs = new FooFieldset();
$fs->setDbDependency(
$sl->get('MyDbDependency')
);
return $fs;
}
Last little note: when you're adding a Fieldset, you don't need to add 'name' => 'foo' within the $this->add(), since the name of the fieldset will be defined via the Fieldset __construct('name').
First ZF2 application, getting there, but I think still missing a think or two when it comes to dependency injection and the ServiceManager.
I have a particular problem at the moment with a new database gateway class I'm writing. I won't to inject a database adapter, so I've implemented AdapterAwareInterface. But the setDbAdapter method is never called in my class. I'm wondering if someone would be so kind as to look at my code and suggest what might be going wrong (or what I'm missing!).
So, here is the class in which I implement AdapterAwareInterface.
<?php
namespace Foo\Database;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Adapter\AdapterAwareInterface;
use Zend\Log\LoggerAwareInterface;
use Zend\Log\LoggerInterface;
class Gateway implements AdapterAwareInterface, LoggerAwareInterface
{
protected $logger = NULL;
protected $db = NULL;
public function setDbAdapter(Adapter $adapter)
{
$this->db = $adapter;
}
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
This is an extract from my module file showing how I configure my service manager:
public function getServiceConfig()
{
return array(
'factories' => array(
....
),
'invokables' => array(
'FooDatabaseGateway' => 'Foo\Database\Gateway',
),
'abstract_factories' => array(
'AbstractFeedParserFactory' => 'Bookmakers\Odds\Feeds\AbstractFeedParserFactory',
),
);
}
This is how I'm testing:
gateway = $this->getServiceLocator()->get('FooDatabaseGateway');
And this is part of my global config:
return array(
'db' => array(
'driver' => 'Pdo',
'dsn' => 'mysql:dbname=kickoff_manager;host=localhost',
'username' => '****',
'password' => '****',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
),
),
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter'
=> 'Zend\Db\Adapter\AdapterServiceFactory',
),
),
);
Many thanks for any help you can provide.
:wq
OK a fresh pair of eyes on this problem this morning. I think this is the write answer.. At least that is to say its working for me. If anyone wants to suggest an entirely different of better approach, then please do so :-).
So the bit is was missing was to use an initializer in my service manager config to call the setDbAdapter function on any class instances that implement AdapterAwareInterface. So in the array I return from getServiceConfig in my Module.php file, I have added the following entry:
public function getServiceConfig() {
return array(
'initializers' => array(
'db' => function($service, $sm)
{
if ($service instanceof AdapterAwareInterface)
{
$service->setDbAdapter($sm->get('Zend\Db\Adapter\Adapter'));
}
}....
I think what I'm missing while learning ZF2 is that there are a lot of building blocks to work with, but you've got to put a lot of them together yourself.
Things are looking good and I'm enjoying the Framework, but there is a lot to learn, and I'm still not convinced by using Server Manager injection rather than good old constructor injection!
:wq