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.

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

  1. Only wanna input οn few general things, It patterrn іs perfect,the content
    iss really gresat : D.

  2. Clayton D says:

    LOTS of people link to this article. It would be fantastic if you could rewrite it to explicitly refer to the places each of these would be placed inside the ZendSkeletonApplication. I presume the places are obvious to Zend masters, but this has to be a use case for many “want to learn Zend” users for whom the locations aren’t remotely obvious.

  3. WOW just what I wass looking for. Came heree by searching for gratis
    sextreffen

Got something to say? Go for it!