Form factories vs my approach?

4 years ago

greeny
Member | 406
+
0
-

Hi,

I have recently come with this approach in my projects and I want just to start discussion whether it is good or not, and why (and if possible compare advantages and disadvantages to typical Form + Form Factory approach).

So here is my idea:

  1. I create an CompilerExtension for my project, with its own config.neon file, containing services part and forms part. Example:
services:
    - App\Model\Repositories\ArticleRepository
    - App\Model\Repositories\UserRepository

forms:
    - App\Forms\PublicModule\User\LoginForm
    - App\Forms\PublicModule\User\RegisterForm
  1. In my extension i just parse it like this:
$config = $this->loadFromFile(__DIR__ . '/config.neon');
$this->compiler->parseServices($builder, $config, $this->name);

if (isset($config['forms']) && is_array($config['forms'])) {
    foreach ($config['forms'] as $form) {
        $builder->addDefinition($this->prefix($this->fixFormServiceName($form)))
            ->setClass($form);
    }
}

// fixFormServiceName is a function, that just strips out of FQN name characters that cannot be used as service name

This might look like it does nothing more than standart service, but I just like to have it separated, also I can easily e.g. set translator for every form. Of course I can just leave forms in services and it will work normally.

  1. Every form extends some BaseForm, which looks like this:
abstract class BaseForm
{

    public function create()
    {
        $form = new Form; // this is Nette\Application\UI\Form or any custom child of it
        $this->initialize($form);

        $form->onValidate[] = [$this, 'validateData'];
        $form->onSuccess[] = [$this, 'formSuccess'];

        return $form;
    }


    abstract public function initialize(Form $form);
    abstract public function validateData(Form $form);
    abstract public function formSuccess(Form $form);

}
  1. And now for some concrete form:
class RegisterForm extends BaseForm
{

    /** @var UserRepository */
    private $userRepository;

    /** @var User */
    private $user;


    public function __construct(UserRepository $userRepository, User $user)
    {
        $this->userRepository = $userRepository;
        $this->user = $user;
    }

    public function initialize(Form $form)
    {
        $form->addText('nick', 'Nick')
            ->setRequired('Please enter nick.')
            ->addRule($form::PATTERN, 'Nick can contain only alphanumerical characters.', '[a-zA-Z0-9]+');

        $form->addPassword('password', 'Password')
            ->setRequired('Please enter password.');

        $form->addPassword('password_check', 'Password again')
            ->addRule($form::EQUAL, 'Passwords must match, please double-check them.', $form['password']);

        $form->addSubmit('register', 'Register');

    }


    public function validateData(Form $form)
    {
        $nickControl = $form['nick'];
        if ($this->userRepository->getByNick($nickControl->getValue())) {
            $nickControl->addError('User with this nick already exists.');
        }
    }


    public function formSuccess(Form $form)
    {
        $values = $form->getValues();
        $user = $this->userRepository->register($values->nick, $values->password);
        $this->user->login(new Identity($user->id, [$user->role], [
            'nick' => $user->nick,
        ]));
    }
}
  1. Finally, we use form in presenter:
/** @var RegisterForm @inject */
public $registerForm;

protected function createComponentRegisterForm()
{
    $form = $this->registerForm->create();
    $form->onSuccess[] = $this->registerFormSuccess;
    return $form;
}

public function registerFormSuccess(Form $form)
{
    $this->flashSuccess('Registration successful, you have been logged in.');
    $this->redirect('somehwere');
}

Okay, thats it. I know the RegisterForm used in example is not really a form, but a factory, and should be named RegisterFormFactory, but it can be easily changed and I have left it there from previous variations of my approach.

Advantages I see against normal Form + Form Factory approach:

  • you dont have to create two files, two classes, it is less writing
  • you can use “natively” form classes (in constructor, you require things you need and you just use them)
  • and this approach just fits me more than the Form + Form Factory

Now it is time to ask you: is this approach complete useless? Or its good enough to use, if it fits me? Or should I just keep the Form + Form Factory approach and forgot about inventing a wheel? Any comment is appreciated, as long as you provide some arguments why to use or not to use :)

Thanks, greeny

4 years ago

amik
Member | 124
+
0
-

Hi,
I just think that you actually implemented the Form + Form Factory approach. You have a service called SomethingForm, which (as you admit at the end) should actually be called SomethingFormFactory, and instance of UI\Form that is created from there. I don't see the difference from F+FF approach.

The advantages you see:

  • With F+FF, you also don't have to create two classes for each form – you can implement just the factory and use plain, unextended UI\Form.
  • You can also use native form factory classes with form factories (this is really confusing in your code that you called SomethingForm what is actually a factory; you also use plain UI\Form, what you a have special class for is the factory).
  • As I don't see the difference, I don't see how it could fit more.

For me it just seems that you added some magic (nette extension with config section and abstract methods in BaseForm) to the F+FF approach.

For organizing config files, I use separate config files, i.e. you can place your factories in “forms.neon”. F+FF solution to your case could look like:

# forms.neon
services:
    - App\PublicModule\Forms\User\RegisterFormFactory
    # I'd rather organize forms by module, than placing forms
    # of all modules into App\Forms namespace; for me namespaces
    # should reflect component hierarchy

The form:

class RegisterFormFactory
{

    /** @var UserRepository */
    private $userRepository;

    /** @var User */
    private $user;


    public function __construct(UserRepository $userRepository, User $user)
    {
        $this->userRepository = $userRepository;
        $this->user = $user;
    }

    public function create()
    {
        $form = new Form;
        $form->addText('nick', 'Nick')
            ->setRequired('Please enter nick.')
            ->addRule($form::PATTERN, 'Nick can contain only alphanumerical characters.', '[a-zA-Z0-9]+');
        # ... add other form controls and buttons

        $form->onValidate[] = function() use ($form) {
            $nickControl = $form['nick'];
            if ($this->userRepository->getByNick($nickControl->getValue())) {
                $nickControl->addError('User with this nick already exists.');
            }
        };
        $form->onSuccess[] = function() use ($form) {
            $values = $form->getValues();
            $user = $this->userRepository->register($values->nick, $values->password);
            $this->user->login(new Identity($user->id, [$user->role], ['nick' => $user->nick,]));
        };
    }
}

Presenter:

/** @var RegisterFormFactory @inject */
public $registerFormFactory;

protected function createComponentRegisterForm()
{
    $form = $this->registerFormFactory->create();
    $form->onSuccess[] = $this->registerFormSuccess;
    return $form;
}

public function registerFormSuccess(Form $form)
{
    $this->flashSuccess('Registration successful, you have been logged in.');
    $this->redirect('somehwere');
}

I think I just did the same, and without magic it looks more straightforward to me :)

3 years ago

greeny
Member | 406
+
0
-

Hi, thanks for response.

  1. I can actually use any kind of Form (Nette\Application\UI\Form is just an example there).
  2. In your example, its maybe less magic, but more writing (I know sometimes its bad, but even in presenter, you have predefined beforeRender method and you do not use $presenter->onBeforeRender[]).
  3. In your example, it will be hard to add something global for all forms (e.g. setting Translator or Twbs renderer), you will have to at least call something like setupTwbsRendering in each form.

Anyway, thanks for your opinion. The one big error in my approach (and yours too) is (as @fabik pointed to me), that you actually cannot create more of these forms on one page (say, add to basket form for each item on page).