I have a CategoryController with a fetchSingle (return 1 record) and fetchAll (returns all) method from dbase.
Now i want to reuse de fetchall method for a Navigation.
What's the best approach?
Following code:
Module.php
public function getServiceConfig()
{
return array(
'factories' => array(
/*
* Category Table
*/
'CategoryTableGateway' => function ($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$resultSetPrototype = new ResultSet();
return new TableGateway('category', $dbAdapter, null, $resultSetPrototype);
},
'CategoryTable' => function($sm) {
$tableGateway = $sm->get('CategoryTableGateway');
$table = new Model\CategoryTable($tableGateway);
return $table;
},
),
);
}
CategoryController:
namespace Front\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class CategoryController extends AbstractActionController
{
protected $categoryTable;
function indexAction() {
//$slug = $this->getEvent()->getRouteMatch()->getParam('slug');
$this->layout()->setVariable('metatitle', 'title1');
$this->layout()->setVariable('metadescription', 'description2');
$this->layout()->setVariable('metakeywords', 'keywords3');
return new ViewModel(array(
'category' => $this->getCategories(),
));
}
public function getCategory( $slug ){
return $this->getTable()->getCategory($slug);
}
public function getCategories(){
return $this->getTable()->getCategories();
}
public function getTable()
{
if (!$this->categoryTable) {
$sm = $this->getServiceLocator();
$this->categoryTable = $sm->get('CategoryTable');
}
return $this->categoryTable;
}
}
CategoryModel:
namespace Front\Model;
use Zend\Db\TableGateway\TableGateway;
class CategoryTable {
protected $tableGateway;
public function __construct(TableGateway $tableGateway)
{
$this->tableGateway = $tableGateway;
}
public function getCategory($slug)
{
$resultSet = $this->tableGateway->select(array('name' => $slug));
$resultSet = $resultSet->toArray();
return $resultSet[0];
}
public function getCategories()
{
$resultSet = $this->tableGateway->select();
$resultSet = $resultSet->toArray();
return $resultSet;
}
}
If you want to have a navigation element on your side, you can start with a custom view heler. The helper will load the categories and construct a navigation menu.
A view helper can look like this:
namespace MyApp\View\Helper;
use MyApp\Repository\CategoryRepository;
use Zend\Navigation\Navigation;
use Zend\View\Helper\AbstractHelper;
class CategoryMenu
{
protected $repository;
public function __construct(CategoryRepository $repository)
{
$this->repository = $repository
}
public function __invoke(array $options = array)
{
$categories = $this->repository->fetchAll();
$container = $this->buildNavigation($categories);
$navigation = $this->getView()->navigation();
return $navigation->menu()->renderMenu($container, $options);
}
private function buildNavigation(array $categories)
{
$pages = [];
foreach ($categories as $category) {
$pages[] = [
'label' => $category->getName(),
'route' => 'categories/view',
'params' => ['id' => $category->getId()]
];
}
return new Navigation($pages);
}
}
This helper requires a repository (do not use fetchSingle() or fetchAll() in controllers, they are mapper / repository methods!). When you invoke the helper, it fetches all categories from the database, builds a navigation object and calls the navigation view helper to render it into a menu.
I am not writing all glueing parts down, but you require a decent repository so you can access the database and a factory for this view helper. Then register the view helper in your module configuration.
The $options from __invoke() are passed onto the menu helper, so you can apply options there (like, the ulClass, min/max depth etc). Usage: <div><?= $this->categoryMenu([])?></div>.
Note I tried to link all categories to a page, but I was missing that information from your question. I assume here the route categories/view exists and it requires a parameter id for the category id. Update the buildNavigation() method to suit it your own needs.
If you aiming the best approach, your controllers shouldn't fetch anything from database. This is a service (better) data layer (best) job. You need to consume that service in controller while consuming fetch/fetchAll methods of the repository (data layer) in service.
When you move that fetch and fetchAll logic into dedicated classes as dedicated methods, you'll easily figure out how to reuse them and question becomes "How can i inject my x service to my CategoryController?".
I suggest you focus on reading few articles on topics like separating of concerns.
Related
I'm trying to create a View Helper to "inject" a database value to my layout.phtml. It's result with a simple string but when I call a table gateway it's not result and not load the other html.
//Conversa/src/View/Helper/Conversas.php
namespace Conversa\View\Helper;
use Conversa\Model\ConversaTable;
use Zend\View\Helper\AbstractHelper;
class Conversas extends AbstractHelper
{
protected $sm;
protected $mensagemTable;
protected $conversaTable;
public function __construct($sm)
{
$this->sm = $sm;
}
public function __invoke()
{
$id = $_SESSION['id_utilizador'];
//$conversas = $this->getConversaTable()->conversasUtilizadorAll($id);
//$conversaTable = new ConversaTable();
$c = $this->getConversaTable()->fetchAll(); // <-When I call this, it doesn't work anymore
$output = sprintf("I have seen 'The Jerk' %d time(s).", $this->conversaTable);
return htmlspecialchars($output, ENT_QUOTES, 'UTF-8');
}
public function getConversaTable()
{
if (!$this->conversaTable) {
$sm = $this->getServiceLocator();
$this->conversaTable = $sm->get('Conversa\Model\ConversaTable');
}
return $this->conversaTable;
}
public function getMensagemTable()
{
if (!$this->mensagemTable) {
$sm = $this->getServiceLocator();
$this->mensagemTable = $sm->get('Mensagem\Model\MensagemTable');
}
return $this->mensagemTable;
}
}
Module.php
public function getViewHelperConfig()
{
return array(
'factories' => array(
'conversas' => function ($sm) {
$helper = new View\Helper\Conversas;
return $helper;
}
)
);
}
Not much to go on here since you didn't include any info about what happens (error message?), however, your view helper factory doesn't look right. Your view helper constructor method has a required argument for the Service Manager, so you need to pass that:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'conversas' => function ($sm) {
$helper = new View\Helper\Conversas($sm);
return $helper;
}
)
);
}
Also, since your view helper requires conversaTable, it might be better to pass that to the view helper instead of the service manager (as the service locator functionality you're relying on was removed in ZF3).
I've made a global variable in bootstrap of Module.php
public function setCashServiceToView($event) {
$app = $event->getParam('application');
$cashService = $app->getServiceManager()->get('Calculator/Service/CashServiceInterface');
$viewModel = $event->getViewModel();
$viewModel->setVariables(array(
'cashService' => $cashService,
));
}
public function onBootstrap($e) {
$app = $e->getParam('application');
$app->getEventManager()->attach(\Zend\Mvc\MvcEvent::EVENT_RENDER, array($this, 'setCashServiceToView'), 100);
}
I can use it inside of my layout.phtml as
$this->cashService;
But I need this variable to use in my partial script of navigation menu, which I call in layout.phtml:
echo $this->navigation('navigation')
->menu()->setPartial('partial/menu')
->render();
?>
How can I use it inside of my partial/menu.phtml? And may be there is a better way, than to declare it in onBootstrap function?
Thank you for your answers. I decided to make an extended class of \Zend\View\Helper\Navigation\Menu to provide there a property of cashService. However I receive an error:'Zend\View\Helper\Navigation\PluginManager::get was unable to fetch or create an instance for Calculator\Service\CashServiceInterface'.
I need this service to display navigation menu. Seems weird, but that's true. I display some diagram in it, using the data, which I get from the service. So why do I have the error?
I added to module.config.php
'navigation_helpers' => array(
'factories' => array(
'mainMenu' => 'Calculator\View\Helper\Factory\MainMenuFactory'
),
MainMenuFactory:
namespace Calculator\View\Helper\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Calculator\View\Helper\Model\MainMenu;
Class MainMenuFactory implements FactoryInterface {
/**
* Create service
*
* #param ServiceLocatorInterface $serviceLocator
* #return mixed
*/
public function createService(ServiceLocatorInterface $serviceLocator) {
return new MainMenu(
$serviceLocator->get('Calculator\Service\CashServiceInterface')
);
}
P.S: CashServiceInterface is an alias to CashServiceFactory
You could remove the event listener and use a custom view helper to access the service in the view.
namespace Calculator\View\Helper;
use Zend\View\Helper\AbstractHelper;
class CashService extends AbstractHelper
{
protected $cashService;
public function __construct(CashServiceInterface $cashService)
{
$this->cashService = $cashService;
}
public function __invoke()
{
return $this->cashService;
}
}
Create a factory.
namespace Calculator\View\Helper;
class CashServiceFactory
{
public function __invoke($viewPluginManager)
{
$serviceManager = $viewPluginManager->getServiceLocator();
$cashService = $serviceManager->get('Calculator\\Service\\CashServiceInterface');
return new CashService($cashService);
}
}
Register the new helper in moudle.config.php.
'view_helpers' => [
'factories' => [
'CashService' => 'Calculator\View\Helper\CashServiceFactory',
],
],
Then you can use the plugin in all view scripts.
$cashService = $this->cashService();
I have some services defined on my module.php that works as intended declared as:
public function getServiceConfig()
{
return array(
'factories' => array(
'Marketplace\V1\Rest\Service\ServiceCollection' => function($sm) {
$tableGateway = $sm->get('ServiceCollectionGateway');
$table = new ServiceCollection($tableGateway);
return $table;
},
'ServiceCollectionGateway' => function ($sm) {
$dbAdapter = $sm->get('PdoAdapter');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new ServiceEntity());
return new TableGateway('service', $dbAdapter, null, $resultSetPrototype);
},
'Marketplace\V1\Rest\User\UserCollection' => function($sm) {
$tableGateway = $sm->get('UserCollectionGateway');
$table = new UserCollection($tableGateway);
return $table;
},
'UserCollectionGateway' => function ($sm) {
$dbAdapter = $sm->get('PdoAdapter');
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new UserEntity());
return new TableGateway('user', $dbAdapter, null, $resultSetPrototype);
},
),
);
}
I use them to map my db tables to an object. From my main classes of my project I can access them without any problem. Take a look of my project file tree:
For example, userResource.php extends abstractResource and this function works:
public function fetch($id)
{
$result = $this->getUserCollection()->findOne(['id'=>$id]);
return $result;
}
Inside ResourceAbstract I have:
<?php
namespace Marketplace\V1\Abstracts;
use ZF\Rest\AbstractResourceListener;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class ResourceAbstract extends AbstractResourceListener implements ServiceLocatorAwareInterface {
protected $serviceLocator;
public function getServiceCollection() {
$sm = $this->getServiceLocator();
return $sm->get('Marketplace\V1\Rest\Service\ServiceCollection');
}
public function getUserCollection() {
$sm = $this->getServiceLocator();
return $sm->get('Marketplace\V1\Rest\User\UserCollection');
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
$this->serviceLocator = $serviceLocator;
}
public function getServiceLocator() {
return $this->serviceLocator;
}
}
There as suggested by the Zf2 documentation I need to implement ServiceLocatorAwareInterface in order to use the serviceManager. So far so good. Then I decided to add a new class, call Auth.
This class is not very different from abstractResource, it gets call in loginController like this:
<?php
namespace Marketplace\V1\Rpc\Login;
use Zend\Mvc\Controller\AbstractActionController;
use Marketplace\V1\Functions\Auth;
class LoginController extends AbstractActionController
{
public function loginAction()
{
$auth = new Auth();
$data = $this->params()->fromPost();
var_dump($auth->checkPassword($data['email'], $data['password']));
die;
}
}
This is Auth:
<?php
namespace Marketplace\V1\Functions;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class Auth implements ServiceLocatorAwareInterface {
protected $serviceLocator;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
$this->serviceLocator = $serviceLocator;
}
public function getServiceLocator() {
return $this->serviceLocator;
}
public function checkPassword($email, $rawPassword) {
$user = $this->getServiceLocator()->get('Marketplace\V1\Rest\User\UserCollection')->findByEmail($email);
if($user)
return false;
$result = $this->genPassword($rawPassword, $user->salt);
if($result['password'] === $user->password)
return true;
else
return false;
}
public function genPassword($rawPassword, $salt = null) {
if(!$salt)
$salt = mcrypt_create_iv(22, MCRYPT_DEV_URANDOM);
$options = [
'cost' => 11,
'salt' => $salt,
];
return ['password' => password_hash($rawPassword, PASSWORD_BCRYPT, $options), 'salt' => bin2hex($salt)];
}
}
As you can see it follows the same path that abtractResource do, BUT, in this case when I execute loginController I get an error:
Fatal error</b>: Call to a member function get() on null in C:\WT-NMP\WWW\MarketPlaceApi\module\Marketplace\src\Marketplace\V1\Functions\Auth.php on line 25
And that refers to this line: $user = $this->getServiceLocator()->get('Marketplace\V1\Rest\User\UserCollection')->findByEmail($email);
Meaning that getServiceLocator is empty. Why I cant get serviceLocator to work on Auth class but I can in abstractResource?
That's because the ServiceLocator is injected through a mechanism called 'setter injection'. For that to happen, something (e.g., ServiceManager) needs to call a setter for a class, in this case setServiceLocator. When you directly instantiate Auth that's not the case. You need to add your class to the service locator, for example as an invokable service.
E.g.:
public function getServiceConfig()
{
return array(
'invokables' => array(
'Auth' => '\Marketplace\V1\Functions\Auth',
),
);
}
or, more appriopriatly as it doesn't use a anonymous function for a factory, put it in your modules config file `modules/Marketplace/config/module.config.php':
// ...
'service_manager' => array(
'invokables' => array(
'Auth' => '\Marketplace\V1\Functions\Auth',
),
),
and the you can get Auth from the service locator in your controller:
$auth = $this->getServiceLocator()->get('Auth');
instead of:
$auth = new Auth;
This way the service locator will construct Auth for you, check what interfaces it implements and when it finds out that it does implement ServiceLocatorAwareInterface then it'll run the setter passing an instance of itself into it. Fun fact: the controller itself gets injected an instance of service locator the same way (it's an ancestor of this class implementing the very same interface). Another fun fact: that behaviour may change in future as discussed here.
What I really dislike in ZF2 is that Controller is aware of storage engine (This is a clear violation of SRP) and that a storage engine has a concept of Tables. I believe that this is not correct way, and Controller should only be aware of services (while only services should be aware of Storage engine)
class AlbumController extends AbstractActionController
{
protected $albumTable;
public function getAlbumTable()
{
if (!$this->albumTable) {
$sm = $this->getServiceLocator();
$this->albumTable = $sm->get('Album\Model\AlbumTable');
}
return $this->albumTable;
}
Nowhere in manual I could find on how to put that into a Service and make controller only aware of actions. How would you put that into a service?
I know that's how it's done in the official tutorial, but in my opinion it's not the best approach. Instead you want to inject your dependencies into your controller class via. its constructor. This makes it easier to see what's going on, and easier to test.
To do this, modify your controller class to add an appropriate constructor:
class AlbumController extends AbstractActionController
{
protected $albumTable;
public function __construct(AlbumTable $albumTable)
{
$this->albumTable = $albumTable;
}
}
Then, remove the invokable line in your module.config.php for this controller, since it can no longer just be instantiated without any arguments. Instead, you define a factory to tell ZF how to instantiate the class. In your Module.php:
use Zend\Mvc\Controller\ControllerManager;
use Album\Controller\AlbumController;
class Module
{
public function getControllerConfig()
{
return array(
'factories' => array(
'Album\Controller\Album' => function(ControllerManager $cm) {
$sm = $cm->getServiceLocator();
$albumTable = $sm->get('Album\Model\AlbumTable');
$controller = new AlbumController($albumTable);
return $controller;
},
),
);
}
}
(Alternatively you could create a separate factory class to do this.)
In your controller actions you can then access the album table via. $this->albumTable instead of $this->getAlbumTable().
Hopefully you can see that this approach can easily be modified to inject a service class instead. If you want your album table injected into the service, and the service injected into the controller; you might end up with something like this:
class Module
{
public function getServiceConfig()
{
return array(
'factories' => array(
'Album\Model\AlbumTable' => function($sm) {
$tableGateway = $sm->get('AlbumTableGateway');
$table = new AlbumTable($tableGateway);
return $table;
},
'AlbumTableGateway' => function($sm) {
[etc...]
},
'Album\Service\AlbumService' => function($sm) {
$albumTable = $sm->get('Album\Model\AlbumTable');
return new AlbumService($albumTable);
}
),
);
}
public function getControllerConfig()
{
return array(
'factories' => array(
'Album\Controller\Album' => function(ControllerManager $cm) {
$sm = $cm->getServiceLocator();
$albumService = $sm->get('Album\Service\AlbumService');
$controller = new AlbumController($albumService);
return $controller;
},
),
);
}
}
Controller:
class AlbumController extends AbstractActionController
{
protected $albumService;
public function __construct(AlbumService $albumService)
{
$this->albumService = $albumService;
}
public function someAction()
{
// do stuff with $this->albumService
}
}
I found a few other posts relevant to this issue, however i wasn't able to achieve what i wanted so i decided to delete everything and start over with some help...
This is my work so far, which does the job but the data are provided hard coded in an array and i need to create a database connection to fetch those data.
In my module class i have:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'liveStreaming' => function() {
return new LiveStreaming();
},
),
);
}
This is the code i have in my view helper:
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
class LiveStreaming extends AbstractHelper
{
protected $liveStreamingTable;
public function __invoke()
{
$events = array(
'1' => array('name' => 'Event name',
'sport' => 'Soccer',
'time' => '11:30'),
'2' => array('name' => 'Event name',
'sport' => 'Soccer',
'time' => '17:00'),
);
return $events;
//this is what should be used (or something like that) to get the data from the db...
//return array('events' => $this->getLiveStreamingTable()->fetchAll() );
}
public function getLiveStreamingTable()
{
if (!$this->liveStreamingTable) {
$sm = $this->getServiceLocator();
$this->liveStreamingTable = $sm->get('LiveStreaming\Model\LiveStreamingTable');
}
return $this->liveStreamingTable;
}
}
So, i want to get the $events array from the database. I've created Application\Model\LiveStreaming and Application\Model\LiveStreamingTable (following the instructions of the ZF2 official tutorial) and i need some help proceeding to the next step, which should probably have to do with the service locator.
You seem to be almost there. The only thing missing is the ability to call $this->getServiceLocator(); from within the view helper (as the AbstractHelper doesn't provide this method).
There are two options
Inject the LiveStreamingTable into the view helper directly
inject the ServiceManager itself and create the LiveStreamingTable within the helper
Option 1 Make LiveStreamingTable a dependency of the view helper (type hint in constructor)
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use LiveStreaming\Model\LiveStreamingTable;
class LiveStreaming extends AbstractHelper
{
protected $liveStreamingTable;
public function __construct(LiveStreamingTable $liveStreamingTable)
{
$this->liveStreamingTable = $liveStreamingTable;
}
public function getLiveStreamingTable()
{
return $this->liveStreamingTable;
}
}
And the factory becomes:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'liveStreaming' => function($sl) {
// Get the shared service manager instance
$sm = $sl->getServiceLocator();
$liveStreamingTable = $sm->get('LiveStreaming\Model\LiveStreamingTable');
// Now inject it into the view helper constructor
return new LiveStreaming($liveStreamingTable);
},
),
);
}
Option 2 - Implement the ServiceLocatorAwareInterface (making it again a dependency of the view helper)
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LiveStreaming extends AbstractHelper implements ServiceLocatorAwareInterface
{
protected $serviceLocator;
protected $liveStreamingTable;
public function __construct(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator);
public function getServiceLocator();
public function getLiveStreamingTable()
{
if (null == $this->liveStreamingTable) {
$this->liveStreamingTable = $this->getServiceLocator()->get('LiveStreaming\Model\LiveStreamingTable');
}
return $this->liveStreamingTable;
}
}
Your factory will then look like:
public function getViewHelperConfig()
{
return array(
'factories' => array(
'liveStreaming' => function($sl) {
// Get the shared service manager instance
$sm = $sl->getServiceLocator();
// Now inject it into the view helper constructor
return new LiveStreaming($sm);
},
),
);
}
Personally, I feel that Option 1 makes more sense from a Dependency Injection (DI) point of view - It's clear that the LiveStreamingTable is what is needed to create the view helper.
Edit
Make sure you have the LiveStreaming\Model\LiveStreamingTable service also registered with the service manager (as we request it in the above code when we did $sm->get('LiveStreaming\Model\LiveStreamingTable');)
// Module.php
public function getServiceConfig()
{
return array(
'factories' => array(
'LiveStreaming\Model\LiveStreamingTable' => function($sm) {
// If you have any dependencies for the this instance
// Such as the database adapter etc either create them here
// or request it from the service manager
// for example:
$foo = $sm->get('Some/Other/Registered/Service');
$bar = new /Directly/Created/Instance/Bar();
return new \LiveStreaming\Model\LiveStreamingTable($foo, $bar);
},
),
);
}