Practical tips for dealing with projects involving legacy code. Covers investigating past projects, static analysis of existing code, and methods for changing legacy code.
Presented at PHP Benelux '10
Time to enter
The Code
Reading
Static analysis
Dynamic analysis
http://www.phpdoc.org/
doc
php
Title
phpdoc -ti 'Sweet Application'
-pp -o HTML:Smarty:PHP
-d Libraries Style
-t Docs
Code in here
Docs out here!
Beware of type-hiding!
Type-hinting
/**
* @param array $opts Current options
* @return array Options with flag set
*/
function setFlag(array $opts) {
$opts['flag'] = true;
return $opts;
}
Type-hiding
/**
* @param int $fullPence Full price in pence
* @return float Discounted price in pence
*/
function applyDiscount($fullPence) {
return ($fullPence * 0.8);
}
ygen
dox
doxygen -s -g ~/doxy.conf
vim ~/doxy.conf
# edit at least this
Code in here OUPUT_DIRECTORY
# play with the rest
cd ~/dev/project
Docs out here
doxygen ~/doxy.conf
http://www.stack.nl/~dimitri/doxygen/
http://ctags.sourceforge.net/
ta gs
c
Code in here
#!/bin/bash Tags out here
cd ~/Dev/ &&
ctags-exuberant -f ~/.vimtags
-h ".php" -R
--exclude=".git"
--links=no
--totals=yes
--tag-relative=yes
--PHP-kinds=+cf
--regex-PHP='/abstracts+classs+([^ ]+)/1/c/'
--regex-PHP='/interfaces+([^ ]+)/1/c/'
--regex-PHP='/(publics+|statics+|abstracts+|protecteds+|privates+) ↵
functions+&?s*([^ (]+)/2/f/'
Voodoo
ffer
esni
co d
rowan@swordbean:~/Dev/ZendFramework-1.9.4/library/Zend/Service$ phpcs
--standard=Zend Exception.php
FILE: /home/rowan/Dev/ZendFramework-1.9.4/library/Zend/Service/Exception.php
--------------------------------------------------------------------------------
FOUND 1 ERROR(S) AND 2 WARNING(S) AFFECTING 3 LINE(S)
--------------------------------------------------------------------------------
17 | WARNING | Line exceeds 80 characters; contains 87 characters
32 | WARNING | Line exceeds 80 characters; contains 87 characters
36 | ERROR | Opening class brace must be on a line by itself
36 | ERROR | Closing brace must be on a line by itself
--------------------------------------------------------------------------------
http://pear.php.net/package/PHP_CodeSniffer/
t in uous
Con io n
Inte grat
http://jenkins-ci.org/
http://phpundercontrol.org/
http://sismo.sensiolabs.org/
Wrapper class
<?php
include('/home/victorvon/secrets.inc');
/**
* @param array $person willing volunteer
*/
function extract_brain(&$person) {
$brain = $person['brain'];
unset($person['brain']);
}
return $brain; Some code
/**
you need
* @param array $person
* @return bool living or not
:(
*/
function create_life($person) {
require_once(LIB_DIR.'../nuts_n_bolts.inc');
kerzap();
$person['living'] = true;
return $person;
}
?>
class VictorWrapper
{ Wrapper class
public function __construct()
{
require_once '/home/victorvon/tragedy.php';
}
public function extractBrain(Person $p) {
// format to legacy style
$pLgcy = $this->toArray($p);
// run legacy code
$bLgy = extract_brain($pLgcy);
// format to new style
$p = $this->toPerson($pLgcy); Some code
$b = $this->toBrain($bLgcy);
return array($p, $b); you can use
}
:)
public function createLife(Person $p)
{
// validate
if ($person->isAlive()) throw new LivingException();
// format to legacy style
$pLgcy = $this->toArray($p);
// run legacy code
$pLgy = create_life($pLgcy);
// format to new style
return $this->toPerson($pLgcy);
}
}
includes
HTML PHP HTML
HTML PHP HTML
PHP HTML PHP
PHP
HTML PHP HTML
HTML PHP HTML
PHP
includes
function
function
function
includes
HTML PHP HTML HTML echo HTML
HTML PHP HTML HTML echo HTML
PHP HTML PHP if HTML if
PHP foreach
HTML PHP HTML HTML echo HTML
HTML PHP HTML HTML echo HTML
PHP foreach
includes includes
function static method
function static method
function static method
function static method
free code static method
includes includes includes
function static method constructor
function static method method
function static method method
function static method method
free code static method method
public function createInvoice(Account $acc, array $charges)
{
$invoice = new Invoice();
foreach ($charges as $chg) {
$invoice->addLine($chg->getDesc(), $chg->getAmount());
}
return $invoice;
}
The existing code
“We just need to be able to give
each client their own personal
discount on certain charges.”
public function createInvoice(Account $acc, array $charges)
{
$invoice = new Invoice();
foreach ($charges as $chg) {
$invoice->addLine($chg->getDesc(), $chg->getAmount());
}
return $invoice;
}
The new code
private function calcDiscount(Account $acc, Charge $chg)
{
$accDisc = new AccountDiscounter($acc);
$discountedCharge = $accDisc->calculate($chg);
return $discountedCharge;
}
public function createInvoice(Account $acc, array $charges)
{
$invoice = new Invoice();
Call it
foreach ($charges as $chg) {
// Sprout new behaviour!
$chg = $this->calcDiscount($acc, $chg);
$invoice->addLine($chg->getDesc(), $chg->getAmount());
}
return $invoice;
}
private function calcDiscount(Account $acc, Charge $chg)
{
$accDisc = new AccountDiscounter($acc);
$discountedCharge = $accDisc->calculate($chg);
return $discountedCharge;
}
The Problem
public function calcDiscount(Account $acc, Charge $chg)
{
$accDisc = new AccountDiscounter($acc);
$discountedCharge = $accDisc->calculate($chg);
return $discountedCharge;
} Untestable!
The Problem
public function calcDiscount(Account $acc, Charge $chg)
{
$accDisc = new AccountDiscounter($acc);
$discountedCharge = $accDisc->calculate($chg);
return $discountedCharge;
} Untestable!
class AccountDiscounter
{
public function __construct(Account $acc)
{
// check cache
// contact the database
// call a web service
}
}
Quick Solution
public function calcDiscount(Account $acc, Charge $chg)
{
$accDisc = $this->getAccountDiscounter($acc);
$discountedCharge = $accDisc->calculate($chg);
return $discountedCharge;
}
Mock object in your test
Override method
protected function getAccountDiscounter(Account $acc)
{
return new AccountDiscounter($acc);
}
Dependency Injection Solution
public function __construct(AccountDiscounter $ad)
{
$this->discounter = $ad;
}
Pass it into the class
public function calcDiscount(Account $acc, Charge $chg)
{
$accDisc = $this->discounter;
$discountedCharge = $accDisc->calculate($chg);
return $discountedCharge;
}
(v2) → Dependency Injection Solution
public function __construct(IAccountDiscounter $ad)
{
$this->discounter = $ad;
}
Make an interface
public function calcDiscount(Account $acc, Charge $chg)
{
$accDisc = $this->discounter;
$discountedCharge = $accDisc->calculate($chg);
return $discountedCharge;
}