Upload
corley-srl
View
694
Download
0
Embed Size (px)
DESCRIPTION
We will cover how to build a custom microframework using ZF2 components as building blocks.
Citation preview
ZFDAY - 2014BUILD A CUSTOM (MICRO)FRAMEWORKWITH ZF2 COMPONENTS AS BUILDING BLOCKS
Author / Walter Dal Mut @walterdalmut
WHO AM I?, I work at
and also at Walter Dal Mut Corley S.r.l.
UpCloo Ltd.
You can contact me at: [email protected]
On Twitter: @walterdalmut
On GitHub: wdalmut
WHY A CUSTOM FRAMEWORK?Tipically because we want to solve a particular problem.
ZendFramework is a general purpose Web Framework thatcan fit different problems easily.
IT SOUNDS LIKE REINVENTING THE WHEELNO, WE DON'T WANT!
We will use ZF2 components as building blocksWe will drive a personal behaviour
IT COULD WORK!!
REQUIREMENTSEvent Driven Design
Dependency Inversion Principle
TESTABLE/TDD
EVENT DRIVEN DESIGN
All framework operations are events (like ZF2)
DEPENDENCY INVERSIONWe will focus on Service Manager
[ WE WANT TO TEST OUR APPLICATIONS ]
COMPOSE IT!{ "require": { "zendframework/zend-eventmanager": "2.*", "zendframework/zend-servicemanager": "2.*", "zendframework/zend-mvc": "2.*", "zendframework/zend-http": "2.*" }, "require-dev": { "phpunit/phpunit": "3.7.*" },}
LET'S GO TO SCHOOLWe have to learn our building blocks!
THE EVENT MANAGER$eventManager = new Zend\EventManager\EventManager();
// Trigger an eventpublic function trigger($event, $target = null, $argv = null);
// Attach a listener to an eventpublic function attach($event, $callback, $priority);
There are many other methods (detach, triggerUntil, etc...)
See it on Github
HOW EVENT MANAGER WORKS?public function testEventManager(){ $ev = new EventManager();
$isCalled = false; $ev->attach('an-event', function($event) use (&$isCalled) { $isCalled = true; });
$ev->trigger("an-event");
$this->assertTrue($isCalled);}
THE SERVICE LOCATOR$serviceManager = new Zend\ServiceManager\ServiceManager();
"services" => [ "invokables" => [ "Your\\Name\\Comp" => "Your\\Name\\Comp", ], "factories" => [ "Your\\Name\\Service" => "Your\\Name\\ServiceFactory", ], "aliases" => [ "your.alias" => "Your\\Name\\Service", ]]
See it on Github
CONFIGURE S.L. FROM ARRAY$serviceManager = new Zend\ServiceManager\ServiceManager();
$config = new Zend\ServiceManager\Config([ "invokables" => [...], "factories" => [...], "abstract_factories" => [...], "aliases" => [...]])$config->configureServiceManager($serviceManager);
HOW SERVICE MANAGER WORKS?public function testServiceManager(){ $sm = new ServiceManager(); $this->config->configureServiceManager($sm);
$this->assertTrue($sm->has("example"));
$this->assertSame( $sm->get("example"), $sm->get("example") );}
THE ROUTER$rtr = Zend\Mvc\Router\Http\TreeRouteStack::factory(array $config);
"routes" => [ "routeName" => [ "type" => "Literal", "options" => [ "route" => "/path", "defaults" => [ "controller" => "ZF\\Name", "action" = > "actionName", ] ], "may_terminate" => true, ]]
$routeMatch = $rtr->match(Request $request);
HOW ROUTER WORKS?public function testRouteMatch(){ $router = TreeRouteStack::factory($this->config);
$request = new Request("/path");
$routeMatch = $router->match($request);
$controllerName = $routerMatch->getParam("controller"); $actionName = $routeMatch->getParam("action");
$this->assertEquals("ZF\\Name", $controllerName)) $this->assertEquals("actionName", $actionName))}
REQUEST//$request = new \Zend\Http\PhpEnvironment\Request();
RESPONSE$response = new \Zend\Http\PhpEnvironment\Response();
$response->addHeaders([...]);$response->setContent("...");$response->send();
HYDRATORSpublic function extract($object);public function hydrate(array $data, $object);
$classMethods = new \Zend\Stdlib\Hydrator\ClassMethods();$objectProp = new \Zend\Stdlib\Hydratror\ObjectProperty();$array = new \Zend\Stdlib\Hydratror\ArraySerializable();
LET'S START!
EVENTS!
THE FRAMEWORK EVENTS LISTBEGIN
The loop begins
ROUTEParse the actual route in order to found a dispatchable action
PRE.FETCHBefore dispatch the action
EXECUTEDispatch all actions
RENDERRender your data
FINISHThe loop ends
ON ERRORS?BEGIN
ROUTE
PRE.FETCH
404/500/HALT
RENDER
FINISH
TEST IT!public function testBaseAppFlow(){ $request = RequestFactory::createRequest("/valid-url", "GET", []);
$app = new App();
$app->setRequest($request); $app->setServiceManager(new Zend\ServiceManager\ServiceManager()); $app->setEventManager(new Zend\EventManager\EventManager());
$response = $app->run()->response();
$this->assertEquals(404, $response->getStatusCode());}
F
FAIL (1 tests, 1 assertions)
REQUESTpublic function setRequest(Request $request){ $this->request = $request;}
public function request(){ if (!$this->request) { $this->request = new Request(); }
return $this->request;}
RESPONSEpublic function setResponse(Response $response){ $this->response = $response;}
public function response(){ if (!$this->response) { $this->response = new Response(); }
return $this->response;}
EVENT MANAGERpublic function setEventManager($eventManager){ $this->eventManager = $eventManager;}
public function events(){ return $this->eventManager;}
EVENT TRIGGER HELPERpublic function trigger($name, array $params = []){ $event = new Event();
$event->setTarget($this); $event->setParams($params);
return $this->events()->trigger($name, $event);}
SERVICE MANAGERpublic function setServiceManager($serviceManager){ $this->serviceManager = $serviceManager;}
public function services(){ return $this->serviceManager;}
ENTRY POINT!public function run(){ $this->trigger("begin");
$eventsList = $this->dispatchUserActionRelatedEvents();
$this->trigger("render", ["data" => $eventsList]);
$this->trigger("finish");
return $this;}
HEY, CAN YOU DO IT?protected function dispatchUserActionRelatedEvents(){ try { $eventsList = $this->dispatchUserRequest(); } catch (HaltException $e) { $eventsList = $this->trigger("halt"); } catch (PageNotFoundException $e) { $this->response()->setStatusCode(404); $eventsList = $this->trigger("404"); } catch (\Exception $e) { $this->response()->setStatusCode(500); $eventsList = $this->trigger("500"); }
return $eventsList;}
JUST TRY IT!protected function dispatchUserRequest(){ $evList = $this->trigger("route", ["request" => $this->request()]); $routeMatch = $evList->last();
if (null === $routeMatch) { throw new PageNotFoundException("Page not found!"); }
$this->response()->setStatusCode(200);
$this->trigger("pre.fetch", ["routeMatch" => $routeMatch]); $controllers = $this->events()->trigger("execute", $routeMatch);
return $controllers;}
Why the "execute" trigger is different?
NOW IT WORKS!public function testBaseAppFlow(){ $request = RequestFactory::createRequest("/a-page", "GET");
$app = new App();
$app->setRequest($request); $app->setServiceManager(new Zend\ServiceManager\ServiceManager()); $app->setEventManager(new Zend\EventManager\EventManager());
$response = $app->run()->response();
$this->assertEquals(404, $response->getStatusCode());}
.
OK (1 tests, 1 assertions)
See it on Github Gist
EASY TO MOCK!public function testOkResponseOnRouteMatch(){ $app = new App(); $eventManager = new Zend\EventManager\EventManager(); $eventmanager->attach("route", function() { return new RouteMatch([]) });
$app->setEventManager($eventManager); $app->setServiceManager(new Zend\ServiceManager\ServiceManager());
$response = $app->run()->response();
$this->assertEquals(200, $response->getStatusCode());}
..
OK (2 tests, 2 assertions)
WE NEED A BOOTSTRAPPER!Or better, something that can prepare the event manager and
the service manager from a default configuration!
LINK DEFAULT LISTENERS"listeners" => [ "route" => [ ["route.listener", "onRouteEvent"], ], "renderer" => [ ["renderer.listener", "render"], ], "finish" => [ ["response.listener", "sendResponse"], ],]
FRAMEWORK BASE SERVICES CONFIG"aliases" => [ "route.listener" => "Listener\\RouteListener", "renderer.listener" => "Listener\\Renderer\\Json", "response.listener" => "Listener\\SendResponseListener",],
"factories" => [ "Listener\\RouteListener" => "Service\\RouteListenerFactory",],"invokables" => [ "Listener\\Renderer\\Json" => "Listener\\Renderer\\Json", "Listener\\SendResponseListener" => "Listener\\SendResponseListener",],
THE ROUTER!
FOLLOW ZF2 IDEAS!return [ "router" => [ "routes" => [ "home" => [ "type" => "Literal", "options" => [ "route" => "/", "defaults" => [ "controller" => "ZF\\Day\\Italy", "action" => "get2014Day" ] ], 'may_terminate' => true, ] ] ]];
ROUTE LISTENERclass RouteListener{ public function __construct($router) { $this->router = $router; }
public function onRouteEvent($event) { ... }}
//RouteListenerFactory$router = TreeRouteStack::factory($config["router"]);$routeListener = new RouteListener($router);
$this->attach("route", [$routeListener, "onRouteEvent"]);
ON ROUTE EVENTpublic function onRouteEvent($event){ $target = $event->getTarget(); $request = $event->getParam("request");
$match = $this->getRouter()->match($request); if ($match) { $act = $match->getParam("action"); $ctrl = $match->getParam("controller"); if ($target->services()->has($ctlr)) { $ctrl = $target->services()->get($ctlr); }
$target->events()->attach("execute", [$ctrl, $act]); } return $match;}
CONTROLLERS [ACTION LISTENERS]Controllers are POPO objects that returns serializable datanamespace ZF\Day;
class Italy{ public function get2014Day() { return [ "talks" => [ "first" => [ "title" => "Just a title..." ] ], ... ]; }}
BUT, POPOS ARE TOO SIMPLEWe want to use other services into actions!
public function theAction(){ //I need a service here! $myService = $this->services()->get("my.service");
//use it...}
But how to do that?
CONTROLLERS USESclass TheController{ use ServiceManager;
public function theAction() { $service = $this->services()->get("my.service");
... }}
TRAITStrait ServiceManager(){ private $serviceManager;
public function setServiceManager($serviceManager) { $this->serviceManager = $serviceManager; }
public function services() { return $this->serviceManager; }}
ZEND\STDLIB\HYDRATOR$data = [ "serviceManager" => $target->services(), "eventManager" => $target->events(), "response" => $target->response(), "request" => $target->request(),];
$hydrator = new \Zend\Stdlib\Hydrator\ClassMethods();$hydrator->hydrate($data, $controller);
AND ALSO CONTROLLERS AS SERVICES"services" => [ "invokables" => [ "ZF\\Day\\Italy" => "ZF\\Day\\Italy", ]]
"factories" => [ "ZF\\Day\\Italy" => function(ServiceLocatorInterface $sl) { $controller = new \ZF\Day\Italy(); $controller->setMyService($sl->get("my.service"));
return $controller; }, ...]
And all others ZF2 services opportunities
RENDERERS!Serialize your data
JSON RENDERERclass Json{ public function render(Event $event) { $target = $event->getTarget(); $dataPack = $event->getParam("data")->last()
$response = $target->response();
$response->addHeaders([ "Content-Type" => "application/json", ]);
$response->setContent(json_encode($dataPack)); }}
// App$this->events()->attach("render", [$renderer, "render"]);
SEND RESPONSE TO THE CLIENTIt's an event of course!
EMIT!class Emit{ public function send($event) { $target = $event->getTarget();
$target->response()->send(); }}
//App$this->events()->attach("finish", [$emitter, "send"]);
We can remove it or mock it out during testing!
SEE THE FIRST IMPLEMENTATIONThe first impl
AT LEAST 3 RESPONSIBILITIESBUT EFFECTIVELY MORE...
Configure the applicationPrepare the applicationRun the application
Single Responsibility Principle
REFACTOR THE FRAMEWORK-> 6d695f6 87ad7e73
OK, RECAP!
EVENTSANY IDEAS?
PERFORMANCE INSPECTION & TRACKINGSymfony2 Stopwatch component
class StopwatchListener public function start($event) { $executionName = get_class($event->getTarget()); $this->getStopwatch()->start($executionName); }
public function lap($event) { ... }
public function stop($event) { $executionName = get_class($event->getTarget()); $execution = $this->getStopwatch()->stop($executionName);
//send to DATADOG SERVICE, LOGGERS... }}
Symfony\Component\Stopwatch\Stopwatch
PERFORMANCE INSPECTION & TRACKING"listeners" => [ "begin" => [ ["listener.stopwatch", "start"], ], "pre.fetch" => [ ["listener.stopwatch", "lap"], ], "finish" => [ ["listener.stopwatch", "stop"], ],]
"services" => [ "aliases" => [ "listener.stopwatch" => "StopwatchListenerFactory" ]]
PERFORMANCE INSPECTION & TRACKING
PERFORMANCE INSPECTION & TRACKING
PERSONAL EVENTSpublic function confirmSubscriptionAction($routeMatchEvent){ $user = new User(); $user->setName(...); ... $entityManager->persist($user); $entityManager->flush();
$this->events()->trigger("subscriber.new", $user);}
"listeners" => [ "subscriber.new" => [ ["subscriber.handler", "notifyViaEmail"] ["stats.counter", "newSubscriber"] ]]
MAINTENANCE PAGES"listeners" => [ "route" => [ ["maintenance.listeners", "maintenancePage"] ], "execute" => [ ["maintenance.controller", "maintenanceAction"] ],]
class MaintenanceRouteListener{ public function maintenancePage(Event $event) { return new RouteMatch([]); }}
Don't use this, there are better ways
TRY IT!Fork it on Github
https://github.com/wdalmut/upcloo-web-framework
THANKS FOR LISTENINGALWAYS EVENTS?Author / Walter Dal Mut @walterdalmut