a php christmas miracle - 3 frameworks, 1 app

Post on 09-May-2015

18.428 Views

Category:

Technology

2 Downloads

Preview:

Click to see full reader

DESCRIPTION

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.

TRANSCRIPT

A PHP Christmas Miracle

Ryan Weaver@weaverryan

A story of deception, wisdom, and finding our common interface

Saturday, December 3, 11

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/enhttp://www.github.com/weaverryan

@weaverryanSaturday, December 3, 11

Act 1:

A History of modern PHP

@weaverryanSaturday, December 3, 11

First, the facts

Saturday, December 3, 11

To put it politely...

Saturday, December 3, 11

We have a surplus of PHP frameworks

Saturday, December 3, 11

To put it honestly...

Saturday, December 3, 11

We have a sh*tload

Saturday, December 3, 11

‣ Symfony‣ Zend Framework‣ Lithium‣ Aura (formerly Solar)‣ Code Igniter‣ Yii‣ Cake‣ Fuel‣ Akelos‣ Kohana‣ Flow3‣ ...

Saturday, December 3, 11

And we solve common problems

@weaverryanSaturday, December 3, 11

‣ HTTP classes (e.g. request, response)‣ Routing‣ Controllers‣ Templates‣ Loggers‣ Database tools‣ Service containers‣ Security‣ Serialization

‣ Forms‣ Events‣ Mailing‣ Validation‣ Pagination‣ Menus‣ Search

Common Problems

@weaverryanSaturday, December 3, 11

Lack of sharing means duplicated efforts

Saturday, December 3, 11

But, there is some hope...

Saturday, December 3, 11

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

PSR-0 with namespaces

class: Symfony\Component\HttpFoundation\Request

path: vendor/src/Symfony/Component/HttpFoundation/Request.php

Namespace your classes and have the namespaces follow the directory structure

Saturday, December 3, 11

PSR-0 with underscores

class: Twig_Extension_Core

path: vendor/twig/lib/Twig/Extension/Core.php

Use underscores in your classes and follow the directory structure

Saturday, December 3, 11

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

@weaverryanSaturday, December 3, 11

What else can we agree on?

Saturday, December 3, 11

So far, not much

Saturday, December 3, 11

But we’ll see how all the libraries can still work

together

Saturday, December 3, 11

And we can always hope for a Christmas miracle

Saturday, December 3, 11

Act 2:

Starting with a Horrible App

@weaverryanSaturday, December 3, 11

How many of you use a framework on a regular

basis?

Saturday, December 3, 11

How many of you have been told that it's a bad idea to not

use a framework?

Saturday, December 3, 11

Was I the one who told you that?

Saturday, December 3, 11

Today we’re going to break the rules...

Saturday, December 3, 11

... build a framework from “scratch” ...

Saturday, December 3, 11

... and see why that’s no longer necessarily a bad

thing.

Saturday, December 3, 11

• 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...

Today’s 2 goals:

@weaverryanSaturday, December 3, 11

Our starting point

• Our app is a single file that fuels two pages

http://bit.ly/php-xmas

• Following along with the code of our app at:

Saturday, December 3, 11

Saturday, December 3, 11

• Shucks, we even have a database connection

Saturday, December 3, 11

Open our Database connection

try { $dbPath = __DIR__.'/data/database.sqlite'; $dbh = new PDO('sqlite:'.$dbPath);} catch(PDOException $e) { die('Panic! '.$e->getMessage());}

// index.php

Saturday, December 3, 11

Try to get a clean URI

$uri = $_SERVER['REQUEST_URI'];if ($pos = strpos($uri, '?')) { $uri = substr($uri, 0, $pos);}

// index.php

Saturday, December 3, 11

Render the homepage

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'] ); }}

// index.php

Saturday, December 3, 11

