You are on page 1of 91
“Hartjes’ book takes an in-depth look at pitfalls common to many legacy web applications, particularly those written in PHP.” ROL) Det BCR Us MND) Oa Elda Refactoring Lega Aaa ns CakePH Refactoring Legacy Applications using CakePHP by Chris Hartjes Preface My thanks go out to various members of the CakePHP community for providing me with help and advice while Iwas writing this book. I apologize to those who I omit here but there are three I wish to single out + Nate Abele for always making fun of me, but always answering my questions too * Garrett Woodworth for patiently explaining the inner workings of CakePHP to me * Joel Parras for his technical editing work: I also wish to thank my wife Claire for putting up with the seemingly-endless hours I spend on my computer working on various projects. Itis only because of her support of my efforts that I am able to do things like this. ‘What this book is NOT is an introduction to programming with CakePHP. There are already books that do a good job of giving you the basics of using CakePHP. If this book is going to help you at all, you really need to have the following qualities: * Understand PHP beyond * Understand CakePHP beyond just using CakePHP’s built-in scaffolding * Actually understand the application you're trying to refactor Without those three qualities, I can almost predict that your refactor will be a failure This book was produced with the help of the following tools vim (http://www. vim. org) reStructured Text (http /docutils sourceforge netrst html) rst2pdf (http //eode. google com/p/rst2pdf) PHP (http://www php.net) Pages (http://www. apple. com/iw ork/pages/) CakePHP (http //nvww.cakephp org) All code examples were meant to be run under PHP 5, with development happening using PHP 5.2.8 All CakePHP examples were written and tested using the “bleeding-edge” version of CakePHP 1.2 from the SVN repository at https’//svn. cakephp.org/repo/branches/1.2.x.x. It was at revision 8052 at the time of publishing While every effort has been made to ensure that the code samples work as advertised and are bug free, I offer no warranty in regards to their usage in your own code. Use the samples at your own risk CakePHP is a registered trademark of the Cake Software Foundation (http://cakefoundation. org) To contact the author, visit the web site for this book at http://www littlehart net/book or visit the author’s blog at http /wrwv littlehart net/atthekeyboard. Feedback and questions are always welcome and while I read every email and comments that comes in, time constraints might prevent me from responding All content, unless otherwise noted, is (c)2009 Chris Hartjes. In lieu of adding Digital Rights Man- agement to this document I ask that people respect copyright laws and not distribute copies of this book without my express written permission. Table of Contents Introduction - Frameworks Get No Respect Legacy Applications Chapter 1 - Understanding Models Turning Tables Into Models Model Associations No Auto-incrementing Primary Key Updating Denormalized Data Removing Cached Data Upon Record Deletion Making Your Models Behave: Behaviors Request Routing Chapter 2 - Creating Controllers Determining Controller Names Linking models to controllers Components for your controllers Chapter 3 - Layouts Setting Character Sets Setting Page Titles Javascript Usage Actual Content Css META content Chapter 4 - Separating Business and Presentation Logic Helpers for your views Chapter 5 -- Untwhirling The Spaghetti Fat Models, Skinny Controllers Hopelessly Twisted 12 12 15 15 16 16 17 18 18 18 19 20 20 22 22 22 Easy Drop-Down Lists Model Association Tweaking Multiple Select Drop-Downs Chapter 6 -- Refactoring Multiple Record Editing Simplified Views Filtering and Updating Records Only Updating The Data You Need To Chapter 7 -- Easy Batch Record Manipulation Using N.field Notation In Your Forms Chapter 8 -- Easy Batch Record Addition Easy Arrays In Forms Saving and Validating Data Chapter 9 -- Displaying Data Simple Data Collection For Display Grouping Display Output By Criteria Business Logic Where It Belongs Chapter 10 -- Playing With Date Ranges Real World Date Forms Date Form Automagic Chapter 11 —- Wrapping It All Up 28 29 30 37 43 46 48 65 70 72 77 79 82 Refactoring Legacy Applications Using CakePHP 1 Introduction - Frameworks Get No Respect Web frameworks just don't get much love in the PHP world these days. It seems that in any given week I can find the following information on the intemet: 1, An announcement about the launch of a new PHP web application framework 2. Aset of benchmarks using the ever-popular "Hello World" method showing that Framework X is awe- some and every other framework out there sucks While it's admirable that so much effort is being put into the creation, maintenance and use of frame- works, I think a lot of people are missing the point of what a framework is supposed to do In my opinion, the job of a framework should be to give your application a structure that is enforced via a set of rules established by the framework itself, and at the same time allow you to rapidly build something. I think most frameworks do an awesome job in regards to the first part, but often fail to provide the tools necessary to make the second part happen. In this book, I'm going to focus on the ability of a framework to provide your legacy application with the structure it needs to make it more viable going forward. Legacy Applications Any developer who has ever gone back to an application that they wrote in the past has worked with legacy code Although it’s used by people as a derogatory label applied to code that you don’t like, legacy code is simply code that you wrote yesterday that you need to fix today When put into that context, it’s not so scary any more Any developer worth his skills is asked on a daily basis to go back into some “old and crufty code” and fix it, or even rewrite it to meet a variety of new goals or situa- tions Sometimes it is because a new version of the language has come out and is ready for production use, often allowing the developer to take advantage of new features or just increased speed. But I would say it is rare that old code is rewritten or refactored for this reason. Sometimes it’s also because the developer is scratching an itch and wants to rewrite something using anew language or framework because they've gotten caught up in the buzz and hype surrounding that language. Every developer is guilty of that at one point or another, but that is usually the worst reason to refactor something. Finally, a refactor can also occur when a developer discovers new techniques or tools that will allow their application to perform better. These include things like rewriting a PHP library as an extension in C, or rewriting a Ruby daemon as a Java application (this has happened where I've worked, go figure) Refactoring usually happens because the code in question has started to become a maintenance nightmare. Code so brittle that you fear making changes to libraries because a change in one place could impact 100 different places. Code so tangled up that it’s impossible to create automated tests to 2 Refactoring Legacy Applications Using CakePHP verify that things are actually working In short, the type of code that has grown organically without any structure. Hey, sometimes this sort of thing Just Happens. But we have a tool that help us give our legacy application the structure it needs going forward. That tool is a web application framework I’ve grown to appreciate just how flexible CakePHP really is once you start looking below the sur- face. It comes with all sorts of tools that are indispensable for when you are refactoring an applica- tion that has gone the “spaghetti PHP” way of mixing up business logic, request handling and display logic together Because that is what this is really about: giving your legacy application a structure that will allow it to grow while being easier to maintain. This book is not just someone talking about how awesome CakePHP is There are enough books and blogs out there that do that already. Instead, you will be following along as I take what can truly be called a legacy app and rewriting it to use CakePHB, showing you much thet the framework has to offer. I’ve been playing in simulation baseball leagues for nearly 20 years, and I’ve written web applications to help run those leagues. One of the tools I’ve created is called WebReg, short for Web Registrar. It’s used to keep track of rosters and trades in the league. It’s the one piece of code I have that has been online the longest, since 2005 at it’s current location and at least a year before that on the original server It's used every week throughout the season Itis, however, a mess. It’s pure spaghetti PHP - raw SQL calls, if-then-else request responses and HTML all mixed up together in one. An application that is ripe for a rewrite with CakePHP so I can start to implement some of the features that the current registrar has requested. I’m hoping that by sharing my experiences with a real-world example of PHP application that needed the refactoring and structure that a framework gives you, you will begin to truly understand and (more importantly) appreciate what CakePHP can do for you. Also, this book is not an exhaustive example of how to use CakePHP in the “real world”. It is an example of porting over a fairly simple application, but the principles I use in porting this over are applicable in almost any other situation. Refactoring Legacy Applications Using CakePHP = 3 Chapter 1 - Understanding Models tion with a very simple web-based front end, allowing ns in the simulation baseball league. It's natural that I would application by looking at how we will represent the tables in the database using CakePHP models. This applica PostgreSQL as the back-end, so some gs might be a little different if you are used to deali for us, we don’ to worry about that because CakePHP abstracts all thos jebReg is auch a databas our registrar to manipulate roster start the process of rewriting our | Since this application shares a database and tables with another application written in CakePHP 1.2, we are lucky enough to already have our table following the convention that CakePHP uses + auto-incremented primary keys, in this case of the ‘integer’ data type + table names that are plural, as the CakePHP convention is to give the Model object representing the table the singular name jebReg has three tables that we need to womry about SM eed Table “public. franchises” (en ee Type etic Bsr ae eee ae | character(58) | fer ee ees nee] Penta i Our ‘franchises! table, which keeps track of teams in the league. This table does not have an auto- cremented integer for the pr key, mainly because the number of franchises in the league will not be exceeding 24 any time soon. ee cee cece tae fot ard Dera Cay er ea eee tne et ee character(3) | Sraee tl Rts Hl i + f eee ae el i i i | integer H 4 Refactoring Legacy Applications Using CakePHP Next, we have a table that gives us ne of the first "code smells" for this application: a table called ‘teams’ that contained information about players. By “code smells” Imean something that when y: look at it in the context of the application, it doesn’t seem right Sometimes these types of things easily fixable, Sometimes they are not. In this case, CakePHP itself gives us a graceful way to get around it This table is supposed to be tracking players, not teams. I’m trying to remember why I called it ‘teams’ in the first place. Since I’m drawing a blank, I’ll have to assume that it was for a good r atthe time, This table should’ve been called ‘players’, but we can’t go back and fix it now. ason, In this table we are tracking the name of the player in the dice-and-cards game that the league uses, along with what franchise the player belongs to in the league. The ‘comments’ field contains info about when the player was drafted or traded for, basically a mini-transaction summary. We determine whether a player is active or inactive through the ‘status’ field, and we differentiate between whether someone is a batter, pitcher or draft pick (teams can trade their picks) using the ‘item _type' field ern rer rey Le ate 1 Type Modifiers re) eu cg reo} I character(3) med ipod coersc ie ee aes Re te em Rt i i i Finally we have our transaction_log' table, where keep track of all transactions on a per-team basis. Notice the primary key is called ‘trans _id’, not ‘id’ as you would expect from the CakePHP coding standards Turning Tables Into Model: Tenet tp is a us to define CalePEIP models that lll sto get data fom these tables The first thing that we should tackle is getting nid of that "code smell* I mentioned before. Instead of using Team as the model name, let's instead call it what it should be called, Player class Player extends Appiiodel { public $useTable = ‘teams*; public $belongsTo = array( “Franchise” => array( “classtiame’ => ‘Franchise’, “foreignkey” d “ibl_team’ ds Refactoring Legacy Applications Using CakePHP 5 What is actually going on in this Model? It's actually quite simple. public $useTable version of the table name, we have to define what table we are actually using. "teams"; public $belongsTo = array( "Franchise" => array( “classilame’ => ‘Franchise’, “foreignKey* ) “ibl_team’ Model Associations Next, we are defining a relationship between two models. CakePHP supports 4 types of relationships: Relationship Association Type one to one hasOne one to many hasMany many to one belongsTo many to many hasAndBelongsToMany The use of these associations allows you to use CakePHP's associated data mapping, {where abequest ko find la one model wll im all aseied dla So, sn hs case we ae establishing tat Player belongs to a Franchise. The other information in the association definition is important as well. I tend to only define the parameters that I need, but you can feel free to set other parameters to their defaults if so desired. Later in this chapter I talk more about those parameters, but for now we only need these ones It's important to note here that you do NOT have to have integer-only foreign keys. As long as it's a valid column in your table, CakePHP can use it in the queries it generates just fine 6 Refactoring Legacy Applications Using CakePHP class Franchise extends Apptodel { public $primarykey = ‘nickname’ ; public $hasMany = array( “Player’ => array( “classilame’ => ‘Player’, “foreignKey’ => ‘ibl_team’ »» “TransactionLog’ => array(‘classtame" “TransactionLog", “ibl_team* “foreignkey” The code in the Franchise model is very similar, except you can see how we've added in another asso- ciation. A franchise has many entries in the transaction log, so it makes sense to define the association between them class TransactionLog extends Appiodel { public $useTable = ‘transaction_log" ; public $primarykey = ‘trans_id’; public $belongsTo = array( "Franchise" => array( “Franchise’, *classtlame* “foreignKey’ => ‘ibl_team’, Previously I mentioned " code smells*, the little signs that perhaps you've made an architectural mis- take in your code or your database schema. In this case we have a primary key that does not follow the CakePHP naming convention of just being ‘id. That's easy enough to fix Refactoring Legacy Applications Using CakePHP = 7 This simply tells CakePHP to use the column 'trans_id' as the primary key for any queries public $belongsTo rray( "Franchise" => array( *classtlame* "Franchise", “foreignKey’ => ‘ibl_team’ ) 3 Much like our hasMany association, this just creates the reverse association for these tables, where transaction log entries belong to a specific team. Now that we have our three models defined, let's explore some common "what if" scenarios you might encounter when porting your legacy application over No Auto-incrementing Primary Key It's entirely conceivable that you could have a database structure where you have a primary key that has not been defined, This isn't always possible, but we have a way in CakePHP to handle this issue CakePHP has several callback methods for it's models that allow you to add in some logic just before au aro nec fre ei Tnthis eae, the solution for code to add // Sample beforeSave() logic class Foo extends Appitodel() { // primary key is a timestamp when saving a new record public beforeSave() { if (empty($this-rdata["id'])) { $this-rdata[‘id’] = time(); } return true; } // Example of how to call it, assuming you have data in $data $this->Foo->set ($data) ; if ($this->Foo->save()) { 8 Refactoring Legacy Applications Using CakePHP $this->Session->setFlash( ‘Created new record"); } Let's take a look at what's going on here public beforeSave() { if (empty(Sthis-rdata["id'])) { $this->data["id'] = time(); } return true; , SO it 1s the ideal place to put any logic that you need executed before you save the record. So, the logic in there says "if we don't already have a value in our primary key column in our data set, assign it the desired value" Then, when the model goes to save() the record, it now has a value for the primary key and CakePHP can do it's internal magic where it figures out if it's updating an existing record or creating a new one Also, be sure to have your beforeSaveQ) method retum true or else your save attempt will fail Updating Denormalized Data At one time I worked for a company that was working on an adult dating web site, which is really nothing more than a search engine with a very nice front end (ugh, what a bad pun). At one point, to try and make various searches work more efficiently, it was decided to denormalize our data for searching purposes ‘We ended up with multiple tables that contained data from our main user database, but not always the same data and it wasn't updated with the same frequency How would you implement something like this in CakePHP? One method would be to create an after- Save() method in the User model that then updates all the search tables // Assume that the data we just saved are in $this->data class User extends Appiiodel { public afterSave($created) { y * only pass through the id, username, gender, age, and city fields * to the quicksearch table Refactoring Legacy Applications Using CakePHP 9 / $quicksearch_data = array( ‘username’ => $this->data[ ‘User’ ]["username’], “gender’ => $this->data[ "User ][gender], ‘age’ => $this-rdata["User"][“age"], “city’ => $this-rdata[ ‘User "][‘city"] 3 $this->QuickSearch->save($quicksearch_data) ; } The afterSave() method accepts a single parameter, a boolean field that tells you whether a new record has been created or not. Obviously, this is not being used in our example. What we're doing here is quite simple, really. We're grabbing a subset of the data that we saved in our User model and then passing that set to our QuickSearch model for it to be updated. In this example, username’ is the primary key for our quick search table. Unlike the beforeSave() method, we do not have to return any value. Removing Cached Data Upon Record Deletion While caching is a great idea for just about application any application where you are expecting to have more than one concurrent user, managing the cache is key to proper use Whenever you delete data from one of your database tables, if you've cached that information anywhere you will want to remove it from the cache. In CakePHP an ideal place for the logic to handle this would be in the be- foreDelete() and afterDelete( callbacks In this example, let's assume that we have written our own caching library, called crudeache, that we have placed in your APP/vendors directory. It supports CRUD (Create, Read, Update, Delete) meth- ods for talking to our cache class User extends Appiiodel { // Assume that we have set $this->user_id prior to deleting public method afterDelete() { App: simport(*Vendor", ‘“crudcache*) ; $crudcache = ClassRegistry: :init(array( ‘class’ => ‘Crudcache")); $cache_key = md5(‘quicksearch_’ . $this->user_id); if (1$crudcache->delete($cache_key)) { 10 Refactoring Legacy Applications Using CakePHP $crudcache->1og( ‘unable to remove quicksearch information for user’ = $this-ruser_id); } // Example of using this, assuming we have the id of the record $this->User->user_id = $user_id; $this->User->delete(suser_id) ; ‘Your afterDelete( callback does not need to return anything, so any information about what's hap- pened needs to be communicated through a different channel. In this simple example we use a log- ging facility built into the CrudCache library itself. Ifyou really wanted to do things the Cake-approved way you could create your own cache engine, which is an exercise I leave to more motivated users as current documentation on that is sparse. If you do go that route, your Model: :delete and Model::del methods will call Model::_clearCache for you. Go with whatever you feel comfortable doing Making Your Models Behave: Behaviors One of the interesting features of CakePHP is that you can create code to improve the functionality of your models. These bits of code are called behaviors, and they can give you very powerful functional- Other behaviors that Ihave come across include things like taking an image that you have just created arecord for, and automatically create thumbnails of different sizes, and adding in geocoding function- ality to your application. I think that any time you have some business logic that needs to do something at the same time you are saving information in your database, a behavior is the way to go. This project does not require any custom behaviors, but it's definitely something to investigate if you have some functionality that does not appear to properly fit anywhere. Request Routing CakePHP has very powerful routing capabilities that are also surprisingly easy to use. Often you will be faced with URL's in a legacy application that you cannot simply rename For example, let’s take a look at a URL from a project I worked on where I was porting over a web service and we had to preserve the URL's One sample URL was /en/nba/games/¥ YY YMMDDXZ xml. To remain flexible, I decided to imple- Refactoring Legacy Applications Using CakePHP = 11 ment a ‘games’ controller, where we would pass in the league (‘nba’ in this case) and then the rest of the information. So, how would we do this? Router “index'), array( ‘pass ‘onnect(' /en/:League/games/:event_key’, array("controller* => array(‘league’, ‘event_key"))); “games*, ‘action’ => So what is going on here? Okay, we can put “placeholders” or “tokens” (call them whatever you want) into a route. In this case I want to pass the league and the event key to my action in my con- troller for further processing, It’s really that simple and it reads like plain English, if you ask me One mistake that is sometimes made is the desire of some developers to create their controllers and actions to give you pretty URL's in advance, Sometimes this is easy, sometimes is not. Given how flexible CakePHP’s routing system is, I think it’s better to focus on functionality first and worry about the URL's second. You might even find that you have one controller powering multiple seemingly- unrelated URL's 12 Refactoring Legacy Applications Using CakePHP Chapter 2 - Creating Controllers Determining Controller Names Now that we've defined our models, the next step is to define the controllers that we will need. In CakePHP, the convention is that your controller maps to a noun (in most cases, a model) and your controller methods are verbs that signify an action you're trying to do. However, in many legacy applications this type of easy mapping is not possible, so you often have to struggle to match the conventions. Once I did a blog post about CakePHP myths, focusing on trying to dispel some of the more common “urban legends" as it were about the framework. One of the bigger ones was the idea that you can only have on Model associated with one Controller This is, of course, false. Once you realize that, you understand that you are actually free to name your controllers and their associated actions what- ever you want, Often you are not lucky enough that your existing URL's will map easily to the noun/verb pattem. That leaves you with taking a look at the existing URL's and doing a little educated guessing, Let's try that here URL. controller/action Functionality 1 Ipagefnome ‘Main Menu Froster_management php rosters Roster management options fmake_a_trade php rosters/trade Trade a player ‘Imodify_roster php rosters/edit Edit existing roster ‘Hfree_agents php free_agents Free agent management options ‘Hfree_agents php?task=sign ‘free_agents/sign Signa free agent ‘/free_agents plp?task—add Iplayeriadd ‘Adda new player ‘free_agents plp?task—view free_agentsiview View existing free agents So far we've done a good job of mapping the old URL's to potential controller / action pairs. But per- haps we can do better? Free agents are simply a type of player, so maybe we could rewrite those actions dealing with free agents to fall under the players controller Now, we have the rosters controller to deal with actions involving rosters and the players controller dealing with players. Imagine that Also, I think it makes more sense to put all the actions dealing with trades under their own controller, as the make_a trade php script is it's own "front controller" (as you'll see later), Let's not forget that we have some internal actions to account for When making a trade we have a screen where we not only pick what teams are involved in a trade, but then you pick the players. So we need to tweak things. Here's the final mapping after digging around. Refactoring Legacy Applications Using CakePHP 13 URL. controller/action Functionality 1 Ipagefnome ‘Main Menu ‘roster_management php rosters Roster Management fmake_a_trade php rade ‘Make a trade, pick teams fmake_a_trade php (after post) hrade/choose_players Make a trade, pick players ‘Imodify_roster php Irosters/choose ‘Modify rosters, pick team ‘Fmodify_roster php (after post) rosters/edit Modify rosters, edit players ‘Hfree_agents php free_agents ‘Manage free agents ‘free_agents php?task=sign free_agents/sign Modify free agents Jdraft_player php players/draft Draft free agents ‘free_agents php?taske-add Iplayersfadd ‘Manage free agents, add player ‘free_agents plp?task—view free_agents/edit View free agents, edit ‘Wwiew transactions php ransactions View transactions, pick dates ‘Wview_transactions php (after post) ransactions/view ‘View transactions, show ‘Wwiew_rosters php rostershview View rosters ‘Wwiew_free_agents php ‘free_agentsiview View free agents There, doesn't that look better now? We now have three controllers that better map to the actual ac- tions being done, with a manageable number of methods in each controller I made the non-intuitive decision to make the home page for the application a static page. The page will never change so there really isno reason to make it dynamic So let's set about creating the skeletons for our new controllers. class RostersController extends AppController { public function index() { } public function choose() { } public function edit() { } public function view() { } 14 Refactoring Legacy Applications Using CakePHP class PlayersController extends AppController { public function add() { } public function draft() { } } class TradeController extends AppController { public function index() { } public function choose _players() { } } class FreedgentsController extends AppController { public function index() { } public function sign() { } public function edit() { } public function view() { } } class TransactionsController extends AppController { public function index() { } public function view() { } Refactoring Legacy Applications Using CakePHP 15 Linking models to controllers Since we are only dealing with three models, and a fairly small data set (no more than about 1300 entries in total) we are not taking much of a memory hit to ask CakePHP to hold those three mod- els in memory for us. In some instances, you are bound to have 10 to 12 models, so often it's better to simply leave the Suses array empty and then instantiate instances of the models you need using App-umportO, class PlayersController extends AppController { public function draft() { $player = ClassRegistry: :init( ‘Player?); } If you wish to follow the path of least-use-of-resources, I recommend using the ClassRegistry method but you really need to experiment with your application to see if it is worth it for you Ifyou look at the two controllers you will see that we are passing some variables into some of the action methods. This is the usual CakePHP convention when editing or viewing data in your standard CRUD application, so there's no need to reinvent the wheel. I've said it before, and I will say it again sticking to the CakePHP conventions as much as possible reduces the amount of work you have to do Isnt that what we're really trying to accomplish here? Components for your controllers Much like you have behaviors to add and extend functionality for your models, components allow you to add and extend functionality in your controller. The most common component I have used is the Auth component, which allows you to add authorization code to your application. In fact, I've written quite a few tutorials dealing with the Auth component. For WebReg, I'm not so sure I need to use it, Right now the application is simply protected by Apache http-auth. For most applications, the collection of core components is more than enough 16 Refactoring Legacy Applications Using CakePHP Chapter 3 - Layouts CakePHP supports the concept of having layouts, meaning a skeleton for your HTML output and then it will grab the output generated by the views and put it in the proper place, CakePHP comes with a pretty decent default layout for HTML, so welll take that and simply modify it for our needs. We would be saving this file in APP/views/layouts/default ctp charset() >> <2= $scripts_for_layout ?>
Being lazy I've started using PHP short tags in my applications, hence the use of in the tem- plates. Less typing, just as clear Believe me, when you work on converting over 100+ views on a site you will appreciate the keystrokes saved Setting Character Sets So what exactly is going on here? First off, we are setting the character set for the layout, which CakePHP defaults to UTF-8. You can set that to other values by passing it as a parameter <2= $html->charset(*IS0-8859-1") ?> I can't imagine too often that you'd be setting that value dynamically in your controllers, so go into your layout files and just set it, and forget Refactoring Legacy Applications Using CakePHP 17 Setting Page Titles Stitle_for_layout is a placeholder for what you would like to place in your tag You can set the value you want to show up here in a couple of different ways You can give a particular controller / action pair a name when you create a route. For WebReg, we have set this value to ‘home' in APP/configiroutes php . See how the display method in the standard Pages controller does it? public function display() { $path = func_get_args(); $count = count ($path) ; if ($count) { $this-rredirect(*/"); } Spage = $subpage = $title = null; if (lempty(Spath[o])) { $page = $path[0]; } if (tempty(Spath[1])) { $subpage = $path[1]; } if (tempty($path[$count - 1])) { $title = Inflector: :humanize($path[$count - 1]); } // For the home page we have to set the title if ($path[$count - 1] *home") { $title = ‘WebReg Home ; } $this->set(compact(‘page’, ‘subpage’, ‘title’)); $this->render(join('/*, $path)); 18 Refactoring Legacy Applications Using CakePHP The most common place to set this is in the controllers on a per-action basis. class RostersController extends AppController { public function index() { $this->pageTitle = ‘Rosters Nain Page’; } Javascript Usage Now, I imagine most applications will be using some sort of Javascript. In the "Web 2.0" environ- ment it's pretty hard to avoid using Javascript for AJAX and other functionality (rounded comers immediately springs to mind) so CakePHP provides you with an easy way to insert calls to load your Javascript into your HTML output up in the <head>..</head> block. When you use the Javascript helper, set the "inline" parameter to be FALSE to tell the helper you don't want the Javascript link to be mline // this code is inside a view <2= $javascript->Link( "foo", false) 2> <1-- you should see the following in the header <link rel="javascript™ type="text /javascript” href="/js/foo.js" > Actual Content Finally, we have the $content_for_layout variable, which is where CakePHP will place the HTML output that your controller / action pairs generate. Not too many options for you here, but without that $content_for_layout your layouts will, well, not display any content Now that we've looked at the layout we'll be using for WebReg, there are some other things we would probably want to use when refactoring css CakePHP makes it super-easy to add CSS files to your layouts. // In your layout <?= $html->css(‘main’) >> <!-- should output <link rel="stylesheet” type="text/css” href="/css/main.css” /> > Refactoring Legacy Applications Using CakePHP 19 ‘You can also include multiple CSS files just as easily // In your Layout <2= $html->css(array("foo", ‘bar’, 'baz")) >> 1-- should output <link rel="stylesheet" type="text/css" href="/css/foo.css” /> <link rel="stylesheet” ty ‘text/css" href="/css/bar .css” /> <link rel="stylesheet” type="text/css” href="/css/baz.css” /> > META content Keywords, refreshes, links to RSS feeds, those are some of the things you might also need to add in when refactoring, CakePHP has $html->meta to help you out // In your Layout <2 $html->met a( ‘favicon.ico’, */favicon.ico", array(‘type” “icon")) > <2 $html->meta( ‘Posts’, '/posts/index.rss*, array(‘type’ => "rss’)) 2> <2 $html->meta(*keywords’, “WebReg, IBL") ?> So, now that we've established our layout it's time to move onto the most difficult part of this, separat- ing the business logic from the presentation logic in our legacy application 20 ‘Refactoring Legacy Applications Using CakePHP Chapter 4 - Separating Business and Presentation Logic The #1 reason for wanting to move your legacy application over to a framework like CakePHP is be- cause the existing application is a twisted mess of PHP code mixed in with HTML and CSS. WebReg isno exception. Like most older applications, the business logic is mixed together with the presenta- tion logic. CakePHP enforces this separation quite well, and provides you with lots of tools to make it easier for you to accomplish this. Helpers for your views Much like Behaviors for Models and Components for Controllers, CakePHP provides Helpers as code that provides functionality to your views. CakePHP by default provides two helpers, the HTML helper and the Form helper To add other helpers, it's as simple as defining the helpers variable in the controller itself. The list includes Session, Ajax, Number, Paginator, Rss, Text, Time and Cache. I’m probably leaving out a few, but you get my point. Helpers will save you lots of time and provide you with a way to encapsulate code that you will frequently use in your views one tinge bet that trips people up all the time Here's the existing page <h3_align=center>WebReg -- Roster Management </h3> <div alignecenter> <a href-make_a_trade.php>Make A Trade</a><br> <a href-modify_roster.php>Nodify An Existing Roster</a><br> <a href=free_agents .php>Manage Free Agents</a><br> <br> <a href=conmit .php><b> COMMIT TRANSACTIONS< /a> <br> <br> Import Roster Filecbr> Import Free Agent Filecbr> <hr> <a href="index.php">Return To WebReg Home< /a> </div> Here's how we would clean this up to use the various CakePHP functions Refactoring Legacy Applications Using CakePHP 21 <h3_align=center>WebReg -- Roster Management </h3> <div alignecenter> <2 $html->link(‘Make A Trade’, */trade*) ?><br /> <?= $html->link( ‘Modify An Existing Roster’, '/rosters") 2><br /> <2 $html->link( ‘Manage Free Agents", */players*) 2><br /> <hr> <2 $html->link( ‘Return To WebReg Home’, */*) ?><br /> </div> I imagine the first thing you notice is that I've removed some links from that page. The "Import Roster File’ and "Import Free Agent File" functionality was never really built, and now that everything is database-driven there is no reason to need that functionality again. I also removed the ‘COMMIT TRANSACTIONS ' link, as it was a very early attempt at trying to make it so that our registrar could roll back things if he messed them up. It never worked properly, but if I really wanted to use it could implement these things at the Model level. The $html->link(..) links are self-explanatory, but if I wanted to be totally formal with it, I should do links like this’ <?= $html->link(‘Make A Trade’, array(*controller’ => ‘trade')) ?> Use whatever method you feel most comfortable with. If you were to twist my arm, I'd tell you to use the array-based method of passing in the controller and/or action you wish to link to. There, that was easy, wasn't it? Let's move onto a more difficult one. 22° Refactoring Legacy Applications Using CakePHP Chapter 5 -- Untwhirling The Spaghetti Fat Models, Skinny Controllers One of the main reasons to refactor this legacy PHP application into CakePHP was that the struc- ture was hopelessly interdependent To make changes to one part of the system risked wrecking something else inside the same file But if I was to do it right, and make this tool more viable going forward then applying a framework like CakePHP to this application was a needed step One of the design philosophies when it comes to MVC-style frameworks is that I follow is “Fat models, skinny controllers” Now sometimes this isn’t possible, and that’s okay. There is no perfect way to do it. Often is is not clear where some particular functionality should go, and there is a danger in over-abstracting things So keep that in mind when looking at the code examples. Hopelessly Twisted Here is the code for the page that deals with making a trade <html> <head> <titlerWebReg -- Nake A Trade WebReg -- Make A Trade You must pick two different teams!
quer y($sql) 5 while ($result->fetchInto($raw)) { $team1_List[]=trim($rou[0]) } $sql=" SELECT tig_name FROM teams WHERE ibl_team="$team2" ORDER BY tig name”; $result=$db->quer y($sql) 5 while ($result->fetchInto($raw)) { $team2_List[]=trim($rou[0]) } $t1_size=count ($team1_list); 24 Refactoring Legacy Applications Using CakePHP $t2_size=count ($team2_list); if ($t1_sizerst2 size) { $drop-doun_size=$t1_size; } else { $drop-doun_size=$t2_size; } $team1_drop-down- " $team2_drop-doun=""; // Let's display the form to do the trade »
method=POST> hidden” name="teami” value="<2php print $team1;?>"> hidden” name="team2" value="<2php print $team2;?>"> center><2php print $team1;?> center><2php print $team2;?> Refactoring Legacy Applications Using CakePHP <2php print $teami_drop-down;?> <2php print $team2_drop-doun;?> 26 Refactoring Legacy Applications Using CakePHP foreach ($team2_trade as $player) { $team2_trade_players[]=$player; $trade_date = date("m/y") ; $comment s ‘Trade {$team2} {$trade_date] $sql="UPDATE teams SET ibl_team="{$teami}", comments = "{$conments}", status = 2 WHERE tig name="{$player}'"; $db->query($sql) } $team1_trade_report $team2_trade_report = ""; if (isset($team1_trade_players)) $team1_trade_report=implode( ers); $teami_trade_play- if (isset($team2_trade_players)) $team2_trade_report=implode( ers); $team2_trade_play- $team1_transaction="Trades {$team1_trade_report} to {$team2} for {$team2_trade_re- port }"5 eam2_transaction="Trades {$team2_trade_report} to {$team1} for {$team1_trade_re- port require_once ‘transaction_Log.php’ ; transact ion_log($team1,$team1_transaction) ; transact ion_log($team2, $team2_transaction) ; print
$teami trades $team1_trade_report to $team2 for $team2_trade_ report
Refactoring Legacy Applications Using CakePHP 27 =center>Please select two teams for the trade
$sql="SELECT DISTINCT(ibl_team) FROM teams” ; $result=$db->query($sql) 5 if ($result !=FALSE) { while ($result->fetchInto($row)) { $ibl_team[, row [0] 5 } $team_option foreach ($ibl_team as $team) { $team_option So our task here is to figure what needs to go into the models and what needs to go into the controller First up, we're going to take a look at how to make the first screen people will see when they try to make a trade Easy Drop-Down Lists ‘When approaching refactoring I like to break down the functionality that I need. For the "make a trade" starting page, I need to do the following + get alist of teams + create a form where you pick two teams * after picking two different teams, send them to another page where they can choose players class TradeController extends AppController { public function index() { $this->pageTitle = ‘WebReg -- Nake A Trade’; Steams = $this->Franchise->find( ‘list’, array( “fields “Franchise.nickname’ , “order * ) *Franchise.nickname* 3 $this-rset( ‘teams’, $teams) ; } Those familiar with CakePHP will recognize the code as bein, twist, standard, but there is one little ow, by default find(list’ tries to create a hash of primary key and the ‘name' field in your model, if you have one. But the existing page is using the nicknames of the team, a 3-character string So we instead need the hash to be made up of primary key and nickname. Easy to do by simply pass- Refactoring Legacy Applications Using CakePHP 29 ing the field you want to the find(list’) command. Next, we create the view: WebReg -- Make A Trade
Please select two teams for the trade
V
flash() ?>
create(array( ‘action’ => '/trade/choose_players*)) ?> <2= $form->select(‘'Franchise.team1", $teams, null, null, false) ?> <2= $form-rselect(‘Franchise.team2", $teams, null, null, false) ?>
<2 $form->submit('Use These Teams") ?> <2= $form-rend() >>

Now that we have the list, the next thing we need to do is process the data coming in and then either spit out an error message that you cannot choose the same teams or show a view that contains the players from both teams. Model Association Tweaking After experimenting with some queries using the CakePHP testing console (well, I did write it) I dis- covered that I had one of my primary keys set up wrong The relationship between Player and Fran- chise is via Franchise nick_name and Player ibl_team, so I decided to use Franchise ibl_team as the primary key for my Franchise model class Franchise extends Apptodel { public $name = ‘Franchise’ ; public $primarykey = ‘nickname’ ; pubic $hasMlany = array( “Player’ => array( “classtame’ => ‘Player’, “foreignkey” ) “ibl_team* 30 Refactoring Legacy Applications Using CakePHP } Now my queries will come up correctly. Multiple Select Drop-Downs The choose_players method needs to do the following + display the multiple-select form fields that let you pick players + process the incoming form POST + update the Player data to make the trade + update the TransactionLog info with the results of the trade class TradeController extends AppController { public function choose _players() { if (1$this-rdata) { $this-rredirect(‘/trade") ; if (lempty($this->data["Player"])) { $this-»Player-pset($this->data) ; $players = $this-»Player->choosePlayers() ; if (1$this-»Player-rsaveall()) { $this->Session->setFlash( ‘Unable to complete trade") ; } else { $this->Session->setFlash( ‘Completed trade"); $this->TransactionLog->set($this->data) ; $this->TransactionLog->savetntries($players) 5 Refactoring Legacy Applications Using CakePHP 31 $this->pageTitle = "WebReg -- Make A Trade’; // Wake sure that the same two teams weren't selected if ($this->data[ ‘Franchise’ ][‘team1"] $this->data[ Franchise’ ][‘team2"]) { $this->Session->setFlash( "Vou must pick two different teams"); $this->redirect(‘*/trade/index* + $team1 = $this-rdatal ‘Franchise’ ]['team1"]; $team2 = $this-rdata[ ‘Franchise’ ]["team2"]; $order = "Player.tig name’; $conditions = array( ‘Player .ibl_team’ => $team1); $roster1 = $this-»Player->find(‘list", array( ‘fields’ => array( ‘Player id’, “Player .tig name’), "conditions’ =» $conditions, ‘order’ => ‘Player.tig name’)); $conditions = array( ‘Player .ibl_team’ => $team2); $roster2 = $this-»Player->find(‘list", array( ‘fields’ => array( ‘Player id", “Player .tig name’), "conditions’ => $conditions, ‘order’ => "Player .tig name’)); $this->set(compact( ‘roster’, ‘roster’, ‘teami*, ‘team2")); + Woah, Is that a serious reduction in the amount of code or what? In keeping with my “fat model, skinny controller” practices I moved a lot of functionality out into two methods in the Player and TransactionLog models respectively. Just remember to use your Model: :set methods to pass the data posted to your action into your model. Otherwise you'll be wasting time building your own param- eters to pass into a method in your model. Less code is good code. It would also be good at this time to mention how simple it is to tell CakePHP you want to use trans- actions if you are using an ACID compliant database. Postgres, which is the database we are using for this project, is capable of doing transactional saves with rollback when things go wrong To gain the use of transactions you can set and pass the ‘atomic’ variable to your model eg Model:saveAll($this->data, array(‘validate’ => ‘first’, ‘atomic’ => true)). That way, it validates your data first and then tries to save things only if the validation was okay. 32 Refactoring Legacy Applications Using CakePHP Unfortunately we don’t have the type of logic that would benefit from using transactions, so we can just leave things they way they are. class Player extends Appiiodel { public function choosePlayers() { $data = array(); $teami_players = array(); $team2_players array()5 foreach ($this->data[ "Player" ]["roster1"] as $player_id) { $data[] = array( ‘id’ = $player_id, “ibl_team’ => $this->data[ ‘Franchise’ ][‘team2"], ‘comments’ => “Trade {$this->data[ ‘Franchise’ ][‘team1"]} " . date("m/y") 3 $team1_players[] = $player_i foreach ($this->data[ "Player" ]["roster2"] as $player_id) { $data[] array( id’ => $player_id, “ibl_team’ => $this->data[ ‘Franchise’ ][‘teami"], *conments* ‘Trade {$this->data[ ‘Franchise’ ]["team2"]} " . date("m/y") D3 $team2_players[] = $player_id; $this-rdata = $data; Refactoring Legacy Applications Using CakePHP 33 return array( ‘teami_players’ => $team1_players, “team2_players’ => $team2_players 3 The smart thing to do is to create an array that contains all the data you want to save, and then call your model's saveAllO method and CakePHP will do all the heavy SQL lifting for you. Again, let CakePHP do the work for you instead of doing it yourself Second, discovering the little tricks you can use with find(list). You can get find(list’) to generate pretty much whatever output you want, so since I knew I needed a hash that was "nickname" => “nickname! and only players from the specific team I want. I passed in what fields I needed for the list and the condition that said I only want a list of players from a specific team It is possible to skip having to set the fields you want Model: find( list’) to return if you don’t plan on returning more than one type of list for a particular model. You can set $displayField to whatever you want the default value to be for lists generated on that model. In fact, there are many other values you can set as default for your model such as primary key for your lists and the default sort order. Again, the online documentation is the best place to check those things out. class TransactionLog extends Appiodel { public function saveEntries($players) { $this-»Player = ClassRegistry::init(array( ‘class’ “Player")); $tradei_players = Set: :extract(*/Player/tig name’, $this->Player->find(‘all", array( ‘conditions’ => array(‘Player.id’ => $players[“teami_players’])))); $trade2_players = Set::extract(‘/Player/tig name’, $this->Player->find(‘all", array( ‘conditions’ => array(‘Player.id’ => $players[“team2_players’])))); $teami_transaction = >data[ ‘Franchise’ ][‘team2"]} for ‘Trades " . implode(', ', $tradei_players) . " to {$this- - implode(", ", $trade2_players); 34 Refactoring Legacy Applications Using CakePHP rades “ . implode(", ', $trade2_players) . “ to {$this- - implode(", ", $trade1_players); $team2_transaction = >data[ ‘Franchise’ ][‘team1"]} for $data = array( @ => array( “ibl_team’ => $this->data[ ‘Franchise’ ][‘teami"], “Log_entry’ => $team1_transaction, “transaction_date’ => "NOw()* » 1 => array( “ibl_team’ => $this->data[ ‘Franchise’ ][‘team2"], “Log_entry’ => $team2_transaction, “transaction_date’ => "NOw()* > 3 return $this->saveall ($data) ; } The syntax for Set: :extract(..) makes so much better sense now that it uses XPath 2.0 syntax instead of the old '{n}. Model field’ syntax. Of course, I'm slightly biased because I've dealt with XPath syntax quite a bit in my day job, which involves shoveling around a large amount of XML. So, Ineeded an easy way to get a comma-separated list of players to enter in the transaction log. Set:-extract did the trick. Here is the view that goes along with it
create( "Player", array(‘url’ => ‘/trade/choose_players*)) 2> $teami)) ?> $team2)) ?> Refactoring Legacy Applications Using CakePHP 35 enter">
flash() ?>
true, ‘size’ => count($roster1)), false) ?> true, ‘size’ count ($roster2)), false) ?>
<= $form->submit( "Nake Trade") ?> <2= $form-rend() >>
‘You might be wondering why I am not going with $form->input( for all the fields, instead prefer- ring to use the actual field-specific methods. There's a simple reason, really. When you use $form- >inputO it often makes assumptions on how you want the data displayed. I mean, it is doing a lot of magic but in the end it is making decisions on how to display the data, decisions that might not match ‘what you want to do Secondly, $form->inputO also produces markup to go with the form field. Here's a sample, straight out of the Cookbook input( ‘field’, array(‘type’ => “file’)); 2> Output
file” name="data[User][field]” valu
36 Refactoring Legacy Applications Using CakePHP Sometimes you might be okay with that. For this application I just need the form input code and nothing else. Experiment and play with $form->input to determine what is right for your application Refactoring Legacy Applications Using CakePHP 37 Chapter 6 -- Refactoring Multiple Record Editing ‘We can move onto the code that lets you edit an existing roster The existing code is this 335 line monster that would simply be padding out the size of this book, so instead I will break it down into smaller chunks to show you what's going on Our index page for roster management, is very simple. vebReg -- Roster Management
Return to WebReg Home
‘We only need an empty action method class RostersController extends AppController { public function index() { } Here's the snippet of the code dealing with letting you pick what roster you want to edit. WebReg -- Modify Rosters query($sql) 5 if ($result { “ALSE) while ($result ->fetchInto($row)) { $ibl_team[ ]=$row[0]; >
method="post">
First thing I did was create the view that I would need for it: WebReg -- Modify Rosters
<2 $form->create( ‘Franchise’, array(‘url* “/rosters/edit")) >
submit( "Choose A Team’) 2> select("Franchise.nickname’, $franchises) ?>
<2= $form-rend() ?>

The controller method is very simple as well Class RostersController extends AppController { public $helpers = array(*Html', “Javascript, ‘Form’); 40 Refactoring Legacy Applications Using CakePHP public function choose() { $this->pageTitle = ‘WebReg -- Nodify Rosters’; ClassRegistry: :init(array( ‘class’ “Franchise")) 5 $franchises = $this->Franchise->find( ‘list’, array(‘fields’ => ‘Franchise.nickname’ , ‘order’ => ‘Franchise.nickname")) ; $this->set( ‘franchises’, $franchises); } The landing page for our form will be /rosters/edit, where we will then want to display a form con- taining every batter, pitcher and draft pick for that team. Also, I should add in something that redi- rects them to /rosters/ if they haven't picked a team or try to access the page directly without posting something. Just looking at this next block of code makes me cringe
Modifying Roster for

”; »
method-post> ] valu >> ] value=> "> hadow_conments[] value="] value=> name=id[] value=> 2 name;?>" siz ment: > Refactoring Legacy Applications Using CakePHP > ] valu 20> <2php print $tig_ ] value=" ]> "> Simplified Views This is an area where CakePHP can really shine, greatly reducing the amount of code you need to write yourself First, we can get rid of all that nonsense I did with shadow fields in the form because CakePHP will handle that for us at the model level. Here's the much-improved view. WebReg Nodify Rosters
Nodifying Roster for

<2php $session->flash() > <2 $form-rcreate( Player", array(‘url’ => ‘/rosters/edit")) 2> hidden( ‘Franchise.nickname’, array(‘ value’ => $nickname)) 44 Refactoring Legacy Applications Using CakePHP <2php $next_id = count($players) ?> <2php $idx = $player[ "Player" ]["id"] ?> <2 $form-rhidden("{$idx}.id", array(‘value’ => $idx)) >> ctd< ‘size’ $form->text ("{$idx}.tig_name", array(‘value’ => $player[‘Player'][‘tig_name"], > 20)) 2> “Pitcher’, 2 checkbox("PlayerAction .{$idx}.delete") ?> <2 $form-rhidden("{$next_id}.ibl_team”, array(‘value’ => $nickname)) 2> “Pitcher’, 2
TIG Name Type Comment s Release
select("{$idx}.item_type", array(@ “Pick’, 1 “Bat- ter’), $player[‘Player"][‘item type'], null, false) ?><2= $form->text ("{$idx}.comments", array(‘value' => $player[‘Player'][‘comments"], $player[‘Player"]["conments’], ‘size’ =» 40)) ?> ‘Active’, 2 “uncard- ed’), $player["Player]['status’], null, false) 2> checkbox("PlayerAction .{$idx}.release”) ?>
20)) > select("{$next_id}.item_type", array(@ => ‘Pick’, 1 “Batter')) 2> select("{$next_id}.status”, array(1 => ‘Active’, 2 => ‘Inactive’, 3 => ‘Un- carded’) >
submit ("Modify Roster") >
Refactoring Legacy Applications Using CakePHP 45 <2= $form-rend() >>

If you're used to doing things like then the CakePHP syntax takes a little getting used to. CakePHP will take a syntax of . (ie. 0 Player item_type) and then handle all the magic behind the scenes. But the import thing is to understand the syntax so that you can group your form post results the way you need them. In this case we are try- ing to group all results associated with one player together, so that's why we're looking at things like {Snext_id}.tig_name to group each set of fields for a Player together By doing it this way, I can get a data result set like this [Player] => Array ¢ [4470] => Array ¢ [id] => 4470 [tig_name] => BAL Cherry [item_type] = 1 [comments] => Free Agent 09/08 [UCO9] [status] => 3 ) [3847] = Array ¢ [id] => 3847 [tig_name] => BAL Markakis [item_type] = 2 [comments] => 1st Round 07 (#13) [status] => 1 ) [3005] => array ¢ 46 Refactoring Legacy Applications Using CakePHP [id] => 3005 [tig_name] => BAL Sherrill [item_type] = 1 [comments] => Free Agent 8/05 [status] => 1 ) Filtering and Updating Records With a result set like the one above, you can just pass that data into your save) method of your model, no manipulation required. It's all about reducing the amount of code you write. The code used to be so jumbled, and I'm quite pleased with how the CakePHP version looks class RostersController extends AppController { public function edit() { $this-rpageTitle = ‘WebReg -- Modify Rosters’; if (empty($this->data)) { $this->Session->setFlash( ‘Please select a team’); $this-rredirect(‘/rosters*); Yempt y($this->data[ "Player*])) { foreach ($this->data[ ‘PlayerAction"] as $id => $player) { y{ $this->data[ ‘Player’ ][$id]["ibl_team'] = ‘FA‘; if ($player[ ‘release’ ] if ($player[ ‘delete’ ] yf unset ($this->data[ ‘Player’ ][$id]) 5 $this-»Player->delete( $id) ; Refactoring Legacy Applications Using CakePHP 47 $player_data = $this-»data[ Player "]; // Remove any records that have empty player names foreach ($player_data as $key => $data) { if (enpt y($datal "tig name") == °*)) { unset ($player_data[$key]) ; if ($this-»Player->saveall($player_data)) { $this->Session->setFlash( ‘Modified roster") ; } else { $this->Session->setFlash( ‘Unable to modify roster"); $conditions = array(‘Franchise.nickname’ => $this->data[ ‘Franchise’ ][ ‘nickname’ ]); $order = ‘Player .tig name’; $this-»Player->contain( Franchise’) ; $players = $this-»Player->find(*all", compact("conditions", ‘order*)); $this-rset( ‘players’, $players); $this-rset( ‘nickname’, $this->data[ ‘Franchise’ ][ “nickname }) ; One of the things I like to see in any framework is that the code you write reads almost like English. Looking at this code, it seems very straightforward. Well, at least to me 48 Refactoring Legacy Applications Using CakePHP Only Updating The Data You Need To Now, if you look at the old code you'll notice how I used shadow fields in the form so that later on I could just update the records in the database that needed to be updated. To be honest, I see that as a lot of busy work and I'm perfectly okay to do all those database calls to update the roster. At most we're talking 40 to 50 updates But if you were dealing with an application that does a lot database updates as it is, then I would definitely recommend doing data comparisons of the form data coming in. Whether or not you do it via hidden fields in the form or by reading those particular records from the database depends on your situation. One thing that is for sure, I would be putting that functionality in it's own method in the model. Assuming that we've created "shadow fields" in the form, here's some sample code to do that. class Player extends Appiiodel { ye * Assume that you've already populated $this->data * using $this-»Player->set($this-rdata) in your controller ” public function modifyRoster() { foreach ($this->data[‘Player"] as $key => $data) { if (lempty($this->data['ShadowPlayer’ ][$key])) { if ($data $this->data[‘ShadouPlayer"][$key]) { unset ($this->data[ "Player ][$key]); } $this->saveall($this->data) ; Refactoring Legacy Applications Using CakePHP 49 Chapter 7 -- Easy Batch Record Manipulation ‘We've got all the existing roster methods taken care of, next up is the functionality dealing with ma- nipulating the free agents themselves. WebReg -- Manage Free Agents

Draft A Player
Add Players to Free Agent Pool
View / Edit Free Agents


Roster Management
The “Sign Free Agents” page is supposed to display all the existing free agents and allow you to add them to teams in batches, hopefully reducing the amount of work the league registrar has to do. Here’s what the old code looked like if (Stasi { // Present a list of all free agents // and then allow them to be assigned to a team $sql="SELECT DISTINCT(ibl_team) FROM teams WHERE ibl_team!="FA’ ORDER BY ibl_team”; $result=$db->query($sql) 5 $ibl_team_drop-down=""; // Wow, get a list of all the players who are free agents $sql $result=$db->query($sql) 5 SELECT id,tig name FROM teams WHERE ibl_team="FA* ORDER BY tig nam »
" method="post» fetchInt o( $row ,DB_FETCHMODE_ASSOC)) { $tig_name=trim($row[ "tig name" }) ; $id = $row[ id" ]; » iden” name="tig_name[ >
<2php print $tig_name;?>
Refactoring Legacy Applications Using CakePHP 51
s_POST["ibl_team™]; 5 POST["tig_name”];
$sign_id) { tus = 2 WHERE id-{$sign_i $sign_team= ibl_team[ $key]; if ($sign_team!="FA") { Sconments = “Free Agent “.date("m/y") 5 $sql="UPDATE teams SET ibl_team="{$sign_team}", comments = "{$conments}", sta- I} $db->query($sql) $log_entr: ‘Signs ".$tig_name[$sign_id]; transaction_log($sign_team,$log entry) ; print "" .$tig_name[$sign_id]." signs with {$sign_team}
WebReg -- Manage Free Agents
<2 $form->create( "Player", array("url’ => ‘/free_agents/sign’, ‘method’ => ‘post')) ?> <2php foreach ($free_agents as $idx => $player) : 2> <2= $form->hidden("{$idx}.id", array(‘value’ => $player[‘Player'][‘id'])) ?> <2= $form->hidden("{$idx}.tig_name", array(‘value’ => $player[‘Player'][‘tig_ name"])) ?>
flash() ?>
select("{$idx}.ibl_team”, $franchises, $player[ ‘Player "]["ibl_ team'], null, false) ?>
<= $form-rend() >> Refactoring Legacy Applications Using CakePHP 53
Return to Roster Management data) { if (lempty($this->data["Player"])) { smsg =" foreach ($this->data[‘Player"] as $idx => $player_data) { if ($player_data["ibl_team’] == "FAa') { unset ($this->data[ ‘Player * ][$idx]); } else { $this->data[ "Player ][$idx][‘comments'] = “Free Agent " . date("m/y") 5 $transaction_data[] = array(‘ibl_team’ => $player_data["ibl_team’], "Log_entry’ => “Signs {$player_data["tig name']}", ‘transaction_date’ => date(‘Y-m-d')); $msg .= “{$player_data[ ‘tig name"]} signs with {$player_ data["ibl_team’]}
"; } $this-»>TransactionLog->contain() ; if (1($this-»Player->saveall($this->data[ "Player"]))) { $msg = “Unable to assign free agents to their teams”; } else { $this->TransactionLog->saveAll($transaction_data) ; 54 Refactoring Legacy Applications Using CakePHP $this-»Session->setFlash( $msg) ; } } $order = ‘Player.tig name" ; $conditions = array(‘Player .ibl_team’ => "FA"); $free_agents = $this-»Player->find(‘all", compact( ‘conditions’, ‘order')); $franchises = array merge(array("FA’ =) "Free Agent’), $this->Franchise- rfind( "List", array(‘fields’ => ‘Franchise.nickname’,,*order’ => ‘Franchise.nickname"))) ; $this->set(‘free_agents’, $free_agents) ; $this->set( ‘franchises’, $franchises) } Remember earlier when I showed a way to eliminate unneeded data from our results array as a func- tion inside one of our models? Here I've shown you an easy way to do the same sort of things inside the controller. Given how simple the functionality really was I figured it wasn't a big violation of my “fat models, skinny controllers” convention to leave it in there ‘As you can see, I’m removing results from $this->data if the user didn’t change the team for a free agent. After a few quick checks and the creation of the entries for the transaction log, we pass the entire contents to the saveAlIO method of our models and we're done. You'll also noticed that I've done a two-step save here, because I didn’t want transaction log entries showing up if the main save of player information didn’t work Really, the goal was to minimize the amount of data I would be passing to the saveAlIQ method, because it really is a waste to update records that don’t really need to be updated unless performance doesn’t really matter Give that I’m expecting one concurrent user at any time, I’d say slapping a profiler onto this application and looking for bottlenecks isn’t a priority. You can also see a little trick where I force the “free agent” team to the front of the list for my select drop-down in the form using array_merge and find( list’). I find myself doing that quite a bit when creating forms with drop-downs, as the default order isn’t always alphabetical Refactoring Legacy Applications Using CakePHP 55 Chapter 8 -- Easy Batch Record Addition With the batch-assigning of free agents to teams take care of, we now can look at the functionality for adding players to the free agent pool, which is something we usually do as preparation for the league's annual draft. The goal of this page is to display an empty form for adding players process the data posted from the form add those players to the pool if ($task=="add") { // Present a form where you can add up to 20 players at a time to the free agent pool. »
" method ost 2php print $_SERVER["PHP_SELF” > You must fill in at least one name "; foreach ($fa_tig name as $key=>$player) { if ($player!= { Refactoring Legacy Applications Using CakePHP 57 $sql="INSERT INTO teams (tig_name,ibl_team,item type,status) VALUES C{$player}", ‘FA’, {$fa_type[Skey]} , 1)"5 print "{$sql}
"; $db-> quer y( $sql) 5 print “Added to the free agent pool
"; } Easy Arrays In Forms Again, very standard spaghetti PHP stuff. The CakePHP view for this is very simple

WebReg -- Manage Free Agents

<2php $session->flash() ?>

TIG Mame
<2 $form->create( "Player", array(‘url’ => ‘/players/add", ‘method’ => ‘post')) >> <2php for ($x =15 $x <= 20; $xH¥) : >> 58 Refactoring Legacy Applications Using CakePHP <2= $form-rend() >>
TIG Mame
select("{$x}.Player item type", $item types, null, null, false) >
submit (‘Add Players") 2>

Return To Roster Management
Again, note the use of the N. Model field notation to group the contents of $this->data nicely for use by saveAll0 Saving and Validating Data Although this application really has very little in the way of data entry, I thought it would be good to. show how I would add in validation to this application. There was no data validation in the original application, so this is sort of violating one of the rules of refactoring which says “thou shalt not add more features” Like so many other things in CakePHP, adding in validation to your forms is easy. In this case, we ‘want to make sure that the tig_name field in the Player model is valid whenever we save a record The rules are as follows * Either 2 or 3 uppercase letters (KC or FLA for example) * one space and then a capitalized word with no spaces So what does that type of rule look like? class Player extends Appiiodel { public $validate = array( “tig name’ => array( “rule’ => array("custom’, */*[A-Z]{2,3}\s[A-Z]\w+$/"), “message” => ‘Invalid player name* ) 3 } Unfortunately you will have to brush up on your regular expression skills in order to fully appreciate that custom rule. In order to do this we had to tell CakePHP that we wanted to use a custom valida- tion rule. Out of the box, CakePHP has an awesome collection of validation rules, 25 at last count. ‘When you add in the fact that you can do custom validations rules that are either regular expressions or full-blown functions that are accessed as a callback, there is absolutely no reason to have form validation in your application The use of callbacks in validation is very powerful, so don’t be afraid of complicated validation rules in your existing application. If you coded it before, you can code it again. Refactoring Legacy Applications Using CakePHP 59 Finally, the controller that ties it all together. class PlayersController extends AppController { public function add() { $this->pageTitle = ‘NebReg -- Manage Free Agents’; if ($this-rdata) { foreach ($this-rdata as $idx => $data) { if ($data[ "Player" ]["tig_name"] == '') { unset ($this->data[$idx]); } else { $this->data[$idx]["Player"]["ibl_team’] // Validate each player that we have $valid_players = true; $msg = null; $new_data = array(); foreach ($this->data as $idx $data) { $this-»Player-»set ($data) ; if ($this-»Player->validates()) { $msg .= “Added {$data[ "Player" ][ ‘tig name’]} to the free agent poolcbr />"; $this-»Player->save(); 60 —_Refactoring Legacy Applications Using CakePHP } else { $valid_players = false; $neu_data[] = $data; if ($valid players == false) { $msg = ‘Please make sure that you have entered player names prop- erly’; $this-rdata = $new_data; $this-»>Session->setFlash( $msg) ; $item_types = array( 1 = ‘Pitcher’, 2 => ‘Batter’ $this-rset( ‘item types’, $item types); } The only bit of logic above that I feel needs explaining is the loop where I’m performing validation on every player passed in foreach ($this->data as $idx => $data) { $this->Player->set ($data) ; if ($this-»Player->validates()) { Refactoring Legacy Applications Using CakePHP 61 $msg .= “Added {$data[ "Player" ]["tig_name’]} to the free agent pool
"; $this-»Player->save() ; unset ($this->data[$idx]) ; } else { $valid players = false; $new_data[] = $data; } Using a common CakePHP practice for when you wish to validate data before doing something to it, I pass the data info my model using a Model::set command, Then, I ask the model to validate the data Ipassed into it Model: -validates() retums TRUE or FALSE and will also populate Model:invalid_ fields if you wish to do something with the report. Since I hadn’t implemented any validation before, I decided to take the easy way out and simply do the following: * save any records that met our validation criteria * add any records that failed to an array and reset $this->data to contain only the failed records so they can be corrected So the result is that anything that is okay gets saved, and everything that wasn’t okay gets put back into the form and I inform the user that they need to correct them. Maybe I could add a message tell- ing them what format it SHOULD be in, but the people using this application are already aware of the requirements of the player name. Another little trick being used is that I loop through the result set making sure that I've set the ibl_ team for each record to ‘FA’, which is the intemal code for being a free agent. I could probably also add a validation rule that says that ibl_team cannot be null I think it’s important to understand that I’m taking shortcuts here because this is a system that is used by people who are intimately familiar with data entry requirements. You should not be so trusting with any site that is used by people not so intimately familiar with how to enter data. Validating incoming data is just something you need to do all the time, and it was only the lack of an easy-to-use validation system that I did not do it originally. 62 Refactoring Legacy Applications Using CakePHP Chapter 9 -- Displaying Data ‘We have all the pages where we enter data into forms and update records based on that info finished Next up are two examples of how to grab data out of your models and display it. First, let’s look at the code that displays the list of free agents. Here's the original code WebReg -- View Free Agents

WebReg -- View Free Agents


"; $sql $result=$db-> quer y($sql) 5 ELECT tig name FROM teams WHERE ibl_team="FA’ AND item type=1 ORDER BY tig name"; Refactoring Legacy Applications Using CakePHP 63 while ($result ->fetchInt o( $row ,DB_FETCHMODE_ASSOC)) { $tig_name=$row["tig_name* ]; print “$tig_namecbr: // Mow, the batters print “
BATTERS
" ; $sql="SELECT tig name FROM teams WHERE ibl_team="FA* AND item type=2 ORDER BY tig name”; $result=$db-> quer y($sql) 5 while ($result ->fetchInt o( $row ,DB_FETCHMODE_ASSOC)) { $tig name = $row[‘tig name"); print "Stig namecbr>”; } »
=center>
How to do it in CakePHP? Very simple. The view.

WebReg -- View Free Agents

PITCHERS
64 Refactoring Legacy Applications Using CakePHP
> <2= $pitcher[ "Player" ]["tig_name"] ?>

BATTERS



Simple Data Collection For Display The action couldn’t be easier class FreeAgentsController extends AppController { public function view) { $this->Player->contain() ; $order = ‘Player .tig_name’ ; $conditions = array( “Player -ibl_team’ => "FA", “Player -item type’ => 1 3 $free_agents["pitchers’] = $this-»Player->find(‘all", compact( ‘order’, ‘condi- tions"); $conditions[ "Player item type'] = 25 $free_agents["batters’] = $this-»Player->find(‘all’, compact( ‘order’, ‘condi- tions"); $this->set(‘free_agents’, $free_agents) ; Refactoring Legacy Applications Using CakePHP 65 } As in other actions, notice the use of contain( to limit the data set being returned to just records from the Player model. Grouping Display Output By Criteria Elsewhere in the application, we also have to display a list of players grouped by roster. This func- tionality isn’t really needed any more after I moved it to the CakePHP application that runs the web- site for the league However, it is a great example of grouping results together by specific criteria Here's the old code: WebReg -- View Rosters enter">WebReg -- View Rosters function display _rosters($team_list) { $db = & DB: :connect (DSH) ; // goes through array displaying the rosters for the teams on the list 66 Refactoring Legacy Applications Using CakePHP foreach ($team_list as $team) { print “$teamcbr>===

"; // Display the pitchers first print “PITCHERS
”; $sql="SELECT ti, type=1 ORDER BY tig nam name, conments status FROM teams WHERE ibl_tear Steam’ AND item $result=$db->quer y($sql) 5 $counter while ($result->fetchInto($row)) { $counter++; $player_name=$row[0] ; Scomments=$row[1] 5 $status-$rou[2]; echo status}
"; } {Scounter}. {$status_flag} {Splayer_name} -- {$comments} {$carded_ // Mow, show the hitters print “
BATTERS
" 5 $sql="SELECT tig_name,conments status FROM teams WHERE ibl_tea type=2 ORDER BY tig name”; Steam’ AND item $result=$db->quer y($sql) 5 Refactoring Legacy Applications Using CakePHP while ($result ->fetchInto($row,DB_FETCHMODE_ASSOC)) { $counter++; $player_name=$row[ "tig name" ]; $conments=$row[ “comments ] ; $status=$rou[ ‘status’ ]; print " $counter. $status_flag $player_name -- $conments
” // Print out blank spots if there are less than 35 spots on the roster if ($counter<35) { while ($counter<35) { $counter++; print " $counter DRAFT PICKS
"; $sql = "SELECT tig_name FROM teams WHERE ibl_team="{$team}’ AND item type=0 $result = $db->query($sql) ; $picks = Array(); 67 68 Refactoring Legacy Applications Using CakePHP while ($result ->fetchInto($row,DB_FETCHMODE_ASSOC)) { $picks[] = trim($rou[ ‘tig name" }) ; print implode(", ", $picks); print "

" // create an array with the rosters seperated by conference $ac_teams=Array(*COU’ , "TRI" ,"HAG", "BUF", "NCH", "WHS", "PHI", "BOW", "PAD", "STL", "POR", “LAW") 5 sort ($ac_teams) ; $nc_teams=Array("SDQ", "CSP" ,"HIN", "CAI", "SCS", "BUZ", "DTR" , “COL”, "SPO", "SEA", "MAD" , “GAS") 5 sort ($nc_teams) ; print “american Conferencecbr>
"; display_rosters($ac_teams) ; print “

Wational Conference

"; display_rosters($nc_teams) ; »
Return to main page
That's quite a bit of code. CakePHP to the rescue!

WebReg -- View Rosters

$rosters) : >>

<2php foreach ($rosters as $team => $contents) : 2> Refactoring Legacy Applications Using CakePHP 69



PITCHERS
<2php foreach ($contents["pitchers’] as $pitcher) : ?> . «?= $pitcher[‘Player"]['tig name"] 2>

BATTERS
<2php foreach ($contents["batters’] as $batter) : 2> <2 $counter ?>. <2= ($batter['Player"]['status*] == 1) > "*" = null 2>

DRAFT PICKS
<2php $draft_picks = Set::extract(‘/Player/tig_name’, $contents["draftpicks’]) > <2= implode(", ', $draft_picks) ?>
>
Return to groupByRoster($teams) ; Steams = array("SDQ",*CSP*,"NIN’ "CAI", "SCS", "BUZ" ,‘DTR*, "COL", "SPO", "SEA", "NAD", * Gas"); $league_rosters[ "National Conference’] = $this-»Player->groupByRoster($teams) ; $this->set(‘league_rosters*, $league_rosters) ; } } Business Logic Where It Belongs ‘Those of you with sharp eyes have noticed that I am calling a method inside Player called groupB- Roster. That does all the “heavy lifting” as it were. Here's what that method looks like class Player extends Appiiodel { public function groupByRoster($teams) { sort ($teams) ; $rosters = array(); foreach (Steams as $team) { $order = ‘Player tig name" ; $item_types = array( Refactoring Legacy Applications Using CakePHP 71. “pitchers’ => 1, “batters’ => 2, “draftpicks* foreach ($item types as $field => $itemtype) { $this->contain() ; $conditions = array( “Player item type’ => $item type, “Player .ibl_team’ => $team 3 $rosters[ Steam] [$field] = $this->find(*all", compact( ‘conditions’, ‘order')); return $rosters; This method accepts an array of teams and then builds a hash out of the result set from the Player model. Again, much tighter code than what we used to have, which is what we're really after here. 72 Refactoring Legacy Applications Using CakePHP Chapter 10 -- Playing With Date Ranges The final bit of functionality that we have to deal with for WebReg is the displaying of league transac- tions based on a date range WebReg -- View Transactions WebReg -- View Transactions $to_date) B { » The “from” date cannot be newer than the “to” date
="$from_date’ AND transaction_date<="$to_date’) ORDER BY ibl_team"; $db =& DB: :connect (DSH) ; $result=$db->quer y($sql) 5 if ($result! “ALSE) { while ($result->fetchInto($rou,DB_FETCHMODE_ASSOC)) { $ibl_team=$row[ “ibl_team’ ]; $log_entry=$row{“log_entry"]; $transaction[$ibl_team][]=$log_entry; } if (count($transaction)>0) 74 Refactoring Legacy Applications Using CakePHP { > Transactions for to

$key) { $printed_team_name=FALSE ; foreach ($key as $log_entry) { » Refactoring Legacy Applications Using CakePHP 77 from_day"> To
-- -— ” . $x . ""; } else { $year_drop-down .= “" ; } } // Display range for months $mont h_drop - down ” ; $month_drop-down .=" $month_drop-down.="" ; $month_drop-down.="" ; 16 > Refactoring Legacy Applications Using CakePHP $month_drop-down.="” ; $month_drop-down.="” ; $month_drop-down.="” ; $month_drop-doun.="tlovember” ; $month_drop-doun.="
Real World Date Forms Man, is that ever a lot of code to do something very simple. Let’s fix this up the CakePHP way. First step was to create a controller / action pair to display the form that allows you to pick the date range

WebReg -- View Transactions

<2= $form->create(‘TransactionLog’, array(‘url" */transactions/view")) ?> 78 ‘Refactoring Legacy Applications Using CakePHP
From year(“TransactionLog.start_date’, 2005, date(‘Y"), date("Y"), null, false) ?> $form->month(‘TransactionLog.start_date’, date('N'), null, false) >> < $form->day(‘TransactionLog.start_date’, date("d'), null, false) ?>
To $form->year(“TransactionLog.end_date’, 2005, date("y"), date('Y"), < $form->month(‘TransactionLog.end_date’, date("N"), null, false) ?> <2 $form->day(“TransactionLog.end_ date’, date(‘d"), null, false) >>
<2 $form->submit(*Run Report") ?> <= $form-rend() >>
‘The view for actually displaying those transactions WebReg -- View Transactions
to


> $items) : >> Refactoring Legacy Applications Using CakePHP 79 > > > >
-- <2= $item ?>

The controller does the work in pulling the transactions together, just as you would expect. Can you believe the controller is just 24 lines? Date Form Automagic class TransactionsController extends AppController { public $name = ‘Transactions’; public $uses = array(‘TransactionLog’); public $helpers = array(*Html', “Javascript, ‘Form’); public function index() { $this->pageTitle “WebReg -- View Transactions’; public function view() { if (1$this-rdata) { $this->redirect(‘/transactions*) ; 80 Refactoring Legacy Applications Using CakePHP } $this->pageTitle = ‘NebReg -- View Transactions"; $transactions = $this->TransactionLog->getByDate($this->data) ; $start_date = implode(‘-", $this->data[ ‘TransactionLog’ ][start_date’]); $end_date = implode(’-", $this->data[ "TransactionLog’ ]["end_date* ]); $this-rset(‘start_date’, $start_date); $this->set(‘end_ date’, $end_date) ; $this->set( ‘transactions’, $transactions) ; } Remember in the original page how I had to parse out the date by hand? CakePHP is smart enough to handle all that date stuff for you. ‘You just knew there had to be a catch. In order to get the results in the format needed for the view, I created a method in the transactions model class TransactionLog extends Appiodel { public function getByDate($data) { $this->contain() ; $start_date = $data[‘TransactionLog"][‘start_date*]["year"] - $data[ ‘TransactionLog' ][‘start_date* ][‘month'] . ' *~. $data[“TransactionLog" ][‘start_ date" ][‘day"]; $end_date = $data[ “TransactionLog’ ]["end_date"]["year"] $data[ “TransactionLog’ ][end_date' ][‘month*] . '' . $data["TransactionLog’ ][‘end_date* ] [day]; $fields = “ibl_team, log entry, transaction_date” $conditions = array( Refactoring Legacy Applications Using CakePHP 81 => $start_date, “TransactionLog.transaction_date >= “TransactionLog.transaction_date <=" => $end_date 3 $order = ‘TransactionLog.transaction_date’ ; $results = $this->find(‘all", compact( ‘fields’, ‘conditions’, ‘order’, ‘group") $transactions = array(); foreach ($results as $result) { $transactions [$result [ ‘TransactionLog’]["ibl_team']][] = $result[ "TransactionLog" }[*log_entry"]; } ksort ($transactions) ; return $transactions; } For those not familiar with PHP's sorting functions, I am using ksort(... to (not surprisingly) sort the transactions in alphabetical by the key for the transactions array, which is the ibl_team name field Since the existing code spit out results in ibl_team order, it’s better to stick that logic in the model where it really belongs 82 Refactoring Legacy Applications Using CakePHP Chapter 11 -- Wrapping It All Up ‘Wow! We've certainly covered a lot of material in a short period of time. Let’s think about what we've actually covered * Why frameworks get no respect, despite the benefits they can give * Why “fat models, skinny controllers” is a great paradigm in CakePHP * How to separate your business logic from your display logic *Tuming database tables into CakePHP models * How to understand and create the associations between your CakePHP models * User-submitted data handling in your controllers * Real-world examples of using the form helper The main thing I hope I’ve been able to show you is that it isn’t as difficult at all to take an older, legacy application written in PHP and convert it over to using CakePHP So many applications re- ally need the structure that a framework can provide. While you could create your own (after all, CakePHP started out the same way) I really encourage you to take a very honest look at your require- ments and see if there is an existing framework that can do the job and allow you to customize what you need. I understand that everyone thinks their problems are unique, but there is no need to rein- vent the wheel as the common saying goes Ifyou have any questions or comments about the book, feel free to send me feedback via my blog @TheKeyboard, which can be found at http://www littlehart net/atthekeyboard. If you wish to contact me via email, send it to me at chartyes@littlehart net. I do read every email I get but volume sometimes does not permit me to respond to each and every one By this point I’m confident that you have gotten a solid understanding of how to move your older legacy code into a CakePHP environment, Some of it will obviously not be as easy to refactor as these examples, but if you really take the time to try and understand how to solve the problem you are facing in terms of CakePHD’s conventions, I'm sure you'll find a way. Refactoring Legacy Applications Using CakePHP 83

You might also like