jurian sluiman

{
Wissel naar

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!

Comments

Tomáš Fejfar

Hi, thanks for this great resource. It's the single most usable article about how the reusability in ZF2 works. And it's awesome!

atukai

Tnanks. It's great article. But how I can rewrite models and controllers?

Jurian Sluiman

atukai, using other models is not as easy as it sounds. There is no agreement how you could reuse models and extend them with for example additional properties. Perhaps in the future "we" as a community get to a consensus how we use domain models in our modules.

Controllers are slightly easier to reuse. I will either add another section to this article, or when more extension points are known, I will write those down together with the controller extension topic. Thanks for the response!

Alex Ross

This was great! The event listener part was particularly insightful and applicable to other use cases.

Question - a week or so ago I needed to set the ZfcUser routes to the root domain ("domain.tld/login") rather than building upon /user ("domain.tld/user/login"), so I went in and pulled each route out from being children under zfcuser. In my Application's module.config.php I thus have several Literal route matches for /register, /login, etc which then use /zfcuser as the controller. Is this the smoothest want to accomplish that? It would be nice to only have to reconfigure the top-level zfcuser route matching, but the only way I can see to do this would be to match it to the root domain, which won't mesh well with my Application's current root match.

Alex Ross

Followup question - I actually used this method to combine all view .phtml files into one directory under my Application module, so ~10 of our internal modules all have their view files in one place. I did this to make it easier on my front-end guys, as the Zend directory structure is tough to navigate without knowing how it works.

Do you think there are any downsides to having all of your module's view files in one place? It does decrease the "modularity" of the application a bit, but none of these are for modules which would be repackaged for distribution to others as vendor modules.

Jurian Sluiman

Alex, there are two cases for your routes. One where you keep the structure of the original zfcuser routes and one where you create your own.

For the first one it would mean "/" is the user profile page. Like my example where "/user" is replaced by "/account": you now replace it with "/". However, I don't think you want your homepage a user's account profile page, right? Then the only solution is to recreate the routes yourself, just like you did. If they are scattered around different places (like "/login" and "/register") you cannot really use a single tree of routes for that.

Then about your modules: I think it makes sense. It's is probably what we'll do too. Just be sure you *also* use views in the modules self. If you copy a module, you want to have it working out of the box. So for example a blog: all our blog views are in the blog module, but very simple. A clean, generic markup which can be styled easily but without additional artifacts.

For a client we will create a ClientName module where all their styles will overwrite the default ones. For example the view to render a feed of the blog isn't that client specific, so we won't overwrite that. The index of articles is, so we use for ClientName another index.phtml.

Don't think of the modules as it's a vendor or not: you might be a vendor for yourself. You probably want to copy a module into a new app and have it working out of the box too, without the need to copy a second module with view scripts. I've seen our frontend devs struggling with it, but now it makes sense they use it as I describe above :)

RWOverdijk

I was wondering how you would add validation for the form element you added. I like the use case, but you can't force the element to be required like that. Does it have the possibility to add something to the InputFilter? Maybe it would be nice to add to your example.

Jurian Sluiman

RWOverdijk, with the write-up of this article I came to the same conclusions. I think InputFilters (which are closely related to Form objects) should behave the same like forms: trigger an event and let others extend the input filter.

Currently, the example module I took (ZfcUser) does not trigger the init event. It's the reason I created an issue for ZfcUser asking to correct this. Since at this moment ZfcUser undergoes a heavy rewrite, I think it will either be included during the rewrite or modified after the merge of the rewrite.

I did not get any comments to my issue so far, but you can follow updates about it here: https://github.com/ZF-Commons/ZfcUser/issues/74

RWOverdijk

Thank you for explaining this. I think that having a standard for this would be a very good idea. Otherwise we'd end up with a bunch of modules, using different hooks, confusing developers. I'll follow this discussion, and let them know that I agree.

Great article by the way.

Alex Ross

Right, you're definitely on the mark with your comment that we'll end up being a vendor for ourselves.

It's good to hear what you're doing internally - as some of the other commenters have mentioned it helps a lot to get closer to having a standard for implementing ZF2 modules etc. on a larger scale than is currently shown by the (as of this writing) few tutorials around the web.

Chris de Kok

Nice post :) only am curious about the public folder in modules, how to map them to the real public folder why doesn't ZF include a default way to do this?

RWOverdijk