Print out some lettersif ($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>';}

// index.php

Saturday, December 3, 11

Got it?

Saturday, December 3, 11

Great, let’s clean this mess up

Saturday, December 3, 11

Act 3:

Symfony's HTTP Foundation

@weaverryanSaturday, December 3, 11

• 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

Problems

@weaverryanSaturday, December 3, 11

• Symfony’s HttpFoundation Component

• Gives us (among other things) a solid Request and Response class

Solution

@weaverryanSaturday, December 3, 11

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/ClassLoader

Saturday, December 3, 11

Current Status

@weaverryanSaturday, December 3, 11

• 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

Autoloading

@weaverryanSaturday, December 3, 11

Create a bootstrap file<?php// bootstrap.php

require __DIR__.'/vendors/Symfony/Component/ClassLoader/UniversalClassLoader.php';use Symfony\Component\ClassLoader\UniversalClassLoader;

// setup the autoloader$loader = new UniversalClassLoader();$loader->registerNamespace( 'Symfony', __DIR__.'/vendors');$loader->register();

Saturday, December 3, 11

... and include it

<?php// index.phprequire 'bootstrap.php';

// ...

Saturday, December 3, 11

So how does this help?

Saturday, December 3, 11

$uri = $_SERVER['REQUEST_URI'];if ($pos = strpos($uri, '?')) { $uri = substr($uri, 0, $pos);}

use Symfony\Component\HttpFoundation\Request;$request = Request::createFromGlobals();

// the clean URI - a lot of logic behind it!!!$uri = $request->getPathInfo();

... becomes ...

Saturday, December 3, 11

if (isset($_GET['name'])) { echo sprintf( '<p>Oh, and hello %s!</p>', $_GET['name'] );}

if ($name = $request->query->get('name')) { echo sprintf(

'<p>Oh, and hello %s!</p>',$name

);}

... becomes ...

Saturday, December 3, 11

• Normalizes server variables across systems

• Shortcut methods to common things like getClientIp(), getHost(), getContent(), etc

• Nice object-oriented interface

The “Request” object

@weaverryanSaturday, December 3, 11

• We also have a Response object

• Instead of echoing out content, we populate this fluid object

The “Response” object

@weaverryanSaturday, December 3, 11

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>';

$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();

... becomes ...

Saturday, December 3, 11

Act 4:

Routing

@weaverryanSaturday, December 3, 11

• Our app is a giant gross “if” statementProblems

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

• 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

Solution

@weaverryanSaturday, December 3, 11

3 Steps to Bringing in an external tool

Saturday, December 3, 11

git submodule add git://github.com/UnionOfRAD/lithium.git vendors/lithium

#1 Download the library

Saturday, December 3, 11

// bootstrap.php// ...

$loader = new UniversalClassLoader();$loader->registerNamespace('Symfony', __DIR__.'/vendors');$loader->registerNamespace('lithium', __DIR__.'/vendors');

$loader->register();

#2 Configure the autoloader

Saturday, December 3, 11

use lithium\net\http\Router;$router = new Router();// ...

#3 Celebrate!

Saturday, December 3, 11

@weaverryan

Current Status

Saturday, December 3, 11

So how do we use the router?

Saturday, December 3, 11

Full disclosure: “use” statements I’m hiding from the

next page

use Symfony\Component\HttpFoundation\Request;use lithium\net\http\Router;use lithium\action\Request as Li3Request;

Saturday, December 3, 11

$request = Request::createFromGlobals();$li3Request = new Li3Request();// get the URL from Symfony's request, give it to lithium$li3Request->url = $request->getPathInfo();

a) Map URI to “controller”

// 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

// 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();

b) Execute the controller*

* each controller is a flat function Saturday, December 3, 11

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);}

The Controllers

Saturday, December 3, 11

function letters(Request $request){ global $dbh;

$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);}

The Controllers$kitten--

Saturday, December 3, 11

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;}

The Controllers

Saturday, December 3, 11

1. Request cleans the URI2. Router matches the URI to a route, returns a “controller” string3. We execute the controller function4. The controller creates a Response object5. We send the Response headers and content

The Big Picture

@weaverryanSaturday, December 3, 11

Your 20 line framework

Saturday, December 3, 11

Act 5:

Pimple!

@weaverryanSaturday, December 3, 11

• We’ve got lots of random, disorganized objects floating around

Problems

• And we can’t easily access them from within our controllers

@weaverryan

