ZF-Commons / ZfcUser

A generic user registration and authentication module for ZF2. Supports Zend\Db and Doctrine2. (Formerly EdpUser)
BSD 3-Clause "New" or "Revised" License
497 stars 343 forks source link

[RFC] Forms and Service layer #561

Open adamlundrigan opened 9 years ago

adamlundrigan commented 9 years ago

I've combined proposed changes to the forms and service layer into one as the latter depends heavily on the former. This one actually is an RFC as I haven't implemented a prototype yet.

Service Layer

We can simplify the service layer by pushing concerns such as the object being hydrated/extracted and password hashing on save -- both of which the service currently handles -- out of the service layer.

Q: Putting the password hasing in the hydrator might be an ill-conceived idea. Any better ideas that avoid having the hashing hard-coded into the service? I just don't like it in the service...it should be part of the form handling process. A filter, maybe?

interface UserServiceInterface
{
    /**
     * @return UserEntityInterface
     * @throws RuntimeException on failure to create user
     */
    public function create(array $data);

    /**
     * @return UserEntityInterface
     * @throws RuntimeException on failure to update user
     */
    public function update(UserEntityInterface $user, array $data);

    /**
     * @return UserEntityInterface
     * @throws RuntimeException on failure to delete user
     */
    public function delete(UserEntityInterface $user);

    // Convenience; proxy directly to mapper methods
    public function findById($value);
    public function findByUsername($value);
    public function findByEmail($value);
}

Q: Proxy find* methods through to mapper, or just provide a getter for the mapper as the current service does? My preference is on proxying the methods so we're not forcing userland code to reach through the service to get at the mapper to run a query.

A class implementing this interface would require three dependencies

NOTE: CreateUserForm and UpdateUserForm are instances of the same class (UserForm). They are created separately by the FormElementManager so that modules extending ZfcUser could attach custom fields differentially based on the action occurring.

Form Layer

The login form and input filter would remain largely unchanged except for some updates now that we're away from PHP 5.3.x. The registration form and input filter will evolve to work for both create and update operations:

UserForm (formerly BaseForm)

In terms of elements it will be unchanged.

Q: Should we drop the CAPTCHA? IMO it shouldn't be baked in as not every application needs/wants it. Consuming app can always pull the form and inject the CAPTCHA if they need it.

UserInputFilter (formerly RegisterFilter)

In terms of elements it will be unchanged.

The RecordExists and NoRecordExists validators should be updated to use the context argument so that they work for both create and update operations.

Q: Do we need both? RecordExists isn't actually used by ZfcUser.

Q: Should the validators be changed to allow calling arbitrary mapper methods, like the new authentication layer does?

claytondaley commented 9 years ago

Zend/Form already has intelligence to handle passwords so this concern is resolve by PR #563, possibly eliminating the need to make yet another BC change.

adamlundrigan commented 9 years ago

@claytondaley good point, the new_password stuff isn't really necessary then. Document updated

claytondaley commented 9 years ago

I also really like the idea of implementing password hashing through a Hydrator Strategy. In fact, it would be great if the Hydrator was implemented to be easily extensible.

It'd really be better if ZfcUserAdmin used (extended) a core hydrator... and I could extend it further. Unfortunately, I'm still on the learning curve for Hydrator Strategies so I'd benefit from a recommended pattern for this. My go-to was the event system, but my research on performance suggests this may not be the best long-term strategy.

I assume a hydrator strategy also eliminates any hard-coding since you could replace the default right there in the hydrator.

adamlundrigan commented 9 years ago

I also really like the idea of implementing password hashing through a Hydrator Strategy. In fact, it would be great if the Hydrator was implemented to be easily extensible.

I've been battling with that myself, as it's not the purpose of the hydrator. The hydrator is meant to have one responsibility: converting between an object and it's array representation. The proper solution would be to do this on the form post-validation but pre-object-populate, so I'm thinking the best course of action would be to create a filter which hashes the value of the password field.

For example, I have PR that converts ZfcUserAdmin's EditForm to use a hydrator. Now I need to extend it to properly Hydrate DateTimes.

I had worked on something similar last summer but never got the chance to complete it and PR it (I did recently get as far as opening a ticket for it: https://github.com/Danielss89/ZfcUserAdmin/issues/65). A lot of what ZfcUserAdmin does should be part of ZfcUser, and that module should just be the UI and integration with ZfcAdmin; the logic of CRUDing users is something ZfcUser should handle itself...and that's the main goal of my recent PR blitz here, which when complete I had planned to turn my attention to ZfcUserAdmin next.

claytondaley commented 9 years ago

You're right. This is really what InputFilters do. The Zend\Form Quick Start gives examples like:

             'filters' => array(
                 array('name' => 'Zend\Filter\StringTrim'),
                 array('name' => 'Zend\Filter\StringToLower'),
             ),

If an InputFilter can trim and change case, why not hash? If the stored hash uses a different strategy, you can always getRawValues() and rehash.

claytondaley commented 9 years ago

... and I finally figured out the clean way to handle hydrator extension.

claytondaley commented 9 years ago

For the ZfcUser forms, I'd also like to recommend a dynamic validation group implementation comparable to Rob Allen's recommendation. I care more about the requirement than the strategy -- add/remove methods for the validation group would be just as good (and perhaps better).

This is particularly important on "edit" forms where you may want to present readonly data that should not be committed back to the database. The current method for achieving this is to create a validation group. Unfortunately, the default validation group implementation depends on explicit knowledge of all of the elements on the form, which is antithetical to easy extension.

I believe the readonly attribute should also exclude by default, with the option to override by setting exclude => false. IMHO the core Zend/Form should be upgraded in this way, but this is as good a place to start as any.

claytondaley commented 9 years ago

A final enhancement would be a universal form renderer placed in a parent project (ZfcMvc?) that is inherited by ZfcUser and ZfcAdmin. ZfcUserAdmin has the right idea importing _form.phtml into numerous pages. I believe I've successfully implemented a fully generic example that would be a drop-in replacement for existing ZfcUser forms. Several notes:

adamlundrigan commented 9 years ago

RE: validation groups this is something I've brought up in discussion about ZF3's forms, as the current implementation of validation groups makes it hard to work with complex forms. IMO there should be a convenience wrapper around that so you can selectively include and exclude fields without having to specify the entire validation group. In one of my projects last year I wrote an atrociously dirty hack to work around the issue, and it's on my someday agenda to extract and clean up that code so it can be released.

RE: form rendering, I think that's best left up to the consuming application. We'll bundle a minimum viable set of view scripts (like we do now) but if those don't fit your use case you'll need to override them. The approach ZfcUser currently uses (looping over every element in the form) is likely the behavior that will remain, though more than likely anyone adding custom fields would also be overriding the view scripts to control how they are displayed. ZfcUserAdmin's handling of this annoys me a bit as it includes the common form template rather than sending it through the view renderer, making it unnecessarily difficult to override.

RE: redirects, I'm not sure about this one. IMO there are better ways to handle the post-login redirect than sticking the target in a hidden form element. That's prone to abuse if not done properly, as we saw last year with the open redirect security issue. It's something I plan to give some thought to once the current round of PRs I've opened are wrapped up.

claytondaley commented 9 years ago

Obviously, there's no code to react to so the current state is the available point of reference


There are several points in your response on forms. Trying to separate them out:

claytondaley commented 9 years ago

Ran into another quirk worth adding to the punchlist. Custom Form Elements only work when you use two specific strategies:

To address an issue, I also (re)discovered: