Hola a todos, Hoy llueve y hace algo de frío. El otoño ha venido y, bueno, qué mejor que un poco de programación para pasar la tarde. Entremos al punto a tratar en este post. He realizado un pequeño código con tres frameworks PHP de los más utilizados. CakePHP es el framework que utilizo en mi día a día y que en cierto modo da buenos resultados, pero que para proyectos de cierto tamaño puede llegar a quedarse corto. Symfony2 es el framework de moda en el mundo PHP, el mejor en cuanto a calidad del código y aplicación de patrones de diseño. Por otro lado, tenemos Yii puede que sea el más desconocido de los tres. Yii me llamó la atención por la disponibilidad por la que presume, y por su organización modular al igual que Symfony2. Probablemente el framework más fuerte que se ha quedado fuera ha sido Zend, básicamente por cuestiones de tiempo del autor :). Bueno, entremos en matería. Se ha desarrollado un pequeño ejemplo que emula la típica petición ajax que hacemos para tomar una serie de datos del servidor, unos datos de productos de nuestra base de datos, proporcionados por un servicio SOA. Primero mostraremos el código de ejemplo desarrollado en cada framework. Al ser un problema tan pequeño, con mostrar el código del controlador y el del modelo en cuestión será suficiente en la mayoría de los casos.

CakePHP

Este es el código para CakePHP utilizando la versión 2.1.3

Modelo Product

<?php
class Product extends AppModel {}

Controlador SoaController

<?php

App::uses('AppController', 'Controller');
App::uses('CakeResponse', 'Network');

class SoaController extends AppController {
    public $uses = array('Product');

    public function index() {
        $products = $this->Product->find('all', array(
            'conditions' => array(
                'price  >= 25.0'
            ),
            'order' => array('price' => 'DESC')
        ));

        return new CakeResponse(array('body' => json_encode($products)));
    }
}

Como podemos ver el código es sencillo, simplemente con hacer una llamada find con el ORM de Cake y haciendo un json_encode conseguimos el resultado deseado.


Symfony2

En Symfony2 (versión 2.1.2), he utilizado como base el DemoBundle que viene con al instalación del framework. No sé si esto puede haber influido en los resultados, a la vista de un no experto la cantidad de código aboga a un framework prácticamente vacío.

Entidad

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/** Acme\DemoBundle\Entity\Product */
class Product {
/**
     * @var integer $id
     */
    private $id;

    /**
     * @var float $price
     */
    private $price;

    /**
     * @var string $description
     */
    private $description;


    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set price
     *
     * @param float $price
     * @return Product
     */
    public function setPrice($price)
    {
        $this->price = $price;
    
        return $this;
    }

    /**
     * Get price
     *
     * @return float 
     */
    public function getPrice()
    {
        return $this->price;
    }

    /**
     * Set description
     *
     * @param string $description
     * @return Product
     */
    public function setDescription($description)
    {
        $this->description = $description;
    
        return $this;
    }

    /**
     * Get description
     *
     * @return string 
     */
    public function getDescription()
    {
        return $this->description;
    }
    
    public function toArray()
    {
        return array(
            'id' => $this->getId(),
            'description' => $this->getDescription(),
            'price' => $this->getPrice()
        );
    }
}

Como se puede ver, se ha utilizado básicamente la clase generada por el asistente de consola del framework, añadiendo un método toArray para hacer más fácil posteriormente la conversión a JSON.

Controlador

<?php

namespace Acme\DemoBundle\Controller;

use Symfony\Component\HttpFoundation\Response;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Acme\DemoBundle\Form\ContactType;

// these import the "@Route" and "@Template" annotations
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class DemoController extends Controller
{
    //...
    
    /**
     * @Route("/soa")
     */
    public function soaAction() {
     $repository = $this->getDoctrine()
     ->getRepository('AcmeDemoBundle:Product');

     $query = $repository->createQueryBuilder('p')
     ->where('p.price > :price')
     ->setParameter('price', '25.00')
     ->orderBy('p.price', 'DESC')
     ->getQuery();

     $products = $query->getResult();
     
     $narr = array();
     
     foreach($products as $product) {
      $narr[] = $product->toArray();
     }
     
     $response = new Response();
     $response->setContent(json_encode($narr)); 
     return $response;
    }

    //...
}

Como se puede ver, he tenido que hacer un foreach que puede haber supuesto un extra de carga, pero realmente no veía otro modo de solucionar el problema de la conversión propiedades del objeto a JSON de manera más eficiente.

Yii

En Yii 1.1.12, a mi gusto la solución es la más elegante de todas. Sin demasiadas complicaciones como en Symfony2, ni demasiada directa y un poco sucia como en CakePHP. A mi gusto, Yii es un framework realmente interesante y que ofrece una modularidad equiparable a los Bundles de Symfony2.

