Skip to content

Entity workflow

Small Forms is most useful as an explicit boundary between untrusted/external input and Entity Manager state.

Create an entity

Start with an entity constructed by its manager so relation metadata and the manager reference are valid:

$user = $userManager->newEntity();

$form = FormBuilder::createFromAttributes($user)
    ->fillFromArray($payload, $user);

The second fillFromArray() argument gives Small Forms the parent AbstractEntity when nested relation objects must be created.

Validate before hydration

use Small\Collection\Collection\StringCollection;

$messages = new StringCollection();
$form->validate($messages);

if ($messages->count() > 0) {
    // Return/translate validation errors.
    return;
}

By default validate() collects failures. Set the second argument to true to throw after collecting them:

$form->validate($messages, true);

The thrown exception is ValidationFailException; the caller-owned StringCollection still contains the field-level messages.

Hydrate and persist

$form->hydrate($user);
$user->persist();

Hydration writes matching form fields directly to reflected object properties, including non-public properties.

Core does not run this validation again during persistence. Database constraints and domain rules remain a separate integrity layer.

Input sources

The same form can consume several transport representations:

$form->fillFromArray($payload, $entity);
$form->fillFromJson($json);
$form->fillFromUrlEncodedString($body);
$form->fillFromObject($dto);

fillFromJson() first calls PHP json_validate() and throws InvalidInputDataException for invalid JSON.

Unknown keys in fillFromArray() are ignored:

$form->fillFromArray([
    'name' => 'Ada',
    'notWritable' => 'ignored',
]);

Unknown-key ignoring is not authorization

Build a form containing only the fields writable by that operation. Do not rely on ignored keys as the security boundary for mass assignment.

Create and update contracts

A single entity can have different input contracts depending on the operation. Attribute metadata is convenient for the common contract; explicit forms are better when create/update permissions differ substantially.

$patchForm = FormBuilder::createInlineForm()
    ->addField('displayName', new StringType(), [new ValidateNotEmpty()])
    ->addField('enabled', new BooleanType());

Then hydrate only the allowed fields into the loaded entity.

Add rules or modifiers at runtime

$form->getField('email')
    ->addRule(new ValidateEmail(canBeNull: false))
    ->addModifier(new TrimModifier());

This is also how to use validators/modifiers that are not declared as PHP attributes.

Reusable form classes

For operation-specific contracts, derive from AbstractForm:

final class CreateUserForm extends AbstractForm
{
    protected function build(): void
    {
        $this->addField(
            'email',
            new StringType(),
            [new ValidateNotEmpty(), new ValidateEmail()],
            [new TrimModifier()],
        );
    }
}

This avoids putting every application workflow rule on the persistent entity class.

Reading and serialization

$value = $form->getFieldValue('email'); // modifiers applied
$raw = $form->toArray();                // serialization of stored form state

toArray() has type-specific behavior for nested forms, dates and arrays. Review Behavior and caveats before using it as a generic normalized payload serializer.