Yii2 dependency injection, configuration and inheritance - php

Let's say I have a configuration like this (snippet from official guide):
$config = [
// ...
'container' => [
'definitions' => [
'yii\widgets\LinkPager' => ['maxButtonCount' => 5],
],
],
// ...
];
I create a class named FancyLinkPager:
class FancyLinkPager extends \yii\widgets\LinkPager
{
// ...
}
When I create an object of class FancyLinkPager like so (please ignore the $pagination object, it's here for correctness sake):
$pagination = \Yii::createObject(Pagination::class);
$linkPager = \Yii::createObject(['class' => LinkPager::class, 'pagination' => $pagination]);
$fancyLinkPager = \Yii::createObject(['class' => FancyLinkPager::class, 'pagination' => $pagination]);
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 10 as LinkPager's default
My problem is that I wished $fancyLinkPager->maxButtonCount to be 5 as configured. I know I can add another line in the configuration or adjust it to specify my custom class, but it's not solution for me because:
I want to keep the code DRY
This is an oversimplified example of my needs - in real world you don't expect to have multiple LinkPager's child classes, but it is highly possible for other objects
My question is: is there any framework-supported way of achieving this? The solutions I came up with are:
Hack a custom __construct in FancyLinkPager (or another intermediate class or trait) so that it would look into App's configuration and call Yii::configure on the instance, but I don't find a good way to do it in generic way
Inject a dependency into FancyLinkPager with "setup" object, like LinkPagerSettings and configure that class in container section of my configuration, but it would make some trouble to work with vanilla LinkPager instances as well
Maybe the only real solution would be to create my own implementation of yii\di\Container that allows for inheriting configuration from parent classes but before I dive into this I would like to know if I haven't overlooked something.

Finally I came up with my own implementation of DI container introducing new configurable property:
public $inheritableDefinitions = [];
Full class code:
<?php
namespace app\di;
/**
* #inheritdoc
*/
class Container extends \yii\di\Container
{
/**
* #var array inheritable object configurations indexed by class name
*/
public $inheritableDefinitions = [];
/**
* #inheritdoc
*
* #param string $class
* #param array $params
* #param array $config
*
* #return object the newly created instance of the specified class
*/
protected function build($class, $params, $config) {
$config = $this->mergeInheritedConfiguration($class, $config);
return parent::build($class, $params, $config);
}
/**
* Merges configuration arrays of parent classes into configuration of newly created instance of the specified class.
* Properties defined in child class (via configuration or property declarations) will not get overridden.
*
* #param string $class
* #param array $config
*
* #return array
*/
protected function mergeInheritedConfiguration($class, $config) {
if (empty($this->inheritableDefinitions)) {
return $config;
}
$inheritedConfig = [];
/** #var \ReflectionClass $reflection */
list($reflection) = $this->getDependencies($class);
foreach ($this->inheritableDefinitions as $parentClass => $parentConfig) {
if ($class === $parentClass) {
$inheritedConfig = array_merge($inheritedConfig, $parentConfig);
} else if (is_subclass_of($class, $parentClass)) {
/** #var \ReflectionClass $parentReflection */
list($parentReflection) = $this->getDependencies($parentClass);
// The "#" is necessary because of possible (and wanted) array to string conversions
$notInheritableProperties = #array_diff_assoc($reflection->getDefaultProperties(),
$parentReflection->getDefaultProperties());
// We don't want to override properties defined specifically in child class
$parentConfig = array_diff_key($parentConfig, $notInheritableProperties);
$inheritedConfig = array_merge($inheritedConfig, $parentConfig);
}
}
return array_merge($inheritedConfig, $config);
}
}
This is how it can be used to accomplish customization of LinkPager described in the question:
'container' => [
'inheritableDefinitions' => [
'yii\widgets\LinkPager' => ['maxButtonCount' => 5],
],
],
Now if I create a class FancyLinkPager that extends yii\widgets\LinkPager the DI container will merge the default configuration:
$pagination = \Yii::createObject(Pagination::class);
$linkPager = \Yii::createObject(['class' => LinkPager::class, 'pagination' => $pagination]);
$fancyLinkPager = \Yii::createObject(['class' => FancyLinkPager::class, 'pagination' => $pagination]);
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 5 as configured - hurrah!
I have also taken into account qiangxue's comment about explicit setting of default property values in class definition, so if we declare the FancyLinkPager class as such:
class FancyLinkPager extends LinkPager
{
public $maxButtonCount = 18;
}
the property setting will be respected:
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 18 as declared
To swap default DI container in your application you have to explicitly set Yii::$container somewhere in an entry script:
Yii::$container = new \app\di\Container();

