Programmatic Form Submission
When to Use
Use programmatic submission for batch operations, migrations, automated testing, and cron jobs. Never for user-submitted forms.
Decision
| Situation | Use Programmatic | Why |
|---|---|---|
| Batch operations | Yes | Process items programmatically |
| Migration/import scripts | Yes | Automated data processing |
| Automated testing | Yes | PHPUnit, kernel tests |
| Cron jobs | Yes | Scheduled operations |
| Drush commands | Yes | CLI automation |
| User-submitted forms | No | Security risk |
| Forms requiring user interaction | No | Use normal form flow |
Security Implications
Bypasses: - CSRF token validation (skipped) - Access checks (can bypass with setProgrammedBypassAccessCheck) - Normal user flow (no render, no UI validation)
Use only in trusted contexts: CLI, Cron, Admin batch, Migration, Automated tests
Pattern
use Drupal\Core\Form\FormState;
$form_state = new FormState();
$form_state->setValues([
'field_name' => 'value',
'another_field' => 'another value',
]);
$form_state->setProgrammed(TRUE); // REQUIRED
$form_builder = \Drupal::formBuilder();
$form_builder->submitForm('Drupal\mymodule\Form\MyForm', $form_state);
// Check for errors
if ($form_state->hasAnyErrors()) {
$errors = $form_state->getErrors();
// Handle errors
} else {
// Success
}
With Dependency Injection
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
class MyService {
protected $formBuilder;
public function __construct(FormBuilderInterface $form_builder) {
$this->formBuilder = $form_builder;
}
public function submitForm($data) {
$form_state = new FormState();
$form_state->setValues($data);
$form_state->setProgrammed(TRUE);
$this->formBuilder->submitForm('Drupal\mymodule\Form\MyForm', $form_state);
return !$form_state->hasAnyErrors();
}
}
Batch Integration
public function submitForm(array &$form, FormStateInterface $form_state) {
$items = $this->getItemsToProcess();
$operations = [];
foreach ($items as $item) {
$operations[] = [[$this, 'batchProcessItem'], [$item]];
}
$batch = [
'title' => $this->t('Processing items...'),
'operations' => $operations,
'finished' => [$this, 'batchFinished'],
];
batch_set($batch);
}
public function batchProcessItem($item, &$context) {
$form_state = new FormState();
$form_state->setValues(['item_id' => $item['id']]);
$form_state->setProgrammed(TRUE);
\Drupal::formBuilder()->submitForm('Drupal\mymodule\Form\ProcessForm', $form_state);
$context['results'][] = $item['id'];
}
Testing Pattern
namespace Drupal\Tests\mymodule\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Core\Form\FormState;
class MyFormTest extends KernelTestBase {
public function testFormSubmission() {
$form_state = new FormState();
$form_state->setValues(['field' => 'value']);
$form_state->setProgrammed(TRUE);
$form_builder = $this->container->get('form_builder');
$form_builder->submitForm('Drupal\mymodule\Form\MyForm', $form_state);
$this->assertEmpty($form_state->getErrors());
}
}
Common Mistakes
- Wrong: No setProgrammed(TRUE) → Right: Required or token validation fails
- Wrong: Using raw form array → Right: Use class name/object
- Wrong: Bypassing access inappropriately → Right: Only for trusted operations
- Wrong: User-submitted data → Right: Only for automated/trusted contexts
See Also
- Batch API Guide
- Testing Forms
- FormBuilder Service
- Reference:
/web/core/lib/Drupal/Core/Form/form.api.phplines 56-95