Monday, July 30, 2012

Migrating a Non-MVC Site to MVC Gradually


I'm currently working on a (really awful) site that is unfortunately not using an MVC (model-view-controller) framework. In fact, this (incredibly horrid) application doesn't use any kind of best practices. It might even be considered a top candidate for the poster child for How To Do Nearly Everything Poorly: it isn't DRY (don't repeat yourself), tightly-coupled, almost wholly uncommented, and on and on. You get the idea.* But it's also in production, and management has been in constant "need" of adding features and "fixing" things. I am sure that none of you know what I'm talking about...

By some miracle, I have found myself with a moment to breathe and to reflect on the architecture while the business tests a major upcoming release, so I've started poking around to see how to address our greatest pain points. Our dev team has been lamenting the lack of an MVC framework for some time, now. I have been a big fan of the Yii Framework for over a year, and I think I've managed to convince a couple of people that matter (including my main team mate) that it's an excellent choice for our app. The problem is convincing the Powers that Be to let us rewrite the entire site from the ground up! I just don't think they're going to allow us that kind of time to make changes that they won't immediately see.



Rather than simply give up, however, I've been using this momentary slow-down to consider how we might make the changes gradually and try a couple of things out. After a bit of Googling and reading up at StackOverflow, things were looking a bit bleak; most people who responded to questions about how to gradually migrate from a non-MVC site to an MVC framework pretty much said, "You have to do it all at once. Forget doing it sections at a time!" (Perhaps I was missing the more constructive articles, but the ones I saw were very similar to each other.) I found that to be defeatist and not particularly creative. It seems to me that, given the right combination of tools, you can do just about anything. I was pretty certain I'd actually worked on projects that went back and forth between different code bases that worked in parallel, using Apache's mod_rewrite to determine which root to use to serve up documents.

That became my mission, this morning - to figure out how to fall back to the old codebase if my attempts to respond to a request using a controller/action pair were unsuccessful. Although I haven't put it through serious rigors, yet, I'm happy to say that my early tests are actually working. I wanted to provide the basic approach, here, in case it helps someone else. Feel free to comment on it for improvements.

First off, some additional background:

We're using LAMP - Linux, Apache, MySQL, PHP. It couldn't get more classic LAMP than that, unless we used Perl. So, I'm discussing this in terms of PHP features, and I'll be using mod_rewrite, because I can. So there.

I also have the ability to edit my Apache conf files. If you can edit just your vhosts directives, you can probably do this. I'm not sure it works if you're using .htaccess. I think it might not, but perhaps someone can help with that.

It seems like we'd like to use Yii, but it's possible that we'll start with a much simpler (less nice) homemade framework that will be easier to then refactor into Yii. (We're going to have to redesign the entire db, and I think it will be better to hold off on Yii until that's done.)

I'm writing this with the assumption that you know something about MVC frameworks.

Finally, the site has a relatively straightforward directory structure. There are some twists and turns, but let's just say they dumped most of the public files in the main document root - call it "application" - and the administrative areas into a subdirectory called "admin." There are images and include directories, as well. In a nutshell, the original application structure looks something like this:

.
|----- application
|   |----- index.php
|   |----- faq.php
|   |----- contact.php
|   |----- images (images go here)
|   |----- includes (some includes)
|   |   |----- header.php
|   |   |----- footer.php
|   | ----- admin
|   |   |----- index.php
|   |   |----- do_important_things.php
|   |   |----- mangle_accounts.php

I think you get the idea. With "application" as the Apache document root, you can refer to "images" as "/images" from anywhere in the app and it "just works." The includes directory is just "/includes" - very convenient. And there's an admin directory with it's own index.php. Everything is accessed by going to the domain followed by some file or directory, e.g., www.crummyapp.com/faq.php or www.crummyapp.com/admin/mangle_accounts.php.

But I really despise this app, and I want to set up an MVC framework and start rewriting functionality like that handled in do_important_things.php in a way that lets me continue to use mangle_accounts.php until I've had a chance to rebuild it and then not simply delete the old file but somehow send it to a fiery File Hell. Until that time, it has to work. Well, it has to "work" as well is it does now.

My main strategy will be to:

  1. Add a front controller to intercept the request for my new MVC way.
  2. Leave the old files intact, somehow, and let them live, for the time being.
  3. Not get too crazy about models and views, just yet. Or maybe I will. But I'm mainly interested in that front controller. 
  4. Not try to set up a fresh Yii app and move it all over there in one fell swoop. This is an in-between solution, for now, that will borrow a lot from Yii, because it's awesome and because it will make later migration to Yii easier.


