Zend Framework 2 : Working with AuthenticationService and Session Db Save Handler
One of the Session SaveHandlers that Zend Framework 2 provide is DbTableGateway Save Handler that utilize Zend\Db as a session save handler. How to combine authentication service with it ? Let’s learn about it! Save Handlers themselves are decoupled from PHP’s save handler functions and are only implemented as a PHP save handler when utilized in conjunction with Zend\Session\SessionManager.
1. Preparation
a. create tables
-- table to save session data.... CREATE TABLE IF NOT EXISTS `session` ( `id` char(32) NOT NULL DEFAULT '', `name` char(32) NOT NULL DEFAULT '', `modified` int(11) DEFAULT NULL, `lifetime` int(11) DEFAULT NULL, `data` text, PRIMARY KEY (`id`,`name`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- users table CREATE TABLE IF NOT EXISTS `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(100) NOT NULL, `password` varchar(100) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1; -- users data with password = md5('admin') INSERT INTO `users` (`id`, `username`, `password`) VALUES (1, 'admin', '21232f297a57a5a743894a0e4a801fc3');
b. create a module with structure like the following ( don’t judge me I’m not explain file location again :p )
2. Create Classes
a. AuthStorage
It’s a class that extends Zend\Authentication\Storage\Session and utilize SessionManager to set Db Handler.
//filename : module/SanAuthWithDbSaveHandler/src/SanAuthWithDbSaveHandler/Storage/AuthStorage.php namespace SanAuthWithDbSaveHandler\Storage; use Zend\ServiceManager\ServiceLocatorAwareInterface; use Zend\ServiceManager\ServiceLocatorInterface; use Zend\Authentication\Storage; use Zend\Session\Config\SessionConfig; use Zend\Db\TableGateway\TableGateway; use Zend\Session\SaveHandler\DbTableGateway; use Zend\Session\SaveHandler\DbTableGatewayOptions; class AuthStorage extends Storage\Session implements ServiceLocatorAwareInterface { protected $serviceLocator; protected $namespace; public function __construct($namespace = null) { parent::__construct($namespace); $this->namespace = $namespace; } public function setDbHandler() { $tableGateway = new TableGateway('session', $this->getServiceLocator() ->get('Zend\Db\Adapter\Adapter')); $saveHandler = new DbTableGateway($tableGateway, new DbTableGatewayOptions()); //open session $sessionConfig = new SessionConfig(); $saveHandler ->open($sessionConfig->getOption('save_path'), $this->namespace); //set save handler with configured session $this->session->getManager()->setSaveHandler($saveHandler); } public function write($contents) { parent::write($contents); /** when $this->authService->authenticate(); is valid, the session automatically called write('username') in this case, i want to save data like ["storage"] => array(4) { ["id"] => string(1) "1" ["username"] => string(5) "admin" ["ip_address"] => string(9) "127.0.0.1" ["user_agent"] => string(81) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:18.0) Gecko/20100101 Firefox/18.0" }*/ if (is_array($contents) && !empty($contents)) { $this->getSessionManager() ->getSaveHandler() ->write($this->getSessionId(), \Zend\Json\Json::encode($contents)); } } public function clear() { $this->getSessionManager()->getSaveHandler()->destroy($this->getSessionId()); parent::clear(); } public function getSessionManager() { return $this->session->getManager(); } public function getSessionId() { return $this->session->getManager()->getId(); } public function setServiceLocator(ServiceLocatorInterface $serviceLocator) { $this->serviceLocator = $serviceLocator; } public function getServiceLocator() { return $this->serviceLocator; } }
b. AuthStorageFactory
Remember, that setting up service via closure is bad for performance, so we need to make a factory class for it.
//filename : module/SanAuthWithDbSaveHandler/src/SanAuthWithDbSaveHandler/Factory/Storage/AuthStorageFactory.php namespace SanAuthWithDbSaveHandler\Factory\Storage; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; use SanAuthWithDbSaveHandler\Storage\AuthStorage; class AuthStorageFactory implements FactoryInterface { public function createService(ServiceLocatorInterface $serviceLocator) { $storage = new AuthStorage('my_storage_namespace'); $storage->setServiceLocator($serviceLocator); $storage->setDbHandler(); return $storage; } }
c. AuthenticationServiceFactory
This is a Class that call AuthStorage class.
//filename : module/SanAuthWithDbSaveHandler/src/SanAuthWithDbSaveHandler/Factory/Storage/AuthenticationServiceFactory.php namespace SanAuthWithDbSaveHandler\Factory\Storage; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; use Zend\Authentication\AuthenticationService; use Zend\Authentication\Adapter\DbTable as DbTableAuthAdapter; class AuthenticationServiceFactory implements FactoryInterface { public function createService(ServiceLocatorInterface $serviceLocator) { $dbAdapter = $serviceLocator->get('Zend\Db\Adapter\Adapter'); $dbTableAuthAdapter = new DbTableAuthAdapter($dbAdapter, 'users', 'username','password', 'MD5(?)'); $authService = new AuthenticationService($serviceLocator->get('AuthStorage'), $dbTableAuthAdapter); return $authService; } }
d. LoginForm class
//filename : module/SanAuthWithDbSaveHandler/src/SanAuthWithDbSaveHandler/Form/LoginForm.php namespace SanAuthWithDbSaveHandler\Form; use Zend\Form\Form; use Zend\InputFilter; class LoginForm extends Form { public function __construct() { parent::__construct(); $this->setAttribute('method', 'post'); $this->add(array( 'name' => 'username', 'type' => 'Text', 'options' => array( 'label' => 'Username : ' ), )); $this->add(array( 'name' => 'password', 'type' => 'Password', 'options' => array( 'label' => 'Password : ' ), )); $this->add(array( 'name' => 'Loginsubmit', 'type' => 'Submit', 'attributes' => array( 'value' => 'Login', 'id' => 'Loginsubmit', ), )); $this->setInputFilter($this->createInputFilter()); } public function createInputFilter() { $inputFilter = new InputFilter\InputFilter(); //username $username = new InputFilter\Input('username'); $username->setRequired(true); $inputFilter->add($username); //password $password = new InputFilter\Input('password'); $password->setRequired(true); $inputFilter->add($password); return $inputFilter; } }
e. AuthController
This is a controller that place a Login Form and authentication service.
//filename : module/SanAuthWithDbSaveHandler/src/SanAuthWithDbSaveHandler/Controller/AuthController.php namespace SanAuthWithDbSaveHandler\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\Authentication\AuthenticationService; use Zend\View\Model\ViewModel; class AuthController extends AbstractActionController { protected $authService; //we will inject authService via factory public function __construct(AuthenticationService $authService) { $this->authService = $authService; } public function indexAction() { if ($this->authService->getStorage()->getSessionManager() ->getSaveHandler() ->read($this->authService->getStorage()->getSessionId())) { //redirect to success controller... return $this->redirect()->toRoute('success'); } $form = $this->getServiceLocator() ->get('FormElementManager') ->get('SanAuthWithDbSaveHandler\Form\LoginForm'); $viewModel = new ViewModel(); //initialize error... $viewModel->setVariable('error', ''); //authentication block... $this->authenticate($form, $viewModel); $viewModel->setVariable('form', $form); return $viewModel; } /** this function called by indexAction to reduce complexity of function */ protected function authenticate($form, $viewModel) { $request = $this->getRequest(); if ($request->isPost()) { $form->setData($request->getPost()); if ($form->isValid()) { $dataform = $form->getData(); $this->authService->getAdapter() ->setIdentity($dataform['username']) ->setCredential($dataform['password']); $result = $this->authService->authenticate(); if ($result->isValid()) { //authentication success $resultRow = $this->authService->getAdapter()->getResultRowObject(); $this->authService->getStorage()->write( array('id' => $resultRow->id, 'username' => $dataform['username'], 'ip_address' => $this->getRequest()->getServer('REMOTE_ADDR'), 'user_agent' => $request->getServer('HTTP_USER_AGENT')) ); return $this->redirect()->toRoute('success', array('action' => 'index'));; } else { $viewModel->setVariable('error', 'Login Error'); } } } } public function logoutAction() { $this->authService->getStorage()->clear(); return $this->redirect()->toRoute('auth'); } }
f. AuthControllerServiceFactory
Controller creation need a factory, so we need to create a factory for it.
//filename : module/SanAuthWithDbSaveHandler/src/SanAuthWithDbSaveHandler/Factory/Controller/AuthControllerServiceFactory.php namespace SanAuthWithDbSaveHandler\Factory\Controller; use Zend\ServiceManager\FactoryInterface; use Zend\ServiceManager\ServiceLocatorInterface; use SanAuthWithDbSaveHandler\Controller\AuthController; class AuthControllerServiceFactory implements FactoryInterface { public function createService(ServiceLocatorInterface $serviceLocator) { $authService = $serviceLocator->getServiceLocator()->get('AuthService'); $controller = new AuthController($authService); return $controller; } }
g. SuccessController
it’s a page that be redirected from AuthController. I’ve made a test only, you can check in real application. Real application should has authService that injected by EventManger in Module::onBootstrap.
//filename : module/SanAuthWithDbSaveHandler/src/SanAuthWithDbSaveHandler/Controller/SuccessController.php namespace SanAuthWithDbSaveHandler\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\Authentication\AuthenticationService; use Zend\View\Model\ViewModel; class SuccessController extends AbstractActionController { public function indexAction() { //here for test only, you should check session //for real application } }
h. login page
<?php //filename : module/SanAuthWithDbSaveHandler/view/san-auth-with-db-save-handler/auth/index.phtml $title = 'Login'; $this->headTitle($title); ?> <h1><?php echo $this->escapeHtml($title); ?></h1> <?php $form = $this->form; $form->setAttribute('action', $this->url( 'auth' )); $form->prepare(); echo $this->form()->openTag($form); echo $this->formCollection($form); echo $this->form()->closeTag(); echo $this->error;
i. success page
<?php //filename : module/SanAuthWithDbSaveHandler/view/san-auth-with-db-save-handler/success/index.phtml ?> login succeded. <a href="<?php echo $this->url('auth', array('action' => 'logout')); ?>">Logout</a>
3. Register services
We should register services at SanAuthWithDbSaveHandler/config/module.config.php
//filename : SanAuthWithDbSaveHandler/config/module.config.php namespace SanAuthWithDbSaveHandler; return array( //controllers services... 'controllers' => array( 'factories' => array( 'SanAuthWithDbSaveHandler\Controller\Auth' => 'SanAuthWithDbSaveHandler\Factory\Controller\AuthControllerServiceFactory' ), 'invokables' => array( 'SanAuthWithDbSaveHandler\Controller\Success' => 'SanAuthWithDbSaveHandler\Controller\SuccessController' ), ), //register auth services... 'service_manager' => array( 'factories' => array( 'AuthStorage' => 'SanAuthWithDbSaveHandler\Factory\Storage\AuthStorageFactory', 'AuthService' => 'SanAuthWithDbSaveHandler\Factory\Storage\AuthenticationServiceFactory', ), ), //routing configuration... 'router' => array( 'routes' => array( 'auth' => array( 'type' => 'segment', 'options' => array( 'route' => '/auth[/:action]', 'defaults' => array( 'controller' => 'SanAuthWithDbSaveHandler\Controller\Auth', 'action' => 'index', ), ), ), 'success' => array( 'type' => 'segment', 'options' => array( 'route' => '/success[/:action]', 'defaults' => array( 'controller' => 'SanAuthWithDbSaveHandler\Controller\Success', 'action' => 'index', ), ), ), ), ), //setting up view_manager 'view_manager' => array( 'template_path_stack' => array( 'SanAuthWithDbSaveHandler' => __DIR__ . '/../view', ), ), );
4. Last but not least, a Module Class ! (It’s very important! haha 😀 )
//filename : SanAuthWithDbSaveHandler/Module.php namespace SanAuthWithDbSaveHandler; class Module { public function getConfig() { return include __DIR__ . '/config/module.config.php'; } public function getAutoloaderConfig() { return array( 'Zend\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . str_replace('\\', '/' , __NAMESPACE__), ), ), ); } }
Now, try to login to application with url : http://YourZF2app/auth with username : admin and password : admin.
Done !
btw, I’ve uploaded source to my github account : https://github.com/samsonasik/SanAuthWithDbSaveHandler
References :
1. https://zf2.readthedocs.org/en/latest/modules/zend.session.save-handler.html
2. https://zf2.readthedocs.org/en/latest/modules/zend.authentication.intro.html
3. https://github.com/cgmartin/ZF2FileUploadExamples/blob/master/src/ZF2FileUploadExamples/Form/CollectionUpload.php
Zend Framework 2 : Register Event Listeners in Configuration File
Zend Framework 2.2 released. It comes with ton of new features. Now, I will show you how to set event listeners in Configuration File. Event Listeners can be registered in config/application.config.php.
For example, I have a sample listener like this :
//module/YourModule/src/YourModule/Event/MySampleListener.php namespace YourModule\Event; use Zend\EventManager\ListenerAggregateInterface; use Zend\EventManager\EventManagerInterface; use Zend\EventManager\EventInterface; class MySampleListener implements ListenerAggregateInterface { protected $listeners = array(); public function attach(EventManagerInterface $events) { $this->listeners[] = $events->attach('eventName', array($this, 'doEvent')); } public function detach(EventManagerInterface $events) { foreach ($this->listeners as $index => $listener) { if ($events->detach($listener)) { unset($this->listeners[$index]); } } } public function doEvent(EventInterface $event) { echo 'param id = '.$event->getParam('id'); } }
Let’s register :
a. in config/application.config.php
return array( 'listeners' => array( 'MySampleListener' ), // This should be an array of module namespaces used in the application. 'modules' => array( 'Application', 'YourModule', //Your module here ), 'module_listener_options' => array( 'module_paths' => array( './module', './vendor', ), 'config_glob_paths' => array( 'config/autoload/{,*.}{global,local}.php', ), ), );
b. register as service at YourModule/config/module.config.php
//module/YourModule/config/module.config.php return array( 'service_manager' => array( 'invokables' => array( 'MySampleListener' => 'YourModule\Event\MySampleListener', ), ), 'controllers' => array( //other config here... ), 'routes' => array( //other config here... ), );
Try calling in Controller :
//module/YourModule/src/YourModule/Controller/IndexController.php namespace YourModule\Controller; use Zend\Mvc\Controller\AbstractActionController; class IndexController extends AbstractActionController { public function indexAction() { $mysampleListener = $this->getServiceLocator()->get('MySampleListener'); $this->getEventManager()->attachAggregate($mysampleListener); $parameter = array('id' => 1); $this->getEventManager()->trigger('eventName', $this, $parameter); } }
Um…, let’s make it separate, attach via Module::onBootstrap :
//module/YourModule/Module.php namespace YourModule; use Zend\Mvc\MvcEvent; class Module { public function onBootstrap(MvcEvent $e) { $eventManager = $e->getApplication()->getEventManager(); $sharedManager = $eventManager->getSharedManager(); $sm = $e->getApplication()->getServiceManager(); $sharedManager->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', function($e) use ($sm) { $controller = $e->getTarget(); $controller->getEventManager()->attachAggregate($sm->get('MySampleListener')); }, 2); } public function getConfig(){/* common code here */} public function getAutoloaderConfig(){ /* common code here */} }
And you just call in controller by :
//module/YourModule/src/YourModule/Controller/IndexController.php namespace YourModule\Controller; use Zend\Mvc\Controller\AbstractActionController; class IndexController extends AbstractActionController { public function indexAction() { $parameter = array('id' => 1); $this->getEventManager()->trigger('eventName', $this, $parameter); } }
I got some question about why need to register at config/application.config.php ? It’s to make it callable during onBootstrap(MvcEvent $e) directly by :
$eventManager->trigger('eventName', $this, array('id' => 1));
at the Module.php.
References :
1. https://github.com/zendframework/zf2/pull/3931
2. http://samminds.com/2013/04/understanding-zf2-configuration/
3. http://framework.zend.com/blog/zend-framework-2-2-0-stable-released.html
Zend Framework 2 : Paginator – Using TableGateway Object
Zend Framework 2.2 is coming, more feature, more improvement.One of features that i like is DbTableGateway adapter for Paginator that can be used at your Table Class to make our life easier. The current ZF2 doc is using DbSelect Adapter, so now i will post an example how to use DbTableGateway Adapter.
I will follow the AlbumTable tutorial in the doc, so AlbumTable should be like the following :
namespace Album\Model; use Zend\Db\TableGateway\TableGateway; use Zend\Paginator\Adapter\DbTableGateway; use Zend\Paginator\Paginator; class AlbumTable { //... public function fetchAll($paginated = false) { if ($paginated) { $dbTableGatewayAdapter = new DbTableGateway($this->tableGateway); $paginator = new Paginator($dbTableGatewayAdapter); return $paginator; } return $this->tableGateway->select(); } //... }
Very easy 😀
Note : currently, you can pass $where and $order to DbTableGateway adapter after tableGateway parameter.
References :
http://zf2.readthedocs.org/en/latest/tutorials/tutorial.pagination.html
106 comments