Related

How to attach event listener via configuration instead of module bootstrap?

In ZF3 you would normally attach your event listener for the MvcEvent's in your module's Module.php like so:
<?php
namespace MyModule;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$eventManager = $event->getApplication()->getEventManager();
$eventManager->attach(MvcEvent::EVENT_DISPATCH, function(MvcEvent $event) {
// Do someting...
});
}
}
Now there are two typical situations where your Module.php can grow big:
Your module has to handle multiple (or even all) MvcEvent's and maybe even treat them in different ways.
Your module has to perform multiple actions on a single MvcEvent.
What I'd like to be able to do is to specify a class name in my module.config.php along with one or multiple MvcEvent names to keep my Module.php nice and clean.
Is there a way to do this in Zend Framework 3?
#Nukeface has a great example but it does not directly answer my specific question.
To answer my own question:
This is possible with the use of listeners. A listener can be configured in the configuration files but it cannot be mapped to an event directly from the configuration alone.
It is possible to check for a specific setting in the configuration and determine what classes to map to what events. Even MvcEvents can be mapped this way.
Here's how to set it up:
1. The listener
We want to listen to multiple MvcEvents with one simple class. Note the class it extends.
namespace Demo\Listener;
class MyListener extends EventClassMapListener
{
public function handleEvent(MvcEvent $event)
{
// Do something
\Zend\Debug\Debug::dump($event->getName());
}
}
2. The abstract listener class
The above class needs a bit more body but that can be provided by the abstract listener class:
namespace Demo\Listener;
abstract class EventClassMapListener implements ListenerAggregateInterface
{
private $configuration;
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
foreach ($this->configuration as $identifier => $settings) {
foreach ($settings as $event => $configPriority) {
$sharedManager->attach($identifier, $event, [$this, 'handleEvent'], $configPriority ?: $priority);
}
}
}
public function detach(EventManagerInterface $events)
{
// Do the opposite of attach
}
abstract public function handleEvent(MvcEvent $event);
}
3. The factory
Now we need a factory that we can reuse for all our classes that need to listen to multiple events:
namespace Demo\Factory\Listener;
class EventClassmapListenerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$globalConfiguration = $container->get('config');
$configuration = [];
if (array_key_exists('event_classmap', $globalConfiguration)
&& array_key_exists($requestedName, $globalConfiguration['event_classmap'])
) {
$configuration = $globalConfiguration['event_classmap'][$requestedName];
}
return new $requestedName($configuration);
}
}
4. Configuration
In your module.config.php:
'service_manager' => [
'factories' => [
Listener\MyListener::class => Factory\Listener\EventClassmapListenerFactory::class,
],
],
'listeners' => [
Listener\MyListener::class,
],
'event_classmap' => [
// Name of the class that needs to listen to events
Listener\MyListener::class => [
// Identifier
\Zend\Mvc\Application::class => [
// List of event names and priorities
MvcEvent::EVENT_BOOTSTRAP => 1,
],
// Another identifier
MyEventEmitterClass::class => [
MyEventEmitterClass::EVENT_ONE,
MyEventEmitterClass::EVENT_TWO,
MyEventEmitterClass::EVENT_THREE,
],
],
],
Conclusion:
Although it might not really refined, I really like this idea. It is now fairly easy to add another listener and make it listen to a list of events from one or more emitters.
My opinion after some research
A listener itself should state what it wants to listen to, to keep things strict. Putting that information in a configuration file might result in a more complicated situation when it is not needed.
You need a few things for Listener classes:
Events
Listeners
Handlers
Factories
Config
Now, 2 & 3 are usually in the same class as you would usually have a Listener class for a specific purpose. Such as "Listen for Rocket launch and steer Rocket to Mars".
As such, you would need to "create" these "events" to listen for somewhere. Such as a DemoEvents class!
namespace Demo\Event;
use Zend\EventManager\Event;
class DemoEvent extends Event
{
const THE_STRING_TO_LISTEN_FOR = 'rocket.ready.for.launch';
const ANOTHER_STRING_TO_LISTEN_FOR = 'rocket.steer.to.mars';
}
Now that we have "events", we need to "listen" for them. For that we need a Listener. Because I'm limiting this to 1 example, the Handler (function(-ality) to be executed when the "event" we're "listening" for is "heard") will be in the same class.
namespace Demo\Listener;
use Demo\Event\DemoEvent;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
class DemoListener implements ListenerAggregateInterface
{
/**
* #var array
*/
protected $listeners = [];
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(Demo::class, DemoEvent::THE_STRING_TO_LISTEN_FOR, [$this, 'doSomethingOnTrigger'], -10000);
}
/**
* Apart from triggering specific Listener function and de-registering itself, it does nothing else. Add your own functionality
*
* #param Event $event
*/
public function doSomethingOnTrigger(Event $event)
{
// Gets passed along parameters from the ->trigger() function elsewhere
$params = $event->getParams();
$specificClass = $params[SpecificClass::class];
// Do something useful here
$specificClass->launchRocketIntoOrbit();
// Detach self to prevent running again
$specificClass->getEventManager()->getSharedManager()->clearListeners(get_class($specificClass), $event->getName());
// NOTE: USE THIS TRIGGER METHODOLOGY ELSEWHERE USING THE STRING FROM THE ATTACH() FUNCTION TO TRIGGER THIS FUNCTION
// Trigger events specific for the Entity/class (this "daisy-chains" events, allowing for follow-up functionality)
$specificClass->getEventManager()->trigger(
DemoEvent::ANOTHER_STRING_TO_LISTEN_FOR,
$specificClass ,
[get_class($specificClass) => $specificClass ] // Params getting passed along
);
}
}
Excellent. We now have a events, a listener and a handler. We just need a factory to create this class when needed.
namespace Demo\Factory;
use Demo\Listener;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class DemoListenerFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $requestedName
* #param array|null $options
* #return object|DemoListener
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
// If you're implementation of the Listener has any requirements, load them here and add a constructor in the DemoListener class
return new DemoListener();
}
}
Lastly, we need some config. Obviously we need to register the Listener + Factory combination. Let's do that first.
namespace Demo;
use Demo\Listener\DemoListener;
use Demo\Listener\DemoListenerFactory;
'service_manager' => [
'factories' => [
DemoListener::class => DemoListenerFactory::class,
],
],
Now for a little known bit of config to make sure that the Listener gets registered as a Listener:
'listeners' => [
DemoListener::class
],
Yep, that's it.
Make sure to add both of these bits of config at the first level of config, they're siblings.

