The symfony platform: Create your very own framework (PHP Quebec 2008)

  • 4,554 views
Uploaded on

 

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
No Downloads

Views

Total Views
4,554
On Slideshare
0
From Embeds
0
Number of Embeds
1

Actions

Shares
Downloads
94
Comments
1
Likes
4

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. The symfony platform Create your very own framework Fabien Potencier / Sensio Labs PHP Quebec 2008 www.symfony-project.com 1 www.sensiolabs.com
  • 2. Sensio / Me • Founder of Sensio – Web Agency – Founded in 1998 – 45 people – Open-Source Specialists • Creator of symfony – PHP Web framework – Based on • 10 years of Sensio experience • Existing Open-Source projects PHP Quebec 2008 www.symfony-project.com 2 www.sensiolabs.com
  • 3. Web Framework « Whatever the application, a framework is build to ease development by providing tools for recurrent and boring tasks. » • Generic components – Built-in – Well integrated – To solve web problems • Professionalize web development PHP Quebec 2008 www.symfony-project.com 3 www.sensiolabs.com
  • 4. The symfony Platform • symfony is made of decoupled classes based on a small number of core classes – Event Dispatcher – Parameter Holder • Classes with no dependency cache, command, database, form, i18n, log, request, response, routing, storage, user, validator, widget PHP Quebec 2008 www.symfony-project.com 4 www.sensiolabs.com
  • 5. The symfony Platform You can use all of those classes by themselves … to create your own framework PHP Quebec 2008 www.symfony-project.com 5 www.sensiolabs.com
  • 6. Let’s do it PHP Quebec 2008 www.symfony-project.com 6 www.sensiolabs.com
  • 7. The Goals • We won’t create a full stack framework • We will create a framework customized for YOUR needs • The code we will write today can be used as a bootstrap for your own framework PHP Quebec 2008 www.symfony-project.com 7 www.sensiolabs.com
  • 8. The Web Workflow The User asks a Resource in a Browser The Browser sends a Request to the Server The Server sends back a Response The Browser displays the Resource to the User PHP Quebec 2008 www.symfony-project.com 8 www.sensiolabs.com
  • 9. session_start(); if (isset($_GET['name'])) { $name = $_GET['name']; } else if (isset($_SESSION['name'])) { $name = $_SESSION['name']; PHP Global arrays } Built-in PHP functions else { $name = 'World'; } $_SESSION['name'] = $name; echo 'Hello '.$name; User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 9 www.sensiolabs.com
  • 10. Move to OOP • Use objects instead of global arrays and functions – $_GET, $_POST, getcookie() Request – echo, header(), setcookie() Response – $_SESSION User • Why ? – To add behaviors to those objects – To have several requests, users, responses in one PHP process (functional testing) – To be able to mock those objects to ease testing PHP Quebec 2008 www.symfony-project.com 10 www.sensiolabs.com
  • 11. sfWebRequest PHP Object $_SERVER[‘REQUEST_METHOD’] >getMethod() $_GET[‘name’] >getParameter(‘name’) get_magic_quotes_gpc() ? >getCookie(‘name’) stripslashes($_COOKIE[$name]) : $_COOKIE[$name]; ( isset($_SERVER['HTTPS']) && ( strtolower($_SERVER ['HTTPS']) == 'on’ >isSecure() || strtolower($_SERVER ['HTTPS']) == 1) ) || ( isset($_SERVER ['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER ['HTTP_X_FORWARDED_PROTO']) == 'https') ) ) Abstract Parameters and Headers PHP Quebec 2008 www.symfony-project.com 11 www.sensiolabs.com
  • 12. sfWebResponse PHP Object echo ‘Hello World!’ >setContent(‘Hello World’) header(‘HTTP/1.0 404 Not Found’) >setStatusCode(404) setcookie(‘name’, ‘value’) >setCookie(‘name’, ‘value’) Abstract Headers and Cookies PHP Quebec 2008 www.symfony-project.com 12 www.sensiolabs.com
  • 13. sfUser / sfStorage PHP Object $_SESSION[‘name’] = ‘value’ >setAttribute(‘name’, ‘value’) >setCulture(‘fr’) >setAuthenticated(true) Native session storage + MySQL, PostgreSQL, PDO, … Abstract $_SESSION Add features PHP Quebec 2008 www.symfony-project.com 13 www.sensiolabs.com
  • 14. sfEventDispatcher • Allow decoupled objects to communicate // sfUser $event = new sfEvent($this, 'user.change_culture', array('culture' => $culture)); $dispatcher->notify($event); // sfI18N Based on $callback = array($this, 'listenToChangeCultureEvent'); Cocoa Notification Center $dispatcher->connect('user.change_culture', $callback); • sfI18N and sfUser are decoupled • « Anybody » can listen to any event • You can notify existing events or create new ones PHP Quebec 2008 www.symfony-project.com 14 www.sensiolabs.com
  • 15. session_start(); if (isset($_GET['name'])) { } $name = $_GET['name']; sfWebRequest else if (isset($_SESSION['name'])) { $name = $_SESSION['name']; } else { sfUser $name = 'World'; } $_SESSION['name'] = $name; echo 'Hello '.$name; sfWebResponse User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 15 www.sensiolabs.com
  • 16. Install symfony • Install symfony 1.1 (via PEAR or Subversion) $ svn co http://svn.symfony-project.com/branches/1.1 symfony • Core classes are autoloaded require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); PHP Quebec 2008 www.symfony-project.com 16 www.sensiolabs.com
  • 17. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $request = new sfWebRequest($dispatcher); session_start(); if ($request->hasParameter('name')) if (isset($_GET['name'])) { { $name = $request->getParameter('name'); $name = $_GET['name']; } } else if (isset($_SESSION['name'])) { $name = $_SESSION['name']; } else { $name = 'World'; } $_SESSION['name'] = $name; echo 'Hello '.$name; User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 17 www.sensiolabs.com
  • 18. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $request = new sfWebRequest($dispatcher); session_start(); if (!$name = $request->getParameter('name')) { if (isset($_SESSION['name'])) { $name = $_SESSION['name']; } else { $name = 'World'; } } >getParameter() returns null $_SESSION['name'] = $name; if the parameter is not defined echo 'Hello '.$name; User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 18 www.sensiolabs.com
  • 19. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $request = new sfWebRequest($dispatcher); session_start(); if (!$name = $request->getParameter('name')) { if (isset($_SESSION['name'])) { $name = $_SESSION['name']; } else { $name = 'World'; } } $_SESSION['name'] = $name; $response = new sfWebResponse($dispatcher); $response->setContent('Hello '.$name); echo 'Hello '.$name; $response->send(); User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 19 www.sensiolabs.com
  • 20. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $storage = new sfSessionStorage(); session_start(); $user = new sfUser($dispatcher, $storage); $request = new sfWebRequest($dispatcher); if (!$name = $request->getParameter('name')) else if (isset($_SESSION['name'])) { { if (!$name = $user->getAttribute('name')) $name = $_SESSION['name']; { } $name = 'World'; } } $user->setAttribute('name', $name); $_SESSION['name'] = $name; $response = new sfWebResponse($dispatcher); $response->setContent('Hello '.$name); $response->send(); User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 20 www.sensiolabs.com
  • 21. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); $request = new sfWebRequest($dispatcher); $name = $request->getParameter('name', $user->getAttribute('name', 'World')); $user->setAttribute('name', $name); $response = new sfWebResponse($dispatcher); $response->setContent('Hello '.$name); $response->send(); User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 21 www.sensiolabs.com
  • 22. sfRouting • Clean URLs <> Resources • Parses PATH_INFO to inject parameters in the sfRequest object • Several strategies: PathInfo, Pattern • Decouples Request and Controller PHP Quebec 2008 www.symfony-project.com 22 www.sensiolabs.com
  • 23. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); /step.php?name=Fabien $routing = new sfPatternRouting($dispatcher); $routing->connect('hello', '/hello/:name'); $request = new sfWebRequest($dispatcher); /step.php/hello/Fabien $name = $request->getParameter('name', $user->getAttribute('name', 'World’)); $user->setAttribute('name', $name); $response = new sfWebResponse($dispatcher); $response->setContent('Hello '.$name); $response->send(); User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 23 www.sensiolabs.com
  • 24. The Web Workflow The User asks a Resource in a Browser The Browser sends a Request to the Server The Server sends back a Response The Browser displays the Resource to the User PHP Quebec 2008 www.symfony-project.com 24 www.sensiolabs.com
  • 25. Let’s create a new framework Code name: fp PHP Quebec 2008 www.symfony-project.com 25 www.sensiolabs.com
  • 26. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); Application $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); Resource $routing = new sfPatternRouting($dispatcher); $routing->connect('hello', '/hello/:name'); Generic $request = new sfWebRequest($dispatcher); $name = $request->getParameter('name', $user->getAttribute('name', 'World')); $user->setAttribute('name', $name); $response = new sfWebResponse($dispatcher); $response->setContent('Hello '.$name); $response->send(); User > Resource > Request > Response Request Response PHP Quebec 2008 www.symfony-project.com 26 www.sensiolabs.com
  • 27. Resource specific code generic application resource Request ? Response fp gives You handle the fp wants resource you a Request specific code A Response PHP Quebec 2008 www.symfony-project.com 27 www.sensiolabs.com
  • 28. • The dispatching logic is the same for every resource • The business logic depends on the resource and is managed by the controller • The controller responsability is to « convert » the request to a response PHP Quebec 2008 www.symfony-project.com 28 www.sensiolabs.com
  • 29. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); $routing = new sfPatternRouting($dispatcher); $routing->connect('hello', '/hello/:name'); $request = new sfWebRequest($dispatcher); $controller = new helloController(); $response = $controller->indexAction($dispatcher, $request, $user); $response->send(); PHP Quebec 2008 www.symfony-project.com 29 www.sensiolabs.com
  • 30. class helloController { function indexAction($dispatcher, $request, $user) { $name = $request->getParameter('name', $user->getAttribute('name', 'World’)); $user->setAttribute('name', $name); $response = new sfWebResponse($dispatcher); $response->setContent('Hello '.$name); return $response; } } PHP Quebec 2008 www.symfony-project.com 30 www.sensiolabs.com
  • 31. The framework creation process • We write code that just works • We abstract the code to make it generic PHP Quebec 2008 www.symfony-project.com 31 www.sensiolabs.com
  • 32. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $dispatcher = new sfEventDispatcher(); $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); $routing = new sfPatternRouting($dispatcher); $routing->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); $request = new sfWebRequest($dispatcher); $class = $request->getParameter('controller').'Controller'; $method = $request->getParameter('action').'Action'; $controller = new $class(); $response = $controller->$method($dispatcher, $request, $user); $response->send(); sfPatternRouting accepts default parameter values PHP Quebec 2008 www.symfony-project.com 32 www.sensiolabs.com
  • 33. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); Application $dispatcher = new sfEventDispatcher(); Resource $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); Generic $routing = new sfPatternRouting($dispatcher); $routing->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); $request = new sfWebRequest($dispatcher); $class = $request->getParameter('controller').'Controller'; $method = $request->getParameter('action').'Action'; $controller = new $class(); $response = $controller->$method($dispatcher, $request, $user); $response->send(); PHP Quebec 2008 www.symfony-project.com 33 www.sensiolabs.com
  • 34. The Request Handler • Handles the dispatching of the request • Calls the Controller • Has the responsability to return a sfResponse PHP Quebec 2008 www.symfony-project.com 34 www.sensiolabs.com
  • 35. $dispatcher = new sfEventDispatcher(); $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); $routing = new sfPatternRouting($dispatcher); $routing->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); $request = new sfWebRequest($dispatcher); $response = fpRequestHandler::handle($dispatcher, $request, $user); $response->send(); $class = $request->getParameter('controller').'Controller'; $method = $request->getParameter('action').'Action'; $controller = new $class(); $response = $controller->$method($dispatcher, $request, $user); PHP Quebec 2008 www.symfony-project.com 35 www.sensiolabs.com
  • 36. class fpRequestHandler { static function handle($dispatcher, $request, $user) { $class = $request->getParameter('controller').'Controller'; $method = $request->getParameter('action').'Action'; $controller = new $class(); $response = $controller->$method($dispatcher, $request, $user); return $response; } } PHP Quebec 2008 www.symfony-project.com 36 www.sensiolabs.com
  • 37. require dirname(__FILE__).'/../symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); Application $dispatcher = new sfEventDispatcher(); Resource $storage = new sfSessionStorage(); $user = new sfUser($dispatcher, $storage); Generic $routing = new sfPatternRouting($dispatcher); $routing->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); $request = new sfWebRequest($dispatcher); $response = fpRequestHandler::handle($dispatcher, $request, $user); $response->send(); PHP Quebec 2008 www.symfony-project.com 37 www.sensiolabs.com
  • 38. Abstract object management •  I need a container for my application objects –  The dispatcher –  The user –  The routing –  The i18n –  The database –  … •  These objects are specific to my Application and do not depend on the Request PHP Quebec 2008 www.symfony-project.com 38 www.sensiolabs.com
  • 39. $dispatcher = new sfEventDispatcher(); $application = new helloApplication($dispatcher); $request = new sfWebRequest($dispatcher); $response = $application->handle($request); $response->send(); PHP Quebec 2008 www.symfony-project.com 39 www.sensiolabs.com
  • 40. class helloApplication { public $dispatcher, $user, $routing; function __construct($dispatcher) { $this->dispatcher = $dispatcher; $storage = new sfSessionStorage(); $this->user = new sfUser($this->dispatcher, $storage); $this->routing = new sfPatternRouting($this->dispatcher); $this->routing->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); } function handle($request) { return fpRequestHandler::handle($this->dispatcher, $request, $this- >user); } } PHP Quebec 2008 www.symfony-project.com 40 www.sensiolabs.com
  • 41. Instead of passing a dispatcher around, pass the application object PHP Quebec 2008 www.symfony-project.com 41 www.sensiolabs.com
  • 42. class helloApplication { // ... function handle($request) { return fpRequestHandler::handle($this, $request); } } class fpRequestHandler { static function handle($application, $request) { $class = $request->getParameter('controller').'Controller'; $method = $request->getParameter('action').'Action'; fpRequestHandler $controller = new $class(); $response = $controller->$method($application, $request); is now generic return $response; } } class helloController { function indexAction($application, $request) { $name = $request->getParameter('name', $application->user->getAttribute('name', 'World’)); $application->user->setAttribute('name', $name); $response = new sfWebResponse($application->dispatcher); $response->setContent('Hello '.$name); return $response; } } PHP Quebec 2008 www.symfony-project.com 42 www.sensiolabs.com
  • 43. class helloApplication { Application public $dispatcher, $user, $routing; function __construct($dispatcher) Generic { $this->dispatcher = $dispatcher; $storage = new sfSessionStorage(); $this->user = new sfUser($this->dispatcher, $storage); $this->routing = new sfPatternRouting($this->dispatcher); $this->routing->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); } function handle($request) { return fpRequestHandler::handle($this->dispatcher, $request, $this- >user); } } PHP Quebec 2008 www.symfony-project.com 43 www.sensiolabs.com
  • 44. Create a fpApplication class PHP Quebec 2008 www.symfony-project.com 44 www.sensiolabs.com
  • 45. abstract class fpApplication { public $dispatcher, $user, $routing; function __construct($dispatcher) { $this->dispatcher = $dispatcher; $this->user = new sfUser($this->dispatcher, new sfSessionStorage()); $this->routing = new sfPatternRouting($this->dispatcher); $this->configure(); } abstract function configure(); function handle($request) { return fpRequestHandler::handle($this, $request); } } class helloApplication extends fpApplication { function configure() { $this->routing->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); } } PHP Quebec 2008 www.symfony-project.com 45 www.sensiolabs.com
  • 46. Move the public properties to accessor methods PHP Quebec 2008 www.symfony-project.com 46 www.sensiolabs.com
  • 47. abstract class fpApplication { protected $dispatcher, $storage, $user, $routing; function __construct($dispatcher) { $this->dispatcher = $dispatcher; $this->configure(); } function getDispatcher() { return $this->dispatcher; } function getStorage() { if (is_null($this->storage)) { $this->storage = new new sfSessionStorage(); } return $this->storage; } function getUser() { if (is_null($this->user)) { $this->user = new sfUser($this->dispatcher, $this->getStorage()); } return $this->user; } function getRouting() { if (is_null($this->routing)) { $this->routing = new sfPatternRouting($this->dispatcher); } return $this->routing; } // ... } PHP Quebec 2008 www.symfony-project.com 47 www.sensiolabs.com
  • 48. Sensible defaults • Most of the time – The dispatcher is a sfEventDispatcher – The request is a sfWebRequest object • Let’s change the Application to take defaults PHP Quebec 2008 www.symfony-project.com 48 www.sensiolabs.com
  • 49. abstract class fpApplication { function __construct($dispatcher = null) { $this->dispatcher = is_null($dispatcher) ? new sfEventDispatcher() : $dispatcher; // ... } function handle($request = null) { $request = is_null($request) ? new sfWebRequest($this->dispatcher) : $request; return fpRequestHandler::handle($this, $request); } } $application = new helloApplication(); $response = $application->handle(); $response->send(); PHP Quebec 2008 www.symfony-project.com 49 www.sensiolabs.com
  • 50. More sensible defaults • Most of the time – The controller creates a sfWebResponse object – … with some content • Let’s introduce a new Controller abstract class PHP Quebec 2008 www.symfony-project.com 50 www.sensiolabs.com
  • 51. class fpController { function __construct($application) { $this->application = $application; } function render($content) { $response = new sfWebResponse($this->application->dispatcher); $response->setContent($content); return $response; } } class helloController extends fpController { function indexAction($request) { $name = $request->getParameter('name', $this->application->getUser()->getAttribute('name', 'World')); $this->application->getUser()->setAttribute('name', $name); return $this->render('Hello '.$name); } } PHP Quebec 2008 www.symfony-project.com 51 www.sensiolabs.com
  • 52. Move the framework • Move the framework code to its own directory structure require dirname(__FILE__).'/../lib/symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); require dirname(__FILE__).'/../lib/framework/fpApplication.class.php'; require dirname(__FILE__).'/../lib/framework/fpController.class.php'; require dirname(__FILE__).'/../lib/framework/fpRequestHandler.class.php'; class helloApplication extends fpApplication { // ... } class helloController extends fpController { // ... } $application = new helloApplication(); $application->handle()->send(); PHP Quebec 2008 www.symfony-project.com 52 www.sensiolabs.com
  • 53. Autoload our framework require dirname(__FILE__).'/../lib/symfony/lib/autoload/sfCoreAutoload.class.php'; sfCoreAutoload::register(); $autoload = sfSimpleAutoload::getInstance(); $autoload->addDirectory(dirname(__FILE__).'/../lib/framework'); $autoload->register(); class helloApplication extends fpApplication { // ... } class helloController extends fpController { // ... } $application = new helloApplication(); $application->handle()->send(); PHP Quebec 2008 www.symfony-project.com 53 www.sensiolabs.com
  • 54. Create a bootstrap file require dirname(__FILE__).'/../lib/framework/bootstrap.php'; class helloApplication extends fpApplication { // ... } class helloController extends fpController { // ... } $application = new helloApplication(); $application->handle()->send(); PHP Quebec 2008 www.symfony-project.com 54 www.sensiolabs.com
  • 55. Move classes • Move the application and controller classes to their own directory structure require dirname(__FILE__).'/../lib/framework/bootstrap.php'; require dirname(__FILE__).'/../hello/application.class.php'; require dirname(__FILE__).'/../hello/controller/helloController.class.php'; $application = new helloApplication(); $application->handle()->send(); PHP Quebec 2008 www.symfony-project.com 55 www.sensiolabs.com
  • 56. Summary • 3 generic classes – fpApplication – fpController – fpRequestHandler • 2 specific classes – helloApplication – helloController • A small boostrap code $application = new helloApplication(); $application->handle()->send(); PHP Quebec 2008 www.symfony-project.com 56 www.sensiolabs.com
  • 57. Conventions • We already have some conventions – Controller class names – Action method names • Let’s add some directory name conventions – Controllers are in the controller directory in the same directory as applications.class.php – The controller file is the controller class name suffixed by .class.php PHP Quebec 2008 www.symfony-project.com 57 www.sensiolabs.com
  • 58. abstract class fpApplication { function __construct($dispatcher = null) { $this->dispatcher = is_null($dispatcher) ? new sfEventDispatcher() : $dispatcher; $r = new ReflectionObject($this); $this->root = realpath(dirname($r->getFileName())); // ... } // ... } class fpRequestHandler { static function handle($application, $request) { $class = $request->getParameter('controller').'Controller'; $method = $request->getParameter('action').'Action'; require_once $application->root.'/controller/'.$class.'.class.php'; $controller = new $class(); $response = $controller->$method($application, $request); return $response; } } PHP Quebec 2008 www.symfony-project.com 58 www.sensiolabs.com
  • 59. Let’s add a simple templating system based on PHP PHP Quebec 2008 www.symfony-project.com 59 www.sensiolabs.com
  • 60. abstract class fpApplication { protected $dispatcher, $storage, $user, $routing, $template; function getTemplate() { if (is_null($this->template)) { $this->template = new fpTemplate(); } return $this->template; } // ... } class fpTemplate { function render($template, $parameters = array()) { extract($parameters); ob_start(); require $template; return ob_get_clean(); } } PHP Quebec 2008 www.symfony-project.com 60 www.sensiolabs.com
  • 61. The directory structure • Add a template directory PHP Quebec 2008 www.symfony-project.com 61 www.sensiolabs.com
  • 62. class fpController { // ... function render($template, $parameters = array()) { $template = $this->application->root.'/template/'.$template; $content = $this->application->getTemplate()->render($template, $parameters); $response = new sfWebResponse($this->application->getDispatcher()); $response->setContent($content); return $response; } } class helloController extends fpController { function indexAction($request) { $name = $request->getParameter('name', $this->application->getUser()->getAttribute('name', 'World')); $this->application->getUser()->setAttribute('name', $name); return $this->render('hello.php', array('name' => $name)); } } PHP Quebec 2008 www.symfony-project.com 62 www.sensiolabs.com
  • 63. Write some tests • Create a test/ directory to host test classes • Use PHPUnit to test our controllers • Change the session storage object PHP Quebec 2008 www.symfony-project.com 63 www.sensiolabs.com
  • 64. <?php require_once 'PHPUnit/Framework.php'; require dirname(__FILE__).'/../../lib/framework/bootstrap.php'; require dirname(__FILE__).'/../application.class.php'; class helloApplicationTest extends helloApplication { function __construct($dispatcher = null) { parent::__construct($dispatcher); $this->root = dirname(__FILE__).'/..'; } function getStorage() { return new sfSessionTestStorage( array('session_path' => '/tmp/quebec_demo', 'session_id' => '123') ); } } PHP Quebec 2008 www.symfony-project.com 64 www.sensiolabs.com
  • 65. class helloControllerTest extends PHPUnit_Framework_TestCase { protected function setUp() { $this->application = new helloApplicationTest(); } public function testWithRequestParameter() { $_SERVER['PATH_INFO'] = '/hello/Fabien'; $request = new sfWebRequest($this->application->getDispatcher()); $response = $this->application->handle($request); $this->assertEquals('Hello Fabien', $response->getContent()); } public function testWithSession() { $_SERVER['PATH_INFO'] = '/hello'; $request = new sfWebRequest($this->application->getDispatcher()); $response = $this->application->handle($request); $this->assertEquals('Hello Fabien', $response->getContent()); } } PHP Quebec 2008 www.symfony-project.com 65 www.sensiolabs.com
  • 66. Refactor the code to create a fpControllerTest class PHP Quebec 2008 www.symfony-project.com 66 www.sensiolabs.com
  • 67. abstract class fpControllerTest extends PHPUnit_Framework_TestCase { protected function setUp() { $r = new ReflectionObject($this); $root = realpath(dirname($r->getFileName()).'/..'); require_once $root.'/application.class.php'; $this->application = $this->getMock( basename($root).'Application', array('getStorage') ); $this->application->root = $root; $storage = new sfSessionTestStorage( array('session_path' => '/tmp/quebec_demo', 'session_id' => '123') ); $this->application-> expects($this->any())-> method('getStorage')-> will($this->returnValue($storage)) ; } // ... } PHP Quebec 2008 www.symfony-project.com 67 www.sensiolabs.com
  • 68. Add XSS protection • Add XSS protection by escaping all template parameters • Use sfOutputEscaper symfony classes to do the job • Update the tests PHP Quebec 2008 www.symfony-project.com 68 www.sensiolabs.com
  • 69. abstract class fpApplication { // ... function getTemplate() { if (is_null($this->template)) { $this->template = new fpTemplate($this->dispatcher); } return $this->template; } // ... } class fpTemplate { function __construct($dispatcher) { $this->dispatcher = $dispatcher; } function render($template, $parameters = array()) { $event = $this->dispatcher->filter(new sfEvent($this, 'template.filter_parameters'), $parameters); $parameters = $event->getReturnValue(); // ... } } PHP Quebec 2008 www.symfony-project.com 69 www.sensiolabs.com
  • 70. class helloApplication extends fpApplication { function configure() { $this->getRouting()->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); $this->getDispatcher()->connect('template.filter_parameters’, array($this, 'escapeTemplateParameters')); } function escapeTemplateParameters(sfEvent $event, $parameters) { $parameters['sf_data'] = sfOutputEscaper::escape(array($this, 'htmlspecialchars'), $parameters); foreach ($parameters['sf_data'] as $key => $value) { $parameters[$key] = $value; } return $parameters; } function htmlspecialchars($value) { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); } } PHP Quebec 2008 www.symfony-project.com 70 www.sensiolabs.com
  • 71. Move the generic code to fpApplication PHP Quebec 2008 www.symfony-project.com 71 www.sensiolabs.com
  • 72. class helloApplication extends fpApplication { function configure() { $this->getRouting()->connect('hello', '/hello/:name', array('controller' => 'hello', 'action' => 'index')); $this->enableOutputEscaping(); } } class fpApplication { // ... function enableOutputEscaping() { $this->dispatcher->connect('template.filter_parameters’, array($this, 'escapeTemplateParameters')); } function escapeTemplateParameters(sfEvent $event, $parameters) { $parameters['sf_data'] = sfOutputEscaper::escape(array($this, 'htmlspecialchars'), $parameters); foreach ($parameters['sf_data'] as $key => $value) { $parameters[$key] = $value; } return $parameters; } function htmlspecialchars($value) { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); } } PHP Quebec 2008 www.symfony-project.com 72 www.sensiolabs.com
  • 73. Add custom 404, 500 • Allow custom 404 and 500 pages – 404 pages > sfError404Exception – 500 pages > Exception • Change fpRequestHandler PHP Quebec 2008 www.symfony-project.com 73 www.sensiolabs.com
  • 74. Customization • Add some events in the RequestHandler – application.request – application.controller – application.response – application.exception • They can return a sfResponse and stop the RequestHandler flow PHP Quebec 2008 www.symfony-project.com 74 www.sensiolabs.com
  • 75. What for? • Page caching – application.request: If I have the page in cache, unserialize the response from the cache and returns it – application.response : Serialize the response to the cache • Security – application.controller: Check security and change the controller if needed PHP Quebec 2008 www.symfony-project.com 75 www.sensiolabs.com
  • 76. If we have time… • Add CSS selector support to the test class • Implement a page cache system • Implement a security mecanism for controllers • Improve performance with caching (routing, framework « compilation », …) • Add a CLI • Implement a layout system • Use symfony Forms • Use a database/model PHP Quebec 2008 www.symfony-project.com 76 www.sensiolabs.com
  • 77. SENSIO S.A. 26, rue Salomon de Rothschild 92 286 Suresnes Cedex FRANCE Tél. : +33 1 40 99 80 80 Fax : +33 1 40 99 83 34 Contact Fabien Potencier fabien.potencier@sensio.com http://www.sensiolabs.com/ http://www.symfony-project.com/ PHP Quebec 2008 www.symfony-project.com 77 www.sensiolabs.com