Dealing with Legacy PHP Applications <ul><li>Clinton R. Nixon </li></ul><ul><li>[email_address] </li></ul>
What is a legacy application? <ul><li>Code you didn't write </li></ul><ul><li>Code you wouldn't write </li></ul><ul><li>Un...
What do we do with legacy code? We  refactor! Refactoring is  safely  changing the implementation of code without changing...
Bad code smells <ul><li>What are some specific problems in legacy PHP code? </li></ul><ul><li>No separation between PHP an...
No separation between PHP and HTML <ul><li><h1>Orders</h1> </li></ul><ul><li><?php </li></ul><ul><li>$account = new Accoun...
Separating controllers and views  <ul><li>Even without a solid MVC architecture, this helps </li></ul><ul><li>You can do t...
Why do I need to do this? <ul><li>Your code complexity will increase </li></ul><ul><li>echo  isn't as fun as it looks </li...
The simplest view class class View { protected static $VIEW_PATH = '/wherever/views/'; public function assign($name, $valu...
Obvious improvements to make <ul><li>Error handling </li></ul><ul><li>Assignment by reference </li></ul><ul><li>Changing v...
The separation process <ul><li>Gather all your code </li></ul><ul><li>Sift and separate controller from view code </li></u...
The rules of view code <ul><li>Allowed: </li></ul><ul><ul><li>Control structures </li></ul></ul><ul><ul><li>echo , or  <?=...
Gather and sift code <ul><li>The step you won't like: gather all code for this controller </li></ul><ul><li>Wipe brow </li...
Code gathered <ul><li><?php // View code goes below here ?> </li></ul><ul><li><h1>Orders</h1> </li></ul><ul><li><?php </li...
Some controller code moved <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account...
Alternative control structures <?php if ($foo): ?> ... <?php endif; ?> <?php foreach ($this as $that): ?> ... <?php endfor...
Using alternative control structures <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><l...
A frustrating problem <ul><ul><li><?php foreach ($account->getOrders() as $order): ?> </li></ul></ul><ul><ul><li><h2><?= $...
Dealing with this problem <ul><li>There are two approaches. </li></ul><ul><li>You can create a new array of variables for ...
Our new order object <ul><li><?php </li></ul><ul><li>class Order { </li></ul><ul><li>... </li></ul><ul><li>public function...
Logic removed from view code <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$accou...
Change all variables to  view object variables <ul><li>Assign variables to the view object. </li></ul><ul><ul><li>$view->a...
View object created <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadO...
Separate the files <ul><li>Create a new file for the view code. </li></ul><ul><li>Important!  Search and replace  $view  w...
Our two files <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(...
Find duplicated views <ul><li>As you do this to multiple controllers, you will see repetition. </li></ul><ul><li>There wil...
Using nested requires  instead of function calls <?php require_once('db_setup_inc.php'); require_once('account_auth_inc.ph...
Untangling a require web <ul><li>Require statements which call other require statements. </li></ul><ul><li>Can be very com...
Important reasons to  untangle this web <ul><li>Remove unneeded complexity. </li></ul><ul><li>Create less procedural code....
The untangling process <ul><li>Identify inputs </li></ul><ul><li>Identify outputs </li></ul><ul><li>Wrap the file in a met...
Identify inputs and outputs <ul><li>Find all variables expected to be set before this file is included. </li></ul><ul><li>...
account_auth_inc.php <?php $auth_token =  $_COOKIE['token'] ; if ($auth_token) { $acct_id  =  $db ->GetOne('SELECT acct_id...
Wrap the file in a function <ul><li>Wrap the entire include in a function. </li></ul><ul><li>Pass all input variables. </l...
Function-wrapped <?php function account_auth($db, $auth_token) { if ( $auth_token ) { $acct_id = $db->GetOne('SELECT acct_...
Refactor until complete <ul><li>Tease out the functions, or objects, inside this function. </li></ul><ul><li>If you are re...
Moved token handling to Account <?php function account_auth($db, $auth_token) { // Instead of null, we now return an unloa...
Move to correct location <ul><li>Finally! </li></ul><ul><li>Figure out where these functions or objects should live in you...
Global variables everywhere <?php $account_id = $_POST['acct_id']; $account = new Account($account_id); function getPurcha...
Removing globals one by one <ul><li>Common globals: </li></ul><ul><li>$_POST  and  $_GET </li></ul><ul><li>Session or cook...
Do you still have  register_globals  on? <ul><li>You may have heard: this is a bad idea. </li></ul><ul><li>You may think t...
$_POST  and  $_GET <ul><li>These aren't horrible. </li></ul><ul><li>But not horrible isn't a very high standard. </li></ul...
The database global object <ul><li>Very common in PHP code </li></ul><ul><li>Again, not  horrible </li></ul><ul><li>Preven...
Parameterizing the DB handle <ul><li>Does it need to be everywhere? </li></ul><ul><li>Can you pass it in to a function or ...
Parameterizing globals <?php $account_id = $_POST['acct_id']; $account = new Account( $database , $account_id); function g...
Maybe it does have to be everywhere. <ul><li>Use a singleton. </li></ul><ul><li>But not really. </li></ul><ul><li>Make a w...
A quick recap <ul><li>What are some specific problems in legacy PHP code? </li></ul><ul><li>Mixed PHP and HTML – confusion...
Further reading <ul><li>Working Effectively With Legacy Code,  Michael Feathers </li></ul><ul><li>Refactoring,  Martin Fow...
Questions? <ul><li>[email_address] </li></ul>
Upcoming SlideShare
Loading in …5
×

Os Nixon

700 views

Published on

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
700
On SlideShare
0
From Embeds
0
Number of Embeds
33
Actions
Shares
0
Downloads
0
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Os Nixon

  1. 1. Dealing with Legacy PHP Applications <ul><li>Clinton R. Nixon </li></ul><ul><li>[email_address] </li></ul>
  2. 2. What is a legacy application? <ul><li>Code you didn't write </li></ul><ul><li>Code you wouldn't write </li></ul><ul><li>Untested code </li></ul><ul><li>Code with competing visions </li></ul>
  3. 3. What do we do with legacy code? We refactor! Refactoring is safely changing the implementation of code without changing the behavior of code.
  4. 4. Bad code smells <ul><li>What are some specific problems in legacy PHP code? </li></ul><ul><li>No separation between PHP and HTML </li></ul><ul><li>Lots of require s, few method calls </li></ul><ul><li>Global variables </li></ul>
  5. 5. No separation between PHP and HTML <ul><li><h1>Orders</h1> </li></ul><ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(); </li></ul><ul><li>foreach ($account->getOrders() as $order) { </li></ul><ul><li>echo '<h2>' . $order['id'] . '</h2>'; </li></ul><ul><li>echo '<p>Status: ' . lookup_status($order['status_id']) . '<br />; </li></ul><ul><li>echo 'Total: '; </li></ul><ul><li>$total = array_reduce($order['purchases'], create_function('$a, $b', '$a += $b; return $a')); </li></ul><ul><li>echo $total . '</p>'; </li></ul><ul><li>} </li></ul><ul><li>?> </li></ul>
  6. 6. Separating controllers and views <ul><li>Even without a solid MVC architecture, this helps </li></ul><ul><li>You can do this in several safe and easy steps </li></ul><ul><li>You absolutely will find pain points </li></ul>
  7. 7. Why do I need to do this? <ul><li>Your code complexity will increase </li></ul><ul><li>echo isn't as fun as it looks </li></ul><ul><li>You will find hidden bugs and mistakes </li></ul>
  8. 8. The simplest view class class 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(); } } }
  9. 9. Obvious improvements to make <ul><li>Error handling </li></ul><ul><li>Assignment by reference </li></ul><ul><li>Changing view path </li></ul><ul><li>Display convenience method </li></ul><ul><li>Use-specific subclasses with helper methods </li></ul>
  10. 10. The separation process <ul><li>Gather all your code </li></ul><ul><li>Sift and separate controller from view code </li></ul><ul><li>Assign variables to the view object </li></ul><ul><li>Change all variable references in the view code </li></ul><ul><li>Split the files </li></ul><ul><li>Find duplicated views </li></ul>
  11. 11. The rules of view code <ul><li>Allowed: </li></ul><ul><ul><li>Control structures </li></ul></ul><ul><ul><li>echo , or <?= $var ?> </li></ul></ul><ul><ul><li>Display-specific functions, never nested </li></ul></ul><ul><li>Not allowed: </li></ul><ul><ul><li>Assignment </li></ul></ul><ul><ul><li>Other function calls </li></ul></ul>
  12. 12. Gather and sift code <ul><li>The step you won't like: gather all code for this controller </li></ul><ul><li>Wipe brow </li></ul><ul><li>Draw a line at the top of the code </li></ul><ul><li>Move controller code above this line, fixing as necessary </li></ul><ul><ul><li>At this point, everything is view code </li></ul></ul>
  13. 13. Code gathered <ul><li><?php // View code goes below here ?> </li></ul><ul><li><h1>Orders</h1> </li></ul><ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(); </li></ul><ul><li>foreach ($account->getOrders() as $order) { </li></ul><ul><li>echo '<h2>' . $order['id'] . '</h2>'; </li></ul><ul><li>echo '<p>Status: ' . lookup_status($order['status_id']) . '<br />; </li></ul><ul><li>echo 'Total: '; </li></ul><ul><li>$total = array_reduce($order['purchases'], create_function('$a, $b', '$a += $b; return $a')); </li></ul><ul><li>echo $total . '</p>'; </li></ul><ul><li>} </li></ul><ul><li>?> </li></ul>
  14. 14. Some controller code moved <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(); </li></ul><ul><li>?> </li></ul><ul><li><?php // View code goes below here ?> </li></ul><ul><li><h1>Orders</h1> </li></ul><ul><li><?php foreach ($account->getOrders() as $order) { ?> </li></ul><ul><li><h2><?= $order['id'] ?></h2> </li></ul><ul><li><p>Status: <?= lookup_status($order['status_id']) </li></ul><ul><li><br /> </li></ul><ul><li>Total: </li></ul><ul><li><?= array_reduce($order['purchases'], create_function('$a, $b', '$a += $b; return $a')) </li></ul><ul><li>?> </li></ul><ul><li></p> </li></ul><ul><li><?php } ?> </li></ul>
  15. 15. Alternative control structures <?php if ($foo): ?> ... <?php endif; ?> <?php foreach ($this as $that): ?> ... <?php endforeach; ?>
  16. 16. Using alternative control structures <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(); </li></ul><ul><li>?> </li></ul><ul><li><?php // View code goes below here ?> </li></ul><ul><li><h1>Orders</h1> </li></ul><ul><li><?php foreach ($account->getOrders() as $order): ?> </li></ul><ul><li><h2><?= $order['id'] ?></h2> </li></ul><ul><li><p>Status: <?= lookup_status($order['status_id']) ?> </li></ul><ul><li><br /> </li></ul><ul><li>Total: </li></ul><ul><li><?= array_reduce($order['purchases'], create_function('$a, $b', '$a += $b; return $a')) ?> </li></ul><ul><li></p> </li></ul><ul><li><?php endforeach; ?> </li></ul>
  17. 17. A frustrating problem <ul><ul><li><?php foreach ($account->getOrders() as $order): ?> </li></ul></ul><ul><ul><li><h2><?= $order['id'] ?></h2> </li></ul></ul><ul><ul><li><p>Status: <?= lookup_status($order['status_id']) ?> </li></ul></ul><ul><ul><li><br /> </li></ul></ul><ul><ul><li>Total: </li></ul></ul><ul><ul><li><?= array_reduce($order['purchases'], create_function('$a, $b', '$a += $b; return $a')) </li></ul></ul><ul><ul><li>?> </li></ul></ul><ul><ul><li></p> </li></ul></ul><ul><ul><li><?php endforeach; ?> </li></ul></ul>
  18. 18. Dealing with this problem <ul><li>There are two approaches. </li></ul><ul><li>You can create a new array of variables for your view. </li></ul><ul><li>Or, you can encapsulate this logic in an object. </li></ul>
  19. 19. Our new order object <ul><li><?php </li></ul><ul><li>class Order { </li></ul><ul><li>... </li></ul><ul><li>public function getStatus() { </li></ul><ul><li>return lookup_status($this->getStatusId()); </li></ul><ul><li>} </li></ul><ul><li>public function getTotal() { </li></ul><ul><li>return array_reduce($this->getPurchases(), </li></ul><ul><li>create_function('$a, $b', '$a += $b; return $a')); </li></ul><ul><li>} </li></ul><ul><li>} </li></ul><ul><li>?> </li></ul>
  20. 20. Logic removed from view code <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(); </li></ul><ul><li>$orders = $account->getOrders(); </li></ul><ul><li>?> </li></ul><ul><li><?php // View code goes below here ?> </li></ul><ul><li><h1>Orders</h1> </li></ul><ul><li><?php foreach ( $orders as $order): ?> </li></ul><ul><li><h2> <?= $order->getId() ?> </h2> </li></ul><ul><li><p>Status: <?= $order->getStatus() ?> </li></ul><ul><li><br /> </li></ul><ul><li>Total: <?= $order->getTotal() ?> </li></ul><ul><li></p> </li></ul><ul><li><?php endforeach; ?> </li></ul>
  21. 21. Change all variables to view object variables <ul><li>Assign variables to the view object. </li></ul><ul><ul><li>$view->assign('foo', $foo); </li></ul></ul><ul><li>One-by-one, change variables in view code. </li></ul><ul><li>Test to convince yourself. </li></ul><ul><li>You will probably iterate back to the previous step. </li></ul><ul><li>Document inputs to the view. </li></ul>
  22. 22. View object created <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(); </li></ul><ul><li>$orders = $account->getOrders(); </li></ul><ul><li>$view = new View(); </li></ul><ul><li>$view->assign('orders', $orders); </li></ul><ul><li>?> </li></ul><ul><li><?php // View code goes below here ?> </li></ul><ul><li><h1>Orders</h1> </li></ul><ul><li><?php foreach ( $view->orders as $order): ?> </li></ul><ul><li><h2><?= $order->getId() ?></h2> </li></ul><ul><li><p>Status: <?= $order->getStatus() ?> </li></ul><ul><li><br /> </li></ul><ul><li>Total: <?= $order->getTotal() ?> </li></ul><ul><li></p> </li></ul><ul><li><?php endforeach; ?> </li></ul>
  23. 23. Separate the files <ul><li>Create a new file for the view code. </li></ul><ul><li>Important! Search and replace $view with $this . </li></ul><ul><li>Test one more time. </li></ul>
  24. 24. Our two files <ul><li><?php </li></ul><ul><li>$account = new Account($account_id); </li></ul><ul><li>$account->loadOrders(); </li></ul><ul><li>$orders = $account->getOrders(); </li></ul><ul><li>$view = new View(); </li></ul><ul><li>$view->assign('orders', $orders); </li></ul><ul><li>$view->display('orders.tpl'); </li></ul><ul><li>?> </li></ul><h1>Orders</h1> <?php foreach ( $this->orders as $order): ?> <h2><?= $order->getId() ?></h2> <p>Status: <?= $order->getStatus() ?> <br /> Total: <?= $order->getTotal() ?> </p> <?php endforeach; ?>
  25. 25. Find duplicated views <ul><li>As you do this to multiple controllers, you will see repetition. </li></ul><ul><li>There will probably be subtle differences. </li></ul><ul><li>Take the time to re-work these so you can re-use view files. </li></ul><ul><li>Note! You can include views in other views with </li></ul><ul><ul><li>$this->render('included_file.tpl'); </li></ul></ul>
  26. 26. 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'); ...
  27. 27. Untangling a require web <ul><li>Require statements which call other require statements. </li></ul><ul><li>Can be very complex. </li></ul><ul><li>Dependent on application structure. </li></ul>
  28. 28. Important reasons to untangle this web <ul><li>Remove unneeded complexity. </li></ul><ul><li>Create less procedural code. </li></ul><ul><li>Prior to PHP 5.2, require_once and include_once are more expensive than you would think. </li></ul><ul><li>If you are requiring class definitions, and you have a standard file naming method, use __autoload(). </li></ul>
  29. 29. The untangling process <ul><li>Identify inputs </li></ul><ul><li>Identify outputs </li></ul><ul><li>Wrap the file in a method </li></ul><ul><li>Refactor method </li></ul><ul><li>Move method to correct location </li></ul>
  30. 30. Identify inputs and outputs <ul><li>Find all variables expected to be set before this file is included. </li></ul><ul><li>One possible way: execute this file by itself. </li></ul><ul><li>Find all variables expected to be set or mutated by this file. </li></ul><ul><li>Set variables are easy: comment out the require and watch the errors. </li></ul><ul><li>Mutated is the set of inputs changed. Learn to search for these! </li></ul>
  31. 31. 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);
  32. 32. Wrap the file in a function <ul><li>Wrap the entire include in a function. </li></ul><ul><li>Pass all input variables. </li></ul><ul><li>Return all output variables as an array. </li></ul><ul><li>And then, call that function at the bottom of the required file! </li></ul><ul><li>This is a mess! </li></ul>
  33. 33. 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']);
  34. 34. Refactor until complete <ul><li>Tease out the functions, or objects, inside this function. </li></ul><ul><li>If you are returning a lot of data, see if it can be an object. </li></ul><ul><li>Leave your temporary big function in place, so that your outside code doesn't break. Keep updating it to deal with your refactoring. </li></ul>
  35. 35. 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']);
  36. 36. Move to correct location <ul><li>Finally! </li></ul><ul><li>Figure out where these functions or objects should live in your application. </li></ul><ul><li>Move them there. </li></ul><ul><li>Find where the require is called throughout your application, and replace that with your new function call or object method. </li></ul>
  37. 37. 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; ... }
  38. 38. Removing globals one by one <ul><li>Common globals: </li></ul><ul><li>$_POST and $_GET </li></ul><ul><li>Session or cookie data </li></ul><ul><li>Database handles </li></ul><ul><li>User account </li></ul><ul><li>Language </li></ul>
  39. 39. Do you still have register_globals on? <ul><li>You may have heard: this is a bad idea. </li></ul><ul><li>You may think that it will be impossible to fix. </li></ul><ul><li>It's not. Turn on E_ALL. </li></ul><ul><li>Spider your site and grep for uninitialized variables. </li></ul><ul><li>It's some work, but not as hard as you think. It's worth it. </li></ul>
  40. 40. $_POST and $_GET <ul><li>These aren't horrible. </li></ul><ul><li>But not horrible isn't a very high standard. </li></ul>class InputVariable { public function __construct($name) {...} public function isSet() {...} public function isGet() {...} public function isPost() {...} public function getAsString() {...} public function getAsInt() {...} ... }
  41. 41. The database global object <ul><li>Very common in PHP code </li></ul><ul><li>Again, not horrible </li></ul><ul><li>Prevents testing </li></ul><ul><li>Prevents multiple databases </li></ul>
  42. 42. Parameterizing the DB handle <ul><li>Does it need to be everywhere? </li></ul><ul><li>Can you pass it in to a function or to a constructor? </li></ul><ul><li>The process is simple. </li></ul><ul><ul><li>Add database parameter. </li></ul></ul><ul><ul><li>Pass in that global variable. </li></ul></ul><ul><ul><li>If the call is not in global scope, find out how to pass in that variable to the current scope. </li></ul></ul><ul><ul><li>Repeat. </li></ul></ul>
  43. 43. 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; ... }
  44. 44. Maybe it does have to be everywhere. <ul><li>Use a singleton. </li></ul><ul><li>But not really. </li></ul><ul><li>Make a way to change the singleton instance. </li></ul><ul><ul><li>Global define or environment variable. </li></ul></ul><ul><ul><li>Static mutator. </li></ul></ul>
  45. 45. A quick recap <ul><li>What are some specific problems in legacy PHP code? </li></ul><ul><li>Mixed PHP and HTML – confusion between controller and view </li></ul><ul><li>Use of require statements instead of function calls </li></ul><ul><li>Unnecessary global variables causing dependencies </li></ul>
  46. 46. Further reading <ul><li>Working Effectively With Legacy Code, Michael Feathers </li></ul><ul><li>Refactoring, Martin Fowler </li></ul>
  47. 47. Questions? <ul><li>[email_address] </li></ul>

×