dealing with legacy php applications
DESCRIPTION
Clinton Nixon describes some common problems found when inheriting legacy PHP applications and how to deal with them. He presents a solution that will encapsulate business logic and clean up the view layer without requiring a full-fledged MVC framework.TRANSCRIPT
![Page 1: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/1.jpg)
Dealing with Legacy PHP ApplicationsPrepared by Clinton R. Nixon, Viget Labs2007 July
![Page 2: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/2.jpg)
Dealing with Legacy PHP Applications2007 Jul
What is a legacy application?Code you didn't writeCode you wouldn't writeUntested code
Code with competing visions
![Page 3: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/3.jpg)
Dealing with Legacy PHP Applications2007 Jul
What do we do with legacy code?We refactor!
Refactoring is safely changing the implementation of code without changing the behavior of code.
![Page 4: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/4.jpg)
Dealing with Legacy PHP Applications2007 Jul
Bad code smellsWhat are some specific problems in legacy
PHP code?‣ No separation between PHP and HTML‣ Lots of requires, few method calls‣ Global variables
![Page 5: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/5.jpg)
Dealing with Legacy PHP Applications2007 Jul
No separation between PHP and HTML<h1>Orders</h1>
<?php
$account = new Account($account_id);
$account->loadOrders();
foreach ($account->getOrders() as $order) {
echo '<h2>' . $order['id'] . '</h2>';
echo '<p>Status: ' . lookup_status($order['status_id']) . '<br />;
echo 'Total: ';
$total = array_reduce($order['purchases'],
create_function('$a, $b', '$a += $b; return $a'));
echo $total . '</p>';
}
?>
![Page 6: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/6.jpg)
Dealing with Legacy PHP Applications2007 Jul
Separating controllers and views Even without a solid MVC architecture, this helpsYou can do this in several safe and easy stepsYou absolutely will find pain points
![Page 7: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/7.jpg)
Dealing with Legacy PHP Applications2007 Jul
Why do I need to do this?Your code complexity will decrease.
echo isn't as fun as it looks.
You will find hidden bugs and mistakes.
![Page 8: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/8.jpg)
Dealing with Legacy PHP Applications2007 Jul
The simplest view classclass View {
protected static $VIEW_PATH = '/wherever/views/';
public function assign($name, $value) {
return $this->$name = $value;
}
public function render($filename) {
$filename = self::$VIEW_PATH . $filename;
if (is_file($filename)) {
ob_start();
include($filename);
return ob_get_clean();
}
}
}
![Page 9: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/9.jpg)
Dealing with Legacy PHP Applications2007 Jul
Obvious improvements to makeError handlingAssignment by referenceChanging view path
Display convenience methodUse-specific subclasses with helper methods
![Page 10: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/10.jpg)
Dealing with Legacy PHP Applications2007 Jul
The separation processGather all your code
Sift and separate controller from view code
Assign variables to the view object
Change all variable references in the view code
Split the files
Find duplicated views
![Page 11: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/11.jpg)
Dealing with Legacy PHP Applications2007 Jul
The rules of view codeAllowed:‣Control structures‣ echo, or <?= $var ?>‣Display-specific functions, never nested
Not allowed:‣Assignment‣Other function calls
![Page 12: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/12.jpg)
Dealing with Legacy PHP Applications2007 Jul
Gather and sift codeThe step you won't like: gather all code for this
controllerWipe browDraw a line at the top of the code
Move controller code above this line, fixing as necessary‣At this point, everything is view code
![Page 13: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/13.jpg)
Dealing with Legacy PHP Applications2007 Jul
Code gathered<?php // View code goes below here ?>
<h1>Orders</h1>
<?php
$account = new Account($account_id);
$account->loadOrders();
foreach ($account->getOrders() as $order) {
echo '<h2>' . $order['id'] . '</h2>';
echo '<p>Status: ' . lookup_status($order['status_id']) .
'<br />';
echo 'Total: ';
$total = array_reduce($order['purchases'],
create_function('$a, $b', '$a += $b; return $a'));
echo $total . '</p>';
}
![Page 14: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/14.jpg)
Dealing with Legacy PHP Applications2007 Jul
Some controller code moved<?php
$account = new Account($account_id);
$account->loadOrders();
?>
<?php // View code goes below here ?>
<h1>Orders</h1>
<?php foreach ($account->getOrders() as $order) { ?>
<h2><?= $order['id'] ?></h2>
<p>Status: <?= lookup_status($order['status_id']) ?>
<br />
Total: <?= array_reduce($order['purchases'], create_function(
'$a, $b', '$a += $b; return $a')) ?>
</p>
<?php } ?>
![Page 15: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/15.jpg)
Dealing with Legacy PHP Applications2007 Jul
Alternative control structures<?php if ($foo): ?>...<?php endif; ?>
<?php foreach ($this as $that): ?>...<?php endforeach; ?>
![Page 16: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/16.jpg)
Dealing with Legacy PHP Applications2007 Jul
Using alternative control structures<?php
$account = new Account($account_id);
$account->loadOrders();
?>
<?php // View code goes below here ?>
<h1>Orders</h1>
<?php foreach ($account->getOrders() as $order): ?>
<h2><?= $order['id'] ?></h2>
<p>Status: <?= lookup_status($order['status_id']) ?>
<br />
Total: <?= array_reduce($order['purchases'], create_function(
'$a, $b', '$a += $b; return $a')) ?>
</p>
<?php endforeach; ?>
![Page 17: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/17.jpg)
Dealing with Legacy PHP Applications2007 Jul
A frustrating problem<?php foreach ($account->getOrders() as $order): ?>
<h2><?= $order['id'] ?></h2>
<p>Status: <?= lookup_status(
$order['status_id']) ?>
<br />
Total: <?= array_reduce($order['purchases'],
create_function('$a, $b', '$a += $b; return $a')) ?>
</p>
<?php endforeach; ?>
![Page 18: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/18.jpg)
Dealing with Legacy PHP Applications2007 Jul
Dealing with this problemThere are two approaches.‣You can create a new array of variables for
your view.‣Or, you can encapsulate this logic in an object.
![Page 19: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/19.jpg)
Dealing with Legacy PHP Applications2007 Jul
Our new order object<?php
class Order {
...
public function getStatus() {
return lookup_status($this->getStatusId());
}
public function getTotal() {
return array_reduce($this->getPurchases(),create_function('$a, $b', '$a += $b; return $a'));
}
}
?>
![Page 20: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/20.jpg)
Dealing with Legacy PHP Applications2007 Jul
Logic removed from view code<?php
$account = new Account($account_id);
$account->loadOrders();
$orders = $account->getOrders();
?>
<?php // View code goes below here ?>
<h1>Orders</h1>
<?php foreach ($orders as $order): ?>
<h2><?= $order->getId() ?></h2>
<p>Status: <?= $order->getStatus() ?>
<br />
Total: <?= $order->getTotal() ?>
</p>
<?php endforeach; ?>
![Page 21: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/21.jpg)
Dealing with Legacy PHP Applications2007 Jul
Change all variables to view object variables
Assign variables to the view object:$view->assign('foo', $foo);
One-by-one, change variables in view code.Test to convince yourself.You will probably iterate back to the
previous step.
Document inputs to the view.
![Page 22: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/22.jpg)
Dealing with Legacy PHP Applications2007 Jul
View object created<?php
$account = new Account($account_id);
$account->loadOrders();
$orders = $account->getOrders();
$view = new View();
$view->assign('orders', $orders);
?>
<?php // View code goes below here ?>
<h1>Orders</h1>
<?php foreach ($view->orders as $order): ?>
<h2><?= $order->getId() ?></h2>
<p>Status: <?= $order->getStatus() ?>
<br />
Total: <?= $order->getTotal() ?>
</p>
<?php endforeach; ?>
![Page 23: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/23.jpg)
Dealing with Legacy PHP Applications2007 Jul
Separate the filesCreate a new file for the view code.
Important! Search and replace $view with $this.Test one more time.
![Page 24: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/24.jpg)
Dealing with Legacy PHP Applications2007 Jul
Our two files<?php
$account = new Account($account_id);
$account->loadOrders();
$orders = $account->getOrders();
$view = new View();
$view->assign('orders', $orders);
$view->display('orders.tpl');
?>
<h1>Orders</h1>
<?php foreach ($view->orders as $order): ?>
<h2><?= $order->getId() ?></h2>
<p>Status: <?= $order->getStatus() ?>
<br />
Total: <?= $order->getTotal() ?>
</p>
<?php endforeach; ?>
![Page 25: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/25.jpg)
Dealing with Legacy PHP Applications2007 Jul
Find duplicated viewsAs you do this to multiple controllers, you will see
repetition.There will probably be subtle differences.Take the time to re-work these so you can re-use
view files.
Note! You can include views in other views with:$this->render('included_file.tpl');
![Page 26: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/26.jpg)
Dealing with Legacy PHP Applications2007 Jul
Using nested requires instead of function calls<?php
require_once('db_setup_inc.php');
require_once('account_auth_inc.php');
require_once('i18n_inc.php');
echo '<h1>Orders for account #' . $account_id .
'</h1>';
require('get_all_orders_inc.php');
...
![Page 27: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/27.jpg)
Dealing with Legacy PHP Applications2007 Jul
Untangling a require webRequire statements which call other require statements.
Can be very complex.
Dependent on application structure.
![Page 28: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/28.jpg)
Dealing with Legacy PHP Applications2007 Jul
Reasons to untangle this webRemove unneeded complexity.
Create less procedural code.
Prior to PHP 5.2, require_once and include_once are more expensive than you would think.
If you are requiring class definitions, and you have a standard file naming method, use __autoload().
![Page 29: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/29.jpg)
Dealing with Legacy PHP Applications2007 Jul
The untangling processIdentify inputsIdentify outputsWrap the file in a method
Refactor methodMove method to correct location
![Page 30: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/30.jpg)
Dealing with Legacy PHP Applications2007 Jul
Identify inputs and outputsFind all variables expected to be set before this
file is included.One possible way: execute this file by itself.Find all variables expected to be set or mutated
by this file.
Set variables are easy: comment out the require and watch the errors.
Mutated is the set of inputs changed. Learn to search for these!
![Page 31: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/31.jpg)
Dealing with Legacy PHP Applications2007 Jul
account_auth_inc.php<?php
$auth_token = $_COOKIE['token'];
if ($auth_token) {
$acct_id = $db->GetOne('SELECT acct_id FROM
logins WHERE auth_token = ?', array($auth_token));
}
if ($acct_id) {
$acct = new Account($acct_id);
} else {
$acct = null;
}
$_COOKIE['token'] = gen_new_token($auth_token);
![Page 32: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/32.jpg)
Dealing with Legacy PHP Applications2007 Jul
Wrap the file in a functionWrap the entire include in a function.
Pass all input variables.
Return all output variables as an array.
And then, call that function at the bottom of the required file!
This is a mess!
![Page 33: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/33.jpg)
Dealing with Legacy PHP Applications2007 Jul
Function-wrapped<?php
function account_auth($db, $auth_token) {
if ($auth_token) {
$acct_id = $db->GetOne('SELECT acct_id FROM
logins WHERE auth_token = ?',
array($auth_token));
}
if ($acct_id) {
$acct = new Account($acct_id);
} else {
$acct = null;
}
return array($acct, gen_new_token($auth_token));
}
list($acct, $_COOKIE['token']) = account_auth($db, $_COOKIE['token']);
![Page 34: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/34.jpg)
Dealing with Legacy PHP Applications2007 Jul
Refactor until completeTease out the functions, or objects, inside this
function.If you are returning a lot of data, see if it can be an
object.Leave your temporary big function in place, so that
your outside code doesn't break. Keep updating it to deal with your refactoring.
![Page 35: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/35.jpg)
Dealing with Legacy PHP Applications2007 Jul
Moved token handling to Account<?php
function account_auth($db, $auth_token) {
// Instead of null, we now return an unloaded Account.
$acct = new Account();
if ($auth_token) {
// SQL code from before
$acct->loadFromToken($auth_token);
// Token generation and cookie setting
$acct->genNewToken($auth_token);
}
return $acct;
}
$acct = account_auth($db, $_COOKIE['token']);
![Page 36: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/36.jpg)
Dealing with Legacy PHP Applications2007 Jul
Move to correct locationFinally!Figure out where these functions or objects should
live in your application.Move them there.
Find where the require is called throughout your application, and replace that with your new function call or object method.
![Page 37: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/37.jpg)
Dealing with Legacy PHP Applications2007 Jul
Global variables everywhere<?php
$account_id = $_POST['acct_id'];
$account = new Account($account_id);
function getPurchases() {
global $account;
global $database;
...
}
function getLanguage() {
global $account;
global $database;
global $i18n;
...
}
![Page 38: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/38.jpg)
Dealing with Legacy PHP Applications2007 Jul
Removing globals one by oneCommon globals:‣ $_POST and $_GET‣Session or cookie data‣Database handles‣User account‣ Language
![Page 39: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/39.jpg)
Dealing with Legacy PHP Applications2007 Jul
Do you still have register_globals on?You may have heard: this is a bad idea.You may think that it will be impossible to fix.It's not. Turn on E_ALL.
Spider your site and grep for uninitialized variables.
It's some work, but not as hard as you think. It's worth it.
![Page 40: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/40.jpg)
Dealing with Legacy PHP Applications2007 Jul
$_POST and $_GETThese aren't horrible.But not horrible isn't a very high standard.
class InputVariable {public function __construct($name) {...}public function isSet() {...}public function isGet() {...}public function isPost() {...}public function getAsString() {...}public function getAsInt() {...}...
}
![Page 41: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/41.jpg)
Dealing with Legacy PHP Applications2007 Jul
The database global objectVery common in PHP codeAgain, not horriblePrevents testing
Prevents multiple databases
![Page 42: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/42.jpg)
Dealing with Legacy PHP Applications2007 Jul
Parameterizing the DB handleDoes it need to be everywhere?Can you pass it in to a function or to a
constructor?The process is simple.
‣Add database parameter.
‣Pass in that global variable.
‣ If the call is not in global scope, find out how to pass in that variable to the current scope.
‣Repeat.
![Page 43: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/43.jpg)
Dealing with Legacy PHP Applications2007 Jul
Parameterizing globals<?php
$account_id = $_POST['acct_id'];
$account = new Account($database, $account_id);
function getPurchases($account) {
global $account;
global $database;
...
}
function getLanguage($account, $i18n) {
global $account;
global $database;
global $i18n;
...
}
![Page 44: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/44.jpg)
Dealing with Legacy PHP Applications2007 Jul
Maybe it does have to be everywhereUse a singleton.But not really.Make a way to change the singleton
instance.‣Global define or environment variable.‣Static mutator.
![Page 45: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/45.jpg)
Dealing with Legacy PHP Applications2007 Jul
A quick recapWhat are some specific problems in legacy
PHP code?‣Mixed PHP and HTML – confusion between
controller and view‣Use of require statements instead of function
calls‣Unnecessary global variables causing
dependencies
![Page 46: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/46.jpg)
Dealing with Legacy PHP Applications2007 Jul
Further readingWorking Effectively With Legacy Code,
Michael FeathersRefactoring, Martin Fowler
![Page 47: Dealing With Legacy PHP Applications](https://reader033.vdocuments.site/reader033/viewer/2022042714/54bd18004a7959f95e8b45b4/html5/thumbnails/47.jpg)
Dealing with Legacy PHP Applications2007 Jul
Slides available at http://www.slideshare.net/viget and http://clintonrnixon.net.