Zend Framework 2 - Controller instantiate Service via (modified?) Factory - zend-framework2

I have an abstract service class SAbstract which is inherited by ConcreteServiceA and ConcreteServiceB. Now I am instantiating ConcreteServiceA in the factory class of my controller and inject the service in my controller.
In a specific action in my controller I want to exchange ConcreteServiceA with ConcreteServiceB to change behavior. Because they have same interface (abstract class SAbstract) I could inject it in my controller as well (the services are a Strategy-Pattern).
But I don't want to instantiate ConcreteServiceB directly in my controller to keep my code clean (for easy refactoring and exchanging behavior).
A possible solution is to create a second factory for my controller which injects ConcreteServiceB instead of ConcreteServiceA but then I have duplicated lots of code which is not good...
Another solution would be to inject both services in my controller (but this "smells" like bad code).
Is a delegator factory the right way to do this? Then I have to implement setters in my controller...
Is there a better way?
I tried to schematically visualize my class relationships.
AbstractService <|--<inherit>- ConcreteServiceA
AbstractService <|--<inherit>- ConcreteServiceB
Controller -<use>--> AbstractService
Controller:ActionA -<use>--> ConcreteServiceA:exportAction()
Controller:ActionB -<use>--> ConcreteServiceB:exportAction()

In a specific action in my controller I want to exchange ConcreteServiceA with ConcreteServiceB to change behavior. Because they have same interface.
You can configure the route to use a different controller service name for each action; then configure a controller factory to inject the required service using configuration.
The route config could look like this.
'router' => [
'routes' => [
'foo' => [
'type' => 'literal',
'options' => [
'route' => '/foo',
'defaults' => [
'controller' => 'MyControllerWithFooService',
'action' => 'actionThatNeedsFooService',
],
],
],
'bar' => [
'type' => 'literal',
'options' => [
'route' => '/bar',
'defaults' => [
'controller' => 'MyControllerWithBarService',
'action' => 'actionThatNeedsBarService',
],
],
],
],
]
Then add the config for the services and controllers.
'app_config' => [
'MyControllerWithFooService' => [
'service_name' => 'FooService',
],
'MyControllerWithFooService' => [
'service_name' => 'BarService',
],
],
'service_manager' => [
'factories' => [
'FooService' => 'FooServiceFactory'
'BarService' => 'BarServiceFactory'
],
],
'controllers' => [
'factories' => [
'MyControllerWithFooService' => 'MyControllerServiceFactory'
'MyControllerWithBarService' => 'MyControllerServiceFactory'
],
]
The MyControllerServiceFactory could be very simple.
class MyControllerServiceFactory
{
public function __invoke($controllerManager, $name, $requestedName)
{
$sm = $controllerManager->getServiceLocator();
$config = $sm->get('config');
if (empty($config['app_config'][$requestedName])) {
throw new ServiceNotCreatedException('No config set!');
}
$serviceName = $config['app_config'][$requestedName]['service_name'];
$service = $sm->get($serviceName);
return new MyController($service);
}
}

Related

Yii 2 - authclient doesn't call onAuthSuccess

I am using Yii authclient to use social login. I had set everything as it is defined in docs but when I try to login with google it does not call onAuthSuccess method. When I try to login it just redirects me to returnUrl but not authenticated.
Here is my code;
config/main.php
'authClientCollection' => [
'class' => \yii\authclient\Collection::class,
'clients' => [
'google' => [
'class' => \yii\authclient\clients\Google::class,
'clientId' => *********, //changed for issue purpose
'clientSecret' => *********, //changed for issue purpose
'returnUrl' => 'http://localhost/site/landing',
],
],
]
controllers/SiteController
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['logout', 'signup', 'auth'],
'rules' => [
[
'actions' => ['signup', 'auth'],
'allow' => true,
'roles' => ['?'],
],
[
'actions' => ['logout'],
'allow' => true,
'roles' => ['#'],
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'logout' => ['post'],
'create-storyboard' => ['post'],
],
],
];
}
/**
* {#inheritdoc}
*/
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
'auth' => [
'class' => 'yii\authclient\AuthAction',
'successCallback' => [$this, 'onAuthSuccess'],
],
];
}
public function onAuthSuccess($client)
{
(new AuthHandler($client))->handle();
}
If you set returnUrl the user is from auth provider redirected directly to the url you've set in that property.
In your case the returnUrl says google, that it should redirect user to http://localhost/site/landing. But there is nothing in your site/landing action that would call the onAuthSuccess.
You need to let user come back to site/auth and redirect them after processing response from OAuth provider. To do that remove the returnUrl from config. That will make the authclient to use default return url which is the action that started the auth process.
Then modify your onAuthSuccess to redirect users to site/landing like this:
public function onAuthSuccess($client)
{
(new AuthHandler($client))->handle();
$this->redirect(['site/landing']);
}
I had solved the problem with the help from #Michal Hynčica. The problem was in my returnUrl which means with authentication url it must follow to authenticate rather the redirecting after authentication. So all I need to do was changing it to as below.
'returnUrl' => 'http://localhost/site/auth?authclient=google'
Also don't forget to add same returnUrl to your google console's redirect url.