Object Mocking - How to replace a factory with a service in the service manager?

I'm having some trouble getting my unit test to work. I'm testing a controller that uses a service that is created by a factory. What I want to achieve is to replace a factory with a mocked service so I can perform tests without using an active database connection.
The setup
In my service manager's configuration file I point to a factory.
The factory requires an active database connection that I don't want to use during my unit test.
Namespace MyModule;
return [
'factories' => [
MyService::class => Factory\Service\MyServiceFactory::class,
],
];
Note: I have changed class names and simplified configuration for illustration purposes.
The service uses a mapper that I won't be going into now because that is not relevant to the situation. The mappers are tested in their own testcases. The service itself has it's own testcase as well but needs to be present for the controller's actions to work.
The controller action simply receives information from the service.
Namespace MyModule\Controller;
use MyModule\Service\MyService;
use Zend\Mvc\Controller\AbstractActionController;
class MyController extends AbstractActionController
{
/**
* #var MyService
*/
private $service;
/**
* #param MyService $service
*/
public function __construct(MyService $service)
{
$this->service = $service;
}
/**
* Provides information to the user
*/
public function infoAction()
{
return [
'result' => $this->service->findAll(),
];
}
}
Note: Again, I have changed class names and simplified the example for illustration purposes.
What I've tried
In my testcase I've tried to override the desired factory like this:
/**
* #return \Prophecy\Prophecy\ObjectProphecy|MyService
*/
private function mockService()
{
$service = $this->prophesize(MyService::class);
$service->findAll()->willReturn(['foo', 'bar']);
return $service;
}
/**
* #param \Zend\ServiceManager\ServiceManager $services
*/
private function configureServiceManager(ServiceManager $services)
{
$services->setAllowOverride(true);
$services->setService(MyService::class, $this->mockService()->reveal());
$services->setAllowOverride(false);
}
At first sight this looks great, but it doesn't work. It just seems to append the service to the service manager's list of services, not overriding the factory.
Changing $services->setService to $services->setFactory requires me to build another factory. What I could do is create a factory that injects a mock-mapper into the service but that feels wrong. I'm testing the controller, not the service or mapper so I am trying to avoid complex solutions like that to keep my test cases simple and clear.
Are there any options regarding my situation? Is it possible to override a factory with a service in the service manager or am I looking at it wrong?
I think you need a separate config file for unit testing.
phpunit.xml
<?xml version="1.0"?>
<phpunit bootstrap="./Bootstrap.php">
Bootstrap.php
require 'vendor/autoload.php';
$configuration = include 'config/phpunit.config.php';
Zend\Mvc\Application::init ($configuration);
config/phpunit.config.php is a config file created for unit testing only:
config/phpunit.config.php
$configuration = include (__DIR__ . '/application.config.php');
$configuration ['module_listener_options'] ['config_glob_paths'] [] = 'config/phpunit/{,*.}local.php';
config/phpunit/yourfile.local.php
return [
'service_manager' => array (
'factories' => [
MyService::class => ...
]
)
];
In config/phpunit/yourfile.local.php you can let MyService::class be whatever you want, even a closure.
There is no need to build new factories for this. Just use a simple closure instead:
/**
* #param \Zend\ServiceManager\ServiceManager $services
*/
private function configureServiceManager(ServiceManager $services)
{
$services->setAllowOverride(true);
$mockedService = $this->mockService();
$services->setFactory(MyService::class, function() use ($mockedService) {
$mockedService->reveal();
});
$services->setAllowOverride(false);
}
Now you can still mock only the required service. Adding expectations in the test case is still as flexible as it should be:
public function testMyCase()
{
$expected = ['foo', 'bar'];
$this->mockService()->findAll()->willReturn($expected);
$result = $this->service->findAll();
$this->assertSame($expected, $result);
}

