Analisis de codigo PHP

Hoy navegando me tope con algunas herramientas interesantes para analizar codigo.

Codigo duplicado

La primer herramienta con la que me tope es phpcpd, basicamente busca codigo duplicado, por ejemplo:

test.php

<?
class Test{
  function suma1($x,$y){ return $x+$y; }
  function suma2($a,$b){ return $a + $b; }
  function suma3($a,$b){ return $b+$a; }
}
function suma1($x,$y){ return $x+$y; }
function suma2($a,$b){ return $a + $b; }
function suma3($a,$b){ return $b+$a; }
eugenio@eugenio-desktop:/var/www$ phpcpd test.php
phpcpd 1.3.1 by Sebastian Bergmann.
0.00% duplicated lines out of 9 total lines of code.

WTF??? al parecer no es muy inteligente, para que funcione el codigo debe ser un copy & paste exacto de el codigo.

eugenio@eugenio-desktop:/var/www$ phpcpd ./wordpress/
phpcpd 1.3.1 by Sebastian Bergmann.
Found 27 exact clones with 472 duplicated lines in 7 files:
 - wp-includes/class-simplepie.php:2512-2520
 wp-includes/class-simplepie.php:3414-3422
...
 - wp-content/plugins/akismet/akismet.php:837-851
 wp-content/plugins/akismet/akismet.php:904-918
0.30% duplicated lines out of 157906 total lines of code.

A partir de esta herramienta encontre otras un poco mas complejas y inteligentes;

phpcs, pdepend y phpmd

phpcs

PHP_CodeSniffer es una script PHP5 que “sniffea” archivos PHP, JavaScript y CSS para detectar violaciones de un standard de codigo definido.

Esta herramienta nos indicara cuando no se siga el standard definido, dentro de la documentacion de phpcs en pear nos indican como hacer para definir un standard propio para cambiar el que viene por defecto.

Muestra de uso;

eugenio@eugenio-desktop:/var/www$ phpcs  ./test.php
FILE: /var/www/test.php
--------------------------------------------------------------------------------
FOUND 25 ERROR(S) AND 0 WARNING(S) AFFECTING 8 LINE(S)
--------------------------------------------------------------------------------
 1 | ERROR | Short PHP opening tag used. Found "<?" Expected "<?php".
 1 | ERROR | Missing file doc comment
 2 | ERROR | Missing class doc comment
 2 | ERROR | Opening brace of a class must be on the line after the definition
 3 | ERROR | Line indented incorrectly; expected 4 spaces, found 2
 3 | ERROR | Missing function doc comment
 3 | ERROR | Opening brace should be on a new line
 3 | ERROR | Closing brace must be on a line by itself
 4 | ERROR | Line indented incorrectly; expected 4 spaces, found 2
 4 | ERROR | Missing function doc comment
 4 | ERROR | Opening brace should be on a new line
 4 | ERROR | Closing brace must be on a line by itself
 5 | ERROR | Line indented incorrectly; expected 4 spaces, found 2
 5 | ERROR | Missing function doc comment
 5 | ERROR | Opening brace should be on a new line
 5 | ERROR | Closing brace must be on a line by itself
 7 | ERROR | Missing function doc comment
 7 | ERROR | Opening brace should be on a new line
 7 | ERROR | Closing brace must be on a line by itself
 8 | ERROR | Missing function doc comment
 8 | ERROR | Opening brace should be on a new line
 8 | ERROR | Closing brace must be on a line by itself
 9 | ERROR | Missing function doc comment
 9 | ERROR | Opening brace should be on a new line
 9 | ERROR | Closing brace must be on a line by itself
-----------------------------------------------------------------

Como veran se queja demasiado… es muy conveniente crear un standard propio.

pdepend

Pdepend realizara un informe sobre la metrica del software, analizara la complejidad de las funciones.

Metrica de software que analizara;
Cyclomatic Complexity
NPath Complexity
CodeRank
Lines Of Code

Y finalmente el que creo mas interesante phpmd

What PHPMD does is: It takes a given PHP source code base and look for several potential problems within that source. These problems can be things like:

  • Possible bugs
  • Suboptimal code
  • Overcomplicated expressions
  • Unused parameters, methods, properties