Modelo Product

<?php
/**
 * This is the model class for table "products".
 *
 * The followings are the available columns in table 'products':
 * @property integer $id
 * @property string $price
 * @property string $description
 */
class Product extends CActiveRecord
{
 /**
  * Returns the static model of the specified AR class.
  * @param string $className active record class name.
  * @return Product the static model class
  */
 public static function model($className=__CLASS__)
 {
  return parent::model($className);
 }

 /**
  * @return string the associated database table name
  */
 public function tableName()
 {
  return 'products';
 }

 /**
  * @return array validation rules for model attributes.
  */
 public function rules()
 {
  // NOTE: you should only define rules for those attributes that
  // will receive user inputs.
  return array(
   array('price, description', 'required'),
   array('price', 'length', 'max'=>10),
   array('description', 'length', 'max'=>255),
   // The following rule is used by search().
   // Please remove those attributes that should not be searched.
   array('id, price, description', 'safe', 'on'=>'search'),
  );
 }

 /**
  * @return array relational rules.
  */
 public function relations()
 {
  // NOTE: you may need to adjust the relation name and the related
  // class name for the relations automatically generated below.
  return array(
  );
 }

 /**
  * @return array customized attribute labels (name=>label)
  */
 public function attributeLabels()
 {
  return array(
   'id' => 'ID',
   'price' => 'Price',
   'description' => 'Description',
  );
 }

 /**
  * Retrieves a list of models based on the current search/filter conditions.
  * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
  */
 public function search()
 {
  // Warning: Please modify the following code to remove attributes that
  // should not be searched.

  $criteria=new CDbCriteria;

  $criteria->compare('id',$this->id);
  $criteria->compare('price',$this->price,true);
  $criteria->compare('description',$this->description,true);

  return new CActiveDataProvider($this, array(
   'criteria'=>$criteria,
  ));
 }
}

Esta es la clase autogenerada por la herramienta Gii. Para aquellos que no gustan del shell puede resultar más cómoda que el shell que lleva Symfony2, pero en mi opinión en este caso apoyo más la solución de Symfony, ya que el shell acaba incrementando la productividad del desarrollador.

Controlador

<?php

/**
 * SiteController is the default controller to handle user requests.
 */
class SiteController extends CController
{
 /**
  * Index action is the default action in a controller.
  */
 public function actionIndex()
 {
  
  $results = Product::model()->findAll('price>=:price', array(':price' => 25.0));
  
  $narr = array();
  
  foreach($results as $result) {
   $narr[] = array('id' => $result->id, 'price' => $result->price, 'description' => $result->description);
  }
  
  echo json_encode($narr);
 }
}

En Yii, también he terminado por usar un bucle foreach para preparar la salida para la conversión JSON, aunque no utilicé un método toArray. Estas diferencias sútiles pueden resultar algo críticas en un benchmark, aunque con una acción tan sencilla y teniendo detrás todo el código de un framework no me parecen muy significativas.

Bueno, ¿qué es un benchmark sin los datos de tiempos y demás? Ahora viene esa parte, antes quería exponer abiertamente el código para evitar malinterpretaciones y dejando claro que es un código muy básico, por lo que los resultados no pueden aplicarse a una aplicación real en producción.

La configuración del servidor utilizada ha sido un servidor LAMP con las siguientes versiones:

Sistema Operativo Ubuntu 12.04 LTS Linux 3.2.0-30-generic
Apache Apache/2.2.22
MySQL Server 5.5.24
PHP PHP 5.3.10-1ubuntu3.4 with Suhosin-Patch Zend Engine v2.3.0, Copyright (c) 1998-2012 Zend Technologies with Xdebug v2.1.0, Copyright (c) 2002-2010, by Derick Rethans

Como herramienta para el benchmark he utilizado siege. siege permite el benchmarking de una o varios URLs dada una concurrencia. Estos son los resultados, de aplicar el siguiente comando con siege con diferentes configuraciones de Apache:

siege -c 5 -b -t30s $URL

Veamos la primera tabla de resultados:

Modo desarrollo sin APC

  CakePHP 2.1.3 Symfony 2.1.2 Yii 1.1.12
Transactions (hits) 1150 hits 152 hits 1191 hits
Availability 100% 100% 100%
Response time 0.13 s 0.96 s 0.12 s
Concurrency 4.97 4.96 4.99
Throughput 0.03 MB/s 0.00 MB/s 0.04 MB/s
Shortest Transaction 0.08 s 0.63 s 0.07 s
Longest Transaction 0.25 s 1.51 s 0.30 s

