Welcome to Abdul Malik Ikhsan's Blog

Zend Framework 2 : Working with AuthenticationService and Session Db Save Handler

Posted in Tutorial PHP, Zend Framework 2 by samsonasik on May 29, 2013

zf2-zendframework2One 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 )

sessionsavehandler

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

Advertisements

Zend Framework 2 : Register Event Listeners in Configuration File

Posted in Tutorial PHP, Zend Framework 2 by samsonasik on May 18, 2013

zf2-zendframework2Zend 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

Posted in Tutorial PHP, Zend Framework 2 by samsonasik on May 6, 2013

zf2-zendframework2Zend 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