eugenio@eugenio-desktop:/var/www$ phpmd test.php text codesize,unusedcode,naming,naming
/var/www/test.php:12    Avoid variables with short names like $x
/var/www/test.php:12    Avoid variables with short names like $y
/var/www/test.php:12    Avoid variables with short names like $x
...
/var/www/test.php:31    Avoid variables with short names like $a
/var/www/test.php:31    Avoid variables with short names like $b
/var/www/test.php:31    Avoid variables with short names like $a
/var/www/test.php:31    Avoid variables with short names like $b

Analizemos el codigo de worpress…

eugenio@eugenio-desktop:/var/www$ time phpmd ./wordpress text codesize,unusedcode,naming,design>wordpress.doc
real	4m1.382s
user	3m56.543s
sys	0m2.624s

Descargar el informe de analisis al wordpress 3.0.1

eugenio@eugenio-desktop:/var/www$ time phpmd ./meneame4/ text codesize,unusedcode,naming,design>meneame.doc
real    0m42.966s
user    0m39.526s
sys    0m1.064s

Descargar el informe de analisis al meneame v4

Conclusion

De todas las herramientas que probe definitivamente me quedo con phpmd, esta herramienta nos alertara de cualquier codesmell de manera efectiva

Monky, un pantallazo

Hola, hace cuanto que no escribo… estoy algo atareado construyendo a monky, un agregador de noticias open source que funciona bajo el framework cakePHP, comenzo siendo algo asi como un refactor de meneame, ahora si bien tomo algunas ideas de lo que escribio Ricardo Galli, podria decirse que no es meneame.

Les dejo un pantallazo para que vean como va el trabajo.

http://code.google.com/p/monky/

Saludos

Lograr que cakephp soporte subdominios

Hola a todos, por si algunos no lo sabian cakePHP no es muy bueno a la hora de manejar urls con subdominio, en este post voy a escribir sobre como hacer posible manejar subdominios haciendo la menor cantidad de modificaciones posibles.

Basicamente la idea es que pase el subdominio por el named parameter subdomain. Todo esto sin tocar el .htaccess

Para esta solucion necesitamos poner unas pocas lineas en el config/bootstrap.php , otras en el config/routes.php (ya que hay que agregar el domain a las rutas), luego agregamos una constante en webroot/index.php y finalmente usar un nuevo htmlhelper para hacer links correctamente que por supuesto extiende de htmlhelper

Primero:

En el archivo config/bootstrap.php agrega al final:
(Recuerda cambiar test.domain.com por el nombre de tu dominio ejemplo: miweb.com tambien puedes usar un subdominio, es importante que siempre lo escribas en minusculas)

// sub domain in lower
define('MYDOMAIN','test.larompe.com.ar');

if(!isset($_GET['url']))$_GET['url']='';

$token=parse_url($_SERVER['SERVER_NAME']);
if((($pos=strpos($token['path'],MYDOMAIN))>0)){
$subdomain=strtolower(substr($token['path'],0,$pos-1));

}else{
$subdomain='www'

}
$_GET['url']=$subdomain.'/'.$_GET['url'];

Segundo

Vas a tener que modificar las rutas de tu aplicacion, estas se encuentran en config/routes.php

Ejemplo;

La ruta;
Router::connect(‘/’, array(‘controller’ => ‘pages’, ‘action’ => ‘display’, ‘home’));

Pasa a llamarse;
Router::connect(‘/:subdomain/’, array(‘controller’ => ‘pages’, ‘action’ => ‘display’, ‘home’),array(‘subdomain’));

