Zend Framework 2 : Getting Closer with PluginManager
In ZF2, there is PluginManager that we can use to collect services with same contract interfaces or ancestor class into single class properties. After that, we can validate whenever the plugins as services are instance of that interfaces or ancestor class ( usually an abstract class ). There are properties of PluginManager class that can collect the services :
1. $factories
2. $invokableClasses
3. $aliases(aliasing registered services at $factories or $invokableClasses)
The basic class structure of PluginManager is like this :
namespace YourModule; use Zend\ServiceManager\AbstractPluginManager; class PluginManager extends AbstractPluginManager { protected $factories = array( //represent factories key ); protected $invokableClasses = array( //represent invokables key ); protected $aliases = array( //represent aliases key ); public function validatePlugin($plugin) { if ($plugin instanceof Plugin\PluginInterface) { // we're okay return; } throw new \InvalidArgumentException(sprintf( 'Plugin of type %s is invalid; must implement %s\Plugin\PluginInterface', (is_object($plugin) ? get_class($plugin) : gettype($plugin)), __NAMESPACE__ )); } }
For example, you need to create collection of services that can convert content into another format, for example, you want to create services that can convert to ‘xls’, ‘pdf’, or other. so you can create pluginmanager to collect that services, like the following :
Let’s code!
1. create the ConverterContentPluginManager PluginManager
//filename : module/Tutorial/src/Tutorial/ConverterContentPluginManager.php namespace Tutorial; use Zend\ServiceManager\AbstractPluginManager; class ConverterContentPluginManager extends AbstractPluginManager { protected $invokableClasses = array( //represent invokables key 'xls' => 'Tutorial\Plugin\Xls', 'pdf' => 'Tutorial\Plugin\Pdf' ); public function validatePlugin($plugin) { if ($plugin instanceof Plugin\PluginInterface) { // we're okay return; } throw new \InvalidArgumentException(sprintf( 'Plugin of type %s is invalid; must implement %s\Plugin\PluginInterface', (is_object($plugin) ? get_class($plugin) : gettype($plugin)), __NAMESPACE__ )); } }
2. Create the plugin interface to be implemented
//filename : module/Tutorial/src/Tutorial/Plugin/PluginInterface.php namespace Tutorial\Plugin; interface PluginInterface { public function convert($content); }
3. Create that services
a. the Xls service
//filename : module/Tutorial/src/Tutorial/Plugin/Xls.php namespace Tutorial\Plugin; class Xls implements PluginInterface { public function convert($content) { echo 'xls convert here'; //implementation of convert $content to convert content into xls } }
b. the Pdf service
//filename : module/Tutorial/src/Tutorial/Plugin/Pdf.php namespace Tutorial\Plugin; class Pdf implements PluginInterface { public function convert($content) { echo 'pdf convert here'; //implementation of convert $content to convert content into pdf } }
4. Create a factory class for the ConverterContentPluginManager
//filename : module/Tutorial/src/Tutorial/Service/ConverterContentPluginManagerFactory.php namespace Tutorial\Service; use Zend\Mvc\Service\AbstractPluginManagerFactory; class ConverterContentPluginManagerFactory extends AbstractPluginManagerFactory { const PLUGIN_MANAGER_CLASS = 'Tutorial\ConverterContentPluginManager'; }
5. Last step, register into service manager ( just one service, the ConverterContentPluginManagerFactory one )
//filename : module/Tutorial/config/module.config.php return array( 'service_manager' => array( 'factories' => array( 'convertercontent' => 'Tutorial\Service\ConverterContentPluginManagerFactory' ), ), );
Ok, now you can grab the plugins via :
$content = 'sample content you want to convert'; $converter = $this->getServiceLocator()->get('convertercontent') $converter->get('xls')->convert($content); $converter->get('pdf')->convert($content);
Done 😉
References :
1. http://raing3.gshi.org/2013/05/26/creating-custom-plugin-manager-in-zend-framework-2/
2. http://zf2.readthedocs.org/en/latest/modules/zend.mvc.services.html
Zend Framework 2 : Handle and Catch E_* PHP errors
I made this post because real application is hard ( err…, not as easy as we imagine ) . One error exposed to user can be harmful for our site. As developer, there is a time that something can go wrong even it unit tested ( because we are human!). The ‘dispatch.error’ and ‘render.error’ are errors that handled by Framework to handle “framework specific” error, like service not found, or view file not found. But what if the error is PHP Error Constant, like you forgot to handle empty array and just :
$array = array(); //many complex things here //and you echoing.. echo $array[1]; //that is empty...
You will got error like this : Notice: Undefined offset: 1 in /your/path/to/file.
It’s very dangerous because it exposed to your user. We need to fix it as soon as possible!, so when we need on site access, we need to automate logging ( for example, save error to file and send mail) with the error described and show user that something is go wrong and developer is working to fix it.
Ok, first, prepare the view file that will show user that something is go wrong :
<!-- //module/Application/view/error/e_handler.phtml --> website is down right now :), we are working on it. please come back again...
Next, we create code to handle the PHP E_* errors :
// ./e_errorhandler.php in root of ZF2 app //adapt from http://stackoverflow.com/questions/277224/how-do-i-catch-a-php-fatal-error define('E_FATAL', E_ERROR | E_USER_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_RECOVERABLE_ERROR); define('DISPLAY_ERRORS', TRUE); define('ERROR_REPORTING', E_ALL | E_STRICT); register_shutdown_function('shut'); set_error_handler('handler'); //catch function function shut() { $error = error_get_last(); if ($error && ($error['type'] & E_FATAL)) { handler($error['type'], $error['message'], $error['file'], $error['line']); } } function handler($errno, $errstr, $errfile, $errline) { switch ($errno) { case E_ERROR: // 1 // $typestr = 'E_ERROR'; break; case E_WARNING: // 2 // $typestr = 'E_WARNING'; break; case E_PARSE: // 4 // $typestr = 'E_PARSE'; break; case E_NOTICE: // 8 // $typestr = 'E_NOTICE'; break; case E_CORE_ERROR: // 16 // $typestr = 'E_CORE_ERROR'; break; case E_CORE_WARNING: // 32 // $typestr = 'E_CORE_WARNING'; break; case E_COMPILE_ERROR: // 64 // $typestr = 'E_COMPILE_ERROR'; break; case E_CORE_WARNING: // 128 // $typestr = 'E_COMPILE_WARNING'; break; case E_USER_ERROR: // 256 // $typestr = 'E_USER_ERROR'; break; case E_USER_WARNING: // 512 // $typestr = 'E_USER_WARNING'; break; case E_USER_NOTICE: // 1024 // $typestr = 'E_USER_NOTICE'; break; case E_STRICT: // 2048 // $typestr = 'E_STRICT'; break; case E_RECOVERABLE_ERROR: // 4096 // $typestr = 'E_RECOVERABLE_ERROR'; break; case E_DEPRECATED: // 8192 // $typestr = 'E_DEPRECATED'; break; case E_USER_DEPRECATED: // 16384 // $typestr = 'E_USER_DEPRECATED'; break; } $message = " Error PHP in file : ".$errfile." at line : ".$errline." with type error : ".$typestr." : ".$errstr." in ".$_SERVER['REQUEST_URI']; if(!($errno & ERROR_REPORTING)) { return; } if (DISPLAY_ERRORS) { //logging... $logger = new Zend\Log\Logger; //stream writer $writerStream = new Zend\Log\Writer\Stream(__DIR__.'/data/logs/'.date('Ymd').'-log.txt'); //mail writer $mail = new Zend\Mail\Message(); $mail->setFrom('system@yoursite.com', 'Sender\'s name'); $mail->addTo('team@yoursite.com', 'Your Site Team'); $transport = new Zend\Mail\Transport\Sendmail(); $writerMail = new Zend\Log\Writer\mail($mail, $transport); $writerMail->setSubjectPrependText("PHP Error : $typestr : $errstr "); $logger->addWriter($writerStream); $logger->addWriter($writerMail); //log it! $logger->crit($message); //show user that's the site is down right now include __DIR__.'/module/Application/view/error/e_handler.phtml'; die; } }
The code line 61-87 is show how to log it and show user custom error page immediately.
Ok, time to include the handler in public/index.php and keep silent ( @ for parse error ) for showing error :
/** * This makes our life easier when dealing with paths. Everything is relative * to the application root now. */ chdir(dirname(__DIR__)); // Decline static file requests back to the PHP built-in webserver if (php_sapi_name() === 'cli-server' && is_file(__DIR__ . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))) { return false; } // Setup autoloading require 'init_autoloader.php'; //Setup error handler require_once 'e_errorhandler.php'; // Run the application! @Zend\Mvc\Application::init(require 'config/application.config.php')->run();
Ok, done! Hope helpful 😉
References :
1. http://stackoverflow.com/questions/277224/how-do-i-catch-a-php-fatal-error
2. http://zf2.readthedocs.org/en/latest/modules/zend.mail.introduction.html
3. http://zf2.readthedocs.org/en/latest/modules/zend.log.writers.html
Zend Framework 2 : using PSR-4 autoloader in your Module
This post is inspired by Phil Sturgeon blog post about Autoloading Laravel application code with PSR-4. It can be applied when you’re using Zend Framework module. Instead of using default structure, you can define the structure like this.
The Classes inside Controller folder will have namespace Foo\Controller, and in the Model folder will have namespace Foo\Model.
Ok, let’s make it happen!
1. Let’s the Module::getAutoloaderConfig() function empty
//module/Foo/Module.php namespace Foo; class Module { public function getConfig() { return include __DIR__ . '/config/module.config.php'; } public function getAutoloaderConfig() { } }
2. Configure composer.json
Add psr-4 autolaod config into your composer.json.
{ "autoload": { "psr-4":{ "Foo\\" : "module/Foo/src/" } }, "require": { "php": ">=5.3.3", "zendframework/zendframework": "2.2.5" } }
3. update composer
$ php composer.phar self-update Updating to version 69e77fbbb564e57a6c1a97eaa3c8b751bab70688. Downloading: 100%
4. Run dump-autoload
$ php composer.phar dump-autoload Generating autoload files
5. Now, You are getting new generated autoload file named vendor/composer/autoload_psr4.php automatically
// autoload_psr4.php @generated by Composer $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( 'Foo\\' => array($baseDir . '/module/Foo/src'), );
6. Perfect!, Let’s creating file for samples
a. the model class
//module/Foo/src/Model/FooModel.php namespace Foo\Model; class FooModel { function __construct() { echo "foo"; } }
b. the controller class
//module/Foo/src/Controller/FooController.php namespace Foo\Controller; use Zend\Mvc\Controller\AbstractActionController; class FooController extends AbstractActionController { public function indexAction() { new \Foo\Model\FooModel(); die; //break for test } }
c. the config/module.config.php
//module/Foo/config/module.config.php return array( 'router' => array( 'routes' => array( 'foo' => array( 'type' => 'Zend\Mvc\Router\Http\Literal', 'options' => array( 'route' => '/foo', 'defaults' => array( 'controller' => 'Foo\Controller\Foo', 'action' => 'index', ), ), ), ), ), 'controllers' => array( 'invokables' => array( 'Foo\Controller\Foo' => 'Foo\Controller\FooController' ), ), );
7. Register your module into config/application.config.php as usual.
return array( 'modules' => array( 'Application', 'Foo' ), 'module_listener_options' => array( 'module_paths' => array( './module', './vendor', ), 'config_glob_paths' => array( 'config/autoload/{,*.}{global,local}.php', ), ), );
8. Let’s call in browser : http://yourapphost/foo , if it’s working as planned, it will show us :
Done! I hope this post helpful.
References :
1. http://philsturgeon.co.uk/blog/2014/01/autoloading-laravel-application-code-with-psr4
2. https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md
3. http://akrabat.com/zend-framework-2/thoughts-on-module-directory-structure/
4. http://zf2.readthedocs.org/en/latest/user-guide/modules.html
Zend Framework 2 : Move out your listeners from Module class
As we already knew. We can have ‘listeners’ with array callback that placed on Module class which can be called via onBootstrap() method. When our application is growing, to many methods on Module class will make project maintenance take harder, and make our application less-readable.
Here is a sample if we don’t have move out the method yet :
class Module { public function onBootstrap(MvcEvent $e) { $app = $e->getApplication(); $eventManager = $app->getEventManager(); $eventManager->attach('dispatch.error', array($this, 'handleDispatchErrorWithLogger'), 100); } public function handleDispatchErrorWithLogger(MvcEvent $e) { $exception = $e->getParam('exception'); //it is just a sample, you can create service for logger $writer = new \Zend\Log\Writer\Stream('./data/logs/'.date('Y-m-d').'-log.txt'); $log = new \Zend\Log\Logger(); $log->addWriter($writer); $log->err($exception); } public function getConfig(){/*common code*/} public function getAutoloaderConfig(){/*common code*/} }
And many more when application growing, so, this is how it can be moved out :
1. Create a class that has __invoke method that will be fired when event triggered
class DispatchErrorHandlerListener { public function __invoke(MvcEvent $e) { $exception = $e->getParam('exception'); //it is just a sample, you can create service for logger $writer = new \Zend\Log\Writer\Stream('./data/logs/'.date('Y-m-d').'-log.txt'); $log = new \Zend\Log\Logger(); $log->addWriter($writer); $log->err($exception); } }
2. Make the listener as object
class Module { public function onBootstrap(MvcEvent $e) { $app = $e->getApplication(); $eventManager = $app->getEventManager(); $eventManager->attach('dispatch.error', new \Tutorial\Listener\DispatchErrorHandlerListener, 100); } public function getConfig(){/*common code*/} public function getAutoloaderConfig(){/*common code*/} }
and if you like the listener as service, you can pass like this :
class Module { public function onBootstrap(MvcEvent $e) { $app = $e->getApplication(); $eventManager = $app->getEventManager(); $service = $app->getServiceManager(); $eventManager->attach('dispatch.error', $sm->get('YourRegisteredErrorHandlerListener'), 100); } public function getConfig(){/*common code*/} public function getAutoloaderConfig(){/*common code*/} }
Done, now your Module class is simplified 😉
Zend Framework 2 : Programmatically handle 404 page
Sometime, in real life project, we need to handle 404 page more than only show the 404 page, but logging, redirecting, forwarding, change template/content, etc. We can handle it by programmatically at Module.php. In Zend Framework 2, there is 4 types of 404 ( page not found ), they are : ERROR_CONTROLLER_CANNOT_DISPATCH, ERROR_CONTROLLER_NOT_FOUND, ERROR_CONTROLLER_INVALID, and ERROR_ROUTER_NO_MATCH. Let’s investigate one by one.
1. ERROR_CONTROLLER_CANNOT_DISPATCH
It means the controller is matched, but the action that passed to the url can’t be dispatched. We can handle it via onBootstrap like the following :
namespace YourModule; use Zend\Mvc\MvcEvent; class Module { public function onBootstrap(MvcEvent $e) { $eventManager = $e->getApplication()->getEventManager(); $sharedManager = $eventManager->getSharedManager(); //controller can't dispatch request action that passed to the url $sharedManager->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', array($this, 'handleControllerCannotDispatchRequest' ), 101); } public function handleControllerCannotDispatchRequest(MvcEvent $e) { $action = $e->getRouteMatch()->getParam('action'); $controller = get_class($e->getTarget()); // error-controller-cannot-dispatch if (! method_exists($e->getTarget(), $action.'Action')) { $logText = 'The requested controller '. $controller.' was unable to dispatch the request : '.$action.'Action'; //you can do logging, redirect, etc here.. echo $logText; } } public function getConfig() { //common code here } public function getAutoloaderConfig() { //common code here } }
2. ERROR_CONTROLLER_NOT_FOUND
It means controller class not found with requested [/:controller] route that defined already at module.config.php
3. ERROR_CONTROLLER_INVALID
It means the controller is not dispatchable, it usually because the controller is not extends Zend\Mvc\Controller\AbstractActionController
4. ERROR_ROUTER_NO_MATCH
It means The requested URL could not be matched by routing, for example, there is no route with prefix /foo that passed at the url.
For point 2, 3, and 4, we can handle by :
namespace YourModule; use Zend\Mvc\MvcEvent; use Zend\Mvc\Application; class Module { public function onBootstrap(MvcEvent $e) { $eventManager = $e->getApplication()->getEventManager(); $eventManager->attach('dispatch.error', array($this, 'handleControllerNotFoundAndControllerInvalidAndRouteNotFound'), 100); } public function handleControllerNotFoundAndControllerInvalidAndRouteNotFound(MvcEvent $e) { $error = $e->getError(); if ($error == Application::ERROR_CONTROLLER_NOT_FOUND) { //there is no controller named $e->getRouteMatch()->getParam('controller') $logText = 'The requested controller ' .$e->getRouteMatch()->getParam('controller'). ' could not be mapped to an existing controller class.'; //you can do logging, redirect, etc here.. echo $logText; } if ($error == Application::ERROR_CONTROLLER_INVALID) { //the controller doesn't extends AbstractActionController $logText = 'The requested controller ' .$e->getRouteMatch()->getParam('controller'). ' is not dispatchable'; //you can do logging, redirect, etc here.. echo $logText; } if ($error == Application::ERROR_ROUTER_NO_MATCH) { // the url doesn't match route, for example, there is no /foo literal of route $logText = 'The requested URL could not be matched by routing.'; //you can do logging, redirect, etc here... echo $logText; } } public function getConfig() { //common code here } public function getAutoloaderConfig() { //common code here } }
Want to handle All ? Let’s do this :
namespace YourModule; use Zend\Mvc\MvcEvent; use Zend\Mvc\Application; class Module { public function onBootstrap(MvcEvent $e) { $eventManager = $e->getApplication()->getEventManager(); $sharedManager = $eventManager->getSharedManager(); //controller can't dispatch request action that passed to the url $sharedManager->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', array($this, 'handleControllerCannotDispatchRequest' ), 101); //controller not found, invalid, or route is not matched anymore $eventManager->attach('dispatch.error', array($this, 'handleControllerNotFoundAndControllerInvalidAndRouteNotFound' ), 100); } public function handleControllerCannotDispatchRequest(MvcEvent $e) { $action = $e->getRouteMatch()->getParam('action'); $controller = get_class($e->getTarget()); // error-controller-cannot-dispatch if (! method_exists($e->getTarget(), $action.'Action')) { $logText = 'The requested controller '. $controller.' was unable to dispatch the request : '.$action.'Action'; //you can do logging, redirect, etc here.. echo $logText; } } public function handleControllerNotFoundAndControllerInvalidAndRouteNotFound(MvcEvent $e) { $error = $e->getError(); if ($error == Application::ERROR_CONTROLLER_NOT_FOUND) { //there is no controller named $e->getRouteMatch()->getParam('controller') $logText = 'The requested controller ' .$e->getRouteMatch()->getParam('controller'). ' could not be mapped to an existing controller class.'; //you can do logging, redirect, etc here.. echo $logText; } if ($error == Application::ERROR_CONTROLLER_INVALID) { //the controller doesn't extends AbstractActionController $logText = 'The requested controller ' .$e->getRouteMatch()->getParam('controller'). ' is not dispatchable'; //you can do logging, redirect, etc here.. echo $logText; } if ($error == Application::ERROR_ROUTER_NO_MATCH) { // the url doesn't match route, for example, there is no /foo literal of route $logText = 'The requested URL could not be matched by routing.'; //you can do logging, redirect, etc here... echo $logText; } } public function getConfig(){ // common code } public function getAutoloaderConfig(){ //common code } }
Done 😉
11 comments