Friday, October 12, 2012

When migration to Zend Framework 2 seems impossible

The problem

When you got a really big codebase and lots of developers working on it, releasing new features and bugfixes every 2-3 weeks, it seems impossible to migrate to Zend Framework 2, soon.
That's because you can't stop further development, you have customers waiting for the new features, they wait for bugfix releases, to get stuff running, that's buggy, you have more than 1mio lines of code and possibly > 100 active developers on that codebase. How can you migrate? I stombled accross this problem already when the first ZF2 betas came out and startet to write some code that should solve the problem.
The result is HumusMvc, it integrates Zend Framework 2's ModuleManager and ServiceManager in a ZF1 application.

The migration path

First of all, you create a new skeleton for you application. Simply clone the HumusMvcSkeletonApplication and use it as base for you skeleton app.
At the next step, you create a repository per module you have in your old ZF1 application. Put the source code in the "src/" directory of that module, create a Module.php file, name the Module, put the module configuration in it, etc. - in other words, create a simple ZF2 module out of your ZF1 application module. If you use HumusMvcAssetManager, too, you can also provide a public directory in your module and use the asset manger. That way you can override assets in a very similar way you override view scripts in a ZF1 application.

The module now can look something like this:
- MyAppDefaultModule/
    - config/
        - module.config.php
    - layouts/
    - public/
        - css/
        - js/
        - img/
    - src/
        - controllers/
        - models/
        - views/
    - tests/
    - autoload_classmap.php
    - composer.json
    - Module.php

If you use ZF1 with the Resource-Autoloader, than you don't have psr-0 compliant classnames, so define your autoloading simply with classmap autoloader.
In order to install your module, just require it with composer.

In your module.config.php you provide your front_controller configuration, you have to tell the front controller, that you have a module, where it can dispatch.

<?php
return array(
    'front_controller' => array(
        'controller_directory' => array(
            'default' => __DIR__ . '/../src/controllers'
        ),
    )
);


You can now define some resources with ZF2's service manager in your module config file. You can remove those resources from bootstrapping later. HumusMvc ships with Navigation, Translator and Locale factories, so you don't need to handle these, just configure them.
If you have something like a db connection, that was bootstrapped in your old application, you need to do this again. Therefore you use the "onBootstrap" event. Well, you could define everything right now with the service manager, but that is exactly that much work, we wanted not to do now, because that would stop the further development for quite a time.

MyAppDefaultModule\Module.php
    /**
     * Listen to the bootstrap event
     *
     * @param EventInterface $e
     * @return array
     */
    public function onBootstrap(EventInterface $e)
    {
        $application = $e->getParam('application');
        $serviceManager = $application->getServiceManager();

        $config = $serviceManager->get('Config');

        // @todo refactor later and use cleanly service manager
        $this->setupDbConnection($serviceManager, $config);

        // we need to put config in zend_registry
        // @todo refactor later
        \Zend_Registry::set('config', new \Zend_Config($config));
    }

    protected function setupDbConnection($serviceManager, $config)
    {
        if (!isset($config['myapp']['db'])) {
            throw new \RuntimeException(
                'No multi db config found.'
            );
        }
        $options = $config['myapp']['db'];

        if (isset($options['defaultMetadataCache'])) {
            $this->setDefaultMetadataCache($options['defaultMetadataCache'], $serviceManager);
            unset($options['defaultMetadataCache']);
        }

        $adapter = $options['adapter'];
        $default = $options['default'];
        unset(
            $options['adapter'],
            $options['default']
        );

        $adapter = \Zend_Db::factory($adapter, $options);

        if ($default) {
            \Zend_Db_Table::setDefaultAdapter($adapter);
        }
    }

Here is a short example you to bootstrap db connection on every request and put the application configuration into Zend_Registry.

Further refactorings

At first, I recommend to remove all "onBootstrap" methods when neccessary and put this stuff in your service manager, so you only instantiate the objects, when they are need, not on every request.
In the next refactoring rounds, you can refactor only one module at a time, so development in the other modules can still be done. Put all stuff in the service manager and use it as a service locator inside your application. Remove Zend_Registry everywhere. When you are done with that, use a php namespacer tool (or do it by manually) and refactor to a namespaced codebase. Remember, code is per module, so you can do this step module per module. Further bugfixing and development of new features is still possible.

Final migration to Zend Framework 2

You should now have a very flexible and modern Zend Framework 1 application, that makes use of a lot of stuff from Zend Framework 2, already: Service Manager, Module Manager, Event Manager, just to name a few. You don't have any expensive upfront bootstrapping any more, you moved everything to the service manger. You should have a namespaced codebase. You use ZF 2 classes much more, than ZF1 classes. - Now it's time to do the final migration.

Refactor all controllers, refactor route configuration refactor all view scripts, and so on. Now it's time for the hard work, we skipped before - but we had a couple of month time to bring our codebase to something, that _can_ be refactored to a ZF2 codebase very fast.

2 comments:

  1. Hi,
    i'm trying the humus-mvc but i'm blocked on the following error, can you help please:

    Fatal error: Uncaught exception 'Zend_Controller_Dispatcher_Exception' with message 'Invalid controller specified (error)' in /var/www/intranet/vendor/zendframework/zendframework1/library/Zend/Controller/Plugin/Broker.php on line 336
    ( ! ) Zend_Controller_Dispatcher_Exception: Invalid controller specified (error) in /var/www/intranet/vendor/prolic/humus-mvc/src/HumusMvc/Dispatcher.php on line 113
    Call Stack
    # Time Memory Function Location
    1 0.0020 238480 {main}( ) ../index.php:0
    2 0.7133 5127656 HumusMvc\Application->run( ) ../index.php:21
    3 0.7134 5128744 Zend\EventManager\EventManager->trigger( ) ../Application.php:263
    4 0.7134 5128744 Zend\EventManager\EventManager->triggerListeners( ) ../EventManager.php:207
    5 0.7135 5130504 call_user_func ( ) ../EventManager.php:468
    6 0.7135 5130704 HumusMvc\DispatchListener->onDispatch( ) ../EventManager.php:468
    7 0.7136 5132224 Zend_Controller_Front->dispatch( ) ../DispatchListener.php:77
    8 0.7362 5280464 Zend_Controller_Plugin_Broker->postDispatch( ) ../Front.php:965
    9 0.7362 5280464 Zend_Controller_Plugin_ErrorHandler->postDispatch( ) ../Broker.php:333
    10 0.7362 5280464 Zend_Controller_Plugin_ErrorHandler->_handleError( ) ../ErrorHandler.php:223

    ReplyDelete
    Replies
    1. I need some more information. What is the controller name and action you want to dispatch to? Is autoloading enabled for these classes? Are the controller configured correctly?

      Delete