Finalmente tenemos que atrapar los /controller /controller/action;
Router::connect(‘/:subdomain/:controller/:action/*’, array(‘subdomain’));
Router::connect(‘/:subdomain/:controller/*’, array(‘subdomain’));

Recuerda que por defecto si el subdominio no esta seteado pone www, osea que las rutas para la aplicacion principal es;
Router::connect(‘/www/users/*’, array(‘controller’=>’users’,’action’=>’index’));

Tercero

Agregar la siguiente linea al principio de webroot/index.php

define(‘WEBROOT_DIR’, ‘/’);

Cuarto

Crea el helper HtmlSubdomain, para ello crea el archivo views/helpers/html_subdomain.php


<?php
class HtmlSubdomainHelper extends HtmlHelper{
function link($title, $url = null, $options = array(), $confirmMessage = false) {
$escapeTitle = true;
if ($url !== null) {
$url = $this->url($url);
}
return parent::link($title,$url,$options,$confirmMessage);
}
function url($url=null,$full=false){
if(is_array($url)){
if(!isset($url['subdomain'])) $url['subdomain'] = 'www';
$subdomain=$url['subdomain'];
$url=parent::url($url);
$pos=strpos($url,'/'.$subdomain.'/')+strlen('/'.$subdomain);
$url=substr($url,$pos);
return 'http://'.$subdomain.'.'.MYDOMAIN.$url;
}
return parent::url($url,$full);
}
}

Por ultimo

Debes recordar agregar el Helper HtmlSubdomain para crear los links en tu vista, ejemplo;

echo $this->HtmlSubdomain->link(‘hola’,array(‘controller’=>’pages’,’view’=>’display’,’home’,’subdomain’=>’subdom1′);

Un componente y comportamiento para realizar bans

Hola, hoy revisando un poco mas el codigo de monky cree un Componente y un Behaviour para hacer bans.

El Componente puede informarnos;

  • si la ip del usuario esta baneada
  • si el proxy que usa el usuario esta baneado
  • si un mail dado esta baneado
  • si el nombre de una casilla (nombre@dominio.com) esta baneado
  • si el dominio de un mail esta baneado
  • si una direccion web esta baneada
  • si un texto tiene palabras baneadas

El Behaviour agrega metodos al modelo que se usaran para validar los datos ($validate);

  • si un mail dado esta baneado
  • si el nombre de una casilla (nombre@dominio.com) esta baneado
  • si el dominio de un mail esta baneado
  • si una direccion web esta baneada
  • si un texto tiene palabras baneadas

A continuacion dejo todo junto, incluyendo tests del componente!!

con un: Code Coverage: 98.51%.

http://code.google.com/p/ban-behaviour-component/downloads/list

el primer contratiempo en monky :(

Monky es un agregador de noticias que estoy escribiendo en base al meneame, y digo escribiendo ya que practicamente voy a tener que rehacer gran parte del codigo y de las bases de datos.

Por que? bien, principalmente las tablas de meneame hace uso del tipo de dato ENUM, hoy creando los fixtures para hacer test cases y asi alivianar el proceso de testeo me tope con que cakePHP 1.3 no soporta este tipo de datos para crear fixtures 😦
cakePHP solo soporta estos tipos de datos para mySQL;
http://book.cakephp.org/view/1002/Creating-Database-Tables#MySQL-1004

CakePHP doesn’t support enum datatypes because there’s no consistent implementation/support for it across various databases

Para solucionar esto se me ocurrio crear un model llamado Enum, que basicamente emula a un enum. Y asi eliminar el uso de enums en las tablas
sql:

CREATE TABLE `meneame3`.`enums` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`name` VARCHAR( 30 ) NOT NULL
) ENGINE = MYISAM ;

Llenamos la tabla con los “enums”, ejemplo, insertamos;
1,in
2,out
3,ok
4,pendent
5,error

Luego agregamos al modelo el belongsTo hacia a el modelo enum, uno por cada enum que tenga.

Despues de descartar esta idea por que en vez de simplificar complicaba me encontre con esto;
http://www.irisoftonline.com/cakephp-testing-errors-database-table-not-found-enum-type-fields-ignored

Basicamente reescribimos los metodos que crea la base de datos y lo hacemos nosotros mismos;
ver archivo /cake/tests/lib/cake_test_fixture.php

Copio y pego el fixture y el test de los trackbacks;


trackback_fixture.php
<?php
/* Trackback Fixture generated on: 2010-07-13 17:07:48 : 1279053768 */
class TrackbackFixture extends CakeTestFixture {
var $name = 'Trackback';
function create(&$db) {
if (!isset($this->fields) || empty($this->fields)) {
return false;
}
return ($db->execute("
CREATE TABLE `test_suite_trackbacks` (
`trackback_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`trackback_link_id` int(11) NOT NULL DEFAULT '0',
`trackback_user_id` int(11) NOT NULL DEFAULT '0',
`trackback_type` enum('in','out') NOT NULL DEFAULT 'in',
`trackback_status` enum('ok','pendent','error') NOT NULL DEFAULT 'pendent',
`trackback_date` timestamp NULL DEFAULT NULL,
`trackback_ip_int` int(10) unsigned NOT NULL DEFAULT '0',
`trackback_link` varchar(250) NOT NULL,
`trackback_url` varchar(250) DEFAULT NULL,
`trackback_title` text,
`trackback_content` text,
PRIMARY KEY (`trackback_id`),
UNIQUE KEY `trackback_link_id_2` (`trackback_link_id`,`trackback_type`,`trackback_link`),
KEY `trackback_link_id` (`trackback_link_id`),
KEY `trackback_url` (`trackback_url`),
KEY `trackback_date` (`trackback_date`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
", array('log' => false)) !== false);
}
function drop(&$db) {
return ($db->execute("DROP TABLE `test_suite_trackbacks`", array('log' => false)) !== false);
}
}
?>


trackback.test.php
<?php
/* Trackback Test cases generated on: 2010-07-13 18:07:49 : 1279058389*/
App::import('Model', 'Trackback');
class TrackbackTestCase extends CakeTestCase {
var $fixtures = array('app.trackback');
function startTest() {
$this->Trackback =& ClassRegistry::init('Trackback');
}
function testFoo() {
sleep(25);
$this->assertEqual(1, 1);
}
function endTest() {
unset($this->Trackback);
ClassRegistry::flush();
}
}
?>

Eso es todo!,
Saludos

cakePHP Bakery: publicaron mi articulo!!!

Hace alrededor de un mes estaba creando una herramienta que usaba consultas muy pesadas y realizaba complejas busquedas. Como con SQL era imposible conseguir ese tipo de consultas de forma eficiente utilice Sphinx

Sphinx viene con un API, de cuasualidad en la “panaderia” de cakePHP me encontre con un comportamiento para los modelos que hacia busquedas utilizando la api de sphinx.

Como todo esto no me bastaba para el proyecto, por cierto pueden verlo aqui; www. gastromap.com.ar es un mapa gastronomico

Yo necesitaba un componente ademas de hacer uso del comportamiento del modelo, por ello cree un componente y le agregue algunas cositas que le faltaban como poder hacer uso de la busqueda geografica de sphinx.

Como me parecio que podia ser util cree un articulo y lo publique en la panaderia, en los ultimos dias habia visto un incremento en el spam, por lo que crei que jamas lo publicarian.

Pero un mes despues… http://bakery.cakephp.org/articles/view/sphinx-component-and-behavior

Que tan ineficiente puede ser incrementar un valor de un modelo

Estoy trabajando para refactorizar el codigo de meneame usando el framework cakephp.

Hoy me encontraba haciendo la logica para votar una noticia y me encontre con el problema de tener que hacer una query simple;
UPDATE `links` AS `Link` SET `Link`.`link_anonymous` = link_anonymous+1 WHERE `link_id` = 1

Esto se logra haciendo desde el modelo;
$this->updateAll(array(‘link_anonymous’=>’link_anonymous+1’),array(‘link_id’=>1));

Pero al hacer en vez de hacer el update simple que escribi mas arriba hace un update con todos los joins de los modelos;
UPDATE `links` AS `Link` LEFT JOIN `users` AS `User` ON (`Link`.`link_author` = `User`.`user_id`) LEFT JOIN `blogs` AS `Blog` ON (`Link`.`link_blog` = `Blog`.`blog_id`) LEFT JOIN `categories` AS `Category` ON (`Link`.`link_category` = `Category`.`category_id`) SET `Link`.`link_anonymous` = link_anonymous+1 WHERE `link_id` = 1 AND `limit` = ‘1’

Este update es muy costoso, intente usando las funciones del modelo saveField y save, pero no entienden el mensaje ‘link_anonymous+1’, lo transforman a 1 y yo quiero que hagan link_anonymous+1

Para solucionar este problemita agregue la funcion update, que elimina las asociaciones entre el modelo que queremos actualizar y luegos las agrega nuevamente, de esta forma el UPDATE vuelve a ser simple.

La funcion fue tomada de http://blog.pepa.info/php-html-css/cakephp/getting-rid-of-joins-in-updateall-query/

agregar a #app_model.php
/**
* A workaround for CakePHP lack of support for recursive
*/
function updateAll($fields, $conditions = true, $recursive = null) {
if (!isset($recursive)) {
$recursive = $this->recursive;
}

if ($recursive == -1) {
$this->unbindModel(array(
‘belongsTo’ => array_keys($this->belongsTo),
‘hasOne’ => array_keys($this->hasOne)
), true);
}

return parent::updateAll($fields, $conditions);
}

Luego hago;
$this->updateAll(array(‘link_anonymous’=>’link_anonymous+1’),array(‘link_id’=>1),-1);

y… voila!
UPDATE `links` AS `Link` SET `Link`.`link_anonymous` = link_anonymous+1 WHERE `link_id` = 1

EDITO: hoy agregue un ticket al equipo de desarrollo de cakephp… a ver que contestan