Zf2 - I am not able to define the form as shared one using its name as well as alias

I am working on ZendFramework 2. I have a form which I want to be used as a shared instance. But the shared key only accepts the actual class rather then the name allocated to it. Sharing some of the code snippet for better understanding of my problem:
SampleForm.php
namespace MyProject\Form;
use Zend\Form\Form;
class Sampleform extends Form
{
public function __construct()
{
parent::__construct('sampelname');
}
/**
* Initialize the form elements
*/
public function init()
{
$this->add(
[
'type' => 'Text',
'name' => 'name',
'options' => [
'label' => 'Enter your name',
]
]
);
}
}
Defining the SampleForm in Module.php:
namespace MyProject;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\FormElementProviderInterface;
use MyProject\Form\SampleForm;
class Module implements ConfigProviderInterface, FormElementProviderInterface
{
/**
* {#inheritDoc}
*/
public function getConfig()
{
return include __DIR__ . '/config/module.config.php';
}
/**
* {#inheritdoc}
*/
public function getFormElementConfig()
{
return [
'invokables' => [
'MyProject\Form\SharedSampleForm' => SampleForm::class,
],
'aliases' => [
'sharedSampleForm' => 'MyProject\Form\SharedSampleForm'
],
'shared' => [
'MyProject\Form\SharedSampleForm' => true
]
];
}
}
It throws me the error like:
Zend\ServiceManager\Exception\ServiceNotFoundException: Zend\Form\FormElementManager\FormElementManagerV2Polyfill::setShared: A service by the name "MyProject\Form\SharedSampleForm" was not found and could not be marked as shared
But it works as expected when I define my getFormElementConfig in Module.php as follows:
public function getFormElementConfig()
{
return [
'invokables' => [
'MyProject\Form\SharedSampleForm' => SampleForm::class,
],
'aliases' => [
'sharedSampleForm' => 'MyProject\Form\SharedSampleForm'
],
'shared' => [
SampleForm::class => true
]
];
}
i.e. In the shared key I provide the reference to the actual Form class name.
If the same definitions are defined under getServiceConfig() then it works as expected without throwing any such error.
Can some one please suggest/help me out how can I be able to use the service name in the shared for forms then providing the actual class reference?
getFormElementConfig() is used for defining Form Element. Not used for defining Form as service. If you wanna define this form as Service, you should define it under getServiceConfig().
Another tips, if you have make an alias, just define the Service name using it's class name.
public function getServiceConfig()
{
return [
'invokables' => [
SampleForm::class => SampleForm::class,
],
'aliases' => [
'sharedSampleForm' => SampleForm::class
],
'shared' => [
SampleForm::class => true
]
];
}
You can call the Form using the alias name like this $this->getServiceLocator()->get('sharedSampleForm');

How to use services for a form fieldset in Zend Framework 2?

