I am working on a project where I am trying to implement a simple dependency injection. I implemented the class below, but am not sure if it's the best practice or should I go with some of the DI Containers around?
The Container Class
namespace App\Includes;
class Container {
private $class=[];
protected $config = [
'user' => 'App\Models\User',
'template' => 'Library\Template',
'database' => 'Library\Database'
];
public function getClass($name) {
if (!isset($this->class[$name])) {
$this->class[$name] = new $this->config[$name]();
}
return $this->class[$name];
}
}
The Controller
namespace App;
use App\Includes\Container as Container;
class Controller{
protected $template;
protected $database;
public function __construct(Container $container){
$this->database = $container->getClass('database');
$this->template = $container->getClass('template');
}
}
Here I initiate the app (The $controller is supplied by the router)
$container = new App\Includes\Container();
$obj = new $controller($container);
Related
I want to create a simple class that will send a predefined email to given email address. So I created the following class:
namespace Custom;
use Zend\Mail\Message;
use Zend\Mail\Transport\Smtp as SmtpTransport;
use Zend\Mail\Transport\SmtpOptions;
class Notification
{
public function sendEmailNotification($to)
{
try {
$message = new Message();
$message->addTo($to)
->addFrom('test#example.com')
->setSubject('Hello')
->setBody('Predefined email body');
$transport = new SmtpTransport();
$options = new SmtpOptions(array(
'name' => 'smtp.example.com',
'host' => 'smtp.example.com',
'port' => '587',
'connection_class' => 'plain',
'connection_config' => array(
'username' => 'test#example.com',
'password' => 'somepasswd',
'ssl' => 'tls',
),
));
$transport->setOptions($options);
$transport->send($message);
} catch (\Exception $e) {
echo $e->getMessage();
}
}
}
From controller, the email is then sent with:
$notification = new \Custom\Notification();
$notification->sendEmailNotification('example#example.com');
This works as intended.
Next thing that I wanted to do is to move mail server configuration parameters into project configuration file (local.php). Problem is - how can I get the configuration parameters in my \Custom\Notification class (which is not a controller)?
Solutions I have found so far seem too complicated to a beginner like me. To do something like $config = $this->getServiceLocator()->get('Config'); you have to do some kind of magic all around the project.
Is there a simple way to get data from configuration file in a custom class?
You have to use injection for your purpose. Just create a factory for your controller, in which you inject the config to your controller.
namespace Application\Controller\Service;
class YourControllerFactory
{
public function __invoke(ContainerInterface $container)
{
$serviceLocator = $container->getServiceLocator();
$config = $serviceLocator->get('config');
$controller = new YourController($config);
return $controller;
}
}
For this purpose your controller needs a constructor which takes the config as parameter.
namespace Application\Controller;
class YourController extends AbstractActionController
{
protected $config;
public function __construct($config)
{
$this->config = $config;
}
public function indexAction()
{
// inject your notification class with the config
$notification = new \Custom\Notification($this->config);
$notification->sendEmailNotification('example#example.com');
}
}
Therefore your notification class needs a constructor which takes the config as parameter.
Other approaches
Another way solving your issue can be the registration of your notification class as a service. Just create a factory for your notification class, in which you create all the needed stuff and then just inject it to your notification class.
namespace Application\Mail;
class Notification
{
protected $config;
public function __construct($config)
{
$this->config = $config;
}
public function sendEmailNotification($to)
{
}
}
The factory itself is simple as pie, because we 've seen nearly the same approach in the controller factory.
namespace Application\Mail\Service;
class NotificationFactory
{
public function __invoke(ContainerInterface $container)
{
$config = $container->get('config');
$notification = new Notification($config);
return $notification;
}
}
Now you just have to note it in your module.config.php file in the service manager section.
'service_manager' => [
'factories' => [
Notification::class => NotificationFactory::class,
],
],
From now on you can access the notification class with the service container of zend framework 2. Remember the factory for your controller instance shown above? Instead of injecting the config to your controller, just inject it with the notification itself. Your notification class will be created with the needed config over the notification factory.
namespace Application\Controller\Service;
class YourControllerFactory
{
public function __invoke(ContainerInterface $container)
{
$serviceLocator = $container->getServiceLocator();
$notification = $serviceLocator->get(Notification::class);
return new YourController($notification);
}
}
Have fun. ;)
I use Zend Expressive framework via default ZE skeleton app with Zend ServiceManager as DIC and Plates as template engine.
Let's say I've got index.phtml template. I want to get some service, which dumps me assets, smth like:
<?= $this->getContainer()->get('my service class')->dumpAssets() ?>
Service is registered via factory and accesible in the app:
<? $container->get('my service class') ?>
How to pass external service instance or its result into template?
It's pretty much bad practice to inject the entire service container into a template (or any other class except a factory). A better approach would be to write an extension to dump the assets.
Extension class:
<?php
namespace App\Container;
use League\Plates\Engine;
use League\Plates\Extension\ExtensionInterface;
use App\Service\AssetsService;
class DumpAssetsExtension implements ExtensionInterface
{
public $assetsService;
/**
* AssetsExtension constructor.
* #param $container
*/
public function __construct(AssetsService $assetsService)
{
$this->assetsService = $assetsService;
}
public function register(Engine $engine)
{
$engine->registerFunction('dumpAssets', [$this, 'dumpAssets']);
}
public function dumpAssets()
{
return $this->assetsService->dumpAssets();
}
}
Factory:
<?php
namespace App\Container;
use Interop\Container\ContainerInterface;
class DumpAssetsFactory
{
public function __invoke(ContainerInterface $container)
{
$assetsService = $container->get(App\Service\AssetsService::class);
return new PlatesExtension($assetsService);
}
}
Configuration:
<?php
return [
// ...
'factories' => [
App\Container\DumpAssetsExtension::class => App\Container\DumpAssetsFactory::class,
]
];
In your template:
<?php
$service = $this->dumpAssets();
?>
I figured out how to access container from template engine via extensions. It's not clear MVC-ly, but...
At first, add plates config into config/autoload/templates.global:
return [
// some othe settings
'plates' => [
'extensions' => [
App\Container\PlatesExtension::class,
],
],
],
At second, create App\Container\PlatesExtension.php with code:
<?php
namespace App\Container;
use League\Plates\Engine;
use League\Plates\Extension\ExtensionInterface;
class PlatesExtension implements ExtensionInterface
{
public $container;
/**
* AssetsExtension constructor.
* #param $container
*/
public function __construct($container)
{
$this->container = $container;
}
public function register(Engine $engine)
{
$engine->registerFunction('container', [$this, 'getContainer']);
}
public function getContainer()
{
return $this->container;
}
}
At third, create factory App\Container\PlatesExtensionFactory.php to inject container into plates extension:
<?php
namespace App\Container;
use Interop\Container\ContainerInterface;
class PlatesExtensionFactory
{
public function __invoke(ContainerInterface $container)
{
return new PlatesExtension($container);
}
}
Next, register plates extension in ServiceManager (config/dependencies.global.php):
return [
// some other settings
'factories' => [
App\Container\PlatesExtension::class => App\Container\PlatesExtensionFactory::class,
]
];
At last, get container and needed service from Plates template:
<?
$service = $this->container()->get('my service class');
?>
I have a PHP class that has a constructor which takes arguments:
ex:
Users.php
namespace Forms;
class Users
{
protected $userName;
protected $userProperties = array();
public function __construct($userName, array $userProperties = null)
{
$this->userName = $userName;
$this->userProperties = $userProperties;
}
public function sayHello()
{
return 'Hello '.$this->userName;
}
}
Now, I am trying to use this class in a Model file like this:
$form = new Forms\Users( 'frmUserForm', array(
'method' => 'post',
'action' => '/dosomething',
'tableWidth' => '800px'
) );
It works just fine. However, in order to write Unit tests, I need to refactor this to a Service Factory, so I can mock it.
So, my Service factory now looks like this:
public function getServiceConfig()
{
return array(
'initializers' => array(
function ($instance, $sm)
{
if ( $instance instanceof ConfigAwareInterface )
{
$config = $sm->get( 'Config' );
$instance->setConfig( $config[ 'appsettings' ] );
}
}
),
'factories' => array(
'Forms\Users' => function ($sm )
{
$users = new \Forms\Users();
return $users;
},
)
);
}
With this refactoring in place, I have two questions:
How do I use the Forms\Users Service in the Model File, considering ServiceLocator is not available in a model file?
How can I change the Service Factory instance to take arguments for the constructor while instantiating Users class in the model.
I faced similar issue some time. Then I decide not to pass arguments to Factory itself. But build setter methods for handling this like.
namespace Forms;
class Users
{
protected $userName;
protected $userProperties = array();
public function setUserName($userName)
{
$this->userName = $userName;
}
public function setUserProperties($userProperties)
{
$this->userProperties = $userProperties;
}
public function sayHello()
{
return 'Hello '.$this->userName;
}
}
You can implement your model ServiceLocatorAwareInterface interface Then it would can call any service like below.
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class MyModel implements ServiceLocatorAwareInterface
{
protected $service_manager;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->service_manager = $serviceLocator;
}
public function getServiceLocator()
{
return $this->service_manager;
}
public function doTask($name, $properties)
{
$obj = $this->getServiceLocator('Forms\Users');
$obj->setUserName($name);
$obj->setUserProperties($properties);
}
}
What I really dislike in ZF2 is that Controller is aware of storage engine (This is a clear violation of SRP) and that a storage engine has a concept of Tables. I believe that this is not correct way, and Controller should only be aware of services (while only services should be aware of Storage engine)
class AlbumController extends AbstractActionController
{
protected $albumTable;
public function getAlbumTable()
{
if (!$this->albumTable) {
$sm = $this->getServiceLocator();
$this->albumTable = $sm->get('Album\Model\AlbumTable');
}
return $this->albumTable;
}
Nowhere in manual I could find on how to put that into a Service and make controller only aware of actions. How would you put that into a service?
I know that's how it's done in the official tutorial, but in my opinion it's not the best approach. Instead you want to inject your dependencies into your controller class via. its constructor. This makes it easier to see what's going on, and easier to test.
To do this, modify your controller class to add an appropriate constructor:
class AlbumController extends AbstractActionController
{
protected $albumTable;
public function __construct(AlbumTable $albumTable)
{
$this->albumTable = $albumTable;
}
}
Then, remove the invokable line in your module.config.php for this controller, since it can no longer just be instantiated without any arguments. Instead, you define a factory to tell ZF how to instantiate the class. In your Module.php:
use Zend\Mvc\Controller\ControllerManager;
use Album\Controller\AlbumController;
class Module
{
public function getControllerConfig()
{
return array(
'factories' => array(
'Album\Controller\Album' => function(ControllerManager $cm) {
$sm = $cm->getServiceLocator();
$albumTable = $sm->get('Album\Model\AlbumTable');
$controller = new AlbumController($albumTable);
return $controller;
},
),
);
}
}
(Alternatively you could create a separate factory class to do this.)
In your controller actions you can then access the album table via. $this->albumTable instead of $this->getAlbumTable().
Hopefully you can see that this approach can easily be modified to inject a service class instead. If you want your album table injected into the service, and the service injected into the controller; you might end up with something like this:
class Module
{
public function getServiceConfig()
{
return array(
'factories' => array(
'Album\Model\AlbumTable' => function($sm) {
$tableGateway = $sm->get('AlbumTableGateway');
$table = new AlbumTable($tableGateway);
return $table;
},
'AlbumTableGateway' => function($sm) {
[etc...]
},
'Album\Service\AlbumService' => function($sm) {
$albumTable = $sm->get('Album\Model\AlbumTable');
return new AlbumService($albumTable);
}
),
);
}
public function getControllerConfig()
{
return array(
'factories' => array(
'Album\Controller\Album' => function(ControllerManager $cm) {
$sm = $cm->getServiceLocator();
$albumService = $sm->get('Album\Service\AlbumService');
$controller = new AlbumController($albumService);
return $controller;
},
),
);
}
}
Controller:
class AlbumController extends AbstractActionController
{
protected $albumService;
public function __construct(AlbumService $albumService)
{
$this->albumService = $albumService;
}
public function someAction()
{
// do stuff with $this->albumService
}
}
In two words: I need to get access to the service manager (locator) from external class.
Details:
I have next structure in my ZF2 project:
Api.php is the class, I use in SOAP server, which is created in Controller:
class IncomingInterfaceController extends AbstractActionController
{
...
public function indexAction()
{
if (isset($_GET['wsdl']))
$this->handleWSDL();
else
$this->handleSOAP();
return $this->getResponse();
}
private function handleWSDL()
{
$autodiscover = new AutoDiscover();
$autodiscover->setClass('\Application\Api\Api')->setUri($this->getURI());
$autodiscover->handle();
}
In this Api.php class I need to get access to services.
I need something like this in my Api.php class:
public function OnNewDeal($uid)
{
$error_log=$this->getServiceLocator()->get('error_log'); // this doesn't work!
$error_log->write_error('error_text');
}
In Module.php
public function getServiceConfig() {
return array(
'invokables' => array(
'Application\Api\Api' => 'Application\Api\Api'
)
);
}
In Api.php
class Api implements ServiceLocatorAwareInterface{
protected $services;
public function OnNewDeal($uid){
$this->getServiceLocator()->get('error_log')->write_error('SOAP ERROR');
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator){
$this->services = $serviceLocator;
}
public function getServiceLocator(){
return $this->services;
}
}
In IncomingInterfaceController.php
class IncomingInterfaceController extends AbstractActionController{
...
protected $api;
public function indexAction()
{
if (isset($_GET['wsdl']))
$this->handleWSDL();
else
$this->handleSOAP();
return $this->getResponse();
}
private function handleWSDL()
{
$autodiscover = new AutoDiscover();
$autodiscover->setClass('\Application\Api\Api')->setUri($this->getURI());
$autodiscover->handle();
}
public getApi(){
if(!$api){
$this->api = $this->getServiceLocator()->get('Application\Api\Api');
}
return $this->api;
}
In controller where you do $this->handleSOAP(); use setObject with already created instance instead setClass.
You should pass into Api __construct $this->getServiceLocator() and handle it there.
class IncomingInterfaceController extends AbstractActionController
{
private function handleSOAP()
{
$soap = new Server(null, array('wsdl'=>$this->getWSDLURI()));
$soapClass = new \Application\Api\Api($this->getServiceLocator());
$soap->setObject($soapClass);
$soap->handle();
}
In Api class, handle serviceManager instance and use as you wish:
class Api
{
protected $serviceManager;
public function __construct($serviceManager)
{
$this->serviceManager = $serviceManager;
}
public function OnNewDeal($uid)
{
$this->serviceManager->get('error_log')->write_error('SOAP ERROR');
}
....
}
Perhaps your API could implement ServiceLocatorAwareInterface like:
class Api implements ServiceLocatorAwareInterface
and add
class Api implements ServiceLocatorAwareInterface
{
protected $serviceManager;
}
Then the service manager would be available
UPDATED
module.config.php example
<?php
return array(
'service_manager' => array(
'factories' => array(
'Api' => 'Namespace\Api'
),
'shared' => array(
'Api' => false
)
),
)
?>
Injecting the Service Manager instance to an user defined "service locator aware class" should responsibility of the framework's itself (via initializers, invokables or user defined factories) not a specific controller's handleSOAP() method.
Yes, #SirJ's solution will work too but that's not a good practice. ZF2 provides ready-to-use Traits and Interfaces exactly for requirements like this. Just use them!
Your very own API class should seem like this:
<?php
namespace Application\Api;
use Zend\ServiceManager\ServiceLocatorInterface;
class Api implements ServiceLocatorInterface
{
// Here is the trait. (php >= 5.4)
use \Zend\ServiceManager\ServiceLocatorAwareTrait;
public function OnNewDeal($uid)
{
$this->getServiceLocator()->get('error_log')->write_error('SOAP ERROR');
}
}
And you should add this key to your module.config.php
<?php
return array(
'service_manager' => array(
'invokables' => array(
'api-service' => 'Application\Api\Api',
)
);
Thats all! Now you can:
<?php
...
$soap = new Server(null, array('wsdl'=>$this->getWSDLURI()));
$soapClass = $this->getServiceLocator()->get('api-service');
$soap->setObject($soapClass);
...