A PHP Christmas Miracle - 3 Frameworks, 1 app

20,401 views

Published on

In this presentation, we walk take a flat PHP4-style application and gently migrate it into our own "framework", that uses components from Symfony2, Lithium, Zend Framework and a library called Pimple. By the end, you'll see how any ugly application can take advantage of the many wonderful tools available to PHP developers.

Published in: Technology
2 Comments
41 Likes
Statistics
Notes
No Downloads
Views
Total views
20,401
On SlideShare
0
From Embeds
0
Number of Embeds
1,104
Actions
Shares
0
Downloads
330
Comments
2
Likes
41
Embeds 0
No embeds

No notes for slide

A PHP Christmas Miracle - 3 Frameworks, 1 app

  1. 1. A PHP Christmas Miracle A story of deception, wisdom, and finding our common interface Ryan Weaver @weaverryanSaturday, December 3, 11
  2. 2. Who is this dude? • Co-author of the Symfony2 Docs • Core Symfony2 contributor • Co-owner of KnpLabs US • Fiancee of the much more talented @leannapelham http://www.knplabs.com/en http://www.github.com/weaverryan @weaverryanSaturday, December 3, 11
  3. 3. Act 1: A History of modern PHP @weaverryanSaturday, December 3, 11
  4. 4. First, the factsSaturday, December 3, 11
  5. 5. To put it politely...Saturday, December 3, 11
  6. 6. We have a surplus of PHP frameworksSaturday, December 3, 11
  7. 7. To put it honestly...Saturday, December 3, 11
  8. 8. We have a sh*tloadSaturday, December 3, 11
  9. 9. ‣ Symfony ‣ Zend Framework ‣ Lithium ‣ Aura (formerly Solar) ‣ Code Igniter ‣ Yii ‣ Cake ‣ Fuel ‣ Akelos ‣ Kohana ‣ Flow3 ‣ ...Saturday, December 3, 11
  10. 10. And we solve common problems @weaverryanSaturday, December 3, 11
  11. 11. Common Problems ‣ HTTP classes (e.g. request, response) ‣ Routing ‣ Controllers ‣ Forms ‣ Templates ‣ Events ‣ Loggers ‣ Mailing ‣ Database tools ‣ Validation ‣ Service containers ‣ Pagination ‣ Security ‣ Menus ‣ Serialization ‣ Search @weaverryanSaturday, December 3, 11
  12. 12. Lack of sharing means duplicated effortsSaturday, December 3, 11
  13. 13. But, there is some hope...Saturday, December 3, 11
  14. 14. PSR-0 • The PHP community came together, sang Kumbaya and wrote up some class-naming standards • PSR-0 isn’t a library, it’s just an agreement to name your classes in one of two ways @weaverryanSaturday, December 3, 11
  15. 15. PSR-0 with namespaces Namespace your classes and have the namespaces follow the directory structure class: SymfonyComponentHttpFoundationRequest path: vendor/src/Symfony/Component/HttpFoundation/Request.phpSaturday, December 3, 11
  16. 16. PSR-0 with underscores Use underscores in your classes and follow the directory structure class: Twig_Extension_Core path: vendor/twig/lib/Twig/Extension/Core.phpSaturday, December 3, 11
  17. 17. But what does this mean? • An "autoloader" is a tool you can use so that you dont have to worry about “including” classes before you use them • Use anyone’s autoloader • Were all still duplicating each others work, but at least everyone’s autoloader does the same thing @weaverryanSaturday, December 3, 11
  18. 18. What else can we agree on?Saturday, December 3, 11
  19. 19. So far, not muchSaturday, December 3, 11
  20. 20. But we’ll see how all the libraries can still work togetherSaturday, December 3, 11
  21. 21. And we can always hope for a Christmas miracleSaturday, December 3, 11
  22. 22. Act 2: Starting with a Horrible App @weaverryanSaturday, December 3, 11
  23. 23. How many of you use a framework on a regular basis?Saturday, December 3, 11
  24. 24. How many of you have been told that its a bad idea to not use a framework?Saturday, December 3, 11
  25. 25. Was I the one who told you that?Saturday, December 3, 11
  26. 26. Today we’re going to break the rules...Saturday, December 3, 11
  27. 27. ... build a framework from “scratch” ...Saturday, December 3, 11
  28. 28. ... and see why that’s no longer necessarily a bad thing.Saturday, December 3, 11
  29. 29. Today’s 2 goals: • Refactor a crappy flat PHP application into a framework that makes sense • Use as many libraries from as many quarreling PHP tribes as possible ‣ Symfony ‣ Zend Framework ‣ Lithium ‣ ... only lack of time prevents more... @weaverryanSaturday, December 3, 11
  30. 30. Our starting point • Following along with the code of our app at: http://bit.ly/php-xmas • Our app is a single file that fuels two pagesSaturday, December 3, 11
  31. 31. Saturday, December 3, 11
  32. 32. • Shucks, we even have a database connectionSaturday, December 3, 11
  33. 33. Open our Database connection // index.php try { $dbPath = __DIR__./data/database.sqlite; $dbh = new PDO(sqlite:.$dbPath); } catch(PDOException $e) { die(Panic! .$e->getMessage()); }Saturday, December 3, 11
  34. 34. Try to get a clean URI // index.php $uri = $_SERVER[REQUEST_URI]; if ($pos = strpos($uri, ?)) { $uri = substr($uri, 0, $pos); }Saturday, December 3, 11
  35. 35. Render the homepage // index.php if ($uri == / || $uri == ) { echo <h1>Welcome to PHP Santa</h1>; echo <a href="/letters">Readletters</a>; if (isset($_GET[name])) { echo sprintf( <p>Oh, and hello %s!</p>, $_GET[name] ); } }Saturday, December 3, 11
  36. 36. Print out some letters // index.php if ($uri == /letters) { $sql = SELECT * FROM php_santa_letters; echo <h1>Read the letters to PHP Santa</h1>; echo <ul>; foreach ($dbh->query($sql) as $row) { echo sprintf( <li>%s - dated %s</li>, $row[content], $row[received_at] ); } echo </ul>; }Saturday, December 3, 11
  37. 37. Got it?Saturday, December 3, 11
  38. 38. Great, let’s clean this mess upSaturday, December 3, 11
  39. 39. Act 3: Symfonys HTTP Foundation @weaverryanSaturday, December 3, 11
  40. 40. Problems • Our code for trying to get a clean URL is a bit archaic and probably error prone • Were echoing content from our controllers, maybe we can evolve @weaverryanSaturday, December 3, 11
  41. 41. Solution • Symfony’s HttpFoundation Component • Gives us (among other things) a solid Request and Response class @weaverryanSaturday, December 3, 11
  42. 42. Bring in HttpFoundation mkdir -p vendors/Symfony/Component git submodule add git://github.com/symfony/HttpFoundation.git vendors/Symfony/Component/HttpFoundation/ git submodule add git://github.com/symfony/ClassLoader.git vendors/Symfony/Component/ClassLoaderSaturday, December 3, 11
  43. 43. Current Status @weaverryanSaturday, December 3, 11
  44. 44. Autoloading • No matter what framework or libraries you use, you’ll need an autoloader • We’ll use Symfony’s “ClassLoader” • Each PSR-0 autoloader is very similar @weaverryanSaturday, December 3, 11
  45. 45. Create a bootstrap file <?php // bootstrap.php require __DIR__./vendors/Symfony/Component/ ClassLoader/UniversalClassLoader.php; use SymfonyComponentClassLoaderUniversalClassLoader; // setup the autoloader $loader = new UniversalClassLoader(); $loader->registerNamespace( Symfony, __DIR__./vendors ); $loader->register();Saturday, December 3, 11
  46. 46. ... and include it <?php // index.php require bootstrap.php; // ...Saturday, December 3, 11
  47. 47. So how does this help?Saturday, December 3, 11
  48. 48. $uri = $_SERVER[REQUEST_URI]; if ($pos = strpos($uri, ?)) { $uri = substr($uri, 0, $pos); } ... becomes ... use SymfonyComponentHttpFoundationRequest; $request = Request::createFromGlobals(); // the clean URI - a lot of logic behind it!!! $uri = $request->getPathInfo();Saturday, December 3, 11
  49. 49. if (isset($_GET[name])) { echo sprintf( <p>Oh, and hello %s!</p>, $_GET[name] ); } ... becomes ... if ($name = $request->query->get(name)) { echo sprintf( <p>Oh, and hello %s!</p>, $name ); }Saturday, December 3, 11
  50. 50. The “Request” object • Normalizes server variables across systems • Shortcut methods to common things like getClientIp(), getHost(), getContent(), etc • Nice object-oriented interface @weaverryanSaturday, December 3, 11
  51. 51. The “Response” object • We also have a Response object • Instead of echoing out content, we populate this fluid object @weaverryanSaturday, December 3, 11
  52. 52. header("HTTP/1.1 404 Not Found"); echo <h1>404 Page not Found</h1>; echo <p>This is most certainly *not* an xmas miracle</p>; ... becomes ... $content = <h1>404 Page not Found</h1>; $content .= <p>This is most certainly *not* an xmas miracle</p>; $response = new Response($content); $response->setStatusCode(404); $response->send();Saturday, December 3, 11
  53. 53. Act 4: Routing @weaverryanSaturday, December 3, 11
  54. 54. Problems • Our app is a giant gross “if” statement if ($uri == / || $uri == ) { // ... } elseif ($uri == /letters) { // ... } else { // ... } • Grabbing a piece from the URL like /blog/my-blog-post will take some work @weaverryanSaturday, December 3, 11
  55. 55. Solution • Lithium’s Routing library • Routing matches URIs (e.g. /foo) and returns information we attached to that URI pattern • All the nasty regex matching is out-of-sight @weaverryanSaturday, December 3, 11
  56. 56. 3 Steps to Bringing in an external toolSaturday, December 3, 11
  57. 57. #1 Download the library git submodule add git://github.com/UnionOfRAD/ lithium.git vendors/lithiumSaturday, December 3, 11
  58. 58. #2 Configure the autoloader // bootstrap.php // ... $loader = new UniversalClassLoader(); $loader->registerNamespace(Symfony, __DIR__./vendors); $loader->registerNamespace(lithium, __DIR__./vendors); $loader->register();Saturday, December 3, 11
  59. 59. #3 Celebrate! use lithiumnethttpRouter; $router = new Router(); // ...Saturday, December 3, 11
  60. 60. Current Status @weaverryanSaturday, December 3, 11
  61. 61. So how do we use the router?Saturday, December 3, 11
  62. 62. Full disclosure: “use” statements I’m hiding from the next page use SymfonyComponentHttpFoundationRequest; use lithiumnethttpRouter; use lithiumactionRequest as Li3Request;Saturday, December 3, 11
  63. 63. a) Map URI to “controller” $request = Request::createFromGlobals(); $li3Request = new Li3Request(); // get the URL from Symfonys request, give it to lithium $li3Request->url = $request->getPathInfo(); // create a router, build the routes, and then execute it $router = new Router(); $router->connect(/letters, array(controller => letters)); $router->connect(/, array(controller => homepage)); $router->parse($li3Request); if (isset($li3Request->params[controller])) { $controller = $li3Request->params[controller]; } else { $controller = error404; }Saturday, December 3, 11
  64. 64. b) Execute the controller* // execute the controller, send the request, get the response $response = call_user_func_array($controller, array($request)); if (!$response instanceof Response) { throw new Exception(sprintf( WTF! Your controller "%s" didnt return a response!!, $controller )); } $response->send(); * each controller is a flat functionSaturday, December 3, 11
  65. 65. The Controllers function homepage(Request $request) { $content = <h1>Welcome to PHP Santa</h1>; $content .= <a href="/letters">Read the letters</a>; if ($name = $request->query->get(name)) { $content .= sprintf( <p>Oh, and hello %s!</p>, $name ); } return new Response($content); }Saturday, December 3, 11
  66. 66. The Controllers function letters(Request $request) { global $dbh; $kitten-- $sql = SELECT * FROM php_santa_letters; $content = <h1>Read the letters to PHP Santa</h1>; $content .= <ul>; foreach ($dbh->query($sql) as $row) { $content .= sprintf( <li>%s - dated %s</li>, $row[content], $row[received_at] ); } $content .= </ul>; return new Response($content); }Saturday, December 3, 11
  67. 67. The Controllers function error404(Request $request) { $content = <h1>404 Page not Found</h1>; $content .= This is most certainly *not* an xmas miracle; $response = new Response($content); $response->setStatusCode(404); return $response; }Saturday, December 3, 11
  68. 68. The Big Picture 1. Request cleans the URI 2. Router matches the URI to a route, returns a “controller” string 3. We execute the controller function 4. The controller creates a Response object 5. We send the Response headers and content @weaverryanSaturday, December 3, 11
  69. 69. Your 20 line frameworkSaturday, December 3, 11
  70. 70. Act 5: Pimple! @weaverryanSaturday, December 3, 11
  71. 71. Problems • We’ve got lots of random, disorganized objects floating around • And we can’t easily access them from within our controllers function letters(Request $request) { global $dbh; // .... } @weaverryanSaturday, December 3, 11
  72. 72. Solution • Pimple! - a Dependency Injection Container • Dependency Injection Container: the scariest word we could think of to describe an array of objects on steroids @weaverryanSaturday, December 3, 11
  73. 73. Remember: 3 Steps to bringing in an external toolSaturday, December 3, 11
  74. 74. #1 Download the library git submodule add git://github.com/fabpot/ Pimple.git vendors/PimpleSaturday, December 3, 11
  75. 75. #2 Configure the autoloader // bootstrap.php // ... require __DIR__./vendors/Pimple/lib/Pimple.php; actually, it’s only one file - so just require it!Saturday, December 3, 11
  76. 76. #3 Celebrate! $c = new Pimple();Saturday, December 3, 11
  77. 77. Pimple Creates Objects • Use Pimple to create and store your objects in a central place • If you have the Pimple container object, then you have access to every other object in your application @weaverryanSaturday, December 3, 11
  78. 78. Centralize the db connection $c = new Pimple(); $c[connection] = $c->share(function() { $dsn = sqlite:.__DIR__./data/database.sqlite; return new PDO($dsn); });Saturday, December 3, 11
  79. 79. Centralize the db connection $c1 = $c[connection]; $c2 = $c[connection]; // they are the same - only one object is created! $c1 === $c2Saturday, December 3, 11
  80. 80. Centralize the db connection $c1 = $c[connection]; $c2 = $c[connection]; // they are the same - only one object is created! $c1 === $c2Saturday, December 3, 11
  81. 81. Access to what we need • So far, we’re using a “global” keyword to access our database connection • But if we pass around our Pimple container, we always have access to anything we need - including the database connection @weaverryanSaturday, December 3, 11
  82. 82. Pass the container to the controller $c = new Pimple(); // ... $response = call_user_func_array( $controller, array($request, $c) );Saturday, December 3, 11
  83. 83. function letters(Request $request, Pimple $c) { $dbh = $c[connection]; $kitten++ $sql = SELECT * FROM php_santa_letters; $content = <h1>Read the letters to PHP Santa</h1>; $content .= <ul>; foreach ($dbh->query($sql) as $row) { // ... } // ... }Saturday, December 3, 11
  84. 84. What else? How about configuration?Saturday, December 3, 11
  85. 85. $c = new Pimple(); // configuration $c[connection_string] = sqlite:.__DIR__ ./data/database.sqlite; $c[connection] = $c->share(function(Pimple $c) { return new PDO($c[connection_string]); });Saturday, December 3, 11
  86. 86. Further? What about dependencies?Saturday, December 3, 11
  87. 87. $c[request] = $c->share(function() { return Request::createFromGlobals(); }); $c[li3_request] = $c->share(function($c) { $li3Request = new Li3Request(); $li3Request->url = $c[request]->getPathInfo(); return $li3Request; }); // ... $li3Request = $c[li3_request];Saturday, December 3, 11
  88. 88. With everything in the container, our “framework” just got skinnySaturday, December 3, 11
  89. 89. $c = new Pimple(); // create objects in Pimple // execute our routing, merge attributes to request $result = $c[router]->parse($c[li3_request]); $c[request]->attributes ->add($c[li3_request]->params); // get controller and execute! $controller = $c[request]->attributes ->get(controller, error404); $response = call_user_func_array( $controller, array($c[request], $c) ); $response->send();Saturday, December 3, 11
  90. 90. Logging with ZF2 @weaverryanSaturday, December 3, 11
  91. 91. Problems • I don’t have enough frameworks in my framework • Oh yeah, and we need logging... @weaverryanSaturday, December 3, 11
  92. 92. Solution • Zend Framework2 • ZF2 has a ton of components, including a logger @weaverryanSaturday, December 3, 11
  93. 93. 3 Steps to bringing in an external toolSaturday, December 3, 11
  94. 94. #1 Download the library git submodule add git://github.com/ zendframework/zf2.git vendors/zf2Saturday, December 3, 11
  95. 95. #2 Configure the autoloader // bootstrap.php // ... $loader = new UniversalClassLoader(); $loader->registerNamespace(Symfony, __DIR__./vendors); $loader->registerNamespace(lithium, __DIR__./vendors); $loader->registerNamespace( Zend, __DIR__./vendors/zf2/library ); $loader->register();Saturday, December 3, 11
  96. 96. #3 Celebrate! use ZendLogLogger; use ZendLogWriterStream; $logger = Logger($pimple[logger_writer]); Yes we did just bring in a 100k+ lines of code for a simple logger :)Saturday, December 3, 11
  97. 97. Current Status @weaverryanSaturday, December 3, 11
  98. 98. Create the Logger in our Fancy Container use ZendLogLogger; use ZendLogWriterStream; $c[log_path] = __DIR__./data/web.log; $c[logger_writer] = $c->share(function($pimple) { return new Stream($pimple[log_path]); }); $c[logger] = $c->share(function($pimple) { return new Logger($pimple[logger_writer]); });Saturday, December 3, 11
  99. 99. And use it anywhere function error404(Request $request, Pimple $c) { $c[logger]->log( Crap, 404 for .$request->getPathInfo(), Logger::ERR ); $content = <h1>404 Page not Found</h1>; // ... }Saturday, December 3, 11
  100. 100. Getting kinda easy, right?Saturday, December 3, 11
  101. 101. What other libraries can you think to integrate?Saturday, December 3, 11
  102. 102. Getting Organized @weaverryanSaturday, December 3, 11
  103. 103. Problems • Our application has 4 major parts: 1) autoloading setup 2) Creation of container 3) Definition of routes 4) Definition of controllers 5) The code that executes everything • For our business, only #3 and #4 are important • ... but it’s all jammed together @weaverryanSaturday, December 3, 11
  104. 104. Solution • Some definitions “Application” - the code that makes you money “Framework” - under-the-hood code that impresses your geek friends • To be productive, let’s “hide” the framework @weaverryanSaturday, December 3, 11
  105. 105. Starting point • Our app basically has 2 files ‣ bootstrap.php: holds autoloading ‣ index.php: holds - container setup - definition of routes - definition of controllers - the code that executes it all @weaverryanSaturday, December 3, 11
  106. 106. Ending point ‣ bootstrap.php: holds - autoloading - container setup - the code that executes it all (as a function called _run_application()) ‣ controllers.php: holds controllers ‣ routes.php: holds routes ‣ index.php: pulls it all together @weaverryanSaturday, December 3, 11
  107. 107. Nothing to see here... <?php // index.php $c = require bootstrap.php; require routing.php; require controllers.php; $response = _run_application($c); $response->send();Saturday, December 3, 11
  108. 108. “Framework” hidden away... // bootstrap.php function _run_application(Pimple $c) { $c[router]->parse($c[li3_request]); $c[request]->attributes ->add($c[li3_request]->params); $controller = $c[request]->attributes ->get(controller, error404); return call_user_func_array( $controller, array($c[request], $c) ); }Saturday, December 3, 11
  109. 109. Routes have a home // routing.php $c[router]->connect( /letters, array(controller => letters) ); $c[router]->connect( /{:name}, array( controller => homepage, name => null ) );Saturday, December 3, 11
  110. 110. Controllers have a home // controllers.php use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpFoundationResponse; function homepage(Request $request) { // ... } function letters(Request $request, $c) { // ... } function error404(Request $request) { // ... }Saturday, December 3, 11
  111. 111. To make $$$, work in routes.php and controllers.phpSaturday, December 3, 11
  112. 112. Final Thoughts @weaverryanSaturday, December 3, 11
  113. 113. It doesn’t matter if you use a framework...Saturday, December 3, 11
  114. 114. ... inherited a legacy spaghetti system...Saturday, December 3, 11
  115. 115. ... or practice “not-invented- here” developmentSaturday, December 3, 11
  116. 116. ...an innumerable number of tools are available...Saturday, December 3, 11
  117. 117. ... so you can stop writing your framework ...Saturday, December 3, 11
  118. 118. ... and start writing your applicationSaturday, December 3, 11
  119. 119. What’s available? Search GitHubSaturday, December 3, 11
  120. 120. Thanks! Questions? Ryan Weaver @weaverryanSaturday, December 3, 11
  121. 121. References • http://bit.ly/php-xmas • http://github.com/symfony • https://github.com/UnionOfRAD/lithium • https://github.com/zendframework/zf2 • http://pimple.sensiolabs.org/ And if we had more time... • http://twig.sensiolabs.org/ • https://github.com/knplabs/KnpMenu • https://github.com/Seldaek/monolog • https://github.com/avalanche123/Imagine @weaverryanSaturday, December 3, 11

×