Formhelper cleanup - markstory/cakephp GitHub Wiki

Note: this page is incomplete.

FormHelper suffers from many of the same problems that PaginatorHelper does:

  • Option bloat makes testing and documentation more complex.
  • Features like multiple text boxes are mixed in with un-related code.
  • God class - FormHelper is an enormous class with an enormous test suite.
  • Highly coupled to Model

Option bloat

Options should be reduced to the minimal set of options. Options that provide class and arbitrary attribute control would be removed and replaced with string templates, much like it was in PaginatorHelper.

Options applied to the form control elements would continue to be required. The $attributes option of each input type would allow arbitrary HTML attributes which would be composed together as they are today. This allows string templates to be used for larger markup sections without impacting flexibility required for individual inputs. Other special options would also be supported.

  • _secure (for integration with SecurityComponent)

Options for generating divs, their classes would no longer be available. Any formatting HTML would need to be handled through string templates.

God class

FormHelper could solve many of its god class problems by being decomposed into a set of classes that can handle the logic for each of the complex input types. Simple types like text, textarea, hidden, file and many HTML5 inputs could share a simple type. More complex input types likes datetime pickers, multiple checkboxes, radios, checkbox sets would be provided by separate objects. These objects would not be directly exposed to the end developer. They would be used by FormHelper to help simplify the internal implementation, allow user-land extensions, or allow developer to augment/replace existing complex types.

Simple types

A single simple type class would handle the simple types that are just a single HTML element. For example, the following types are easily handled by the same block of code.

  • text
  • hidden
  • date
  • tel
  • url
  • number
  • email

The simple type would also act as a catch basin for any unknown types.

Complex widgets

Complex widget would use one class per complex input a list of existing complex widgets are:

  • datetime
  • date
  • time
  • checkbox
  • multi-checkbox
  • radio
  • select
  • button
  • file (Only because of the custom logic required for secured form handling.)

These classes would have a common interface that allows FormHelper to use them. Each complex widget would have the following methods:

  • __construct(StringTemplate $templates, FieldContext $context) - Constructor takes the template set and context which allows access to the field metadata and fields participating in form tampering prevention.
  • render() string - Get the HTML/rendered content for the type.
  • set(array) - Set the data/options for the type. Each widget would implement the validation/logic required for the options they need. For example radio would support the 'options' and 'value' keys.

Extensible widgets

By extracting the core widgets into separate classes we can easily allow FormHelper to be extensible to custom userland widgets and allow the core widgets to be replaced as needed.

<?php
// Add widgets at runtime. Either a classname or closure factory.
$this->Form->addWidget('myinput', 'App\View\Helper\Form\MyInput');
$this->Form->addWidget('myinput', function ($templates, $context) {
  return new App\View\Helper\Form\MyInput($templates, $context);
});

Custom widgets could also be injected into the FormHelper at construction (allowing definition in $helpers)

<?php
$helpers = ['Form' => [
  'widgets' => ['myinput' => 'App\View\Helper\Form\MyInput']
]];

The settings option would also accept closure factories.

Tightly coupled to Model/Entity

Presently FormHelper uses ClassRegistry to access Model objects and then digs through the object properties looking for things it is interested in. Ideally FormHelper would be able to accept either Entity classes, or an array of metadata to allow use independent of the internal Entity class. While Entity will be an integral part of CakePHP supporting array metadata allows simpler testing.

<?php
$this->Form->create($article);
$this->Form->create($metadata);

When an Entity or object with matching methods is provided, the following pseudo code would run:

<?php
if ($entity instanceof Entity) {
  $metadata = [
    'schema' => $entity->table()->schema(),
    'required' => $entity->validator()->requiredFields();
    'validationErrors' => $entity->validator()->errors(),
    'values' => array_merge($entity->attributes(), $this->request->data()),
  ];
}

FormHelper usage

FormHelper would continue to provide the same easy to use interface as it always had with a few small adjustments. An example simple form would look like:

<?php
// $article is an Entity and its related associations.
echo $this->Form->create($article);

// Still creates the div, label, input, and validation errors.
// All the HTML comes from templates and the type classes now.
echo $this->Form->input('title');

// Core complex types would have built-in methods.
echo $this->Form->date('created');

// Select/Multiple select - now requires options parameter
echo $this->Form->input('Tag', ['options' => $tags]);

// Custom types could be invoked in just the widget,
// Or full input forms.
// Field names without dots would have the current model scope appended.
echo $this->Form->widget('custom', 'title');
echo $this->Form->input('title', ['type' => 'custom']);

// Example of multi record forms
// Any field names with '.' in them would be output as provided.
echo $this->Form->create($articles);
echo $this->Form->input('0.title');
echo $this->Form->input('1.title');

// Example of associated forms
echo $this->Form->create($article);
echo $this->Form->input('title');
echo $this->Form->input('Comment.0.body');
echo $this->Form->input('Comment.0.published');

While many form inputs will be created the same as before, some types will differ:

<?php
// Select/multi checkbox/radio - now require options parameter
echo $this->Form->input('Tag', ['options' => $tags]);

// Multiple checkboxes now have a specific type
$this->Form->input('Tag', [
  'type' => 'multiplecheckbox'
  'options' => $tags
]);

// No longer generates a button
// Use button() or submit() to make buttons.
$this->Form->end();

Field entity handling

In previous versions of CakePHP handling field names was a reasonably complex operation that involved maintaining multiple pieces of state and then inferring what the developer wanted based on what they asked for. This has historically been complicated and bug prone code. For 3.0, I would like to greatly simplify this process with two main goals:

  1. Simpler for the end developer. Simpler logic will mean less confusing scenarios for developers.
  2. More efficient code that is easier to maintain. Fewer complex rules means the code is easier to maintain and less likely to harbour future issues.

Field paths should be simplified to the following rules:

  • Using the create() method starts a model scope.
  • Fields created that do not contain a . will have the model scope appended. For example input('title') will result in name="Article[title]" in the resulting HTML.
  • Fields containing a dot will not be modified. This means that creating fields like input('User.name') will result in name="User[name]".

Datetime fields have historically required special casing. These special cases will be handled inside the datetime widget.