In my job I am dealing with a legacy app running on ZF2. There is a model which is sending out a variety of different emails to difference addresses. The one thing they have in common is they all need to BCC to one particular address.
At first, in my head I was cursing the previous developer, for foolishly hard coding the email address 20 different times in one file. I assumed that it would be a piece of cake to grab an application config with a simple call $this->config->get('x') (like in Laravel) or something along them lines. Now I find myself feeling bad, because I understand why the previous dev did hard code the email addresses.
So to the question, how the hell do I grab a config item from application.config.php inside the model? I keep reading about how I need to implement the ServiceLocaterAware Interface. Is this really necessary? There must be a way to grab configs easily, surely?!?
How the hell do I grab a config item from application.config.php inside the model?
You shouldn't do so inside, do it 'outside'.
Register your model class as a service in module.config.php.
'service_manager' => [
'factories' => [
'Email\Model\EmailModel' => 'Email\Model\EmailModelFactory',
]
],
Then create the factory Email\Model\EmailModelFactory, this uses the service manager to fetch the 'email' config key and injects it into the model's constructor.
namespace Email\Model;
use Email\Model\EmailModel;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class EmailModelFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
return new EmailModel($this->getEmailOptions($serviceLocator));
}
// Return the email options
public function getEmailOptions(ServiceLocatorInterface $serviceLocator)
{
$options = $serviceLocator->get('config');
return $options['email'];
}
}
The issue you will now have is all calls to your model classes will have to use $serviceManager->get('Email\Model\EmailModel') (rather than new \Email\Model\EmailModel) in order for the configuration to be injected. Even without seeing any of your legacy application my guess is that this would be difficult.
The model should not be responsible for sending emails; you could replace it with an service class, e.g. 'EmailService' and repeat the injection example above just for this class.
EmailService::send(EmailModel $email, array $options);
This would keep your model independent and there would be no need to replace the calls to new Model etc.
You need the service locator / service manager
Within your controller:
public function xxxAction()
{
$sm = $this->getServiceLocator();
$config = $sm->get('config');
}
Related
I'm creating an interface to allow switching between various direct debit providers.
I've hit a stumbling block with the way that they / SOAP APIs require the updating of a record, they require you to pass in the whole object rather than just update a single value.
The issue here is that the different API's use different names for their fields, take the following example:
$directDebitInterface->updateAccount($reference, [
'AccountName' => 'NEW ACCOUNT NAME',
]);
Now, the current direct debit provider uses 'AccountName' but another references 'AccountHolderName'.
If I'm passing in the field to be updated but I don't know which provider I'll be interfacing to, how do I know which field to map this to?
Is there a technique that maps the field names for each provider?
My first thought is to use constants in each provider's class that inherits the interface (what is the name for that btw?)
E.g.
Provider 1
const ACCOUNT_NAME = 'AccountHolderName';
public function update($newValue)
{
// logic to post, pseudo array
[
self::ACCOUNT_NAME => $newValue
]
}
My issue now comes down to how to pass in the correct field as it ends up the same issue, I'll need to create some sort of mapping between what I call a field and how it is on the provider's end.
Thanks,
One way to do it is to use an interface throughout your code and let Laravel bind a specific implementation to use. The basic steps are,
Create an interface
Create an abstract class / Implement interface
Extend the abstract class
Bind to a specific implementation
Use the interface
Create an interface
Create an interface for each debit provider. You can probably think of a better name but I used BankInterface.
interface BankInterface
{
public function updateAccount(): void;
public function createPayload(): array;
}
Create an abstract class / Implement interface
The abstract class implements the interface. Since it is abstract you know that there must be child classes that further extend it.
abstract class Bank implements BankInterface
{
protected $account = '';
protected $amount = 0;
public function updateAccount(): void
{
// #todo implement the update
}
}
Extend the abstract class
This is where each one "knows" about what property names to use.
class BankA extends Bank
{
public function createPayload(): array
{
return [
'AccountName' => $this->account,
'Amount' => $this->amount,
];
}
}
class BankB extends Bank
{
public function createPayload(): array
{
return [
'AccountHolderName' => $this->account,
'AmountToDebit' => $this->amount,
];
}
}
class BankC extends Bank
{
public function createPayload(): array
{
return [
'Identifier' => $this->account,
'AmountInCents' => $this->amount * 100,
];
}
}
Bind to a specific implementation
Create a new entry in your .env file. This will control which specific implementation you are using.
DIRECT_DEBIT_PROVIDER=BankB
Now, bind to a specific implementation based on a value in your .env file. You are free to use any method to determine which implementation to use. You are not restricted to an env file.
class DirectDebitProvider extends ServiceProvider
{
public function register()
{
$provider = match(env('DIRECT_DEBIT_PROVIDER')) {
'BankA' => BankA::class,
'BankB' => BankB::class,
'BankC' => BankC::class,
default => BankA::class,
};
$this->app->bind(BankInterface::class, $provider);
}
}
Register DirectDebitProvider in config/app.php
...
'providers' => [
...,
App\Providers\DirectDebitProvider ::class,
...,
],
...
Use the interface
Finally, use, by type-hinting, BankInterface throughout your codebase. Not entirely sure what $reference refers to here, but you can probably consolidate this whole thing into $bank->update() and let the abstract class construct the payload/reference, but I'll leave that to you.
public function index(BankInterface $bank) {
$bank->updateAccount($reference, $bank->createPayload());
}
You should stick to one name in your code base. The communication to the external entity (direct debit provider) should be in one class per provider, and those classes need to implement an interface so it doesn't matter in the rest of the code which class is actually used.
The connector class (or whatever you want to call it) needs to do the mapping from your naming scheme to the scheme of the external party when sending data, and back when receiving data. The naming scheme of the external party should not leak into the rest of your code.
I have a Class object which is used in many modules in my zend structure :
/module/
--|Aplication
--|MyClassModule
----|config
----|src
------|Factory
------|Model
---------|> MyObjectClass.php
----Module.php
--|AnotherModule
So my idea is to use this MyObjectClass.php in other modules so I can avoid duplication and have its own configuration. So far, for this is ok, however I want to get the variables set from my config/autoload files injected in this class but I don't know how.
How can I load this config data into my class model? Which is the best approach ? I can load it by accessing this directly but I don't think this is very elegant
e.g: $configArray = require './config/autoload/config.local.php';
I am not very experienced with zend so I dont know where to start with. I have seen many tutorials of how to do this via controllers, views.. etc but not in specific classes.
Thank you.
All config files are merged into one config, when your ZF2 application is bootstrapped. That includes local.php, global.php from config/autoload and all used modules' module.config.php. With a bit of more research, you can overwrite the standard loading, e.g. loading custom configs.
After bootstrapping, your are able to access the config from the ServiceManager. There are preserved keys for some ZF2-specific configs, service_manager, etc.
$serviceManager->get('config');
There is a "standard" service pattern in ZF2: Factory. This can be applied for Controllers, Services. What ever you want.
namespace Application\Factory;
use Application\Model\MyObjectClass;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class MyObjectFactory implements FactoryInterface
{
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
* #return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
// get some config parameter, inject it into model
$config = $serviceLocator->get('config');
$myObjectClass = new MyObjectClass();
// ... e.g. $myObjectClass->setConfig($config);
return $myObjectClass;
}
}
It should be clear, what this factory is made for: create and return an instance of your custom object ;) You may configure your instance with some config params. With ServiceLocator as method param, you are able to access the config, other services etc.
Further, you have to register your own service/factory in the factories section of service_manager config in your module's module.config.php:
return array(
'service_manager' => array(
'factories' => array(
'MyObjectFactory' => 'Application\Factory\MyObjectFactory',
),
),
);
Now you should be able to access your factory, e.g. in an ActionController or wherever you have access to ServiceManager. That means, you can also access this factory from different modules.
public function someCustomAction() {
$myObjectClass = $this->getServiceLocator()->get('MyObjectFactory');
$myObjectClass2 = $this->getServiceLocator()->get('MyObjectFactory');
var_dump($myObjectClass);
var_dump($myObjectClass2);
if ($myObjectClass === $myObjectClass2) {
echo '<br />equal';
}
$myObjectClass = new MyObjectClass();
$myObjectClass2 = new MyObjectClass();
var_dump($myObjectClass);
var_dump($myObjectClass2);
}
Note:
Be aware, that ServiceManager returns the same instance of your object. So, that seems like what you ask for? In contrast, creating a new instance will create different objects.
Note 2:
Tested with ZF2 v2.4.9
The latest update of zend-mvc has caused a break in compatibility due to phasing out ServiceLocatorAwareInterface. I use the servicelocator within a controller to dynamically load dependencies, eg:
class IndexController extends AbstractActionController
/**
*
* #return \UserManagement\Form\User\Details
*/
protected function getUserDetailsForm(){
return $this->getFormManager()->get('User\Details');
}
/**
*
* #return FormElementManager
*/
protected function getFormManager(){
return $this->getServiceLocator()->get('FormElementManager');
}
}
This is now raising an exception (E_USER_DEPRECEATED) with the following message:
You are retrieving the service locator from within the class
User\Controller\IndexController. Please be aware that
ServiceLocatorAwareInterface is deprecated and will be removed in
version 3.0, along with the ServiceLocatorAwareInitializer. You will
need to update your class to accept all dependencies at creation,
either via constructor arguments or setters, and use a factory to
perform the injections.
My question is, what is the best way of getting the forms into the controller? My service layer and other controller-specific dependencies are injected into the constructor, but i don't really want to polluate a constructor with all the forms that a controller may need, nor do i want the overhead of creating form objects that will not be used. The forms cannot be created in the controller ie $form = new Form() as they are also created dynamically eg:
Module.php
public function getFormElementConfig ()
{
return array(
'factories' => array(
'User\Details' => function($sm){
$userMapper = $sm->getServiceLocator()->get('Model\User\Mapper');
$form = new \User\Form\Details($userMapper);
return $form;
}
)
);
}
Have more and more specific controllers.
That way you can instantiate one controller and then have to inject exactly all the objects that are definitely needed to perform any task you may need.
There is no use to combine actions into one single controller class if all they share is a common URL path fragment. From the software design point of view, a class that does plenty of independent things in different methods is just doing too much. And doing too much is reflected in the number of dependencies you have to inject.
There are few solutions.
Make controllers smaller. Less methods, less dependencies. However, more factories.
Use the ZF2 proxy manager. It essentially replaces expensive object instantiation with proxy objects.
Another option would be to add a wrapper or container that lazy loads the forms via the form element manager. You can then inject this container into the controller or service layer. Using a service locator in this way isn't "ideal" as you loose the explicitly defined class dependencies.
I have a FormElementLazyProvider class and its associated factory that might be worth checking out.
For example.
$elementConfig = [
'create' => 'MyModule\Form\CreateForm',
'edit' => 'MyModule\Form\EditForm',
'delete' => 'MyModule\Form\DeleteForm',
];
$factory = function($serviceName, $elementName) use ($formElementManager) {
if ($formElementManager->has($serviceName)) {
return $formElementManager->get($serviceName);
}
return false;
};
$provider = new \ArpForm\Service\FormElementLazyProvider($factory, $elementConfig);
$provider->hasElement('create'); // true
$provider->getElement('edit'); // Callback executed and form object return
The crux of the problem is the service locator hiding the dependencies of the controller class.
The Zend framework is instructing you to move away from the ServiceRepository pattern and use proper dependency injection using DI containers or using constructor injection or setter injection. You can use a factory to inject the dependencies as well.
Please read about Service Repository which many see it as an anti-pattern.
Good day!
The more I read, the more I get confused about this. What is the difference between a Facade and Aliases?
I have this Class:
/app/libraries/Project/Data.php
namespace PJ;
class Data {
// It is much like a data container, with static methods and properties for saving info
}
And the corresponding facade, so I can access by using just PJD:: .
According to some webpage around:
... Laravel Facades are proxies. They wrap around and call functions
on the underlying true implementation of the code. Further, in the
context of a Laravel application, these Facades are accessed by
assigning them to aliases. This use of the Dependency Injection
container allow you to reference something like
Illuminate\Support\Facades\Filesystem by simply calling File.
(http://ryantablada.com/post/proxies-service-locators-alias-facades-and-war)
But, I've also found and successfully tested that adding something like:
__app/config/app.php__
'aliases' => array(
//....,
'PJD' => 'PJ\Data',
),
I can also access my class the same way.
So, what's the difference?
Thanks
EDIT #01
I have created a class named Data in /app/libraries/Project/Data.php
namespace PJ;
class Data {
// It is much like a data container, with static methods and properties for saving info
}
I have a Facade Class for this Class Data /app/libraries/Project/DataFacade.php
use Illuminate\Support\Facades\Facade;
class PJD extends Facade {
protected static function getFacadeAccessor() {
return 'PJData';
}
}
And I have a Service Provider for them: /app/libraries/Project/DataServiceProvider.php
use Illuminate\Support\ServiceProvider;
class DataServiceProvider extends ServiceProvider {
public function register() {
$this->app->singleton('PJData', function() {
return new PJ\Data;
});
}
}
I also have added to /app/config/app.php:
'providers' => array(
// ....
'DataServiceProvider',
),
and in composer.json I've added a psr-4 line to direct PJ namespace to /app/libraries/Project
"psr-4": {
"PJ\\": "app/libraries/Project"
},
By doing all this, I can access my class from anywhere in the project just by PJD:: instead of PJ\Data::.
However, I've also noticed that just by adding to /app/config/app.php
'aliases' => array(
//....,
'PJD' => 'PJ\Data',
),
I get exactly the same result without all that facades and ServiceProviders. So, what's the point of one or another?
Thanks, and sorry for the large post.
Facade and Alias are two totally different concepts.
you can not access PJ\Data\ by PJD:: unless you have setup alias in the service provider while binding.
If you are accessing it, without defining it in config/app.php, then you have set it up in the service provider file itself.
Definition of alias,
used to indicate that a named person is also known or more familiar under another specified name.
It simply means you are giving a different name to the class so that it will be easier to call.
e.g.
if you have a class like this: Foo\Bar\AVeryLongNamespaceClassName\Data, you can just give an alias, (e.g. PJD) and access its methods and properties by this alias.
Note:
Unit testing is an important aspect of why facades work the way that they do. In fact, testability is the primary reason for facades to even exist.
I've been trying to figure it out by myself reading online and applying tons of answers form here, but to no avail unfortunately.
I have two Modules on my zf2 application, one called Services and one called Agent.
Now, in my Services module everything seems to work fine, I can get my serviceLocator, hence my configuration, and work with it. In my Agent Module's controller however, I don't seem to be able to do the same.
This is part of my AgentController:
use Zend\Mvc\Controller\AbstractActionController;
class AgentController extends AbstractActionController
{
protected $serviceLocator;
public function ValidateAction()
{
$this->serviceLocator = $this->getServiceLocator()->get('config');
//... Using the config
}
}
In my module.cofig.php I have the following:
'controllers' => array(
'invokables' => array(
'Agent\Controller\Agent' => 'Agent\Controller\AgentController',
),
),
I have tried many solutions: changing and adding methods to the Module.php, changing the module.config etc.. Where am I wrong?
Thanks,
Andrea
The class variable $this->serviceLocator is used by the controller class to hold the service locator instance. In your example you assigning the config array to this variable (thus replacing the service locator instance with an array). Subsequent calls to $this->getServiceLocator() will then return the config array instead of the service locator object, which is the likely cause of the error you're getting.
I'd suggest either using a local variable instead:
public function ValidateAction()
{
$config = $this->getServiceLocator()->get('config');
//... Using the config
}
or assigning to a class variable with a different name:
public function ValidateAction()
{
$this->config = $this->getServiceLocator()->get('config');
//... Using the config
}