diff --git a/core/core.services.yml b/core/core.services.yml
index 5b0de75..aae6d5f 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -359,7 +359,7 @@ services:
     arguments: ['@stream_wrapper_manager', '@settings', '@logger.channel.file']
   form_builder:
     class: Drupal\Core\Form\FormBuilder
-    arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@theme.manager', '@?csrf_token']
+    arguments: ['@form_validator', '@form_submitter', '@form_cache', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@?csrf_token']
   form_validator:
     class: Drupal\Core\Form\FormValidator
     arguments: ['@request_stack', '@string_translation', '@csrf_token', '@logger.channel.form', '@form_error_handler']
@@ -372,6 +372,12 @@ services:
     class: Drupal\Core\Form\FormCache
     arguments: ['@app.root', '@keyvalue.expirable', '@module_handler', '@current_user', '@csrf_token', '@logger.channel.form', '@request_stack', '@page_cache_request_policy']
     public: false  # Private to form_builder
+  form_builder.alter:
+    class: Drupal\Core\Form\FormAlter
+    arguments: ['@module_handler', '@theme.manager']
+    public: false  # Private to form_builder
+    tags:
+      - { name: event_subscriber }
   keyvalue:
     class: Drupal\Core\KeyValueStore\KeyValueFactory
     arguments: ['@service_container', '%factory.keyvalue%']
diff --git a/core/lib/Drupal/Core/Form/FormAlter.php b/core/lib/Drupal/Core/Form/FormAlter.php
new file mode 100644
index 0000000..7010c6b
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAlter.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Form\FormAlter.
+ */
+
+namespace Drupal\Core\Form;
+
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Theme\ThemeManagerInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Allows the module handler and theme manager to alter forms.
+ */
+class FormAlter implements EventSubscriberInterface {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface
+   */
+  protected $themeManager;
+
+  /**
+   * Base event name for altering any form.
+   *
+   * @var string
+   */
+  const ANY = 'form_alter';
+
+  /**
+   * Constructs a new FormAlter.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
+   *   The theme manager.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
+    $this->moduleHandler = $module_handler;
+    $this->themeManager = $theme_manager;
+  }
+
+  /**
+   * Returns the event name for altering a specific form ID.
+   *
+   * @param string $form_id
+   *   The form ID.
+   *
+   * @return string
+   *   The event name.
+   */
+  public static function id($form_id) {
+    return static::ANY . ':' . $form_id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterForm(FormAlterEvent $event) {
+    $form_id = $event->getFormId();
+    $form = &$event->getForm();
+    $form_state = $event->getFormState();
+
+    // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
+    // hook_form_FORM_ID_alter() implementations.
+    $hooks = ['form'];
+    $build_info = $form_state->getBuildInfo();
+    if (isset($build_info['base_form_id'])) {
+      $hooks[] = 'form_' . $build_info['base_form_id'];
+    }
+    $hooks[] = 'form_' . $form_id;
+    $this->moduleHandler->alter($hooks, $form, $form_state, $form_id);
+    $this->themeManager->alter($hooks, $form, $form_state, $form_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      static::ANY => 'alterForm',
+    ];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormAlterEvent.php b/core/lib/Drupal/Core/Form/FormAlterEvent.php
new file mode 100644
index 0000000..bae20ba
--- /dev/null
+++ b/core/lib/Drupal/Core/Form/FormAlterEvent.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Core\Form;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event fired to facilitate extensions altering forms as they are built.
+ */
+class FormAlterEvent extends Event {
+
+  /**
+   * The ID of the form being built.
+   *
+   * @var string
+   */
+  protected $formId;
+
+  /**
+   * The form structure.
+   *
+   * @var array
+   */
+  protected $form;
+
+  /**
+   * The current form state.
+   *
+   * @var \Drupal\Core\Form\FormStateInterface
+   */
+  protected $formState;
+
+  /**
+   * FormAlterEvent constructor.
+   *
+   * @param string $form_id
+   *   The ID of the form being built.
+   * @param array $form
+   *   The form structure.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   */
+  public function __construct($form_id, array &$form, FormStateInterface $form_state) {
+    $this->formId = $form_id;
+    $this->form = $form;
+    $this->formState = $form_state;
+  }
+
+  /**
+   * Gets the ID of the form being built.
+   *
+   * @return string
+   */
+  public function getFormId() {
+    return $this->formId;
+  }
+
+  /**
+   * Gets the form structure.
+   *
+   * @return array
+   */
+  public function &getForm() {
+    return $this->form;
+  }
+
+  /**
+   * Gets the current form state.
+   *
+   * @return \Drupal\Core\Form\FormStateInterface
+   */
+  public function getFormState() {
+    return $this->formState;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 878967b..966a4d5 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -10,11 +10,9 @@
 use Drupal\Core\Access\CsrfTokenGenerator;
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
-use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\Exception\BrokenPostRequestException;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Render\ElementInfoManagerInterface;
-use Drupal\Core\Theme\ThemeManagerInterface;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\FileBag;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -28,13 +26,6 @@
 class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface {
 
   /**
-   * The module handler.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
    * The event dispatcher.
    *
    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
@@ -77,13 +68,6 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
   protected $currentUser;
 
   /**
-   * The theme manager.
-   *
-   * @var \Drupal\Core\Theme\ThemeManagerInterface
-   */
-  protected $themeManager;
-
-  /**
    * @var \Drupal\Core\Form\FormValidatorInterface
    */
   protected $formValidator;
@@ -148,8 +132,6 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
    *   The form submission processor.
    * @param \Drupal\Core\Form\FormCacheInterface $form_cache
    *   The form cache.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
    *   The event dispatcher.
    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
@@ -158,22 +140,18 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS
    *   The class resolver.
    * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
    *   The element info manager.
-   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
-   *   The theme manager.
    * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
    *   The CSRF token generator.
    */
-  public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, ModuleHandlerInterface $module_handler, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ElementInfoManagerInterface $element_info, ThemeManagerInterface $theme_manager, CsrfTokenGenerator $csrf_token = NULL) {
+  public function __construct(FormValidatorInterface $form_validator, FormSubmitterInterface $form_submitter, FormCacheInterface $form_cache, EventDispatcherInterface $event_dispatcher, RequestStack $request_stack, ClassResolverInterface $class_resolver, ElementInfoManagerInterface $element_info, CsrfTokenGenerator $csrf_token = NULL) {
     $this->formValidator = $form_validator;
     $this->formSubmitter = $form_submitter;
     $this->formCache = $form_cache;
-    $this->moduleHandler = $module_handler;
     $this->eventDispatcher = $event_dispatcher;
     $this->requestStack = $request_stack;
     $this->classResolver = $class_resolver;
     $this->elementInfo = $element_info;
     $this->csrfToken = $csrf_token;
-    $this->themeManager = $theme_manager;
   }
 
   /**
@@ -807,16 +785,13 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
         $form['#theme'][] = $build_info['base_form_id'];
       }
     }
+    $event = new FormAlterEvent($form_id, $form, $form_state);
+    $this->eventDispatcher->dispatch(FormAlter::ANY, $event);
 
-    // Invoke hook_form_alter(), hook_form_BASE_FORM_ID_alter(), and
-    // hook_form_FORM_ID_alter() implementations.
-    $hooks = array('form');
     if (isset($build_info['base_form_id'])) {
-      $hooks[] = 'form_' . $build_info['base_form_id'];
+      $this->eventDispatcher->dispatch(FormAlter::id($build_info['base_form_id']), $event);
     }
-    $hooks[] = 'form_' . $form_id;
-    $this->moduleHandler->alter($hooks, $form, $form_state, $form_id);
-    $this->themeManager->alter($hooks, $form, $form_state, $form_id);
+    $this->eventDispatcher->dispatch(FormAlter::id($form_id), $event);
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Form/FormAlterTest.php b/core/tests/Drupal/Tests/Core/Form/FormAlterTest.php
new file mode 100644
index 0000000..9f19404
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Form/FormAlterTest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Form\FormAlterTest.
+ */
+
+namespace Drupal\Tests\Core\Form;
+
+use Drupal\Core\Form\FormAlter;
+use Drupal\Core\Form\FormAlterEvent;
+use Drupal\Core\Form\FormState;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Form\FormAlter
+ * @group Form
+ */
+class FormAlterTest extends UnitTestCase {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $moduleHandler;
+
+  /**
+   * The theme manager.
+   *
+   * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $themeManager;
+
+  /**
+   * The form alter service.
+   *
+   * @var \Drupal\Core\Form\FormAlter
+   */
+  protected $formAlter;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
+    $this->formAlter = new FormAlter($this->moduleHandler, $this->themeManager);
+  }
+
+  /**
+   * @covers ::alterForm
+   *
+   * @dataProvider providerTestAlterForm
+   */
+  public function testAlterForm($form_id, $expected_hooks, $form_state_additions = []) {
+    $form = [];
+    $form_state = (new FormState())->setFormState($form_state_additions);
+
+    $this->moduleHandler->expects($this->once())
+      ->method('alter')
+      ->with($expected_hooks, $form, $form_state, $form_id);
+    $this->themeManager->expects($this->once())
+      ->method('alter')
+      ->with($expected_hooks, $form, $form_state, $form_id);
+
+    $event = new FormAlterEvent($form_id, $form, $form_state);
+    $this->formAlter->alterForm($event);
+  }
+
+  /**
+   * Provides test data for testAlterForm().
+   */
+  public function providerTestAlterForm() {
+    $data = [];
+    $data['no_build_info'] = [
+      'red',
+      [
+        'form',
+        'form_red',
+      ],
+    ];
+    $data['with_build_info'] = [
+      'red',
+      [
+        'form',
+        'form_blue',
+        'form_red',
+      ],
+      [
+        'build_info' => [
+          'base_form_id' => 'blue',
+        ],
+      ],
+    ];
+    return $data;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index dfbf843..43cd350 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -555,7 +555,7 @@ public function testExceededFileSize() {
     $request_stack = new RequestStack();
     $request_stack->push($request);
     $this->formBuilder = $this->getMockBuilder('\Drupal\Core\Form\FormBuilder')
-      ->setConstructorArgs([$this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $request_stack, $this->classResolver, $this->elementInfo, $this->themeManager, $this->csrfToken])
+      ->setConstructorArgs([$this->formValidator, $this->formSubmitter, $this->formCache, $this->eventDispatcher, $request_stack, $this->classResolver, $this->elementInfo, $this->csrfToken])
       ->setMethods(['getFileUploadMaxSize'])
       ->getMock();
     $this->formBuilder->expects($this->once())
diff --git a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
index 078d30e..a518f70 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormTestBase.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\Core\Form;
 
 use Drupal\Component\Utility\Html;
+use Drupal\Core\Form\FormAlter;
 use Drupal\Core\Form\FormBuilder;
 use Drupal\Core\Form\FormInterface;
 use Drupal\Core\Form\FormState;
@@ -43,13 +44,6 @@
   protected $urlGenerator;
 
   /**
-   * The mocked module handler.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $moduleHandler;
-
-  /**
    * The form cache.
    *
    * @var \Drupal\Core\Form\FormCacheInterface|\PHPUnit_Framework_MockObject_MockObject
@@ -57,11 +51,11 @@
   protected $formCache;
 
   /**
-   * The cache backend to use.
+   * The form alter service.
    *
-   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
+   * @var \Drupal\Core\Form\FormAlter|\PHPUnit_Framework_MockObject_MockObject
    */
-  protected $cache;
+  protected $formAlter;
 
   /**
    * The current user.
@@ -134,12 +128,6 @@
    */
   protected $logger;
 
-  /**
-   * The mocked theme manager.
-   *
-   * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
-   */
-  protected $themeManager;
 
   /**
    * {@inheritdoc}
@@ -150,12 +138,9 @@ protected function setUp() {
     // Add functions to the global namespace for testing.
     require_once __DIR__ . '/fixtures/form_base_test.inc';
 
-    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
-
     $this->formCache = $this->getMock('Drupal\Core\Form\FormCacheInterface');
-    $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
     $this->urlGenerator = $this->getMock('Drupal\Core\Routing\UrlGeneratorInterface');
-
+    $this->formAlter = $this->prophesize(FormAlter::class)->reveal();
     $this->classResolver = $this->getClassResolverStub();
 
     $this->elementInfo = $this->getMockBuilder('\Drupal\Core\Render\ElementInfoManagerInterface')
@@ -172,7 +157,6 @@ protected function setUp() {
       ->disableOriginalConstructor()
       ->getMock();
     $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
-    $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
     $this->request = new Request();
     $this->eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
     $this->requestStack = new RequestStack();
@@ -189,7 +173,7 @@ protected function setUp() {
       ->getMock();
     $this->root = dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))));
 
-    $this->formBuilder = new FormBuilder($this->formValidator, $this->formSubmitter, $this->formCache, $this->moduleHandler, $this->eventDispatcher, $this->requestStack, $this->classResolver, $this->elementInfo, $this->themeManager, $this->csrfToken);
+    $this->formBuilder = new FormBuilder($this->formValidator, $this->formSubmitter, $this->formCache, $this->eventDispatcher, $this->requestStack, $this->classResolver, $this->elementInfo, $this->csrfToken);
   }
 
   /**
