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.