ZF3 PhpRenderer not finding template path - php

I know there are dozen of questions about PHPRenderer not finding the path of a template, but I think the problem is quite different here.
First, the goal is to render a view to a variable in order to send it to a PDF Renderer (I use ZF3 TCPDF module). If there is any better way to do that, please tell me.
Here is roughly the architecture of the project: https://imgur.com/UhQ7hgP
In AlertAction() of ToolsController, I return the view like this, and it works, which make me think the template path is alright.
$view = new ViewModel();
$view->setTemplate('tools/tools/alert');
return $view;
However, when I try to render the same view with the same path in exportPDFAction(), it does not work and gives the following error.
Zend\View\Renderer\PhpRenderer::render: Unable to render template "tools/tools/alert"; resolver could not resolve to a file
The code in exportPDFAction() is:
$view = new ViewModel();
$renderer = new PhpRenderer();
$view->setTemplate('tools/tools/alert');
$html = $renderer->render($view);
I assume the last line screws it as it is the difference, but I can't get why, does anyone have any clue ?
Quite all the topics about Template path on SO were talking about the template map in module.config.php, but I think this is not the problem here since it works perfectly in AlertAction().
EDIT
The PhpRenderer is injected in the controller directly in module.config.php:
'controllers' => [
'factories' => [
ToolsController::class => function($container) {
return new ToolsController(
$container->get(Adapter::class),
$container->get(\TCPDF::class),
$container->get(PhpRenderer::class)
);
},
],
],
EDIT 2
This is the controller constructor:
public function __construct($db, $tcpdf, $renderer)
{
$this->db = $db;
$this->tcpdf = $tcpdf;
$this->renderer = $renderer;
...
}

The error you're getting might be due to the fact your Renderer is not injected via the Factory.
Try:
class MyCustomControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var \Zend\View\Renderer\PhpRenderer $renderer */
$renderer = $container->get('ViewRenderer')
return new MyCustomController($renderer);
}
}
In the Controller, require it be set in the __construct() function:
public function __construct(PhpRenderer $renderer)
{
// ... set it somewhere, e.g.:
$this->setRenderer($renderer);
}
Then use it in your function:
$view = new ViewModel();
$renderer = $this->getRenderer();
$view->setTemplate('tools/tools/alert');
$html = $renderer->render($view);
Why, you ask?
Because the Renderer is configured via the Zend Configuration. You can find that in the \Zend\Mvc\Service\ServiceManageFactory class. The alias configuration provided is the following:
'ViewPhpRenderer' => 'Zend\View\Renderer\PhpRenderer',
'ViewRenderer' => 'Zend\View\Renderer\PhpRenderer',
'Zend\View\Renderer\RendererInterface' => 'Zend\View\Renderer\PhpRenderer',
The alias'es are mapped to Factory:
'Zend\View\Renderer\PhpRenderer' => ViewPhpRendererFactory::class,
That Factory is:
class ViewPhpRendererFactory implements FactoryInterface
{
/**
* #param ContainerInterface $container
* #param string $name
* #param null|array $options
* #return PhpRenderer
*/
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
$renderer = new PhpRenderer();
$renderer->setHelperPluginManager($container->get('ViewHelperManager'));
$renderer->setResolver($container->get('ViewResolver'));
return $renderer;
}
}
As such, it has some presets included when you use it with $this->getRenderer, namely it has the HelperPluginManager and the Resolver set. So it knows where to get additional resources (if needed) and it knows how to resolve (ie render) a View.

Related

"Call to a member function updateJob() on null"