@Chris de Kok: You can accomplish that in a multitude of ways. One of which (the fastest one) is by using AliasMatch (http://httpd.apache.org/docs/2.0/mod/mod_alias.html, highly recommend this method as it's very flexible) and the other is building an (or using an existing: https://github.com/DASPRiD/AssetLoader) asset manager. This has not been added by default as this is a custom solution and is entirely up to you. Remember, the skeleton has not been built to show you how you MUST do it. It has been built to show you how you CAN do it. I hope this helps.

teff

Hi,

with latest ZfcUser and ZF2, there seems to be some changes. I couldn't extend the Form anymore. If somebody has a solution, pls let me know. Otherwise you can check https://github.com/ZF-Commons/ZfcUser/issues/98 .

Thx!

Jurian Sluiman

teff, the bug is also noted by some others. It came out there is no bug in ZfcUser, but it's a ZF2 related issue. The root cause is found and a PR submitted by Mez (Martin Meredith). At this moment, tests needs to be appended to take care for regression, but can follow the progress here:

https://github.com/zendframework/zf2/pull/1971

teff

Thank u very much Jurian.

Vlada

Application Module.php bootstrap code instead of
_code_
$form->add(array(
'name' => 'accept',
'attributes' => array(
'label' => 'Accept Terms of Use',
'type' => 'checkbox'
),
));
_/code_
should be 'options' array for label:
_code_
$form->add(array(
'name' => 'accept',
'options' => array(
'label' => 'Accept Terms of Use',
),
'attributes' => array(
'type' => 'checkbox'
),
));
_/code_

Thanx!
Any chance of update examp;e for form validation filters?

Jurian Sluiman

Vlada, thanks for you comment. In the time between I wrote this post and now, some things changed in Zend\Form. One of those was the explicit differences made between options and attributes.

I made the update to the code and added the example for the input filter as well. Here again: at the time of this blog post there was no option to add items to the RegisterFilter. I just checked it and now it is possible to modify the filter. In the same section I added the example how you can add the "accept" element to your input filter.

Vlada

Excelent. Thank You!

dd

dd

Vinicius Garcia

Good article! Thanks!
I'm have a question: I trying to customize the zfcUser Register Form, how can I put a form element between the original form elements?
When I use $form->add(...), the new element is appended to the form but a want to put elements before the password field, for example. Thanks!

Brown

@Vinicius I am not 100% sure of the procedure, but I would investigate setting the order priority on the add method. see http://framework.zend.com/apidoc/2.0/classes/Zend.Form.Form.html. Crude way would be to remove them and then re-add them


I have a question of my own. I have a registration form that has about 15 input fields. Is there an alternative approach I can use? defining the 12 element and 15 inputFilter seems to be too long for the bootstrap method. And I thought best practice was to always keep the bootstrap method light as it is called on every single request? Wont the above approach affect performance?

Jurian Sluiman

@Vinicius: these forms are rendered in their own view. For example, in your customizing module you can create a script "zfc-user/user/login.phtml" where you position the new elements at their correct place, take a look at the original one: https://github.com/ZF-Commons/ZfcUser/blob/master/view/zfc-user/user/login.phtml

@Brown: the listener is inside the bootstrap method. The only thing you do is attaching a listener which costs some resources every request (but that is very light weight). The execution of the listener (to add the elements and input filter) is only done when the form is built. So this practice is the best possible for this use case (for this moment, then).

The only concern might be that you have a lot of code in your module class, but you can relocate that code to a dedicated class (listener object) to make the Module.php more clean.

Brown

Thank for you reply. Makes sense now that you describe it. However, I have failed to get it to work. I have

$em->attach('ZfcUser\Form\Register', 'init', 'Application\Form\Register');

And have that class:

namespace Application\Form;

use Zend\EventManager\EventManagerInterface;

use Zend\EventManager\ListenerAggregateInterface;


class Register implements ListenerAggregateInterface
{

public function attach(EventManagerInterface $e)
{


$form = $e->getTarget();

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

}

public function detach(EventManagerInterface $e)
{

}

}

BU I am getting the error:

Zend\Stdlib\Exception\InvalidCallbackException' with message 'Invalid callback provided; not callable

Ay thoughts?

Jurian Sluiman

Brown, you have made some errors in your line of thought. Because my post is rather about the principle, I don't have much room to provide support for specific cases like yours.

Please try to ask your question on the mailing list of Zend Framework. More information about the mailing list is provided here: http://framework.zend.com/wiki/display/ZFDEV/Mailing+Lists

If you prefer StackOverflow you can ask your question there. If you tag your question with zend-framework2, the experts will probably find you too. More information is here: http://stackoverflow.com/tags/zend-framework2/info

The last option is to get online on IRC. The Freenode channel #zftalk enables a chat with other Zend Framework users. More info is here: http://www.zftalk.com

modosayca

hello,
i'm new to zf2 and would like to override zfcuser with my own controller, but really don't understand how to do it.
following your tutorial, i could override the views, but non the controller.

so, here what i did:
1) i decided to call my module: myuser

