I was reading Zend 3 documentation on Service Manager and i got this problem.
In documentation it says that if we have some DI in our controller we should update module.config.php file and add controllers key and invoke controller not with InvokableFactory::class but with custom factory class and add another key service_manager that contains array of classes that my first controller uses.
Ok so i do that:
module.config.php
'service_manager' => [
'factories' => [
Controller\Controller2::class => Factory\Controller2Factory::class,
Controller\Controller3::class => Factory\Controller3Factory::class,
],
],
'controllers' => [
'factories' => [
Controller\Controller1::class => Factory\Controller1Factory::class
],
]
Controller1Factory.php
class Controller1Factory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new Controller1(
$container->get(Controller2::class),
$container->get(Controller3::class),
);
}
}
But now i have error that Controller2 and Controller3 also have DI in their constuctors, so i make new custom factories and so on and so on...until i get to my models.
And Models also have Dependency that is injected in their controller which is zend native \Zend\Db\TableGateway\TableGatewayInterface and i now have to edit my conf file again and add TableGatewayInterface.
And that is wrong. I should never be forced to inject native zend classes and services this way.
So what am i doing wrong?
If your Controller has no dependency, it's the best way to declare it in module.config.php as you did.
But if it has dependecies, it's better to do it in Module.php. You first declare your services, then the controller (don't forget to remove it from module.config.php), injecting in it the services it depends :
public function getServiceConfig()
{
return [
'factories' => [
Model\MyObjectTable::class => function($container) {
$tableGateway = $container->get(Model\MyObjectTableGateway::class);
return new Model\MyObjectTable($tableGateway);
},
Model\MyObjectTableGateway::class => function($container) {
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Model\User());
return new TableGateway('myObject', $dbAdapter, null, $resultSetPrototype);
},
]
];
}
public function getControllerConfig()
{
return [
'factories' => [
Controller\MyObjectController::class => function($container) {
return new Controller\MyObjectController(
$container->get(Model\MyObjectTable::class)
);
},
]
];
}
And in your controller:
private $table;
public function __construct(MyObjectTable $table)
{
$this->table = $table ;
}
It is described in This ZF3 tutorial page and following.
Related
I've a controller Controller\Api\ProductController for rest call and it's defined in module.config.php
'controllers' => [
'factories' => [
Controller\Api\ProductController::class => function($container) {
return new Controller\Api\ProductController(
$container->get(\Commerce\Model\Product::class), $container->get(\Commerce\Controller\Plugin\ProductPlugin::class)
);
}
]
]
In the above code you can see I'm injecting a plugin class \Commerce\Controller\Plugin\ProductPlugin::class which is defined in module.config.php
'controller_plugins' => [
'factories' => [
Controller\Plugin\ProductPlugin::class => InvokableFactory::class,
],
'aliases' => [
'product' => Controller\Plugin\ProductPlugin::class,
]
]
Now when I'm hitting the rest url it shows error message
Unable to resolve service "Commerce\Controller\Plugin\ProductPlugin" to a
factory; are you certain you provided it during configuration?
What I'm missing ?
Plugin code is
<?php
namespace Commerce\Controller\Plugin;
use Zend\Mvc\Controller\Plugin\AbstractPlugin;
class ProductPlugin extends AbstractPlugin
{
//....
}
Controller plugins do not get injected to the controller.
Remove
$container->get(\Commerce\Controller\Plugin\ProductPlugin::class)
from the factory callback and also remove the 2nd parameter from the constructor of your ProductController
To use the plugin, just do:
$plugin = $this->plugin(Plugin\ProductPlugin::class);
or
// using the alias
$plugin = $this->product();
in your action controllers.
https://docs.zendframework.com/zend-mvc/plugins/
Controller plugin example
I'm using a Base controller and I had to inject plugins through dependencies
based on the request.
Above configuration was fine. All I had to do is define my plugin in service_manager section.
'service_manager' => [
'factories' => [
Controller\Plugin\ProductPlugin::class => function($sm) {
$dependencies = /////
$model = new \Commerce\Model\Product($dependencies);
return new Controller\Plugin\ProductPlugin($model);
},
\Commerce\Model\Product::class => function($sm) {
$dependencies = /////
return new \Commerce\Model\Product($dependencies);
}
],
],
I have a ZF3 project and an index controller that accesses a single postgres table. I have the usual factories setup
return array(
'factories' => [
Model\IsdepotstockTable::class => function($container) {
$tableGateway = $container->get(Model\IsdepotstockTableGateway::class);
return new Model\IsdepotstockTable($tableGateway);
},
Model\IsdepotstockTableGateway::class => function ($container) {
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Model\Isdepotstock());
return new TableGateway('isdepotstock', $dbAdapter, null, $resultSetPrototype);
},
],
I also have my controller constructor thus:
public function __construct(IsdepotstockTable $table)
{
$this->isdepotstockTable = $table;
}
My question is, if I wish to access a second table, how do I modify the construct statement to handle multiple tables? Obviously I have to add the factories for the additional table that much I understand.
I've looked through the ZF3 documentation but cannot find any example.
Thanks
I believe you are looking for a controller factory function which will instantiate the controller and pass the arguments to the constructor.
Add your second table class as the 2nd argument in the __construct() method, then create your factory in module.config.php.
<?php
use Zend\ServiceManager\Factory\InvokableFactory;
return [
// ...
'controllers' => [
'factories' => [
Controller\IndexController::class => InvokableFactory::class
// Put other controllers registration here
],
],
// ...
];
Here is a free open source book on ZF3, I link to the controller registration section for your reference. Good luck! https://olegkrivtsov.github.io/using-zend-framework-3-book/html/en/Model_View_Controller/Controller_Registration.html
We are used to work with ZF2, but for our last project, we decided to start with ZF3.
Now I am facing a problem in the form creation.
What I want to do is to create a custom select populated with values retrieved from database.
What I did in ZF2 was creating a class extending a select, with the ServiceLocatorAwareInterface, like:
class ManufacturerSelect extends Select implements ServiceLocatorAwareInterface {
public function init() {
$manufacturerTable = $this->getServiceLocator()->get('Car\Model\ManufacturerTable');
$valueOptions = [];
foreach ($manufacturerTable->fetchAll() as $manufacturer) {
$valueOptions[$manufacturer->getManufacturerId()] = $manufacturer->getName();
}
$this->setValueOptions($valueOptions);
}
public function getServiceLocator() {
return $this->serviceLocator;
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
$this->serviceLocator = $serviceLocator;
}
}
Then, to use it in a form, it was enough to give the full name
$this->add(
array(
'name' => 'manufacturer_id',
'type' => 'Car\Form\Element\ManufacturerSelect'
)
);
Now this is not possible anymore, since the service locator was removed and the use of factories is necessary, but I'm struggling to find how to do the same thing.
Keeping in mind to use factories, I tried this configuration in module.config.php:
'form_elements' => [
'factories' => [
'Car\Form\Element\ManufacturerSelect' => function ($services) {
$manufacturerTable = $services->get('Car\Model\ManufacturerTable');
return new ManufacturerSelect($manufacturerTable);
},
'Car\Form\CarForm' => function ($services) {
$manufacturerTable = $services->get('Car\Model\ManufacturerTable');
return new CarForm($manufacturerTable, 'car-form');
}
]
]
Result: factory of CarForm is always called, but factory of ManufacturerSelect is not.
A simple solution would be to populate the select directly in the form class, but I would prefer to use the factory for the element and reuse it everywhere I want, like I was doing in ZF2.
Does anyone already encountered this problem and found a solution?
Do you add that element in "__construct" function? If so try "init"
EDIT:
First of all you don't need to create a custom select to fill in it via database. Just create a form with factory, fetch data from db in factory and pass to form. And use the data in form class as select's value options.
$this-add([
'type' => Element\Select:.class,
'name' => 'select-element'
'options' => [
'label' => 'The Select',
'empty_option' => 'Please choose one',
'value_options' => $this-dataFromDB
]
]);
If you create form as:
new MyForm();
Form Element Manager doesn't trigger custom elements' factories. But;
$container->get('FormElementManager')->get(MyForm::class);
triggers custom elements' factories. Here's a working example. It's working on ZF3.
Config:
return [
'controllers' => [
'factories' => [
MyController::class => MyControllerFactory::class
]
],
'form_elements' => [
'factories' => [
CustomElement::class => CustomElementFactory::class,
MyForm::class => MyFormFactory::class,
]
]
];
don't forget to add 'Zend\Form' to application config's 'modules'.
Element:
class CustomElement extends Text
{
}
Element Factory:
class CustomElementFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
echo 'element factory triggered';
return new CustomElement();
}
}
Fieldset/Form:
class MyForm extends Form
{
public function init()
{
$this
->add([
'type' => CustomElement::class,
'name' => 'name',
'options' => [
'label' => 'label',
],
])
;
}
}
Fieldset/Form Factory:
class MyFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
echo 'form factory triggered';
return new MyForm();
}
}
Controller's Factory:
class MyControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
echo 'controller factory triggered';
return new MyController(
$container->get('FormElementManager')->get(MyForm::class);
);
}
}
I have a service defined in Module.php, where I inject my mail config, defined in config/autoload/global.php this way:
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
public function getServiceConfig()
{
return [
'factories' => [
'Mailer' => function($container) {
return new MailService($this->getConfig()['mail']);
},
]
];
}
But I want to do it the ZF3 way (wich I'm learning, so I defined my service in my module.config.php this way:
return [
'services' => [
'factories' => [
Service\MailService::class => MailServiceFactory::class
]
],
And my MailServiceFactory.php is:
class MailServiceFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new MailService();
}
}
But how can I retreive my config defined in global.php and inject it in the factory, needed by my service?
OK, after some debug and var_dump(), I have it. I can access the config array thanks to $container->get('configuration'). So my factory is now:
class MailServiceFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('configuration');
return new MailService($config['mail']);
}
}
I'm building my first Zend Framework 2 application with the skeleton tutorial. But whenever I try to call any plugin from any controller, I get the error message:
A plugin by the name "PLUGIN_NAME" was not found in the plugin manager
Zend\Mvc\Controller\PluginManager
Unfortunately I don't know which part of the code could help you to help me. I post some files which I think could be important.
config/modules.config
return [
'Zend\Router',
'Zend\Validator',
'Zend\Form',
'Album',
'Application',
'User',
];
module/User/src/Module.php
<?php
namespace User;
use User\Model\User;
use User\Model\UserTable;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\TableGateway;
class Module
{
const VERSION = '1.0.0dev';
public function getAutoloaderConfig()
{
return array(
'Zend\Loader\ClassMapAutoloader' => array(
__DIR__ . '/autoload_classmap.php',
),
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
public function getServiceConfig()
{
return array(
'factories' => array(
'User\Model\UserTable' => function($sm) {
$tableGateway = $sm->get('UserTableGateway');
$table = new UserTable($tableGateway);
return $table;
},
'UserTableGateway' => function ($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new User());
return new TableGateway('user', $dbAdapter, null, $resultSetPrototype);
},
),
);
}
}
module/User/config/module.config.php
<?php
namespace User;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\Router\Http\Segment;
return [
'controllers' => [
'factories' => [
Controller\UserController::class => InvokableFactory::class,
],
'invokables' => [
'User\Controller\User' => 'User\Controller\UserController',
],
],
'router' => [
'routes' => [
'user' => [
'type' => Segment::class,
'options' => [
'route' => '/user[/:action[/:id]]',
'constraints' => [
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
'id' => '[0-9]+',
],
'defaults' => [
'controller' => Controller\UserController::class,
'action' => 'index',
],
],
],
],
],
'view_manager' => [
'template_path_stack' => [
'user' => __DIR__ . '/../view',
],
],
];
I'm trying to call my plugins like this from a controller action:
$this->flashMessanger()->...
$this->identity();
$this->getServiceLocator();
Because I really needed the Service Locator, I found a workaround, which is not so nice I think, but works for me:
$sm = $this->getEvent()->getApplication()->getServiceManager();
But I guess something is wrong here.
Edit:
For a better reproduction of what I did (I installed Zend Framework again and it still gives me the same error):
Installed Zend Framework (2.4 I guess?) by this installation guide (https://framework.zend.com/manual/2.4/en/ref/installation.html). I installed it with the following command:
composer create-project -sdev --repository-url="https://packages.zendframework.com" zendframework/skeleton-application
On the installation, when I was asked if I want to install a minimum install, I chose "No". Every next question I answered with "Yes" (the installer asks to install a lot of modules, I installed them all).
The installer asked in which config I want to inject "ZendDeveloperTools". I answered 2 (config/development.config.php.dist). For all others I chose 1 (config/modules.config.php).
I tested the skeleton application by calling the url in my browser, it worked.
I downloaded the Album module from GitHub here (https://github.com/Hounddog/Album).
I copied the album module to my modules folder.
I added the entry 'Album' to my config/modules.config.php file
I browsed to the album page
I'm getting the error:
A plugin by the name "getServiceLocator" was not found in the plugin
manager Zend\Mvc\Controller\PluginManager
I assume the reason for this error is the getAlbumTable() method in the AlbumController
public function getAlbumTable()
{
if (!$this->albumTable) {
$sm = $this->getServiceLocator();
$this->albumTable = $sm->get('Album\Model\AlbumTable');
}
return $this->albumTable;
}
In the ZF2 docs for controller plugins you can read that your controller has to implement the following methods: setPluginManager, getPluginManager and plugin. You can also read:
For an extra layer of convenience, both AbstractActionController and AbstractActionController have __call() implementations that allow you to retrieve plugins via method calls:
$plugin = $this->url();
Does your controller extend AbstractActionController or AbstractActionController? If yes, it should work as mentioned in the docs (so those methods you mention in your question should work).
Since you didn't share any controller code it is hard to say whether this is the problem...
UPDATE
The error you get is not related to the configuration of your ControllerPluginManager, but you get this error because you are doing:
$sm = $this->getServiceLocator();
Since the method getServiceLocator doesn't exist the magic __call() method is executed and this leads to the error.
This is because in the latest versions of ZF2 the controller classes are no longer 'service locator aware' meaning you cannot retrieve the ServiceManager by calling $this->getServiceLocator().
Instead you will have to inject your Album\Model\AlbumTable service into the controller class inside a factory:
1) Add a constructor method to your controller class:
public function __construct(AlbumTable $albumTable){
$this->albumTable = $albumTable;
}
2) Create a factory for your controller:
<?php
namespace Album\Controller\Factory;
use Album\Controller\AlbumController;
class AlbumControllerFactory implements FactoryInterface
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return AlbumController
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
$controllerPluginManager = $serviceLocator;
$serviceManager = $controllerPluginManager->get('ServiceManager');
$albumTable = $serviceManager->get('Album\Model\AlbumTable');
return new AlbumController($albumTable);
}
}
3) Register your controller factory inside your module.config.php:
'factories' => [
`Album\Controller\AlbumController` => `Album\Controller\Factory\AlbumControllerFactory`,
],
The function getAlbumTable() was used in ZF2 tutorial but in ZF3 it was replaced by member table.
Change the deleteAction() function in AlbumController.php to read:
public function deleteAction()
{
$id = (int) $this->params()->fromRoute('id', 0);
if (!$id) {
return $this->redirect()->toRoute('album');
}
$request = $this->getRequest();
if ($request->isPost()) {
$del = $request->getPost('del', 'No');
if ($del == 'Yes') {
$id = (int) $request->getPost('id');
$this->table->deleteAlbum($id);
}
// Redirect to list of albums
return $this->redirect()->toRoute('album');
}
return array(
'id' => $id,
'album' => $this->table->getAlbum($id)
);
}
Note the two lines reading $this->table->... instead of $this->getAlbumTable()->...