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.
Unraveling Multimodality with Large Language Models.pdf
A PHP Christmas Miracle - 3 Frameworks, 1 app
1. A PHP Christmas Miracle
A story of deception, wisdom, and finding our
common interface
Ryan Weaver
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
3. Act 1:
A History of modern PHP
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
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.php
Saturday, December 3, 11
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.php
Saturday, December 3, 11
17. But what does this mean?
• An "autoloader" is a tool you can use so that
you don't have to worry about “including”
classes before you use them
• Use anyone’s autoloader
• We're all still duplicating each other's work,
but at least everyone’s autoloader does the
same thing
@weaverryan
Saturday, December 3, 11
27. ... build a framework from
“scratch” ...
Saturday, December 3, 11
28. ... and see why that’s no
longer necessarily a bad
thing.
Saturday, December 3, 11
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...
@weaverryan
Saturday, December 3, 11
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 pages
Saturday, December 3, 11
32. • Shucks, we even have a database connection
Saturday, December 3, 11
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. 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. 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. 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
39. Act 3:
Symfony's HTTP Foundation
@weaverryan
Saturday, December 3, 11
40. Problems
• Our code for trying to get a clean URL is a bit
archaic and probably error prone
• We're echoing content from our controllers,
maybe we can evolve
@weaverryan
Saturday, December 3, 11
41. Solution
• Symfony’s HttpFoundation Component
• Gives us (among other things) a solid Request
and Response class
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
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. ... and include it
<?php
// index.php
require 'bootstrap.php';
// ...
Saturday, December 3, 11
47. So how does this help?
Saturday, December 3, 11
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. 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. The “Request” object
• Normalizes server variables across systems
• Shortcut methods to common things like
getClientIp(), getHost(), getContent(), etc
• Nice object-oriented interface
@weaverryan
Saturday, December 3, 11
51. The “Response” object
• We also have a Response object
• Instead of echoing out content, we populate
this fluid object
@weaverryan
Saturday, December 3, 11
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. Act 4:
Routing
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
56. 3 Steps to Bringing in an
external tool
Saturday, December 3, 11
57. #1 Download the library
git submodule add git://github.com/UnionOfRAD/
lithium.git vendors/lithium
Saturday, December 3, 11
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. #3 Celebrate!
use lithiumnethttpRouter;
$router = new Router();
// ...
Saturday, December 3, 11
61. So how do we use the router?
Saturday, December 3, 11
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. a) Map URI to “controller”
$request = Request::createFromGlobals();
$li3Request = new Li3Request();
// get the URL from Symfony's 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. 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" didn't return a response!!',
$controller
));
}
$response->send();
* each controller is a flat function
Saturday, December 3, 11
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. 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. 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. 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
@weaverryan
Saturday, December 3, 11
70. Act 5:
Pimple!
@weaverryan
Saturday, December 3, 11
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;
// ....
}
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
73. Remember: 3 Steps to
bringing in an external tool
Saturday, December 3, 11
74. #1 Download the library
git submodule add git://github.com/fabpot/
Pimple.git vendors/Pimple
Saturday, December 3, 11
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. #3 Celebrate!
$c = new Pimple();
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
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. Centralize the db connection
$c1 = $c['connection'];
$c2 = $c['connection'];
// they are the same - only one object is created!
$c1 === $c2
Saturday, December 3, 11
80. Centralize the db connection
$c1 = $c['connection'];
$c2 = $c['connection'];
// they are the same - only one object is created!
$c1 === $c2
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
82. Pass the container to the
controller
$c = new Pimple();
// ...
$response = call_user_func_array(
$controller,
array($request, $c)
);
Saturday, December 3, 11
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. What else?
How about configuration?
Saturday, December 3, 11
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. Further?
What about dependencies?
Saturday, December 3, 11
91. Problems
• I don’t have enough frameworks in my
framework
• Oh yeah, and we need logging...
@weaverryan
Saturday, December 3, 11
92. Solution
• Zend Framework2
• ZF2 has a ton of components, including a
logger
@weaverryan
Saturday, December 3, 11
93. 3 Steps to bringing in an
external tool
Saturday, December 3, 11
94. #1 Download the library
git submodule add git://github.com/
zendframework/zf2.git vendors/zf2
Saturday, December 3, 11
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. #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
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. 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
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
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
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
@weaverryan
Saturday, December 3, 11
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
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. 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. To make $$$, work in
routes.php and controllers.php
Saturday, December 3, 11