2) in application.config.php
'modules' => array(
'Application',
...

'ZfcBase',
'ZfcUser',

'Myuser',
),

3) module structure:
Myuser
/config/module.config.php
/src
/Myuser
/Controller/MyuserController.php
/view
/zfc-user
/user
/index.phtml
/login.phtml

4) Myuser/config/module.config.php:
'controllers' => array(
'invokables' => array(
'zfcuser' => 'Myuser\Controller\MyuserController',
),
),

'router' => array(
'routes' => array(
'zfcuser' => array(
'type' => 'segment',
'options' => array(
'route' => '/user[/:action][/:content]',
'constraints' => array(
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
'content' => '[a-zA-Z][a-zA-Z0-9_-]*',
),
'defaults' => array(
'controller' => 'Myuser\Controller\Myuser',
'action' => 'login',
),
),
),
),
),

'view_manager' => array(
'template_path_stack' => array(
'myuser' => __DIR__ . '/../view',
),
),

4) Myuser/src/Myuser/Controller/MyuserController.php:

namespace Myuser\Controller;

use ZfcUser\Controller\UserController;

class MyuserController extends UserController
{
public function indexAction()
{
return new ViewModel(array(
'myvar' => "hello world",
));
}

public function loginAction()
{
return new ViewModel(array(
'myvar' => "my login action",
));
}

5) now, if i point my browser to: public/user/index or /user/login
i get:
A 404 error occurred
Page not found.
The requested controller could not be mapped to an existing controller class.
Controller:
Myuser\Controller\Myuser(resolves to invalid controller class or alias: Myuser\Controller\Myuser)

i'm really confused and don't understand what's wrong.
can you please help me to solve this?
thank you

Wesley Overdijk

@modosayca, You must also define the controller in the config.

'controllers' => array(
'invokables' => array(
'Myuser\Controller\Myuser' => 'Myuser\Controller\MyuserController'
),
),


Now it's looking for a class which you've set the alias wrongly for.

Also, don't post stuff like this on people's blogs. Especially not if the code is not hosted somewhere like gist.github.com, pastebin.com or pastie.org. It makes a mess. :)

modosayca

@wesley.
first of all, thank you for your reply: i'll try it.
second: it is the first time i post a request of this kind and did it here because i saw other similar posts in this same page. if jurian wants, he can very well delete my post. anyway, i'm sorry. best regards

Wesley Overdijk

@modosayca no problem. I just know how messy it can get so I tried helping you out by pointing to those pasting sites.

Good luck! :)

Jurian Sluiman

Modosayca, I don't really mind people asking questions about above code. If it gets too off topic, I usually ask them to go to Stack Overflow or something.

I will surely not delete comments, as it might be useful for other visitors later. It is however a good idea to use http://gist.github.com, http://pastie.org or http://pastebin.com to display large amounts of text, as I will not format my comments so it gets pretty unreadable.

Ruben

Thanks for this useful article Jurian.
What would be the correct way to attach custom stylesheets and javascript files to the view?
For instance I'm using the ZfcUser module and I'm overriding the login form view already. But on top of that I would like to load a custom css file for the login view and also a custom javascript file with some jQuery code specifically for the login form.

Do I have to override the controller and load the scripts from there or can it also be done by hooking in to the event in my Application module's onBootstrap method?

Ruben

Actually ignore that last question. It's probably best to just add it to my custom view script.

Angus

Hi,

Thanks for your post that is still helpful to understand how it works ^^

As Alex Ross ask at the beginning of comments, i'm trying to get the zfcuser route à the root of my module. I mean i try to delete the "/user/login?..." and replace it with "/login?..."
I tried to set "/" route for zfcuser but it doesn't works.
How could i route zfcuser children to get what i'm looking for ?

Place a comment

If you have a user account for this site, you can login to click here.

Please note your comment below will be removed if you login!

 
 

The address is stored internally but not displayed on this site. We will respect your privacy.

In your message no html is allowd. A blank line creates a new paragraph, an url gets a hyperlink.