Loading clients views before default views if exists

In my project (BtoB project), I have a global application with a lot of modules in it.
Each module provides common functionnalities, for all of my clients.
I have also in the root directory a clients folder, in it, I have all clients specificities, in their folder.
Thoses folders, aren't modules. So they are not loaded with Zf2. I usually load those specificities with abstractFactories.
This architecture follow is what I have currently :
- clients
- clientOne
- Invoice
- Cart
- Orders
- clientTwo
- Invoice
- Orders
- clientThree
- Reporting
- module
- Application
- CartModule
- InvoiceModule
- OrdersModule
- Reporting
My clients wants to have some custom views, sometimes, they ask us to provide those views. But my application give a common view for all of them. I have to modify this architecture to load a client view if it exist, or load the common view.
To handle this case I Imagine to have into each clients folder this :
- client
- clientOne
- Invoice
- Cart
- View
- cartView.phtml
- Orders
EDIT :
After some good answers (#AlexP & #Wilt), I tried to implements this solution :
So I have a ClientStrategy; it's factory is Like This :
<?php
namespace Application\View\Strategy;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Application\View\Resolver\TemplateMapResolver;
use Zend\View\Resolver;
class ClientStrategyFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$viewRenderer = $serviceLocator->get('ViewRenderer');
$session = new \Zend\Session\Container('Session');
$map = $serviceLocator->get('config')['view_manager']['template_map'];
$resolver = new Resolver\AggregateResolver();
$map = new TemplateMapResolver($map, $this->clientMap($session->offsetGet('cod_entprim')));
$resolver
->attach($map)
->attach(new Resolver\RelativeFallbackResolver($map));
$viewRenderer->setResolver($resolver);
return new ClientStrategy($viewRenderer);
}
/**
* permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
* #return array
*/
public function clientMap($codprim)
{
$clients = array(
21500 => 'clientOne',
32000 => 'clientTwo',
// ..
);
return (isset($clients[$codprim])) ? $clients[$codprim]: false;
}
}
My clientMap method allow me to load my client folder, and views it may have in it like this :
class ClientOne
{
/**
* get The main Code
* #return integer
*/
public function getCodEntPrim()
{
return 21500;
}
/**
* Load all customs views
* #return array
*/
public function customViews()
{
return array(
'addDotations' => __DIR__ . '/Dotations/view/dotations/dotations/add-dotations.phtml',
);
}
/**
* GetName
* #return string
*/
public function getName()
{
return get_class();
}
}
So when it comes to my TemplateMapResolver to do his job I do this :
<?php
namespace Application\View\Resolver;
class TemplateMapResolver extends \Zend\View\Resolver\TemplateMapResolver
{
/**
* Client name to use when retrieving view.
*
* #param string $clientName
*/
protected $clientName;
/**
* Merge nos vues avec celle clients avant de repeupler l'arrayMap global
* #param array $map [description]
*/
public function __construct(array $map, $client)
{
$this->setClientName($client);
if ($this->getCLientName()) {
$map = $this->mergeMap($map);
}
parent::__construct($map);
}
/**
* Merge les map normales avec les map clients, pas propre ?
* #param array $map
* #return array
*/
public function mergeMap($map)
{
$name = $this->getClientName() . '\\' . $this->getClientName() ;
$class = new $name;
$clientMap = $class->customViews();
return array_replace_recursive($map, $clientMap);
}
/**
* Retrieve a template path by name
*
* #param string $name
* #return false|string
* #throws Exception\DomainException if no entry exists
*/
public function get($name)
{
return parent::get($name);
}
/**
* Gets the Client name to use when retrieving view.
*
* #return string
*/
public function getClientName()
{
return $this->clientName;
}
/**
* Sets the Client name to use when retrieving view.
*
* #param mixed $clientName the client name
*
* #return self
*/
public function setClientName($clientName)
{
$this->clientName = $clientName;
return $this;
}
}
I tried a lot of things, this works but somes issues cames up :
My template_path_stack not works anymore, so a lot of my views are broken.
I think this is a complete mess, to do this, that way.
Hard to maintain.
I understand a bit better, how it works, but i'm still unable to implements it the good way.
If you really want to do that (I am not so sure if it is the best way) then you can extend the TemplateMapResolver with your custom logic and set it in your Renderer instance.
Make your custom class:
<?php
Application\View\Resolver
class TemplateMapResolver extends \Zend\View\Resolver\TemplateMapResolver
{
/**
* Client name to use when retrieving template.
*
* #param string $clientName
*/
protected $clientName;
/**
* Retrieve a template path by name
*
* #param string $name
* #return false|string
* #throws Exception\DomainException if no entry exists
*/
public function get($name)
{
if ($this->has($clientName . '_' . $name)) {
return $this->map[$clientName . '_' . $name];
}
if (!$this->has($name)) {
return false;
}
return $this->map[$name];
}
}
And now something like:
$resolver = new TemplateMapResolver();
$resolver->setClientName($clientName);
// Get the renderer instance
$renderer->setResolver($resolver);
You might still have to take care of setting the map in the resolver. Maybe you can just get it from the old resolver? I am not sure... That is for you to find out. This is just to get you on the correct way.
So if you set cart_view as a template it will first try to get client_name_cart_view if not found it sets cart_view.
UPDATE
If you want to take this to the next level, then what you can do is make a custom view model for example ClientViewModel that extends the normal ViewModel class.
The constructor for this ClientViewModel takes both a client and a template name:
new ClientViewModel($client, $template, $variables, $options);
$variables and $options are optional and can be passed to the parent::__construct (constructor of the normal ViewModel)
The next step would be to create a Application\View\ClientStrategy.
This strategy is connected on render event and in this strategy you add a ViewRenderer instance with your custom TemplateMapResolver set. During rendering you can get your client from your ViewModel and find the correct template in your TemplateMapResolver using this client.
More details can be found online, there are examples. Check for example here.
The advantage will be that other views with ViewModel or JsonModel will be rendered as normally, only your ClientViewModel gets a special treatment. Thus you are not breaking your applications default logic.
Requirements
Multiple possible views per client
Default view fallback if client specific view not found
Create a new service, say TemplateProviderService which has a simple interface.
interface ViewTemplateProviderInterface
{
public function hasTemplate($name);
public function getTemplates();
public function setTemplates($templates);
public function getTemplate($name);
public function setTemplate($name, $template);
public function removeTemplate($name);
public function removeTemplates();
}
Inject and hard code the template name in controller classes.
// Some controller class
public function fooAction()
{
$view = new ViewModel();
$view->setTemplate($this->templateProvider->get('some_view_name'));
return $view;
}
Now you can create client specific factories that inject custom template script config into your template provider. All you would then need to do is decide which template provider service you want to inject into your controller.
class ViewTemplateProviderFactory
{
public function __invoke($sm, $name, $rname)
{
$config = $sm->get('config');
if (! isset($config['view_template_providers'][$rname])) {
throw new ServiceNotCreatedException(sprintf('No view template provider config for \'%s\'.', $rname));
}
return new ViewTemplateProvider($config['view_template_providers'][$rname]);
}
}
The key here is ALL view scripts, for all clients, are registered under the 'view_manager' key as normal however the name of the template in the controller never changes.
Edit
You could just use one factory and pull from config (see changes above).
return [
'view_template_providers' => [
'ClientOneTemplateProvider' => [
'some_view_name' => 'name_of_script_1'
],
'ClientTwoTemplateProvider' => [
'some_view_name' => 'name_of_script_2'
],
'ClientThreeTemplateProvider' => [
'some_view_name' => 'name_of_script_3',
],
],
'service_manager' => [
'factories' => [
'ClientOneTemplateProvider' => 'ViewTemplateProviderFactory',
'ClientTwoTemplateProvider' => 'ViewTemplateProviderFactory',
'ClientThreeTemplateProvider' => 'ViewTemplateProviderFactory',
],
],
'view_manager' => [
'template_map' => [
'name_of_script_1' => __DIR__ . 'file/path/to/script',
'name_of_script_2' => __DIR__ . 'file/path/to/script',
'name_of_script_3' => __DIR__ . 'file/path/to/script',
],
],
];
It seems I solved my problem, but i'm not sure it's the good way to do it. So if someone can do better, I let the bounty runs for a better solution, if exists.
Here is what I've done :
/**
* Factory permettant d'établir que les vues client soient chargé si elle existent, avant les vues par défaut.
*/
class ClientStrategyFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$viewRenderer = $serviceLocator->get('ViewRenderer');
$session = new \Zend\Session\Container('Session');
$clientList = $serviceLocator->get('Config')['customers_list'];
$clientName = $this->clientMap($session->offsetGet('cod_entprim'), $clientList);
$clientMap = new TemplateMapResolver($clientName);
$viewRenderer->resolver()->getIterator()->insert($clientMap, 2);
return new ClientStrategy($viewRenderer);
}
/**
* permet de retourner le namespace du client selectionné avec la relation codpriml / nom de dossier
* #param integer $codprim
* #param array $clientList
* #return array
*/
public function clientMap($codprim, $clientList)
{
return (isset($clientList[$codprim])) ? $clientList[$codprim]: false;
}
}
You can see that my custom TemplateMapResolver needs a clientName, this is for loading custom views. But the most important thing is : I don't create a new Resolver, I just add my Resolver to the list by this line :
$viewRenderer->resolver()->getIterator()->insert($clientMap, 2);
The second argument means, that this resolver is top priority (Default priority is 1)
My TemplateMapResolver is pretty much simple, the most important thing is this :
public function __construct($client)
{
$this->setClientName($client);
if ($this->getCLientName()) {
$map = $this->getMap();
} else {
$map = array();
}
parent::__construct($map);
}
/**
* Return all custom views for one client
* #param array $map
* #return array
*/
public function getMap()
{
$name = $this->getClientName() . '\\' . $this->getClientName() ;
$class = new $name;
return $class->customViews();
}
My solution, force me to create then a class in my clients folder with the same name of the folder so, if my clientName is TrumanShow i will have an architecture like :
- [clients]
-- [TrumanShow]
--- TrumanShow.php
--- [Cart]
---- [view]
----- [cart]
------ [index]
------- cart-view.phtml
--- [Invoice]
--- [Reporting]
And in this file I will have this function that declare all my custom views :
/**
* Ici nous mettons nos custom views afin de les charger dans le template Map
* #return array
*/
public function customViews()
{
return array(
'cartView' => __DIR__ . '/Cart/view/cart/index/cart-view.phtml',
);
}
So it's possible to do this without break template_path_stack or my others routes. Now I have to call setTemplate method in my Controller, like this :
// code ...
public function cartAction() {
$view->setTemplate('cartView');
return $view;
}
And ZendFramework will check first if a custom view exists in my clients folder, or load the common view if no view is found.
Thanks to #Wilt and #AlexP for their contribution and help.
Don't overcomplicate things. Just set the ViewModel's template before you render it.
$vm = new ViewModel();
$vm->setTemplate( $user_service->getTemplate( $this->getRequest() ) );
return $vm;
Pretty clean if you inject your user into this fictitious user service, and use it to ascertain which template to inject.
The concern of the $user_service should be completely disparate from the concern for your Controller action.

