ZF2 - ServiceManager and 'aware' interfaces - php

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

Related

ZF3 multiple database adapters

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);
}

Abstract Factory for custom Validators - options discarded?

My goal is to pass custom options to validators, as is done by the ZF2-provided validators. Consider this validator config:
'filters' => array(
'leaderboard' => array(
'required' => true,
'filters' => array(
array('name' => 'stringtrim'),
),
'validators' => array(
array(
'name' => '\LDP\Form\Validator\UniqueAtom',
'options' => array(
'key' => 'foo',
),
),
),
),
),
In this case, my validator is provided by an abstract factory which is specified in my application's getValidatorConfig(). It seems though, judging by lines 95+ in AbstractPluginManager that this function sequence ignores creation options:
public function get($name, $options = array(), $usePeeringServiceManagers = true)
{
// Allow specifying a class name directly; registers as an invokable class
if (!$this->has($name) && $this->autoAddInvokableClass && class_exists($name)) {
$this->setInvokableClass($name, $name);
}
$this->creationOptions = $options;
$instance = parent::get($name, $usePeeringServiceManagers);
$this->creationOptions = null;
$this->validatePlugin($instance);
return $instance;
}
In short, the creation options make their way there, but they're never ferried about. What's the best solution?
After banging my head against a wall for a very long time, I've just found the solution in the source. You have to make your factory implement Zend\ServiceManager\MutableCreationOptionsInterface
You can then use whatever it passes to instantiate the "next thing". Feels like an official band-aid hehe, but it works.
Hope this helps.

Getting database adapter in a model in zend framework 2

I am a zf1 developer. I started zf2. I am creating a Authentication module. I created a Auth class as mentioned in the doc
<?php
namespace Application\Model;
use Zend\Authentication\Adapter\AdapterInterface;
use Zend\Authentication\Adapter\DbTable as AuthAdapter;
class Myauth implements AdapterInterface {
/**
* Sets username and password for authentication
*
* #return void
*/
public function __construct($username, $password) {
// Configure the instance with constructor parameters...
$authAdapter = new AuthAdapter($dbAdapter,
'users',
'username',
'password'
);
$authAdapter
->setTableName('users')
->setIdentityColumn('username')
->setCredentialColumn('password');
$result = $authAdapter->authenticate();
if (!$result->isValid()) {
// Authentication failed; print the reasons why
foreach ($result->getMessages() as $message) {
echo "$message\n";
}
} else {
// Authentication succeeded
// $result->getIdentity() === $username
}
}
}
Issue1 : How to get $dbAdapter here?
Issue2 : Is this correct way to create auth module?
I have a couple things to say:
1. About Database Adapter
This link shows you how to configure database adapter.
In config/autoload/global.php:
return array(
'db' => array(
'driver' => 'Pdo',
'dsn' => 'mysql:dbname=zf2tutorial;host=localhost',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
),
),
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter'
=> 'Zend\Db\Adapter\AdapterServiceFactory',
),
),
);
In config/autoload/local.php:
return array(
'db' => array(
'username' => 'YOUR USERNAME HERE',
'password' => 'YOUR PASSWORD HERE',
),
)
Now, from ServiceLocatorAware classes, you can get Database Adapter as
$dbAdapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
2. About Creating Authentication
Dude, why reinvent the square wheel? As mentioned here, ZfcUser is built to be a foundation for a very large percentage for Zend Framework 2 applications.
Nearly anything is customizable as mentioned here. A lot of modules are available such as ScnSocialAuth which have dependancy on ZfcUser and are really awesome.
As in ZF 2, Models are not ServiceLocatorAware classes, so you cannot use the solution in Ojjwal Ojha's answer.
You can:
1. Get dbAdapter in your Controller by calling:
$dbAdapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');
Pass dbAdapter to your Model when you create it:
$model = new Model($dbAdapter);
Write an init function in your Model.

ZF2 Zend\Log + Doctrine2

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.

zf2 ServiceManagerAwareInterface in Fieldset

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').

Categories