# Create Controller Form Actions

> Create the form page actions in the admin controller: create (add) and edit, plus any entity-specific actions. Uses FormBuilder/FormHandler pattern — never builds commands directly. Trigger: "create form actions for {Domain}", "create add/edit for {Domain}".

- Skill: `gabrielmoreira/create-controller-form-actions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/create-controller-form-actions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/create-controller-form-actions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/create-controller-form-actions

---


# create-controller-form-actions

Read `@.ai/Component/Controller/CONTEXT.md` for controller conventions (base class, DI, security attributes, error mapping).
Read `@.ai/Component/Forms/CONTEXT.md` for form patterns (FormBuilder, FormHandler, FormDataProvider, FormDataHandler).

## 1. Create action

`FormBuilderInterface` and `FormHandlerInterface` are injected as **action arguments** (preferred DI mode — see `Controller/CONTEXT.md`), each with an explicit `#[Autowire(service: '…')]` so the action picks up the domain-specific form services:

```php
#[AdminSecurity("is_granted('create', request.get('_legacy_controller'))")]
public function createAction(
    Request $request,
    #[Autowire(service: 'prestashop.core.form.identifiable_object.builder.{domain}_form_builder')]
    FormBuilderInterface ${domain}FormBuilder,
    #[Autowire(service: 'prestashop.core.form.identifiable_object.handler.{domain}_form_handler')]
    FormHandlerInterface ${domain}FormHandler,
): Response
```

- GET: build form via `FormBuilderInterface::getForm()`, render form template
- POST: handle request via `FormHandlerInterface::handle()` — it internally calls `FormDataHandler::create()` which dispatches the Add command
- Never instantiate commands directly in the controller
- On success: flash message + redirect to index or edit page (return the new entity ID via `getIdentifiableObjectId()`)
- On failure: re-render form with errors

**Reference:** `TaxController::createAction()`, `ManufacturerController::createAction()`

## 2. Edit action

Same injection mode — `FormBuilderInterface` and `FormHandlerInterface` as action arguments:

```php
#[AdminSecurity("is_granted('update', request.get('_legacy_controller'))")]
public function editAction(
    Request $request,
    int ${domain}Id,
    #[Autowire(service: 'prestashop.core.form.identifiable_object.builder.{domain}_form_builder')]
    FormBuilderInterface ${domain}FormBuilder,
    #[Autowire(service: 'prestashop.core.form.identifiable_object.handler.{domain}_form_handler')]
    FormHandlerInterface ${domain}FormHandler,
): Response
```

- GET: `${domain}FormBuilder->getFormFor(${domain}Id)` — `FormDataProvider::getData($id)` is called internally to pre-fill the form
- POST: `${domain}FormHandler->handle($form)` — calls `FormDataHandler::update()` which dispatches Edit command
- Catch `{Domain}NotFoundException` → error flash, redirect to index (entity may have been deleted concurrently)

**Reference:** `TaxController::editAction()`, `ManufacturerController::editAction()`

## 3. Entity-specific actions

Some entities require additional actions beyond create/edit:

- **Image deletion:** `deleteCoverImageAction(int $id)`
- **Export:** `exportAction(Request $request)` — generates CSV/PDF
- **View page:** `viewAction(int $id)` — read-only detail page (e.g. Customer, Order)
- **Position update:** `updatePositionAction(Request $request)` — handled by `PositionDefinition`

For these single-purpose actions, **skip FormBuilder/FormHandler** — they exist to bridge a Symfony form lifecycle to a CQRS command, which is overkill for an action that just dispatches one command or query. Inject the bus (or specific handler) and dispatch directly:

```php
#[AdminSecurity("is_granted('delete', request.get('_legacy_controller'))")]
public function deleteCoverImageAction(int ${domain}Id): RedirectResponse
{
    try {
        $this->dispatchCommand(new DeleteCoverImageCommand(${domain}Id));
        $this->addFlash('success', /* … */);
    } catch ({Domain}NotFoundException $e) {
        $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages()));
    }

    return $this->redirectToRoute('admin_{domain}s_edit', ['{domain}Id' => ${domain}Id]);
}
```

The FormBuilder/FormHandler pair is required for the create/edit form lifecycle — not for every controller action.

## Rules

- See `@.ai/Component/Controller/CONTEXT.md` for all controller conventions (DI order, security attributes, error mapping)
- Create/edit go through FormBuilder/FormHandler — never build the create/edit commands manually
- Single-purpose actions (toggle, delete sub-resource, export, view) dispatch commands/queries directly
- FormDataProvider transforms query results into form-ready data, not the controller
- Catch NotFoundException on edit — the entity may have been deleted between page load and save

