Showing posts with label Slim. Show all posts
Showing posts with label Slim. Show all posts

PHP Framework Comparison (update: opcode caching)

Reading about PHP frameworks, you'll get a lot about Symfony and Zend Framework. It is true that these frameworks have a lot of features and do great marketing. But what about performance and scalability?

There are 2 kinds of frameworks: those written in C as an extension and those written in PHP
Using a framework written in C will definitively give the best performance. But fixing (security) bugs and maintaining them requires C knowledge. If the documentation is not complete or incorrect, you might need to read the code to understand the functionality or wait for the maintainer to help you. So if you don't need super high performance, my recommendation is using a framework written in PHP.

Here is a test from Laruence (PHP core developer):

higher numbers are better, source

Here is another test from Zeev Suraski (Zend/PHP core developer):

higher numbers are better, source

Here is another excellent test from Wang Rui:


smaller numbers are better, source (with call graphs, response time, etc.)

Looking at the range of the numbers, you'll see that choosing the right framework can reduce the number of servers significantly. Also, response times can vary a lot between them. Using opcode caching for PHP (e.g. APC) is a key factor for good performance.

Are PHP frameworks dying?

PDO has shown that the best features from frameworks will make their way to the PHP core. I'm sure we'll see features like object relational mapping and expression based request routing some day in the PHP core. But PHP frameworks are growing much faster than the PHP core, so it is unlikely to see them disappearing.

Next generation frameworks

At the moment, there is no concurrency in PHP. Each script runs inside of one thread to generate the content. But most pages have several areas which could be generated simultaneously. A new extension called pthreads brings threads to PHP. So you can easily render several content areas in parallel, kill slow queries from your script, monitor the execution or do logging in a separate thread. This will bring up new frameworks which do all kinds of things in parallel to serve dynamic pages a lot faster.

Write your own framework?

Big players in IT business (Google, Facebook, Twitter, Yahoo) all write their own frameworks. If you have good ideas and implement exactly what your business needs, that's the way to go. Most full-stack frameworks are maintained by only 1 or 2 persons and are deeply integrated into consulting businesses, so writing your own is not more expensive or risky.
Using small (micro) frameworks with <10k lines is a good choice because you can adapt them quickly and maintenance costs are still low. If you want to get a new business started very quickly and sell it early, you should use an existing framework.

We must use a framework

The language PHP is already a great framework. Since most PHP frameworks only wrap existing functions into new methods, there is no direct need to use a framework ("reduce waste"). So you should look out where a framework reduces code size and complexity in your code. When extending or changing a framework, you need a strategy to migrate your changes to newer versions.

Using a framework from Facebook or Twitter will give me their success?

Yes, if you're exactly cloning Facebook and Twitter. No for all other cases. Think about problem and solution: choosing the solution before looking at the problem will not solve the problem and might produce new problems.

Typical problems in frameworks

  • code that is doing nothing but needs to be there to fit in a structure (example)
  • code that is only used if an extension is not loaded (e.g. json_decode())
  • too much abstraction (example)
  • too many lines of code (e.g. ZF1 260,000, ZF2 146,000 loc, Symfony2 173,000 loc, Doctrine2 70,000 loc), too hard to optimize, too hard to maintain
  • too many public methods (e.g. ZF1 13,000, ZF2 7,800, Symfony2 9,500, Doctrine2 4,400), too hard to learn
  • too many get/set methods (e.g. ZF2 4,080, Symfony2 3,800 (analysis from nikic))
  • too much documentation (e.g. ZF2 1000 pages and 500 examples)
  • too many includes (example)
  • unneeded wrapping (e.g. ZF2 echo StaticFilter::execute($str, "HtmlEntities", array("quotestyle"=>ENT_QUOTES)); echo (new Zend\Filter\HtmlEntities(array("quotestyle" => ENT_QUOTES)))->filter($str);)
  • code bloat (e.g. running a database query needs more than one line in the code)
  • migration from ZF1, Symfony1, Doctrine1

Build your own framework

Related articles:

How to write a really small and fast controller with PHP (update: benchmark Slim, Silex, Zend Framework, Symfony2)

To handle a lot of traffic, we need a fast controller with very little memory overhead. First, we implement a dynamic controller.

The design is based on the micro frameworks Slim and Silex. The first example maps the URL "http://server/index.php/blog/2012/03/02" to a function with the parameters $year, $month and $day:

// index.php, handle /blog/2012/03/02
$app = new App();
$app->get('/blog/:year/:month/:day', function($year, $month, $day) {
printf('%d-%02d-%02d', $year, $month, $day);
});
Our controller is a class named App and uses the get() function to map a GET request. Parameters mapped to the function are marked with a colon. Optional parameters are written inside brackets. Here is an example:

// handle /blog, /blog/2012, /blog/2012/03 and /blog/2012/03/02
$app = new App();
$app->get('/blog(/:year(/:month(/:day)))', function($year=2012, $month=1,
$day=1) {
printf('%d-%02d-%02d', $year, $month, $day);
});

Instead of printf(), we can use quote() to replace < and > with HTML entities. In the second example we map "index.php/product/42/super-coding-book" to a function with the parameter $id. We use * in the URL to match any character different from "/":

// index.php, handle /product/42/seo-text
$app = new App();
$app->get('/product/:id/*', function($id) use ($app) {
echo 'You selected '.$app->quote($id);
});

In the third example we map "index.php/profile/jdoe" to a function with the parameter $username and render the output with a PHP template:

// index.php, handle /profile/jdoe
$app = new App();
$app->get('/profile/:username', function($username) use ($app) {
$app->message = 'Hello '.$username;
$app->display('profile.php');
});

// profile.php
<html><body>
Message: <?= $this->quote($this->message) ?>
</body></html>

Instead of using anonymous functions, we can also forward the request to a normal function. In the next example, the request is forwarded to the static method greet() in the class Hello:

// forwards /hello/world to Hello::greet('world')
$app = new App();
$app->get('/hello/:name', 'Hello::greet');

class Hello {
static function greet($name) {
echo 'Hello '.$name;
}
}

The controller is also able to forward the request to more than one function. In this example, the request also calls header() and footer() from the Page class:

// forwards /welcome to Page::header(); User::show(); Page::footer();
$app->get('/welcome', ['Page::header', 'User::show()', 'Page::footer']);

To make testing easier, we can use a decorator subclassing to convert the output of a function to JSON:

$app = new AppJson();
$app->get('/json/range', function() {
return range(0, 10);
});

// output
[0,1,2,3,4,5,6,7,8,9,10]

Instead of named parameters, we can also use anonymous parameters with the get_p() function:

// index.php, handle /blog/2012/03/02
$app = new App();
$app->get_p('/blog/:p/:p/:p', function($year, $month, $day) {
printf('%d-%02d-%02d', $year, $month, $day);
});
Note that get_p() is 40 percent faster than get().

And finally, here is the controller:

set_exception_handler('App::exception'); // bootstrap

class App {
protected $_server = [];

public function __construct() {
// skipped mocking here
$this->_server = &$_SERVER;
}

public function get($pattern, $callback) {
$this->_route('GET', $pattern, $callback);
}

public function get_p($pattern, $callback) {
$this->_route_p('GET', $pattern, $callback);
}

public function delete($pattern, $callback) {
$this->_route('DELETE', $pattern, $callback);
}

protected function _route($method, $pattern, $callback) {
if ($this->_server['REQUEST_METHOD']!=$method) return;

// convert URL parameter (e.g. ":id", "*") to regular expression
$regex = preg_replace('#:([\w]+)#', '(?<\\1>[^/]+)',
str_replace(['*', ')'], ['[^/]+', ')?'], $pattern));
if (substr($pattern,-1)==='/') $regex .= '?';

// extract parameter values from URL if route matches the current request
if (!preg_match('#^'.$regex.'$#', $this->_server['PATH_INFO'], $values)) {
return;
}
// extract parameter names from URL
preg_match_all('#:([\w]+)#', $pattern, $params, PREG_PATTERN_ORDER);
$args = [];
foreach ($params[1] as $param) {
if (isset($values[$param])) $args[] = urldecode($values[$param]);
}
$this->_exec($callback, $args);
}

protected function _route_p($method, $pattern, $callback) {
if ($this->_server['REQUEST_METHOD']!=$method) return;

// convert URL parameters (":p", "*") to regular expression
$regex = str_replace(['*','(',')',':p'], ['[^/]+','(?:',')?','([^/]+)'],
$pattern);
if (substr($pattern,-1)==='/') $regex .= '?';

// extract parameter values from URL if route matches the current request
if (!preg_match('#^'.$regex.'$#', $this->_server['PATH_INFO'], $values)) {
return;
}
// decode URL parameters
array_shift($values);
foreach ($values as $key=>$value) $values[$key] = urldecode($value);
$this->_exec($callback, $values);
}

protected function _exec(&$callback, &$args) {
foreach ((array)$callback as $cb) call_user_func_array($cb, $args);
throw new Halt(); // Exception instead of exit;
}

// Stop execution on exception and log as E_USER_WARNING
public static function exception($e) {
if ($e instanceof Halt) return;
trigger_error($e->getMessage()."\n".$e->getTraceAsString(), E_USER_WARNING);
$app = new App();
$app->display('exception.php', 500);
}

public function quote($str) {
return htmlspecialchars($str, ENT_QUOTES);
}

public function render($template) {
ob_start();
include($template);
return ob_get_clean();
}

public function display($template, $status=null) {
if ($status) header('HTTP/1.1 '.$status);
include($template);
}

public function __get($name) {
if (isset($_REQUEST[$name])) return $_REQUEST[$name];
return '';
}
}