ZF2 How to use global variables in the view

In ZF1 I used to declare variables in the application.ini
brandname = "Example"
weburl = "http://www.example.com/"
assetsurl = "http://assets.example.com/"
And in the Bootstrap I did this so i could access them in the view
define('BRANDNAME', $this->getApplication()->getOption("brandname"));
define('WEBURL', $this->getApplication()->getOption("weburl"));
define('ASSETSURL', $this->getApplication()->getOption("assetsurl"));
Whats the ZF2 way to do this, I know that i can create an array in the local.php config file like:
return array(
'example' => array(
'brandname' => 'Example',
'weburl' => 'http://www.example.com/',
'asseturl' => 'http://assets.example.com/',
),
);
When I want to access that variable in the controller I can do
$config = $this->getServiceLocator()->get('Config');
$config['example']['brandname']);
So far so good... but how do i access this variable in the view?
I don't want to create a view variable for it in every controller. And when i try the above in a view phtml file i get an error.
Zend\View\HelperPluginManager::get was unable to fetch or create an instance for getServiceLocator
Any ideas?
You could create a sinmple view helper to act as a proxy for your config, (totally un tested).
Module.php
public function getViewHelperConfig()
{
return array(
'factories' => array(
'configItem' => function ($helperPluginManager) {
$serviceLocator = $helperPluginManager->getServiceLocator();
$viewHelper = new View\Helper\ConfigItem();
$viewHelper->setServiceLocator($serviceLocator);
return $viewHelper;
}
),
);
}
ConfigItem.php
<?php
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceManager;
/**
* Returns total value (with tax)
*
*/
class ConfigItem extends AbstractHelper
{
/**
* Service Locator
* #var ServiceManager
*/
protected $serviceLocator;
/**
* __invoke
*
* #access public
* #param string
* #return String
*/
public function __invoke($value)
{
$config = $this->serviceLocator->get('config');
if(isset($config[$value])) {
return $config[$value];
}
return NULL;
// we could return a default value, or throw exception etc here
}
/**
* Setter for $serviceLocator
* #param ServiceManager $serviceLocator
*/
public function setServiceLocator(ServiceManager $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
}
You could then do something like this in your view, assuming you have them set in your config of course :)
echo $this->configItem('config_key');
echo $this->configItem('web_url');
I would personally tend to just pass the values through to the view every time though, keeping the view a dumb as possible.
I answered this before on a different post.
/* Inside your action controller method */
// Passing Var Data to Your Layout
$this->layout()->setVariable('stack', 'overflow');
// Passing Var Data to Your Template
$viewModel = new ViewModel(array( 'stack' => 'overflow' ));
/* In Either layout.phtml or {Your Template File}.phtml */
echo $this->stack; // Will print overview
That's it... No need to mess with view helpers, event manager, service manager, or anything else.
Enjoy!

