I'm a little confused on how DI works with ZF2. I've spent the last couple of days trying to get my head around it. While I have made some progress a lot of it still baffles me...
Using this (http://akrabat.com/getting-started-with-zend-framework-2/) tutorial I managed to get a grasp that the following:
'di' => array('instance' => array(
'alias' => array(
'album' => 'Album\Controller\AlbumController',
),
'Album\Controller\AlbumController' => array(
'parameters' => array(
'albums' => 'Album\Model\Albums',
),
),
works because in our Album Controller class we have a setAlbum function. So when the DI class will call that setAlbums function and pass it the 'Album\Model\Albums' class.
Fine get that no problem..
Now let's look at this (which comes in the skeleton application off the zend site)
'Zend\View\HelperLoader' => array(
'parameters' => array(
'map' => array(
'url' => 'Application\View\Helper\Url',
),
),
),
Now i would expect there within the Zend\View\HelperLoader (or an inherited class) would contain a setMap() function that the DI class would pass an array. But this appears not to be the case. As I cannot find a setMap anywhere.
My question is first what am I not understanding about the way DI works with the ZF2... But also what does the code above (about zend\view\helper) actually do. I mean what does injecting 'map' => array('url' => 'Application\View\Helper\Url') into the Zend\View\HelperLoader actually do?
Thanks for any help anyone can give. I appreciate it's a beta framework and what answers I may get now not apply in a months time. But this all seems pretty fundamental and i'm just no getting it!
The DI configuration of ZF2 works indeed with the names of the arguments in the signature. It does not matter if this is done with a constructor or a explicit setter. The setter must, however, start with "set" to be recognized by Zend\Di\Di.
So if you have a class like this:
<?php
namespace Foo;
class Bar
{
public function __construct ($baz) {}
public function setSomethingElse ($bat) {}
}
You can inject both a $baz and a $bat:
'di' => array(
'instance' => array(
'Foo\Bar' => array(
'parameters' => array(
'baz' => 'Something\Here',
'bat' => 'Something\There',
),
),
),
)
For Zend\Di it does not matter what the function name exactly is, as long as it starts with "set" and the name of the argument is correct. That is why Foo\Bar::setSomethingElse($bat) works just like Foo\Bar::setBat($bat).
Just make sure you name your arguments correctly. For example, it is easy to do something like this:
<?php
namespace Foo;
class Bar
{
public function setCacheForBar ($cache) {}
public function setCacheForBaz ($cache) {}
}
But that will not work nicely together with Zend\Di.
Related
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.
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').
I have been trying to configure our Module.php to use the Module Manager Listeners for configuration (i.e interfaces that are available under Zend\ModuleManager\Feature\*). Specifically, I want to be able to configure the routes of my module outside of the main module.config.php. I have not been able to find any actual examples of this.
What I have found, if I have read the documentation correctly, is that the method getRouteConfig() should merge in my routes into the array provided by getConfig()?
Module.php
class Module implements Feature\RouteProviderInterface
{
//...
public function getRouteConfig()
{
return include __DIR__ . '/config/route.config.php';
}
//...
}
/config/route.config.php
return array(
'route_manager' => array(
'router' => array (
'routes' => array(
//.. routes that were working correctly when added to module.config.php
),
),
),
);
I can see the array returned via getRouteConfig() so I know the method is being called correctly.
Perhaps I am misunderstanding the purpose of the above interface, or I have not provided the correct "key" (route_manager) for this to be merged correctly, as I'm getting 404 for my routes.
Any help would be appreciated!
I haven't done this in the way you mentioned yet, but the key route_manager is not required within the getRouteConfig() Method.
This is due to the fact that all of the get{$specificManager}Config()-Methods are called directly from their respective Manager-Classes. Therefore the initial key is not required. Using another terminology, when using getRouteConfig() you are already in the scope of route_manager. Same as when you use getServiceConfig() you're already in the scope of service_manager. However getConfig() is within the application-scope and therefore accessing configuration of application-parts, you need to address tose specificaly.
One thing to note is: the configuration of getConfig() can be cached to increase performance, whereas all the other get{$specificManager}Config() methods are not. Especially in the case of the RouteConfiguration I'd highly suggest to use the getConfig()-Method for your RouteConfig.
If you really need to separate the configuration, then I'd suggest the way that #Hendriq displayed for you.
Well I have it working but I only use the getConfig(). What is do is I use an array_merge in the getConfig().
public function getConfig()
{
return array_merge(
require_once 'path_to_config/module.config.php',
require_once 'path_to_config/routes.config.php'
);
}
My router.config.php looks then like:
return [
'router' => [
'routes' => [
// routes
]
]
];
This way I also got some other config files seperated (ACL).
Edit
Thanks to the article Understanding ZF2-Configuration, I got an idea. I think your array should not be:
return array(
'route_manager' => array(
'router' => array (
'routes' => array(
//.. routes that were working correctly when added to module.config.php
)
)
)
);
but rather be
return array(
'router' => array (
'routes' => array(
//.. routes that were working correctly when added to module.config.php
),
),
);
The getRouteConfig is similar to the other providers it is there so you're able to create some custom routes. I guess what you're trying to do is most appropiate through hendriq's method.
An example of getRouteConfigcan be found at http://zf2cheatsheet.com/
public function getRouteConfig()
{
return array(
'factories' => array(
'pageRoute' => function ($routePluginManager) {
$locator = $routePluginManager->getServiceLocator();
$params = array('defaults' => array('controller' => 'routeTest','action' => 'page','id' => 'pages'));
$route = Route\PageRoute::factory($params);
$route->setServiceManager($locator);
return $route;
},
),
);
}
In our Module\Route namespace we create the class PageRoute which implements Zend\Mvc\Http\RouteInterface and, in our specific case for the example, Zend\ServiceManager\ServiceManagerAwareInterface. Now just implement the functions of the interface... In the sample he uses Doctrine to load the pages from the database.
Finally we can add our new custom route to our module.config.php so it can be used:
'page' => array(
'type' => 'pageRoute',
),
As you can see in this last step we go back to Hendriq's solution as the intended use is not to load the routes into the router, but creating custom routes.
Hope this helps
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
I can't get Zend to autoload a custom form element class. I did things exactly as Marcin describes here (except that my classes start with 'Zend' and not 'my' but I'm getting this error:
Warning: include_once(Zend\Form\Element\Div.php) [function.include-once]: failed to open stream: No such file or directory
I have Zend_Form_Element_Div inside forms\elements\ and Zend_View_Helper_FormDiv inside views\helpers\
Basically, every folder in the error message is missng an 's', the right path is Zend\Forms\Elements\Div.php
I also have this in my bootstrap, though I'm not sure if it's necessary, but I'm also using this for my forms and models folder (and some others, but I don't think there's need to post them all):
<?php
$resourceLoader->addResourceTypes(array(
'model' => array(
'namespace' => 'Model',
'path' => 'models'
),
'element' => array(
'namespace' => 'Element',
'path' => 'elements'
),
'form' => array(
'namespace' => 'Form',
'path' => 'forms'
)
));
?>
(Is there actually any other way of doing this autoloading? Instead of declaring every single folder?)
Update:
Element_Div in application/forms/elements/Div.php
In my forms init() method: $this->addElementPrefixPath('Element_', APPLICATION_PATH . '/forms/elements');
Error I'm getting: Fatal error: Class 'Element_Div' not found in C:\xampplite\htdocs\code\application\forms\PostForm.php on line 63
You essentially have to tell the form where to find custom elements by using:
$form->addElementPrefixPath()
In your case, you would use - either within the form's init() or __construct() method - something like:
$this->addElementPrefixPath('Zend_Form_Element_', APPLICATION_PATH . '/elements);;
However, I have agree with #Marcin. Naming your own classes with the Zend_ pseudo-namespace is ill-advised. Either:
Decide on an application namespace and declare it in your Bootstrap when you create your $resourceLoader
Create an custom library that resides on your include path - probably at the same level as the Zend library - and put your custom stuff out there.
Let me know if you need more details on either of these suggestions and I'll fatten up the explanations a bit.
Update based on comments
Using an empty appnamespace, your call to addElementPrefixPath() now changes to:
$this->addElementPrefixPath('Element_', APPLICATION_PATH . '/elements);
And I guess you could remove the elements entry from the $resourceLoader definition in your Bootstrap since it's really not doing anything.
Update 2
I assumed that you were adding the element to the form using the shortname, something like:
$form->addElement('div', 'my_div');
In this circumstance, we need to tell the $form and its plugin registry where to find an element of type 'div'. That's why we dealt with $form->addElementPrefixPath().
However, from the error message you are reporting, it appears that you are adding your custom element to the form using something like:
$div = new Element_Div();
$form->addElement($div, 'my_div');
In this case, it is not the $form and its plugin registry that has to worry about finding/loading/instantiating the custom element; it is the $autoloader via its $resourceLoader. In that case, there is no need for the $form->addElementPrefixPath(), which is essentially a hint to the form on how to find custom elements invoked by shortname.
What we need is to configure the $resourceLoader back in Bootstrap so it knows where to find the class. Assuming you stick with empty appnamespace (so your class is named Element_Div) and you place the file in application/forms/elements/Div.php, then the $resourceLoader call is as follows:
$resourceLoader->addResourceTypes(array(
'model' => array(
'namespace' => 'Model_',
'path' => 'models'
),
'element' => array(
'namespace' => 'Element_',
'path' => 'forms/elements'
),
'form' => array(
'namespace' => 'Form_',
'path' => 'forms'
)
));
That should do it. [Famous last words, eh?]
I prefer creating forms like this:
$form->addElement(new My_Form_Element_Whatever(array(
'name' => 'my_element',
'label' => 'My element',
)));
or
$form->addElement($whatever = new My_Form_Element_Whatever(array(
'name' => 'my_element',
'label' => 'My element',
)));
$whatever->removeDecorator('Errors');
when I need to further modify the element.