Oh, if I had a nickel for every time I'd lamented, "My kingdom for a front controller!" And although I do want all the other goodies that come with MVC, the first step will be getting a front controller working. Why is that? Because that's really the lynch pin for everything else. If I can gain control of that initial request, I can do pretty much whatever I want with it. It doesn't need to be fancy - I just need to be able to wrangle control away from the old faq.php and take my requests somewhere else. Whether that ends up initially being cleaner static files that are then further refactored into views and models and such isn't as important to me right now. (One thing I've learned as I've gotten older, my young grasshoppers, is that patience is a really good thing. Rome wasn't built in a day, etc., etc.)

My new application directory structure is something like this:


.
|----- application
|   |----- index.php (front controller)
|   |----- images
|   |----- css
|   | ----- protected
|   |   |----- config
|   |   |----- controllers
|   |   |----- models
|   |   |----- views


|   | ----- framework (core libraries for the framework)

That's probably familiar to a lot of you. The "framework" directory is like the "yii" directory. The "application" directory is still the document root. index.php is the front controller, as labeled.

The main question is, "How in the world do I make this new thing work in parallel with the old thing?" To answer that, I started by planning the directory structure. I decided to leave the old application in it's own directory at the top of the document root. Here's what it all looks like combined:


.
|----- application

|   |----- index.php (front controller)
|   |----- images
|   |----- css
|   |----- protected
|   |   |----- config
|   |   |----- controllers
|   |   |----- models
|   |   |----- views


|   |----- framework (core libraries for the framework)
|   |----- oldapplication

|   |   |----- index.php
|   |   |----- faq.php
|   |   |----- contact.php
|   |   |----- images
|   |   |----- includes (some includes)
|   |   |   |----- header.php
|   |   |   |----- footer.php
|   |   |----- admin
|   |   |   |----- index.php
|   |   |   |----- do_important_things.php
|   |   |   |----- mangle_accounts.php


All of the pre-existing files are now in a separate directory, oldapplication.

If that's where I left it, assuming I was using a .htaccess file typical of MVC frameworks like this (Google it - they're everywhere), when the visitor arrived at the application, Apache would serve up the index.php front controller file. That would do some not terribly fancy front controller stuff that I won't get into, but the main thing to note is that, like most MVC front controllers, it would be expecting the URL to contain some reference to a controller and an action. For example, the URL might look like:

http://www.crummyapp.com/home/index

in which case it would know to execute the "index" function (the action) contained in the controller named "home" (i.e., protected/controllers/HomeController.php).

That's great for my new sections of the app, but what if I want to oldapplication/faq.php where it is, for now, and serve it up old-school? Unfortunately, my standard .htaccess file would have a hard time with that.

The solution came, in part, from this article: MVC Framework Routing (static content vs. dynamic). The writer modified their app's Apache config file, adding the following to their vhosts directives (edited to match my directory structure):


RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}/oldapplication%{REQUEST_URI} !-f
RewriteCond %{DOCUMENT_ROOT}/oldapplication/admin/%{REQUEST_URI} !-f
RewriteRule ^(.*)$ %{DOCUMENT_ROOT}/index.php [L]
RewriteRule (.*) %{DOCUMENT_ROOT}/oldapplication$1 [L]


A brief dissection, line by line:

  1. Make sure the RewriteEngine is set to "On" or this might not work at all.
  2. Once that is taken care of, there are two conditions specified by the "RewriteCond" directive. The first condition checks to see if the request is NOT a file in the old application's main directory. The next condition checks to see if the request is NOT a file in the old application's "admin" subdirectory. (The "!-f" means, "see if this is not a regular file.")
  3. After that, is the first RewriteRule. That RewriteRule is what happens if those two conditions are BOTH true. (The conditions are basically chained together by an "and" unless I explicitly use "OR" which isn't what I want.)
  4. That second RewriteRule is not preceded by any conditions, so it stands as-is.

Without getting into too much detail about mod_rewrite, these lines will check for the existence of the requested file in both the original app's home directory and in it's admin subdirectory. If it cannot find the file, it will go to the front controller (index.php in the doc root). But, if those conditions fail, and it doesn't go to the front controller, it proceeds to the next rule which will rewrite the request as www.crummyapp.com/oldapplication/whatevertherequestwas.php.

So, that's pretty great! I can now serve up both old and new files. If it doesn't find the old, it will go to the MVC version. Nice! But I still had a problem, at that point. Do you remember those "images" and "includes" directories? Well, throughout the application, there are references to "/images" and "/includes" - ugh! I'm not going to edit all of those URLs!

To solve that problem, I used PHP's ability to set configuration options dynamically using ini_set(). The configuration setting I needed to edit is called "include_path" which does what it sounds like it does, sets the default include path for PHP. In other words, when PHP goes looking to include something, it checks any and all include paths specified until it runs out of them. The line of code looks like this:

ini_set("include_path", ini_get("include_path").";".$_SERVER['DOCUMENT_ROOT']."/oldapplication");

So, the include_path setting now has the "oldapplication"directory in it when looking for things like "images."

Believe it or not, the basic tests I've run serve up the old files beautifully, with no broken images or missing included classes, headers, or whatever. Neat!

One loose end that I still need to address is:

index.php - Because this is the default index page in any directory of my application, it is in conflict with my front controller's name. I could rename the front controller (and I might just do that), but it is still going to be a problem for those links that have the directory name but do not include index.php as the desired file! (This is a pretty common thing to do, e.g., http://www.crummyapp.com/admin/.) mod_rewrite won't know which filename is being requested, so I think I have to go through and explicitly include that for all URLs that need it.

There are probably others. We'll see.

Anyway, I'm pretty excited, because that was a pretty big hurdle, and it appears to be working! If I run into other things, I'll try to remember to post them here.

Cheers!

* I did NOT have a hand in writing or designing this thing. My present employer purchased it from a competitor that was sinking like the Titanic, and no techies had a chance to look behind the curtain before they agreed to buy it. 'Nuff said.

No comments:

Post a Comment