I have a form (Zend\Form\Form) with some nested fieldsets (Zend\Form\Fieldset) in it. The construction is pretty similar to that in the Form Collections tutorial.
Storage\Form\MyForm
|_'Storage\Form\Fieldset\FooFieldset'
|_'Storage\Form\Fieldset\BarFieldset'
|_'Storage\Form\Fieldset\BazFieldset'
...
MyForm
class MyForm {
public function __construct()
{
...
$this->add(
[
'type' => 'Storage\Form\Fieldset\FooFieldset',
'options' => [
'use_as_base_fieldset' => true
]
]
);
}
}
FooFieldset
class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('foo');
$this->setHydrator(new ClassMethodsHydrator())->setObject(new Foo()); // dependencies!
}
}
It works, but the fieldset class has two dependencies in it. I want to inject them. In order to do it, I created a FooFieldsetFactory and extended the /module/MyModule/config/module.config.php by:
'service_manager' => [
'factories' => [
'Storage\Form\Fieldset\FooFieldset' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
The factory is simply being ignored. I guess, the service locator first tries to find the class by namespace and only if nothing is found, takes a look in the invokables and factories. OK. Then I created an alias:
'service_manager' => [
'factories' => [
'Storage\Form\Fieldset\FooFieldset' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
'aliases' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\FooFieldset',
],
... and tried to use it instead of Storage\Form\Fieldset\FooFieldset in my form class. But now I get an exception:
Zend\Form\FormElementManager::get was unable to fetch or create an instance for Storage\Form\Fieldset\Foo
I've also tried this directly:
'service_manager' => [
'factories' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
No effect, the same error.
And this also didn't work (the same error):
'form_elements' => [
'factories' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
So the referencing a service for a fieldset seems not to work. Or am I doing something wrong?
How to use services for form fieldsets?
UPDATE
With some debuggin I found out, that my Foo fieldset factory cannot be found, because it has not be added to the factories list of the Zend\Form\FormElementManager. Here is the place in the Zend\Form\Factory:
So my config
'form_elements' => [
'factories' => [
'Storage\Form\Fieldset\Foo' => 'Storage\Form\Fieldset\Factory\FooFieldsetFactory',
],
],
is ignored. How to fix this?
UPDATE Additional information, how I'm creating my Form object.
/module/Foo/config/module.config.php
return [
'controllers' => [
'factories' => [
'Foo\Controller\My' => 'Foo\Controller\Factory\MyControllerFactory'
]
],
'service_manager' => [
'factories' => [
'Foo\Form\MyForm' => 'Foo\Form\Factory\MyFormFactory',
],
],
];
/module/Foo/src/Foo/Form/Factory/MyFormFactory.php
namespace Foo\Form\Factory;
use ...;
class MyFormFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$form = new MyForm();
$form->setAttribute('method', 'post')
->setHydrator(new ClassMethods())
->setInputFilter(new InputFilter());
return $form;
}
}
/module/Foo/src/Foo/Controller/Factory/MyControllerFactory.php
namespace Foo\Controller\Factory;
use ...;
class MyControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$fooPrototype = new Foo();
$realServiceLocator = $serviceLocator->getServiceLocator();
// $myForm = $realServiceLocator->get('Foo\Form\MyForm'); <-- This doesn't work correctly for this case. The FormElementManager should be used instead.
$formElementManager = $realServiceLocator->get('FormElementManager');
$myForm = $formElementManager->get('Foo\Form\MyForm');
return new MyController($myForm, $fooPrototype);
}
}
This issue is because you are adding your form elements in the forms __construct() method rather than init() as suggested in the documentation.
You can use a factory instead of an invokable in order to handle dependencies in your elements/fieldsets/forms.
And now comes the first catch.
If you are creating your form class by extending Zend\Form\Form, you must not add the custom element in the __construct-or (as we have done in the previous example where we used the custom element’s FQCN), but rather in the init() method:
The reason is that the new form's factory (which is used to create new elements using add()) must have the application's form element manager injected after the form's constructor has been called This form element manager instance contains all the references to your custom forms elements which are registered under the form_elements configuration key.
By calling add() in the form __construct the form factory will lazy load a new instance of the form element manager; which will be able to create all default form elements but will not have any knowledge of your custom form element.

Zfcuser route has a child route

I'm working on a ZF2 project, and using ZfcUser to manage website users.
I just want to know is it possible to have a child route to the zfcuser route?
Something like that, in the configuration of my module:
return [
'router' =>
[
'routes' =>
[
'admin' =>
[
'type' => 'Segment',
'options' =>
[
'route' => '/admin',
'defaults' =>
[
'__NAMESPACE__' => 'admin',
'controller' => 'admin.index',
'action' => 'index',
],
],
'may_terminate' => true,
'child_routes' =>
[
'zfcuser' =>
[
'type' => 'Literal',
'options' =>
[
'route' => '/account',
]
],
],
],
],
],
];
This is a problem I've tried overcoming recently, not using ZfcUser but whilst developing my own modules. There are a few solutions but the most suitable would depend on the type of application you're developing.
The easiest solution would be to override the zfcuser route path and prefixing it with admin.
return [
'router' => [
'routes' => [
'zfcuser' => [
'options' => [
'route' => '/admin/user',
],
],
],
],
];
If you're like me and want all you admin routes contained under a single route then you're better off removing the zfcuser route completely and implementing your own which could utilize the ZfcUser controllers.
namespace Application;
use Zend\ModuleManager\ModuleEvent;
use Zend\ModuleManager\ModuleManager;
class Module
{
public function init(ModuleManager $moduleManager)
{
$events = $moduleManager->getEventManager();
$events->attach(ModuleEvent::EVENT_MERGE_CONFIG, array($this, 'onMergeConfig'));
}
public function onMergeConfig(ModuleEvent $event)
{
$configListener = $event->getConfigListener();
$configuration = $configListener->getMergedConfig(false);
if (isset($configuration['router']['routes']['zfcuser']))
{
unset($configuration['router']['routes']['zfcuser']);
}
$configListener->setMergedConfig($configuration);
}
}

ZF2 translator is not working in controller?

I am trying to translate in the controller by ServiceLocator, but this is not translating and I have tried many sulotions in stackoverflow but with out success. My system uses multiple languages and my goal is to use transtor in view, controller, form and filter. Tranlator in my view is working. Any sugestion and help will be appreciated.
Not working in controller:
$this->getServiceLocator()->get('translator')->translate('my text',$myLocale);
My Application mudole.config.php:
'service_manager' => array(
'abstract_factories' => array(
'Zend\Cache\Service\StorageCacheAbstractServiceFactory',
'Zend\Log\LoggerAbstractServiceFactory',
),
'factories' => array(
'translator' => 'Zend\I18n\Translator\TranslatorServiceFactory',
),
),
'translator' => array(
'locale' => 'en_US',// 'locale' => 'dk_DK',
'translation_file_patterns' => array(
array(
'type' => 'gettext',
'base_dir' => __DIR__ . '/../language',
'pattern' => '%s.mo',
),
),
),
I changed the local in mudole.config.php to another language but still not translating.
View Helper/Forms
ZF2 ships with the view helper Zend\I18n\View\Helper\Translate; this is why you can already use the method $this->translate($text) in the view.
However all view helper classes that extend from Zend\I18n\View\Helper\AbstractTranslatorHelper (which includes all form view helpers) are also 'translation capable'.
You would need to pass in the translator using $viewHelper->setTranslator($translator) and enabling translation via $viewHelper->setTranslatorEnabled(true).
Controller Plugin
Unfortunately there is no default plugin (that I could find) to handle translators in controllers; I guess you could argue that text content shouldn't be in the controller anyway.
You could easily create one such as the example below. The key is to pass your new translator service as a dependency via a factory.
namespace MyModule\Controller\Plugin;
use Zend\Mvc\Controller\AbstractPlugin;
use Zend\I18n\Translator\Translator as TranslatorService;
class Translator extends AbstractPlugin
{
protected $translatorService;
public function __construct(TranslatorService $translatorService)
{
$this->translatorService = $translatorService;
}
public function invoke($text = null, array $options = [])
{
if (null == $text) {
return $this;
}
return $this->translate($text, $options);
}
public function translate($text, array $options = [])
{
return $this->translatorService->translate($text);
}
}
And create the factory class.
namespace MyModule\Controller\Plugin;
use MyModule\Controller\Plugin\Translator;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class TranslatorFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $controllerPluginManager)
{
$serviceManager = $controllerPluginManager->getServiceLocator();
return new Translator($serviceManager->get('translator'));
}
}
Register the service in module.config.php.
return [
'controller_plugins' => [
'factories' => [
'translate' => 'MyModule\\Controller\\Plugin\\TranslateFactory',
]
],
];
Then you can just call it within a controller class.
// Directly
$this->translate($text, $options);
// Or fetch the plugin first
$this->translate()->translate($text, $options);
It seems that the locale is sets not directly in the translating text, but by $this->getServiceLocator()->get('translator')->setLocale($locale), and now it is translating my text.
My Application mudole.config.php:
'service_manager' => array(
'factories' => array(
'translator' => 'Zend\I18n\Translator\TranslatorServiceFactory',
),
),
'translator' => array(
'locale' => 'en_US',
'translation_file_patterns' => array(
array(
'type' => 'gettext',
'base_dir' => __DIR__ . '/../language',
'pattern' => '%s.mo',
),
),
),
And in the controller:
$this->getServiceLocator()->get('translator')->setLocale($locale);
echo $c=$this->getServiceLocator()->get('translator')->translate('Book'); // Print(Danish): Bog
You can use my ZfTranslate controller plugin.
installation
composer require mikica/zf2-translate-plugin
You need to register new module. Add in file config/application.config.php:
'modules' => array(
'...',
'ZfTranslate'
),
Usage in controller
<?php
$this->translate('translate word');
$this->translate('translate word', 'locale');
AlexP answer is the best way to do it.
But remains a question, why your way doesn't work?
It should work. But it doesn't because you are in different namespaces, so you are using distinct domains among the files. You doing something like that:
namespace MyModule\Controller;
class MyController {
public function someAction() {
$this->getServiceLocator()->get('translator')->translate('my text',__NAMESPACE__,$myLocale);
}
}
While at your `module.config.php', you probably using this namespace:
namespace MyModule;
return array(
//...
'translator' => array(
//...
),
);
Note that in the example of the controller __NAMESPACE__ is equals MyModule\Controller. While at the config file the __NAMESPACE__ is equals MyModule. You need to fix it, passing the same value in both cases.
In other words, there are several approachs to solve this, like AlexP's, for instance. But, any one of them need to have the domain of translator (the value of the 'text_domain' key) when you configure it equals the domain parameter (second paramater) of the translate method when you call it.
The faster solution is changing the $domain parameter to string at the controller file:
$this->getServiceLocator()->get('translator')->translate('my text','MyModule',$myLocale);
Another solution should be creating a constant and using it at the files (controllers, views and config).

Resources