Sonata Admin Bundle: possible to add a child admin object that can have different parents?

I'm using doctrine inheritance mapping to enable various objects to be linked to a comment entity. This is achieved through various concrete "Threads", which have a one-to-many relationship with comments. So taking a 'Story' element as an example, there would be a related 'StoryThread' entity, which can have many comments.
That is all working fine, but I'm having troubles trying to define a CommentAdmin class for the SonataAdminBundle that can be used as a child of the parent entities. For example, I'd want to be able to use routes such as:
/admin/bundle/story/story/1/comment/list
/admin/bundle/media/gallery/1/comment/list
Does anyone have any pointers about how I can go about achieving this? I'd love to post some code extracts but I haven't managed to find any related documentation so don't really know the best place to start.
I've been trying to use the SonataNewsBundle as a reference because they've implemented a similar parent/child admin relationship between posts and comments, but it appears as though this relies on the 'comment' (child) admin class to be hardcoded to know that it belongs to posts, and it also seems as though it needs to have a direct many-to-one relationship with the parent object, whereas mine is through a separate "Thread" entity.
I hope this makes sense! Thanks for any help.
Ok I managed to get this working eventually. I wasn't able to benefit from using the $parentAssociationMapping property of the CommentAdmin class, as the parent entity of a comment is a concrete instance of the Thread entity whereas the parent 'admin' class in this case is a Story (which is linked via the StoryThread). Plus this will need to remain dynamic for when I implement comments on other types of entity.
First of all, I had to configure my StoryAdmin (and any other admin classes that will have CommentAdmin as a child) to call the addChild method:
acme_story.admin.story:
class: Acme\Bundle\StoryBundle\Admin\StoryAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: content, label: Stories }
arguments: [null, Acme\Bundle\StoryBundle\Entity\Story, AcmeStoryBundle:StoryAdmin]
calls:
- [ addChild, [ #acme_comment.admin.comment ] ]
- [ setSecurityContext, [ #security.context ] ]
This allowed me to link to the child admin section from the story admin, in my case from a side menu, like so:
protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)
{
// ...other side menu stuff
$menu->addChild(
'comments',
array('uri' => $admin->generateUrl('acme_comment.admin.comment.list', array('id' => $id)))
);
}
Then, in my CommentAdmin class, I had to access the relevant Thread entity based on the parent object (e.g a StoryThread in this case) and set this as a filter parameter. This is essentially what is done automatically using the $parentAssociationMapping property if the parent entity is the same as the parent admin, which it most likely will be if you aren't using inheritance mapping. Here is the required code from CommentAdmin:
/**
* #param \Sonata\AdminBundle\Datagrid\DatagridMapper $filter
*/
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add('thread');
}
/**
* #return array
*/
public function getFilterParameters()
{
$parameters = parent::getFilterParameters();
return array_merge($parameters, array(
'thread' => array('value' => $this->getThread()->getId())
));
}
public function getNewInstance()
{
$comment = parent::getNewInstance();
$comment->setThread($this->getThread());
$comment->setAuthor($this->securityContext->getToken()->getUser());
return $comment;
}
/**
* #return CommentableInterface
*/
protected function getParentObject()
{
return $this->getParent()->getObject($this->getParent()->getRequest()->get('id'));
}
/**
* #return object Thread
*/
protected function getThread()
{
/** #var $threadRepository ThreadRepository */
$threadRepository = $this->em->getRepository($this->getParentObject()->getThreadEntityName());
return $threadRepository->findOneBy(array(
$threadRepository->getObjectColumn() => $this->getParentObject()->getId()
));
}
/**
* #param \Doctrine\ORM\EntityManager $em
*/
public function setEntityManager($em)
{
$this->em = $em;
}
/**
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
*/
public function setSecurityContext(SecurityContextInterface $securityContext)
{
$this->securityContext = $securityContext;
}
An alternative to your code for direct related entities :
public function getParentAssociationMapping()
{
// we grab our entity manager
$em = $this->modelManager->getEntityManager('acme\Bundle\Entity\acme');
// we get our parent object table name
$className = $em->getClassMetadata(get_class($this->getParent()->getObject($this->getParent()->getRequest()->get('id'))))->getTableName();
// we return our class name ( i lower it because my tables first characted uppercased )
return strtolower( $className );
}
be sure to have your inversedBy variable matching the $className in order to properly work

Categories