| 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:
#[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:
#[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:
#[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