Use 3rd party modules in Zend Framework 2

The release of the first RC (release candidate) of Zend Framework 2 is getting close. One last beta (beta5) and then the RC will be announced! With the current pace of modules spawning on GitHub, I think it is a good idea to give some insights in how you can use 3rd party modules. In this blog post I will focus on MVC modules: modules with routes pointing to controllers and view scripts for rendering.

Because using a 3rd party MVC module does not mean you are enforced to follow their routing scheme, use their view scripts or use the predefined forms, I will explain how you can modify those options to your needs. In short, this post contains three different topics:

  1. Change a route
  2. Change a view script
  3. Change a form

To give you a concrete example, I will use the ZfcUser module as use case, currently available on GitHub and one of the best examples of a generic 3rd party MVC module. It provides domain models (a User), controllers (for login/logout etcetera) and views. There are also some forms for ZfcUser, to be able to login and register.

Change the route

ZfcUser provides entry points for the profile, login, logout and registration under "/user" and subroutes of "/user" like "/user/login". However, I you prefer "/account" but still want to use ZfcUser, you are able to do so. Because module configurations are merged recursively, you can have another module (for example, your Application module) and use that configuration to overwrite a part of the ZFcUser configuration. Make sure the module in which you provide this config is registered later than ZfcUser!

return array(
    // Perhaps some other config here

    // Start to overwrite zfcuser's route
    'router' => array(
        'routes' => array(
            'zfcuser' => array(
                'options' => array(
                    'route' => '/account',
                ),
            ),
        ),
    ),
);

In this case, you overwrite only the "route" value from the "zfcuser" route. All other parts are untouched, so now you can go to "/account/login" to login and to "/account" to view your profile. It's that simple!

Change a view script

View scripts are resolved by a folder having the transformed value of the namespace. By transformed I mean: everything is written in lowercase, and all uppercase parts are separated by dashes. So the namespace "ZfcUser" has the transformed value "zfc-user". And like routes, if you provide a view script in a module registered later than the module you want to override, it will just work.

For example the registration page of ZfcUser is located under "view/zfc-user/user/register.phtml". If you have your own module (again, for example Application) you can put a new register.phtml file under the exact same path. So the Application module has also the file located at "view/zfc-user/user/register.phtml". The only thing left to do is to make sure this "view/" folder from Application is used by the view layer to resolve this template. So make sure this piece of code is part of your Application module:

return array(
    // Perhaps some other config here

    // Start to overwrite zfcuser's view folder
    'view_manager' => array(
        'template_path_stack' => array(
            'application' => __DIR__ . '/../view',
        ),
    ),
);

Note that the other actions still use the view scripts from ZfcUser. There is no need to copy index.phtml and login.phtml to the Application module if you want to keep them the same: only copy the view scripts you need to change!

Change a form

This third topic is quite a tricky part. That is because not all modules follow the same pattern, but I hopefully can assume other 3rd party modules will provide the same logic as ZfcUser, so you are still able to modify all the forms of 3rd party MVC modules. What is this about? ZfcUser triggers an event when the form is built. After the form instance has created all of its elements, the event "init" is triggered.

As example, you want to add a checkbox to the registration form (a use case might be a user needs to agree with the terms of service). This checkbox is added with a listener. In another module, you attach a listener to the event. The event identifier is "ZfcUser\Form\Register" and the event name is "init". Again I will take the Application module to write this listener. Make your Module class to implement Zend\ModuleManager\Feature\BootstrapListenerInterface. Make sure you import Zend\EventManager\Event and then add this method:

public function onBootstrap(Event $e) {
    $app = $e->getParam('application');
    // $em is a Zend\EventManager\SharedEventManager
    $em  = $app->getEventManager()->getSharedManager();

    $em->attach('ZfcUser\Form\Register', 'init', function ($e) {
        // $form is a ZfcUser\Form\Register
        $form = $e->getTarget();

        $form->add(array(
            'name' => 'accept',
            'options' => array(
                'label' => 'Accept Terms of Use',
            ),
            'attributes' => array(
                'type' => 'checkbox'
            ),
        ));
    });
}

This method waits until a ZfcUser\Form\Register form triggers the "init" event. Then it grabs that form instance and adds another element to the form. To display this element, you probably also want to override the register.phtml view script from ZfcUser, because it will not get displayed otherwise.

A similar method applies for the ZfcUser\Form\RegisterFilter filter. This filter is also event aware and it triggers a similar named "init" event as with forms:

public function onBootstrap(Event $e) {
    $app = $e->getParam('application');
    // $em is a Zend\EventManager\SharedEventManager
    $em  = $app->getEventManager()->getSharedManager();

    $em->attach('ZfcUser\Form\RegisterFilter', 'init', function ($e) {
        // $filter is a ZfcUser\Form\RegisterFilter
        $filter = $e->getTarget();

        $filter->add(array(
            'name' => 'accept',
            'required' => true
        ));
    });
}

Please be aware not every module uses the event manager to trigger events like this! You need to look into the source code to know this kind of behavior unfortunately. However, I have a feeling every good module written with the intention to be reusable (and almost every module should be reusable!) will trigger events like this. But don't assume it and check the documentation of the module or even the source code, to be certain you can extend forms like this.

If you have other topics I might add to this list, please leave something in the comments. I will be glad to update the post with more information!