Skip to content

Programmatic Form Submission

When to Use

Use programmatic submission for batch operations, migrations, automated testing, and cron jobs. Never for user-submitted forms.

Appropriate Use Cases:

  • Batch operations processing items
  • Migration/import scripts
  • Automated testing (programmatic, not browser)
  • Cron jobs creating/updating content
  • Drush commands
  • Admin operations (bulk updates)

When NOT to Use:

  • User-submitted forms (security risk)
  • Forms requiring user interaction
  • When form validation should block
  • Public-facing operations

Reference: 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 (Drush, Console)
  • Cron operations
  • Admin-initiated batch operations
  • Migration processes
  • Automated tests

Pattern: Basic Pattern

Simple Submission:

use Drupal\Core\Form\FormState;

$form_state = new FormState();
$form_state->setValues([
  'field_name' => 'value',
  'another_field' => 'another value',
]);
$form_state->setProgrammed(TRUE);

$form_builder = \Drupal::formBuilder();
$form_builder->submitForm('Drupal\mymodule\Form\MyForm', $form_state);

With Dependency Injection:

use Drupal\Core\Form\FormBuilderInterface;

class MyService {
  protected $formBuilder;

  public function __construct(FormBuilderInterface $form_builder) {
    $this->formBuilder = $form_builder;
  }

  public function submitFormProgrammatically($data) {
    $form_state = new FormState();
    $form_state->setValues($data);
    $form_state->setProgrammed(TRUE);

    $this->formBuilder->submitForm('Drupal\mymodule\Form\MyForm', $form_state);
  }
}

Pattern: Advanced Patterns

Bypass Access Checks:

$form_state->setProgrammedBypassAccessCheck(TRUE); // Use with caution

Check for Errors:

$form_builder->submitForm('Drupal\mymodule\Form\MyForm', $form_state);

if ($form_state->hasAnyErrors()) {
  $errors = $form_state->getErrors();
  // Handle errors
}
else {
  // Success
}

Get Return Value:

$result = $form_builder->submitForm('Drupal\mymodule\Form\MyForm', $form_state);
// $result = form render array (usually not useful for programmatic)

Nested Values:

$form_state->setValues([
  'container' => [
    'field1' => 'value1',
    'field2' => 'value2',
  ],
]);

Pattern: Batch Integration

Form Submission in Batch:

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) {
  // Programmatically submit form for each item
  $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'];
  $context['message'] = $this->t('Processed @id', ['@id' => $item['id']]);
}

Reference:

Pattern: Testing Pattern

PHPUnit Kernel Test:

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

Not Setting Programmed Flag:

// WRONG - will fail token validation
$form_state->setValues([...]);
\Drupal::formBuilder()->submitForm(...);

// CORRECT
$form_state->setProgrammed(TRUE);

Using Raw Form Array:

// WRONG
$form = \Drupal::formBuilder()->getForm('Drupal\mymodule\Form\MyForm');
// Can't submit $form array

// CORRECT - use class name/object
\Drupal::formBuilder()->submitForm('Drupal\mymodule\Form\MyForm', $form_state);

Bypassing Access Inappropriately:

// DANGEROUS - only for trusted admin operations
$form_state->setProgrammedBypassAccessCheck(TRUE);

See Also