Migrating to Zend Framework: Legacy Scripts

Migrating an existing PHP application to Zend Framework can be a daunting task, especially if the migration must occur all at once. It is much easier to migrate the application in sections over a longer period of time. This approach requires some modification from a normal Zend Framework setup, in which all requests for non-existent files are passed to the bootstrap file and dispatched, but all requests for files which do exist (images, css and, in our case, legacy scripts) bypass the bootstrap and are served directly. In our scenario, we want to be able to integrate features of the Zend Framework into the legacy scripts without rewriting them as MVC components, and without having to duplicate code from the bootstrap (ie: in an auto_prepend file).

The following mod_rewrite rules will perform the normal routing of non-existent URIs to the bootstrap, but will also route requests for PHP scripts to the bootstrap (this example assumes that the rewrite rules are specified in a per-directory context):

RewriteEngine   On
RewriteCond     %{REQUEST_FILENAME} !-f    [OR]
RewriteCond     %{REQUEST_FILENAME} \.php$
RewriteCond     %{REQUEST_FILENAME} !-d
RewriteRule     ^(.*)$ /bootstrap.php/$1 [L,NS]

Once legacy scripts are successfully being sent to the bootstrap, we need to modify it so that it does the right thing. The following code checks to see if the current request is for an existing script, and if so that script is served instead of running the front controller dispatch. Any bootstrapping that occurs before this code will be available to the legacy scripts; for example, the Zend_Registry, any config loaded with Zend_Config, Zend_Loader, etc.

$request  = new Zend_Controller_Request_Http();
$docroot = $request->get('DOCUMENT_ROOT');
$uri     = $request->getPathInfo();

if (file_exists($docroot . $uri)) {

    ob_start();
    include $docroot . $uri;

    $response = new Zend_Controller_Response_Http();
    $response->setBody(ob_get_clean());
    $response->sendResponse();

    exit;

}

This approach works well for basic sites, but what happens if you already have mod_rewrite rules in place to create SEO-friendly URLs that redirect to actual scripts? This causes a problem because the value of the actual script is not available to Zend Framework, so the request is very likely to be handled by the default /controller/action route. Consider the following example:

RewriteRule ^/calendar/([0-9a-zA-Z_]+) /view_calendar.php?user_name=$1

As long as this rule appears in your apache configuration prior to the bootstrap routing rules outlined previously, the request for /calendar/someusername will be redirected via an internal subrequest to /view_calendar.php?username=someusername. This operation updates the %{REQUEST_FILENAME} value used by mod_rewrite, so the legacy script matching rule is invoked and this request is handed off to the bootstrap script.

The problem that we run into here, is that the bootstrap script is unaware of the update in URI, and still believes that it is handling a request for /calendar/somuser. Since there is a default routing rule in the front controller for paths matching the format /controller/action, our request is dispatched to a non-existent controller and an error is thrown.

This issue can be avoided by converting the SEO mod_rewrite rules into Zend Framework router rules that route to a custom subclass of Zend_Controller_Action that encapsulates the bootstrap code that we developed earlier for serving legacy scripts. Here is Zend Framework rewrite router implementation of the calendar mod_rewrite rule listed above:

$front  = Zend_Controller_Front::getInstance();
$router = $front->getRouter();
$route  = new Zend_Controller_Router_Route_Regex(
    'calendar/([0-9a-zA-Z_]+)',
    array(
        'controller' => 'legacy',
        'action'     => 'index',
        'script'     => '/view_calendar.php'
    ),
    array(
        1 => 'user_name'
    ),
    'calendar/%s'
);
$router->addRoute('calendar', $route);

In order to handle this route, we need to implement a subclass of Zend_Controller_Action called LegacyController. This class will have a single action called indexAction which is where our legacy script handling code will go:

class LegacyController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
        $viewRenderer->setNoRender(true);

        $request = $this->getRequest();
        $docroot = $request->get('DOCUMENT_ROOT');
        $uri     = '/' . ltrim( $request->getParam('script'), '/' ); // Force leading '/'

        if (file_exists($docroot . $uri)) {
            ob_start();
            include $docroot . $uri;
            $output = ob_get_clean();
            $response = $this->getResponse();
            $response->setBody($output);
            $response->sendResponse();
        } else {
            // You could redirect to a custom 404 handler here.
        }
    }
}

This works great, but now we have some code duplication between the LegacyController and the bootstrap file. To get rid of the duplication, we can add an additional route that forwards requests for php scripts to the LegacyController like this:

$front  = Zend_Controller_Front::getInstance();
$router = $front->getRouter();
$route  = new Zend_Controller_Router_Route_Regex(
    '(.+\.php)',
    array(
        'controller' => 'legacy',
        'action'     => 'index',
    ),
    array(
        1 => 'script'
    ),
    '%s'
);
$router->addRoute('legacy', $route);

This new route allows us to remove the legacy script handling code from the bootstrap file and use the LegacyController for all legacy requests. One item to note is that this rule does NOT provide leading ‘/’ to the value of ‘script’, which is why the LegacyController must use the '/' . ltrim($script) construct.

Chris Abernethy
PHP Wrangler, MySQL DBA, Linux SysAdmin and all around computer guy, developing LAMP applications since Slackware came on 10 floppy disks.

13 Comments on "Migrating to Zend Framework: Legacy Scripts"

  1. Holy smokes, this is awesome. This is exactly the situation in which I find myself: lots of legacy scripts, most that are reached via custom .htaccess rewrites, that I would love to migrate over to ZF MVC handling.

    Now I just need to convince the customer that it’s worth the cost to gradually migrate over. ;-)

    Thanks again for an awesome post! ;-)

  2. One interesting issue that I came across was that my script was being rendered twice (ZF v1.11):

    1. In LegacyController::indexACtion() via our direct call to $response->sendResponse().

    2. Since we do not exit() or die() after sending the response form the action above, the framework completes the dispatch cycle and the front controller eventually sends the response body again.

    It may be that earlier versions of the ZF – for example, the version that was current when this post originally was written – had different handling.

    Of course, the solution for the more recent version is to either remove the call to $response->sendResponse() from the action, or to die() immediately after that call.

    Still, I stand by original assessment of awesomeness. Thanks for an excellent article.

  3. Andrew says:

    Am I completely missing something or can this not be achieved in the .htaccess file.

    1. Add [L] to end of current rewrites
    2. Place new rewrite for Zend at bottom of .htaccess file

    Done!! Worked perfectly for me. The code rewrites anything that is not a real file or directory, so all current scripts (.php) work fine. The old rewrites work fine because they are processed first. The [L] flag halts the processing when the old rewrite is trigged.

Trackbacks for this post

  1. Zend Framework in Action » Moving existing applications to Zend Framework
  2. Zend Framework Tutorials « PHP::Impact ( [str blog] )
  3. PHP::Impact ( [str blog] ) » Blog Archive » Zend Framework Tutorials
  4. Tutoriales de Zend frameword « CuatroXL - Cuatro Xl
  5. Zend Framework教程大全
  6. Recopilacion de tutoriales Zend Framework | Stekl
  7. IP、IC、IQ卡,统统告诉你密码 » Zend Framework 教程大全
  8. 網站製作學習誌 » [Web] 連結分享

Got something to say? Go for it!