Now, I know this question has been asked a lot, but I searched and searched but I just can't figure it out and I've been stuck for hours now. I'm really sorry if it turnes out to be a really dumb mistake (It probably will).
So, I have my Controller which instatiates the editAction() function when a button on my website is pressed. It checks if the request is a _POST request, passes on the data and checks if the input given is valid, all this works fine.
I then try to access a function in my Manager class. And that's where the error is happening and my website spits out:
"Call to a member function updateJob() on null".
Now, PhpStorm is not saying there's an error or a warning, it recognizes the jobManager class and I've checked the namespaces and class names, but all are correct. The variables are also defined correctly, as far as I can see. I'd be really thankful if someone could tell me what I am doing wrong. The code is below.
current state after adding $jobManager to __construct:
class IndexController extends AbstractActionController
{
/**
* Entity manager.
* #var EntityManager
*/
private $entityManager;
/**
* Post manager.
* #var JobManager
*/
private $jobManager;
public function __construct($entityManager, $jobManager)
{
$this->entityManager = $entityManager;
/***
* Edit from comment advice:
* I have added this line to my __construct
* But this does not solve the issue.
***/
$this->jobManager = $jobManager;
}
public function indexAction()
{
// Get recent jobs
$jobs = $this->entityManager->getRepository(Jobs::class)
->findBy(['status'=>Jobs::STATUS_READY]
);
// Render the view template
return new ViewModel([
'jobs' => $jobs
]);
}
public function editAction()
{
// Create the form.
$form = new JobForm();
// Get post ID.
$jobId = $this->params()->fromRoute('id', -1);
// Find existing job in the database.
$jobs = $this->entityManager->getRepository(Jobs::class)
->findOneById($jobId);
if ($jobs == null) {
$this->getResponse()->setStatusCode(404);
return;
}
// Check whether this job is a POST request.
if ($this->getRequest()->isPost()) {
// Get POST data.
$data = $this->params()->fromPost();
// Fill form with data.
$form->setData($data);
if ($form->isValid()) {
// Get validated form data.
$data = $form->getData();
// Use job manager service to add new post to database.
$this->jobManager->updateJob( $jobs, $data);
// Redirect the user to "backups" page.
return $this->redirect()->toRoute('backups');
}
} else {
$data = [
'id' => $jobs->getId(),
'jobName' => $jobs->getJobName(),
'status' => $jobs->getStatus(),
'vmId' => $jobs->getVmId(),
'targetfolderPrefix' => $jobs->getTargetFolderPrefix(),
'numberOfBackups' => $jobs->getNumberOfBackups(),
'lastBackupUsed' => $jobs->getLastBackupUsed(),
'repeat' => $jobs->getRepeat(),
'scheduleRunAtMinute' => $jobs->getScheduleRunAtMinute(),
'scheduleRunAtHour' => $jobs->getScheduleRunAtHour(),
'scheduleRunAtDOW' => $jobs->getScheduleRunAtDOW(),
'hostId' => $jobs->getHostId()
];
$form->setData($data);
}
// Render the view template.
return new ViewModel([
'form' => $form,
'jobs' => $jobs
]);
}
}
What is wrong
$this->jobManager->updateJob( $jobs, $data);
You are telling PHP:
"In $this class, look in the jobManager object and run the method updateJob with these variables.... "
But in $this class you have written:
/**
* Post manager.
* #var JobManager
*/
private $jobManager;
But you have nowhere set jobManager to be anything. You have no setter function in the class as well as no other function setting what a jobManager variable actually is... so jobManager can never be anything.
So what you're in effect doing is saying to PHP
"In $this class, look in the jobManager empty null-space and run the method updateJob with these variables..."
This is clearly not going to end well.
How to fix it
You need to set what jobManager is before you can use it, as referenced by Xatenev. Typically when you construct the class or using a Setter method, if preferred.
ONE:
public function __construct(EntityManager $entityManager, JobManager $jobManagerVar)
{
$this->entityManager = $entityManager;
$this->jobManager = $jobManagerVar;
}
Alternatively - if ->jobManager method needs to be defined after the object IndexController is created; then you need to use a Setter class (because the jobManager var is *private*.
Thus TWO:
class IndexController extends AbstractActionController
{
...
public function setJobManager($jobManagerVar){
$this->jobManager = $jobManagerVar
}
...
}
And then when you instantiate the IndexController you can do:
// ONE from above:
$theClass = new IndexController($entity,$jobManager);
or
// TWO from above
$theClass = new IndexController($entity);
...
$theClass->setJobManager($jobManger);
There are various other nuances as to methods of setting values in classes, I'm not going to go over them all, it will depend on what's going on in your wider project.

ZF2 - How to Listen for Events and Trigger a Service because of it?

Have been trying to learn how to implement Services because they get Triggered by a Listener. Have been doing a serious lot of reading the last few days to get it to work, but have been finding it difficult. Thus I'm thinking my understanding of the order of things might be flawed.
The use case I'm trying to get to work is the following:
Just before an Address Entity (with Doctrine, but that's not
important) gets saved (flushed), a Service must be triggered to check
if the Coordinates for the Address are set, and if not, create and
fill a new Coordinates Entity and link it to the Address. The
Coordinates are to be gotten from Google Maps Geocoding API.
Will show below what and how I'm understanding things in the hope I make myself clear. Will do it in steps to show added code in between and tell you what does and doesn't work, as far as I know.
Now, my understanding of all of the information I've gotten the last few days is this:
A Listener has to be registered with ZF2's ServiceManager. The listener "attaches" certain conditions to the (Shared)EventManager. An EventManager is unique to an object, but the SharedEventManager is 'global' in the application.
In the Address module's Module.php class I've added the following function:
/**
* #param EventInterface $e
*/
public function onBootstrap(EventInterface $e)
{
$eventManager = $e->getTarget()->getEventManager();
$eventManager->attach(new AddressListener());
}
This gets works, the AddressListener gets triggered.
The AddressListener is as follows:
use Address\Entity\Address;
use Address\Service\GoogleCoordinatesService;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Stdlib\CallbackHandler;
class AddressListener implements ListenerAggregateInterface
{
/**
* #var CallbackHandler
*/
protected $listeners;
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events)
{
$sharedEvents = $events->getSharedManager();
// Not sure how and what order params should be. The ListenerAggregateInterface docblocks didn't help me a lot with that either, as did the official ZF2 docs. So, been trying a few things...
$this->listeners[] = $sharedEvents->attach(GoogleCoordinatesService::class, 'getCoordinates', [$this, 'addressCreated'], 100);
$this->listeners[] = $sharedEvents->attach(Address::class, 'entity.preFlush', [GoogleCoordinatesService::class, 'getCoordinates'], 100);
}
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
public function addressCreated()
{
$foo = 'bar'; // This line is here to as debug break. Line is never used...
}
}
I was expecting a Listener to work as a sort-of stepping stone point to where things get triggered, based on the ->attach() functions in the function attach(...){}. However, this does not seem to work, as nothing gets triggered. Not the addressCreated() function and not the getCoordinates function in the GoogleCoordinatesService.
The code above is supposed to trigger the GoogleCoordinatesService function getCoordinates. The Service has a few requirements though, such as the presence of the EntityManager of Doctrine, the Address Entity it concerns and configuration.
To that effect, I've created the following configuration.
File google.config.php (gets loaded, checked that)
return [
'google' => [
'services' => [
'maps' => [
'services' => [
'geocoding' => [
'api_url' => 'https://maps.googleapis.com/maps/api/geocode/json?',
'api_key' => '',
'url_params' => [
'required' => [
'address',
],
'optional' => [
'key'
],
],
],
],
],
],
],
];
And in module.config.php I've registered the Service with a Factory
'service_manager' => [
'factories' => [
GoogleCoordinatesService::class => GoogleCoordinatesServiceFactory::class,
],
],
The Factory is pretty standard ZF2 stuff, but to paint a complete picture, here is the GoogleCoordinatesServiceFactory.php class. (Removed comments/typehints/etc)
class GoogleCoordinatesServiceFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator, $options = [])
{
$serviceManager = $serviceLocator->getServiceLocator();
$entityManager = $serviceManager->get(EntityManager::class);
$config = $serviceManager->get('Config');
if (isset($options) && isset($options['address'])) {
$address = $options['address'];
} else {
throw new InvalidArgumentException('Must provide an Address Entity.');
}
return new GoogleCoordinatesService(
$entityManager,
$config,
$address
);
}
}
Below is the GoogleCoordinatesService class. However, nothing ever gets triggered to executed in there. As it doesn't even gets called I'm sure the problem lies in the code above, but cannot find out why. From what I've read and tried, I'm expecting that the class itself should get called, via the Factory and the getCoordinates function should be triggered.
So, the class. I've removed a bunch of standard getters/setters, comments, docblocks and typehints to make it shorter.
class GoogleCoordinatesService implements EventManagerAwareInterface
{
protected $eventManager;
protected $entityManager;
protected $config;
protected $address;
/**
* GoogleCoordinatesServices constructor.
* #param EntityManager $entityManager
* #param Config|array $config
* #param Address $address
* #throws InvalidParamNameException
*/
public function __construct(EntityManager $entityManager, $config, Address $address)
{
$this->config = $config;
$this->address = $address;
$this->entityManager = $entityManager;
}
public function getCoordinates()
{
$url = $this->getConfig()['api_url'] . 'address=' . $this->urlFormatAddress($this->getAddress());
$response = json_decode(file_get_contents($url), true);
if ($response['status'] == 'OK') {
$coordinates = new Coordinates();
$coordinates
->setLatitude($response['results'][0]['geometry']['location']['lat'])
->setLongitude($response['results'][0]['geometry']['location']['lng']);
$this->getEntityManager()->persist($coordinates);
$this->getAddress()->setCoordinates($coordinates);
$this->getEntityManager()->persist($this->getAddress());
$this->getEntityManager()->flush();
$this->getEventManager()->trigger(
'addressReceivedCoordinates',
null,
['address' => $this->getAddress()]
);
} else {
// TODO throw/set error/status
}
}
public function urlFormatAddress(Address $address)
{
$string = // format the address into a string
return urlencode($string);
}
public function getEventManager()
{
if ($this->eventManager === null) {
$this->setEventManager(new EventManager());
}
return $this->eventManager;
}
public function setEventManager(EventManagerInterface $eventManager)
{
$eventManager->addIdentifiers([
__CLASS__,
get_called_class()
]);
$this->eventManager = $eventManager;
return $this;
}
// Getters/Setters for EntityManager, Config and Address
}
So, that's the setup to handle it when a certain event gets triggered. Now it should, of course, get triggered. For this use case I've setup a trigger in the AbstractActionController of my own (extends ZF2's AbstractActionController). Doing that like so:
if ($form->isValid()) {
$entity = $form->getObject();
$this->getEntityManager()->persist($entity);
try {
// Trigger preFlush event, pass along Entity. Other Listeners can subscribe to this name.
$this->getEventManager()->trigger(
'entity.preFlush',
null,
[get_class($entity) => $entity] // key = "Address\Entity\Address" for use case
);
$this->getEntityManager()->flush();
} catch (\Exception $e) {
// Error thrown
}
// Success stuff, like a trigger "entity.postFlush"
}
So yea. At the moment at a bit of a loss on how to get it working.
Any help would be very much appreciated and would love explanations as to the "why" of it is that a solution works. That would really help me out making more of these services :)
Been at it for a while, but have managed to figure out why it was not working. I was attaching Listeners to EventManagers, but should have been attaching them to the SharedEventManager. This is because I have the triggers (in this instance) in the AbstractActionController, thus they all create their own EventManager (as they're unique) when instantiated.
Has been a tough few days wrapping my head around it all, but this article helped me out most, or perhaps it just made things click with my original research in the question and subsequent trial & error + debugging.
Below the code as it is now, in working order. I'll try to explain along as the code comes as to how I understand that it works. If I get it wrong at some point I hope someone corrects me.
First up, we need a Listener, a class which registers components and events to "listen" for them to trigger. (They listen for certain (named) objects to trigger certain events)
The realization quickly came that pretty much every Listener would need the $listeners = []; and the detach(EventManagerInterface $events){...} function. So I created an AbstractListener class.
namespace Mvc\Listener;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
/**
* Class AbstractListener
* #package Mvc\Listener
*/
abstract class AbstractListener 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]);
}
}
}
}
After the above mentioned realization about having to use the SharedEventManager and with the AbstractListener created, the AddressListener class has ended up like so.
namespace Address\Listener;
use Address\Event\AddressEvent;
use Admin\Address\Controller\AddressController;
use Mvc\Listener\AbstractListener;
use Zend\EventManager\EventManagerInterface;
/**
* Class AddressListener
* #package Address\Listener
*/
class AddressListener extends AbstractListener
{
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(AddressController::class, 'entity.postPersist', [new AddressEvent(), 'addCoordinatesToAddress']);
}
}
The main difference with attaching events to EventManager versus the SharedEventManager is that the latter listens for a specific class to emit a trigger. In this instance it will listen for the AddressController::class to emit the trigger entity.postPersist. Upon "hearing" that it's triggered it will call a callback function. In this case that is registered with this array parameter: [new AddressEvent(), 'addCoordinatesToAddress'], meaning that it will use the class AddressEvent and the function addCoordinatesToAddress.
To test if this works, and if you're working along with this answer, you can create the trigger in your own Controller. I've been working in the addAction of the AbstractActionController, which gets called by the addAction of the AddressController. Below the trigger for the Listener above:
if ($form->isValid()) {
$entity = $form->getObject();
$this->getEntityManager()->persist($entity);
$this->getEventManager()->trigger(
'entity.postPersist',
$this,
[get_class($entity) => $entity]
);
try {
$this->getEntityManager()->flush();
} catch (\Exception $e) {
// Error stuff
}
// Remainder of function
}
The ->trigger() function in the above code shows the usage of the following parameters:
'entity.postPersist' - This is the event name
$this - This is the "component" or object the event is called for. In this instance it will be Address\Controller\AddressController
[get_class($entity) => $entity] - These are parameters to send along with this Event object. It will cause you to have available $event->getParams()[Address::class] which will have the $entity value.
The first two parameters will trigger the Listener in the SharedEventManager. To test if it all works, it's possible to modify the Listener's attach function.
Modify it to this and create a function within the the Listener so you can see it working:
public function attach(EventManagerInterface $events)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(AddressController::class, 'entity.postPersist', [$this, 'test']);
}
public function test(Event $event)
{
var_dump($event);
exit;
}
Lastly, to make sure that the above actually works, the Listener must be registered with the EventManager. This happens in the onBootstrap function in the Module.php file of the module (Address in this case). Register like below.
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$eventManager->attach(new AddressListener());
}
If you debug the code of the addAction in the AbstractActionController, see it pass the trigger and next you're in the test function, then your Listener works.
The above code also implies that the AddressListener class can be used to attach more than one listener. So you could also register stuff for entity.prePersist, entity.preFlush, entity.postFlush and anything else you can think of.
Next up, revert the Listener back to what it was at the beginning (revert the attach function and remove the test function).
I also noticed that pretty much every Event handling class would need to be able to set and get the EventManager. Thus, for this I've created an AbstractEvent class, like below.
namespace Mvc\Event;
use Zend\EventManager\EventManager;
use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerInterface;
abstract class AbstractEvent implements EventManagerAwareInterface
{
/**
* #var EventManagerInterface
*/
protected $events;
/**
* #param EventManagerInterface $events
*/
public function setEventManager(EventManagerInterface $events)
{
$events->setIdentifiers([
__CLASS__,
get_class($this)
]);
$this->events = $events;
}
/**
* #return EventManagerInterface
*/
public function getEventManager()
{
if (!$this->events) {
$this->setEventManager(new EventManager());
}
return $this->events;
}
}
To be honest, I'm not quite sure why we set 2 identifiers in the setEventManager function. But suffice to say that it's used to register callbacks for Events. (this could use more/detailed explanation if someone feels so inclined as to provide it)
In the AddressListener we're trying to call the addCoordinatesToAddress function of the AddressEvent class. So we're going to have to create that, I did it like below.
namespace Address\Event;
use Address\Entity\Address;
use Address\Service\GoogleGeocodingService;
use Country\Entity\Coordinates;
use Mvc\Event\AbstractEvent;
use Zend\EventManager\Event;
use Zend\EventManager\Exception\InvalidArgumentException;
class AddressEvent extends AbstractEvent
{
public function addCoordinatesToAddress(Event $event)
{
$params = $event->getParams();
if (!isset($params[Address::class]) || !$params[Address::class] instanceof Address) {
throw new InvalidArgumentException(__CLASS__ . ' was expecting param with key ' . Address::class . ' and value instance of same Entity.');
}
/** #var Address $address */
$address = $params[Address::class];
if (!$address->getCoordinates() instanceof Coordinates) {
/** #var GoogleGeocodingService $geocodingService */
$geocodingService = $event->getTarget()->getEvent()->getApplication()->getServiceManager()->get(GoogleGeocodingService::class);
$geocodingService->addCoordinatesToAddress($address);
}
$params = compact('address');
$this->getEventManager()->trigger(__FUNCTION__, $this, $params);
}
}
In the above you can see that first we check if the parameter we expect has been passed along with the Event $event parameter. We know what we should expect and what name the key should have, so we check explicitly.
Next we check if the received Address Entity object already has a Coordinates object associated with it, if it doesn't, we call a Service to make it happen.
After the if() statement has run, we fire another trigger. We pass along this Event object and the parameters. This last step is not required, but can be handy if you wish to chain events.
In the question I mentioned a use case. The above code enables the Service (GoogleGeocodingService) to get passed the it's requirements and combined with the configuration for the Factory, it gets created via Zend Magic with the ServiceManager.
The code to add a new Coordinates object to the existing Address object was not modified, so I won't make it part of the answer, you can find that in the question.