Bueno, por lo visto Yii Framework supera en velocidad al resto en modo desarrollo. Aunque realmente, ¿quien despliega su aplicación en producción en modo desarrollo? Vamos pues a mostrar los datos poniendo cada uno de estos frameworks en modo producción.

Modo producción sin APC

  CakePHP 2.1.3 Symfony 2.1.2 Yii 1.1.12
Transactions (hits) 1135 hits 265 hits 1353 hits
Availability 100% 100% 100%
Response time 0.13 s 0.54 s 0.11 s
Concurrency 4.99 4.95 4.99
Throughput 0.03 MB/s 0.01 MB/s 0.04 MB/s
Shortest Transaction 0.08 s 0.33 s 0.06 s
Longest Transaction 0.32 s 1.28 s 0.30 s

Symfony2 prácticamente duplica su rendimiento, mientras que Yii mejora un poco y CakePHP parece que su distinción entre modo desarrollo y modo producción es simplemente el error_reporting,...

Veamos ahora dándoles un poco más de caña

Modo producción sin APC (Concurrencia 50)

  CakePHP 2.1.3 Symfony 2.1.2 Yii 1.1.12
Transactions (hits) 1104 hits 97 hits 1336 hits
Availability 100% 100% 100%
Response time 1.31 s 7.31 s 1.09 s
Concurrency 48.81 24.14 49.02
Throughput 0.03 MB/s 0.00 MB/s 0.04 MB/s
Shortest Transaction 0.15 s 0.85 s 0.11 s
Longest Transaction 2.35 s 25.23 s 1.72 s

Symfony2 da un rendimiento muy pobre con 50 clientes concurrentes, mientras que Yii y Cake más o menos se mantienen, no bajando en concurrencia pero sí en tiempo de respuesta.

Bueno, añadamos ahora una mejora importante al rendimiento del PHP. Vamos a añadir el módulo APC para ver como los distintos frameworks mejoran cuando el PHP tiene este módulo añadido.

Modo producción con APC

  CakePHP 2.1.3 Symfony 2.1.2 Yii 1.1.12
Transactions (hits) 2358 hits 542 hits 2307 hits
Availability 100% 100% 100%
Response time 0.06 s 0.28 s 0.06 s
Concurrency 4.99 4.97 4.99
Throughput 0.07 MB/s 0.01 MB/s 0.07 MB/s
Shortest Transaction 0.03 s 0.19 s 0.03 s
Longest Transaction 0.41 s 0.58 s 0.16 s

La instalación ha sido crítica en todos los frameworks. La cantidad de código opcode cacheado por APC que no ha de volver a ser interpretado se nota con creces, teniendo todos los frameworks un aumento de rendimiento aproximadamente del 100%.

Pero volvamos a dar caña con concurrencia 50 :)

Modo producción con APC (Concurrencia 50)

  CakePHP 2.1.3 Symfony 2.1.2 Yii 1.1.12
Transactions (hits) 2485 hits 517 hits 2429 hits
Availability 100% 100% 100%
Response time 0.60 s 2.74 s 0.60 s
Concurrency 49.41 47.51 49.42
Throughput 0.07 MB/s 0.01 MB/s 0.07 MB/s
Shortest Transaction 0.07 s 0.31 s 0.05 s
Longest Transaction 1.21 s 5.87 s 1.10 s

Finalmente podemos ver como Symfony2 con APC es capaz de mantener la concurrencia, aunque dando un tiempo de respuesto muy inferior al que Yii o CakePHP son capaces de responder.

Análisis

Mi análisis sobre estos datos es que Symfony2 sin usar Varnish como el proyecto propone, puede dar un rendimiento bastante pobre, mientras que Yii y CakePHP con la instalación de APC son capaces de dar un rendimiento aceptable. Este aumento en rendimiento no justifica el no usar Symfony2, ya que es el framework mejor preparado para proyectos grandes, pero por este mismo motivo complica el desarrollo en equipos de pocas personas. Finalmente, quería hacer notar la demasiada igualdad de rendimiento entre Yii y CakePHP. Cuando empecé el benchmark creía que Yii mejoraría mucho más el rendimiento de CakePHP, ya que por lo que se ve en el algunos benchmarks publicados, es mucho más rápido que este. En cambio, en este caso no se ha notado la diferencia. Espero algún comentario que rebata este benchmark, sobretodo si viene de usuarios de Yii o Symfony2, para ver si hay algún punto en el que no se haya tratado en igualdad a estos frameworks con CakePHP. Esto es todo por hoy, espero seguir dando actividad al blog y hablando de PHP, Javascript y Web en general.