in an older ZF2 application I change the layout in a dispatch listener, if the matched route starts with admin. Now I started a new project and want to use ZF3 components, but the event manager does have some changes and I get the following exception:
Uncaught TypeError: Argument 2 passed to Zend\EventManager\EventManager::attach() must be callable, none given
I don't know really how to handle this in ZF3. Here are my relevant source codes to change the layout in my ZF2 application:
Module.php
namespace Admin;
use Zend\EventManager\EventInterface;
use Zend\ModuleManager\Feature\BootstrapListenerInterface;
class Module implements BootstrapListenerInterface {
public function onBootstrap(EventInterface $event) {
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$serviceManager = $application->getServiceManager();
$eventManager->attach($serviceManager->get('Admin\Listener\Dispatch'));
}
}
DispatchListener.php
namespace Admin\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventInterface;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\MvcEvent;
class DispatchListener extends AbstractListenerAggregate {
public function attach(EventManagerInterface $eventManager) {
$this->listeners[] = $eventManager->attach(
MvcEvent::EVENT_DISPATCH, array($this, 'onDispatch'), 100
);
}
public function onDispatch(EventInterface $event) {
$matchedRouteName = $event->getRouteMatch()->getMatchedRouteName();
if (strpos($matchedRouteName, 'admin') === 0) {
$event->getViewModel()->setTemplate('layout/admin');
}
}
}
zf3 is more focused on decoupling components, it seems aggregates has been removed to attach event see the api document
event manager
for short the attach message says
attach($eventName, callable $listener, $priority = 1) : callable
I hope since you are not specifying the eventName you are getting the error message
update:
see the link to migration guide from v2 to v3 for event manager
Removed functions
In ZF3 you can change your layout for your controller this easy way:
<?php
namespace YourCompanyModule;
use Zend\ModuleManager\ModuleManager;
use Zend\Mvc\MvcEvent;
class Module
{
// The "init" method is called on application start-up and
// allows to register an event listener.
public function init(ModuleManager $manager)
{
// Get event manager.
$eventManager = $manager->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
// Register the event listener method.
$sharedEventManager->attach(__NAMESPACE__, 'dispatch',
[$this, 'onDispatch'], 100);
}
// Event listener method.
public function onDispatch(MvcEvent $event)
{
// Get controller to which the HTTP request was dispatched.
$controller = $event->getTarget();
// Get fully qualified class name of the controller.
$controllerClass = get_class($controller);
// Get module name of the controller.
$moduleNamespace = substr($controllerClass, 0, strpos($controllerClass, '\\'));
// Switch layout only for controllers belonging to our module.
if ($moduleNamespace == __NAMESPACE__) {
$viewModel = $event->getViewModel();
$viewModel->setTemplate('layout/layout2');
}
}
// ...
}
Related
I have a view helper that acts as a factory by returning an entity-specific renderer.
I would like the factory to implement the FactoryInterface and MutableCreationOptionsInterface, so i can return different renderers depending on the type of object passed to it, eg:
$serviceLocator->get('entityRenderer', ['entity' => $user]); // returns UserRenderer
$serviceLocator->get('entityRenderer', ['entity' => $admin]); // returns AdminRenderer
$serviceLocator->get('entityRenderer'); // returns DefaultRenderer
However, there is no access to the servicelocator from within a view, and the factory view helper i have created is called using it's __invoke method. This means the type check is occuring here and returning the specific renderer without using the service manager, which is not desirable. eg
class EntityRendererFactory extends AbstractHelper{
public function __invoke(Entity $entity){
if($entity instanceof User){
$renderer = new UserRenderer($entity);
$renderer->setView($this->view);
}
if($entity instanceof Admin){
$renderer = new AdminRenderer($entity);
$renderer->setView($this->view);
}
if($renderer){
return $renderer;
}
}
}
Note how this "factory" is having to extend AbstractHelper (view) simply just to pass on the instance of the current view.
My "ideal" would be something like this (proof of concept, not working code):
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\MutableCreationOptionsInterface;
class EntityRendererFactory implements FactoryInterface, MutableCreationOptionsInterface{
protected $options = [];
protected $renderers = [];
public function createService(ServiceLocatorInterface $serviceLocator){
$this->addRenderer($serviceLocator->get('ViewHelperManager')->get('UserRenderer'), User::class);
$this->addRenderer($serviceLocator->get('ViewHelperManager')->get('AdminRenderer'), Admin::class);
$this->addRenderer($serviceLocator->get('ViewHelperManager')->get('DefaultRenderer'), 'default');
if(!array_key_exists('entity', $this->options)){
return $this->getRenderer('default');
}
$entity = $this->options['entity'];
foreach($this->getRenderers() as $renderer){
if($renderer->canRender($entity)){
return $renderer;
}
}
//Alternatively, more specific hard-coding interface type check
if($entity instanceof User){
return $serviceLocator->get('ViewHelperManager')->get('UserRenderer');
}
//etc.
}
public function setCreationOptions(array $options){
$this->options = $options;
}
}
...but with the above demonstration, i would be unsure how to call it from within the view (as view helpers are typically called from their __invoke method and not from the service manager)?
(With an eye to migrating to ZF3, i do not want to use the ServiceLocatorAwareInterface).
You can declare your viewhelperin the factories section of module.config.php as :
return [
...
'view_helpers' => [
'factories' => [
'entityRenderer' => EntityRendererFactory::class
]
],
...
]
then use the following model :
class EntityRendererFactory extends AbstractHelper implement FactoryInterface
{
private $sm;
public function createService(ServiceLocatorInterface $serviceLocator){
$this->sm = $serviceLocator;
return $this;
}
public function _invoke() {
// your code
}
}
Personally, I start by creating a specific service manager containing only the necessary classes and it is this one that I record in the class.
$this->sm = $servicelocator->getServiceLocator()->get('mySpecificSM');
Incidentally, this model does not work under ZF3 where you need a factory class that builds the viewhelper class. A change not too complicated however.
I am writing a new ZF2 app. I have noticed that ServiceLocator usage pattern of calling services "from anywhere" has been deprecated from ZF3. I want to write code in mind for ZF3.
I was able to set up my Controller to call all dependencies at constructor time. But that means loading i.e. Doctrine object upfront before I need it.
Question
How do I set it up so that it is only loaded when I need it immediately? (lazy-loaded). I understand that ZF3 moves loading to Controller construction, which makes it not apparent as to how to load something Just-In-Time.
Old Code
class CommissionRepository
{
protected $em;
function getRepository()
{
//Initialize Doctrine ONLY when getRepository is called
//it is not always called, and Doctrine is not always set up
if (! $this->em)
$this->em = $this->serviceLocator->get('doctrine');
return $this->em;
}
}
Current Code after Refactor of ServiceLocator pattern
class CommissionRepository
{
protected $em;
function getRepository()
{
return $this->em;
}
function setRepository($em)
{
$this->em = $em;
}
function useRepository($id)
{
return $this->em->find($id);
}
}
class CommissionControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $controllerManager->getServiceLocator();
// set up repository
$repository = new CommissionRepository();
$repository->setRepository($parentLocator->get('doctrine'));
// set up controller
$controller = new CommissionController($repository);
$controller->setRepository();
return $controller;
}
}
class CommissionController extends AbstractActionController
{
protected $repository;
public function setRepository(CommissionRepository $repository)
{
$this->repository = $repository;
}
public function indexAction()
{
//$this->repository already contains Doctrine but it should not
//I want it to be initialized upon use. How?
//Recall that it has been set up during Repository construction time
//and I cannot call it from "anywhere" any more in ZF3
//is there a lazy loading solution to this?
$this->repository->useRepository();
}
If you don't have any valid/strong reason to instantiate a custom entity repository, you should prefer extending of Doctrine\ORM\EntityRepository in your repositories like CommissionRepository. For example;
use Doctrine\ORM\EntityRepository;
class CommissionRepository extends EntityRepository
{
// No need to think about $em here. It will be automatically
// injected by doctrine when you call getRepository().
//
function fetchCommissionById($id)
{
// You can easily get the object manager directly (_em) or
// using getEntityManager() accessor method in a repository
return $this->_em->find($id);
}
}
By this way, entity manager will be automatically injected to the repository on construction when you call the $em->getRepository('App\Entity\Commission') method.
I assume that you already have a Commission entity in your app's Entity namespace:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repo\CommissionRepository")
* #ORM\Table
*/
class Commission
{
}
Then you can simplify the injecting process of the repository in your factory something like:
// ZF2 Way
class CommissionControllerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $services)
{
$em = $services->getServiceLocator()->get('doctrine');
$repository = $em->getRepository('App\Entity\Commission');
return new CommissionController($repository);
}
}
UPDATE - With the release of Service Manager V3, FactoryInterface has been moved to Zend\ServiceManager\Factory namespace (1), factories are literally invokables (2) and works with any container-interop compatible DIC (3) Updated factory would be like below:
// ZF3 Way
use Zend\ServiceManager\Factory\FactoryInterface;
use Interop\Container\ContainerInterface;
use Doctrine\ORM\EntityManager;
class CommissionControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $dic, $name, array $options = null) {
$em = $dic->get(EntityManager::class);
$repository = $em->getRepository('App\Entity\Commission');
return new CommissionController($repository);
}
}
For the question; as of marcosh's said, Lazy Services are way to go to create services when need it immediately. ZF3 will use the zend-servicemanager 3.0 component when released. (Currently zend-expressive uses it) As of servicemanager v3 you can create some proxied services by defining lazy_services and delegators in your service configuration:
'factories' => [],
'invokables' => [],
'delegators' => [
FooService::class => [
FooServiceDelegatorFactory::class,
],
],
'lazy_services' => [
// map of service names and their relative class names - this
// is required since the service manager cannot know the
// class name of defined services up front
'class_map' => [
// 'foo' => 'MyApplication\Foo',
],
// directory where proxy classes will be written - default to system_get_tmp_dir()
'proxies_target_dir' => null,
// namespace of the generated proxies, default to "ProxyManagerGeneratedProxy"
'proxies_namespace' => null,
// whether the generated proxy classes should be written to disk or generated on-the-fly
'write_proxy_files' => false,
];
Also, starting with service manager v3 factories are compatible with the ContainerInterface. For the forward-compatibility, you may want to keep both __invoke() and createService() methods in your factories for a smooth migration.
In the end, your ZF3 compatible factory may look like:
class CommissionControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
$em = $container->get('doctrine');
$repository = $em->getRepository('App\Entity\Commission');
return new CommissionController($repository);
}
public function createService(ServiceLocatorInterface $container, $name = null, $requestedName = null)
{
return $this($container, $requestedName, []);
}
}
Hope it helps.
I'm having an issue getting an event to trigger. Here is my code...
controller.php
function get($id)
{
$this->getEventManager()->trigger('hmac.check');
}
When this trigger is ran it will not run the hmac.check event even though it is attached.
module.php
class Module
{
/**
* Init the methods
*
* #param ModuleManager $moduleManager
*/
public function init(ModuleManager $mm)
{
$mm->getEventManager()
->attach(
'hmac.check',
function(MvcEvent $evt)
{
echo "The trigger has worked";
$key = $evt->getParams()->fromHeader('key');
$ts = $evt->getParams()->fromHeader('when');
$uri = $evt->getParams()->fromHeader('uri');
$hmac = new \Scc\Hmac\Hmac(new HmacConfig, new HmacStorage);
}
);
}
}
If i echo out a message before or after the $mm->getEventManager->attach(); it displays the test fine so i know it is executing the init method.
any help with this would be great.
Thanks in advance
EDIT:
This is a restful controller if that makes any difference (i don't think it does).
The problem is that you're attaching listeners to the ModuleManagers EventManager instance, and not the main Application EventManager.
There's no way to attach to the Application EventManager directly from module init(), the module manager doesn't have access to it, so you need to instead get the SharedManager from the ModuleManager's EventManager and attach your event listeners to that.
Here's an example of doing that by listening to the hmac.check event when triggered by any controller that extends Zend\Mvc\Controller\AbstractRestfulController, but you could listen to a specific controller by replacing that with your controllers FQCN instead.
class Module
{
/**
* Init the methods
*
* #param ModuleManager $moduleManager
*/
public function init(ModuleManager $mm)
{
$mm->getEventManager()->getSharedManager()
->attach(
'Zend\Mvc\Controller\AbstractRestfulController', 'hmac.check',
function(MvcEvent $evt)
{
echo "The trigger has worked";
$key = $evt->getParams()->fromHeader('key');
$ts = $evt->getParams()->fromHeader('when');
$uri = $evt->getParams()->fromHeader('uri');
$hmac = new \Scc\Hmac\Hmac(new HmacConfig, new HmacStorage);
}
);
}
}
I want to set a basePath to be the same for every component of my Mvc for a given request. I mean when I call these methods I want to get the same result, lets say '/spam/ham/':
echo $this->headLink()->prependStylesheet($this->basePath() . '/styles.css') // $this->basePath() has to be '/spam/ham/'
$this->getServiceLocator()
->get('viewhelpermanager')
->get('headLink')
->rependStylesheet($this->getRequest()->getBasePath() . '/styles.css') // $this->setRequest()->getBasePath() has to be /spam/ham/
How to set the basePath for the first case I have found already, here's my question. By the way, the original manual doesn't have any info I received from the answer.
And now the second one - the basePath has to be set in the Request:
$this->getRequest()->getBasePath()
Here I found some answer that in fact doesn't work at all http://zend-framework-community.634137.n4.nabble.com/Setting-the-base-url-in-ZF2-MVC-td3946284.html. As said here StaticEventManager is deprecated so I changed it with SharedEventManager :
// In my Application\Module.php
namespace Application;
use Zend\EventManager\SharedEventManager
class Module {
public function init() {
$events = new SharedEventManager();
$events->attach('bootstrap', 'bootstrap', array($this, 'registerBasePath'));
}
public function registerBasePath($e) {
$modules = $e->getParam('modules');
$config = $modules->getMergedConfig();
$app = $e->getParam('application');
$request = $app->getRequest();
$request->setBasePath($config->base_path);
}
}
}
And in my modules/Application/configs/module.config.php I add:
'base_path' => '/spam/ham/'
But it desn't work. The problems are:
1) The run never comes to the registerBasePath function. But it has to. I've attached an event with the listener in the init function.
2) When I change SharedEventManager for just EventManager it happens to come to the registerBasePath function but an exeption is thrown:
Fatal error: Call to undefined method Zend\EventManager\EventManager::getParam()
What do I do wrong? Why the run of the program doesn't come to the registerBasePath function? If this is the only way to set the basePath globally then how to do it right?
I know the documentation is lacking of these kinds of things. But you are right in the way to approach this:
Be early (so at bootstrap)
Grab the request from the application
Set the base path in the request
The docs are lacking this information and the post you refer to is quite old. The fastest and easiest way to do this is using the onBootstrap() method:
namespace MyModule;
class Module
{
public function onBootstrap($e)
{
$app = $e->getApplication();
$app->getRequest()->setBasePath('/foo/bar');
}
}
If you want to grab the base path from your config, you can load the service manager there:
namespace MyModule;
class Module
{
public function onBootstrap($e)
{
$app = $e->getApplication();
$sm = $app->getServiceManager();
$config = $sm->get('config');
$path = $config->base_path;
$app->getRequest()->setBasePath($path);
}
}
I want to write some code to run before every actions in my module. I have tried hooking onto onBootstrap() but the code run on the other modules too.
Any suggestions for me?
There are two ways to do this.
One way is to create a serice and call it in every controllers dispatch method
Use onDispatch method in controller.
class IndexController extends AbstractActionController {
/**
*
* #param \Zend\Mvc\MvcEvent $e
* #return type
*/
public function onDispatch(MvcEvent $e) {
//Call your service here
return parent::onDispatch($e);
}
public function indexAction() {
return new ViewModel();
}
}
don't forget to include following library on top of your code
use Zend\Mvc\MvcEvent;
Second method is to do this via Module.php using event on dispatch
namespace Application;
use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;
class Module {
public function onBootstrap(MvcEvent $e) {
$sharedEvents = $e->getApplication()->getEventManager()->getSharedManager();
$sharedEvents->attach(__NAMESPACE__, 'dispatch', array($this, 'addViewVariables'), 201);
}
public function addViewVariables(Event $e) {
//your code goes here
}
// rest of the Module methods goes here...
//...
//...
}
How to create simple service using ZF2
reference2
reference3