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}".
Installs into .claude/skills of the current project.
Are you the author of Create Controller Form Actions?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jeffsenso-create-controller-form-actions)
---
name: create-controller-form-actions
description: >
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}".
needs: [create-cqrs-commands, create-cqrs-queries, create-form-type, create-form-data-handling]
produces: "createAction, editAction, and entity-specific actions in {Domain}Controller.php"
---
# 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