function letters(Request $request){ global $dbh;

// .... }

Saturday, December 3, 11

• Pimple! - a Dependency Injection Container

• Dependency Injection Container:

the scariest word we could think of todescribe an array of objects on steroids

Solution

@weaverryanSaturday, December 3, 11

Remember: 3 Steps to bringing in an external tool

Saturday, December 3, 11

git submodule add git://github.com/fabpot/Pimple.git vendors/Pimple

#1 Download the library

Saturday, December 3, 11

// bootstrap.php// ...

require __DIR__.'/vendors/Pimple/lib/Pimple.php';

#2 Configure the autoloader

actually, it’s only one file - so just require it!

Saturday, December 3, 11

$c = new Pimple();

#3 Celebrate!

Saturday, December 3, 11

• 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

Pimple Creates Objects

@weaverryanSaturday, December 3, 11

$c = new Pimple();

$c['connection'] = $c->share(function() { $dsn = 'sqlite:'.__DIR__.'/data/database.sqlite'; return new PDO($dsn);});

Centralize the db connection

Saturday, December 3, 11

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

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

• 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

Access to what we need

@weaverryanSaturday, December 3, 11

Pass the container to the controller

$c = new Pimple();

// ...

$response = call_user_func_array( $controller, array($request, $c));

Saturday, December 3, 11

function letters(Request $request, Pimple $c){ $dbh = $c['connection'];

$sql = 'SELECT * FROM php_santa_letters'; $content = '<h1>Read the letters to PHP Santa</h1>'; $content .= '<ul>'; foreach ($dbh->query($sql) as $row) { // ... } // ...}

$kitten++

Saturday, December 3, 11

What else?

How about configuration?

Saturday, December 3, 11

$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

Further?

What about dependencies?

Saturday, December 3, 11

$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

With everything in the container, our “framework”

just got skinny

Saturday, December 3, 11

$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

Logging with ZF2

@weaverryanSaturday, December 3, 11

• I don’t have enough frameworks in my framework

Problems

• Oh yeah, and we need logging...

@weaverryanSaturday, December 3, 11

• Zend Framework2

• ZF2 has a ton of components, including a logger

Solution

@weaverryanSaturday, December 3, 11

3 Steps to bringing in an external tool

Saturday, December 3, 11

git submodule add git://github.com/zendframework/zf2.git vendors/zf2

#1 Download the library

Saturday, December 3, 11

// bootstrap.php// ...

$loader = new UniversalClassLoader();$loader->registerNamespace('Symfony', __DIR__.'/vendors');$loader->registerNamespace('lithium', __DIR__.'/vendors');$loader->registerNamespace( 'Zend', __DIR__.'/vendors/zf2/library');

$loader->register();

#2 Configure the autoloader

Saturday, December 3, 11

use Zend\Log\Logger;use Zend\Log\Writer\Stream;

$logger = Logger($pimple['logger_writer']);

#3 Celebrate!

Yes we did just bring in a 100k+ lines of code for a simple logger :)

Saturday, December 3, 11

Current Status

@weaverryanSaturday, December 3, 11

use Zend\Log\Logger;use Zend\Log\Writer\Stream;

$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']);});

Create the Logger in our Fancy Container

Saturday, December 3, 11

function error404(Request $request, Pimple $c){ $c['logger']->log( 'Crap, 404 for '.$request->getPathInfo(), Logger::ERR ); $content = '<h1>404 Page not Found</h1>'; // ...}

And use it anywhere

Saturday, December 3, 11

Getting kinda easy, right?

Saturday, December 3, 11

What other libraries can you think to integrate?

Saturday, December 3, 11

Getting Organized

@weaverryanSaturday, December 3, 11

• Our application has 4 major parts:1) autoloading setup2) Creation of container3) Definition of routes4) Definition of controllers5) The code that executes everything

• For our business, only #3 and #4 are important

• ... but it’s all jammed together

Problems

@weaverryanSaturday, December 3, 11

• 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

Solution

@weaverryanSaturday, December 3, 11

• 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

Starting point

@weaverryanSaturday, December 3, 11

‣ 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

Ending point

@weaverryanSaturday, December 3, 11

<?php

// index.php$c = require 'bootstrap.php';require 'routing.php';require 'controllers.php';

$response = _run_application($c);$response->send();

Nothing to see here...

Saturday, December 3, 11

// bootstrap.phpfunction _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) );}

“Framework” hidden away...

Saturday, December 3, 11

// routing.php$c['router']->connect( '/letters', array('controller' => 'letters'));

$c['router']->connect( '/{:name}', array( 'controller' => 'homepage', 'name' => null ));

Routes have a home

Saturday, December 3, 11

// controllers.phpuse Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;

function homepage(Request $request) { // ...}

function letters(Request $request, $c){ // ...}

function error404(Request $request){ // ...}

Controllers have a home

Saturday, December 3, 11

To make $$$, work in routes.php and controllers.php

Saturday, December 3, 11

Final Thoughts

@weaverryanSaturday, December 3, 11

It doesn’t matter if you use a framework...

Saturday, December 3, 11

... inherited a legacy spaghetti system...

Saturday, December 3, 11

... or practice “not-invented-here” development

Saturday, December 3, 11

...an innumerable number of tools are available...

Saturday, December 3, 11

... so you can stop writing your framework ...

Saturday, December 3, 11

... and start writing your application

Saturday, December 3, 11

What’s available? Search GitHub

Saturday, December 3, 11

Thanks!Questions?

Ryan Weaver@weaverryan

Saturday, December 3, 11

• http://bit.ly/php-xmas• http://github.com/symfony• https://github.com/UnionOfRAD/lithium• https://github.com/zendframework/zf2• http://pimple.sensiolabs.org/

References

@weaverryan

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

Saturday, December 3, 11

top related