How to access service locator object inside a controller in ZF3?

I am trying to access service locator object inside my controller but unable to do this.
I tried online help but most of them are following approach for ZF2
Previously Servicelocator access in zf2 was a breeze, I just had to do $this->getServiceLocator();
I have tried creating factory Class for my controller and Created createService method there but it says I have to implement __invoke() method too.
My Objective is to do something like this
public function getPropertyTable()
{
if (!$this->PropertyTable) {
$sm = $this->getServiceLocator();
$this->PropertyTable = $sm->get('Application\Model\PropertyTable');
}
return $this->PropertyTable;
}
Can anyone provide me a complete steps to achieve this?
I have tried to implement almost all Answers related to Servicelocator before asking this question, so please help me before marking this as Duplicate or something, ignore the typos
Thanks everyone to tell me I am doing it the wrong way. Some more research on this topic helped me to get my issue resolved here is what I have done to solve it
Create Factory class for your Controller
You have to create a factory class for your controller which will implement FactoryInterface of zend.
in there you have to call
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new ListController($container->get(PropertyInterface::class));
}
here I am passing PropertyInterface refrence which is implemented by my another table called Property Table in which I have given body to all my inteface functions for model
like searchProperty()
Add factory class in config file module.config.php for your controller
instructing config file to let our factory create the object for our controller
'controllers' => [
'factories' => [
// Update the following line:
Controller\ListController::class => Factory\ListControllerFactory::class,
],
],
Register your Model in config file
You have to add new section for service manager and provide your model classes there.
// Add this section:
'service_manager' => [
'aliases' => [
Model\PropertyInterface::class => Model\PropertyTable::class,
],
'factories' => [
Model\PropertyTable::class => InvokableFactory::class,
],
],
Now only thing left is add functions in your PropertyInterface and Implentation in PropertyTable for them and then call them in your controller
This Complete Steps for implementation Helped me in implementing the new flow.
Thanks to community. You all are best.
As long as the new factory interface in ZF3 is:
interface FactoryInterface
{
/**
* Create an object
*
* #param ContainerInterface $container
* #param string $requestedName
* #param null|array $options
* #return object
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null);
}
you have to implements your factory like this.
For helping you, you can use that link
EDIT :
In practise you should have this :
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$propertyTable = $container->get('Application\Model\PropertyTable');
return new Controller($propertyTable);
}
Another way
/**
* Retrieve service manager instance
* #throws \Psr\Container\ContainerExceptionInterface
* #throws \Psr\Container\NotFoundExceptionInterface
* #return ContainerInterface
*/
public function getServiceLocator()
{
return $this->getEvent()->getApplication()->getServiceManager();
}
In ZF3 it is not recommended to pass service locator into controller.
You need to get all your dependencies from $container inside factory of the controller and pass they into the controller through the constructor (or setters).

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!

ZF2: How to get Zend\Navigation inside custom route?

I have custom router and I have to get access to Zend\Navigation inside this custom router. I was googling, asking and searching and no results :/
All I need is to find nodes with 'link' param using Zend\Navigation in my Alias::match function.
Here is my module.config.php:
'navigation' => array(
'default' => array(
'account' => array(
'label' => 'Account',
'route' => 'node',
'pages' => array(
'home' => array(
'label' => 'Dashboard',
'route' => 'node',
'params' => array(
'id' => '1',
'link' => '/about/gallery'
),
),
),
),
),
),
[...]
And here is my Alias class:
// file within ModuleName/src/ModuleName/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class Alias extends Http\Segment implements ServiceLocatorAwareInterface
{
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function match(Request $request, $pathOffset = null)
{
[...]
return parent::match($request, $pathOffset);
}
}
EDITED:
Now i know that I should inject service manager into my custom router. Let me know if You know how to do this :)
EDITED:
Ok, its not custom router but route. My bad. I was talking on #zftalk irc chanell and AliasSegment class should implements ServiceLocatorAwareInterface. Ok I've tried it but now there is another problem.
In setServiceLocator function i can't get service locator. It returns null object, however $serviceLocator is class Zend\Mvc\Router\RoutePluginManager.
public function setServiceLocator(ServiceLocatorInterface $serviceLocator){
$sl = $serviceLocator->getServiceLocator();
var_dump($sl); // NULL
}
Any ideas how to get Zend navigation from it ?
EDITED
Corresponding to what #mmmshuddup said, I've changed my custom router class. (New version is above). Also in my Module.php, within onBootstrap function, I added this line:
$sm->setFactory('Navigation', 'Zend\Navigation\Service\DefaultNavigationFactory', true);
Navigation works and its instantiated before route so it should be visible within my Alias class but it's not.
I've put into my match function in Alias class this line:
$servicesArray = $this->getServiceLocator()->getRegisteredServices();
and $servicesArray is almost empty. There is no service, no factories. The same line inserted into onBootstrap, just after setting new factory (as above) returns array with navigation and other services.
The question is: how can i share this array (or ServiceManager) with my custom router: Alias ?
I have to say that all I want to do was possible in ZF1 and it was quite easy.
EDIT
I found a solution. The answer is below
That is because the object itself really doesn't have any properties declared. But if you do this:
echo get_class($sl);
You will see that it is indeed an instance of Zend\ServiceManager\ServiceManager
You should be able to get your navigation instance by doing something like:
$nav = $sl->get('Navigation');
EDIT:
I just notice you have some stuff in the wrong location of your code. You're calling getServiceLocator() on $serviceLocator which is already the instance of that. Also you're calling it within setServiceLocator(). You should change it to:
// EDIT - file within ModuleName/src/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
class Alias extends Http\Segment implements ServiceLocatorAwareInterface
{
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function match(Request $request, $pathOffset = null)
{
$nav = $this->getServiceLocator()->get('Navigation');
// ...
return parent::match($request, $pathOffset);
}
}
I found the solution but this is NOT elegant solution i think. However everything works perfectly. If somebody knows disadvantages of this solution, please comment this answer or add another, better. I had to modify #mmmshuddup's idea (you can read the conversation).
First of all, the implementation of ServiceLocatorAwareInterface in custom route class is no more necessary.
In Module.php within onBootstrap function:
$app = $e->getApplication();
$sm = $app->getServiceManager();
$sm->get('translator');
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$sm->setFactory('Navigation',
'Zend\Navigation\Service\DefaultNavigationFactory', true);
$nav = $sm->get('Navigation');
$alias = $sm->get('Application\Router\Alias');
$alias->setNavigation($nav);
First we instantiate Navigation factory in ServiceManager and then our custom route. After that we can pass Navigation class into custom route using setNavigation function.
To complete instantiate of our custom route we need in getServiceConfig in the same file:
return array(
'factories' => array(
'Application\Router\Alias' => function($sm) {
$alias = new \Application\Router\Alias('/node[/:id]');
return $alias;
},
'db_adapter' => function($sm) {
$config = $sm->get('Configuration');
$dbAdapter = new \Zend\Db\Adapter\Adapter($config['db']);
return $dbAdapter;
},
)
);
And here is a tricky part. This instance is temporary. While routing, this class will be instantiated one more time and this is why, I think, it's not very elegant. We have to insert parameter into constructor however at this moment value of this parameter is not important.
The custom route class:
// file within ModuleName/src/ModuleName/Router/Alias.php
namespace Application\Router;
use Traversable;
use Zend\Mvc\Router\Exception;
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Mvc\Router\Http;
class Alias extends Http\Segment
{
private static $_navigation = null;
public function match(Request $request, $pathOffset = null)
{
//some logic here
//get Navigation
$nav = self::$_navigation;
return parent::match($request, $pathOffset);
}
public function setNavigation($navigation){
self::$_navigation = $navigation;
}
}
Because first instance is temporary, we have to collect our Navigation class in static variable. It's awful but works nice. Maybe there is a way to instantiate it only once and in route configuration get instance of it, but at this moment this is best answer for my question. Simply enough and working correctly.

Categories