class AppJson extends App {
protected function _exec(&$callback, &$args) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(call_user_func_array($callback, $args));
throw new Halt(); // Exception instead of exit;
}
}

// use Halt-Exception instead of exit;
class Halt extends Exception {}

The controller fits perfectly into a high traffic scenario:
  • less than 100 lines of code
  • memory overhead less than 256 KB
  • runtime less than 1 ms

Here are some benchmarks (1.4 GHz):
Name without APC [seconds] with APC [seconds]
App 0.0009 0.0005
Slim 1.6.4 0.0159 0.0083
Silex 0.0596 0.0221
ZendFramework 1.11 0.1625 0.0631
Symfony 2.0.16 0.1968 0.0362
The numbers show that our controller is 17 times faster than Slim, 44 times faster than Silex, 72 times faster than Symfony and 126 times faster than ZF.

Here is the code:

// App
$start = microtime(true);
require 'App.php';
try {
$app = new App();
$app->get('/hello/:name', function ($name) use ($app) {
$app->name = $name;
$app->display('foo.php');
// foo.php: echo 'Hello '.$this->quote($this->name);
});
} catch (Exception $e) {}
echo ' '.(microtime(true)-$start);

// Slim
$start = microtime(true);
require 'Slim/Slim.php';
$app = new Slim();
$app->get('/hello/:name', function ($name) use ($app) {
$app->render('foo.php', ['name' => $name]);
// foo.php: echo 'Hello '.htmlspecialchars($name);
});
$app->run();
echo ' '.(microtime(true)-$start);

// Silex
$start = microtime(true);
require 'silex/vendor/autoload.php';
$app = new Silex\Application();
$app->get('/hello/{name}', function($name) use($app) {
require('templates/foo.php');
// foo.php: echo 'Hello '.htmlspecialchars($name);
});
$app->run();
echo ' '.(microtime(true)-$start);

// ZendFramework
// zf create project helloworld
// helloworld/application/controllers/IndexController.php
class IndexController extends Zend_Controller_Action {
public function helloAction() {
$this->view->name = $this->getRequest()->getParam('name');
}
}
// helloworld/application/views/scripts/index/hello.phtml
Hello <?= $this->escape($this->name); ?>
// helloworld/public.php
$start = microtime(true);
...
echo ' '.(microtime(true)-$start);
// GET /index.php/index/hello/?name=world

// Symfony2
$start = microtime(true);
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
$kernel = new AppKernel('dev', false);
$kernel->loadClassCache();
$kernel->handle(Symfony\Component\HttpFoundation\Request::createFromGlobals())->send();
echo ' '.(microtime(true)-$start);
// src\Acme\DemoBundle\Controller\DemoController.php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DemoController extends Controller {
/**
* @Route("/hello/{name}", name="_demo_hello")
*/
public function helloAction($name) {
return new Response('Hello '.htmlspecialchars($name, ENT_QUOTES));
}
}
// GET /web/index.php/demo/hello/World

Lessons learned:
  • A good controller can speed up requests by a factor of 100
  • A good controller is the base for all kinds of performance optimizations
  • Controllers included in PHP frameworks are slow

Next: even more performance with static controller, parameter validation, Post and Put methods, File uploads, benchmark ZendFramework 2.0, Symfony 2.1