3 namespace Drupal\Core\Entity;
5 use Drupal\Core\Form\FormBase;
6 use Drupal\Core\Extension\ModuleHandlerInterface;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\Core\Render\Element;
9 use Drupal\Core\Routing\RouteMatchInterface;
12 * Base class for entity forms.
16 class EntityForm extends FormBase implements EntityFormInterface {
19 * The name of the current operation.
21 * Subclasses may use this to implement different behaviors depending on its
29 * The module handler service.
31 * @var \Drupal\Core\Extension\ModuleHandlerInterface
33 protected $moduleHandler;
38 * @var \Drupal\Core\Entity\EntityManagerInterface
40 * @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
42 * @see https://www.drupal.org/node/2549139
44 protected $entityManager;
47 * The entity type manager.
49 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
51 protected $entityTypeManager;
54 * The entity being used by this form.
56 * @var \Drupal\Core\Entity\EntityInterface
63 public function setOperation($operation) {
64 // If NULL is passed, do not overwrite the operation.
66 $this->operation = $operation;
74 public function getBaseFormId() {
75 // Assign ENTITYTYPE_form as base form ID to invoke corresponding
76 // hook_form_alter(), #validate, #submit, and #theme callbacks, but only if
77 // it is different from the actual form ID, since callbacks would be invoked
79 $base_form_id = $this->entity->getEntityTypeId() . '_form';
80 if ($base_form_id == $this->getFormId()) {
89 public function getFormId() {
90 $form_id = $this->entity->getEntityTypeId();
91 if ($this->entity->getEntityType()->hasKey('bundle')) {
92 $form_id .= '_' . $this->entity->bundle();
94 if ($this->operation != 'default') {
95 $form_id = $form_id . '_' . $this->operation;
97 return $form_id . '_form';
103 public function buildForm(array $form, FormStateInterface $form_state) {
104 // During the initial form build, add this form object to the form state and
105 // allow for initial preparation before form building and processing.
106 if (!$form_state->has('entity_form_initialized')) {
107 $this->init($form_state);
110 // Ensure that edit forms have the correct cacheability metadata so they can
112 if (!$this->entity->isNew()) {
113 \Drupal::service('renderer')->addCacheableDependency($form, $this->entity);
116 // Retrieve the form array using the possibly updated entity in form state.
117 $form = $this->form($form, $form_state);
119 // Retrieve and add the form actions array.
120 $actions = $this->actionsElement($form, $form_state);
121 if (!empty($actions)) {
122 $form['actions'] = $actions;
129 * Initialize the form state and the entity before the first form build.
131 protected function init(FormStateInterface $form_state) {
132 // Flag that this form has been initialized.
133 $form_state->set('entity_form_initialized', TRUE);
135 // Prepare the entity to be presented in the entity form.
136 $this->prepareEntity();
138 // Invoke the prepare form hooks.
139 $this->prepareInvokeAll('entity_prepare_form', $form_state);
140 $this->prepareInvokeAll($this->entity->getEntityTypeId() . '_prepare_form', $form_state);
144 * Gets the actual form array to be built.
146 * @see \Drupal\Core\Entity\EntityForm::processForm()
147 * @see \Drupal\Core\Entity\EntityForm::afterBuild()
149 public function form(array $form, FormStateInterface $form_state) {
150 // Add #process and #after_build callbacks.
151 $form['#process'][] = '::processForm';
152 $form['#after_build'][] = '::afterBuild';
158 * Process callback: assigns weights and hides extra fields.
160 * @see \Drupal\Core\Entity\EntityForm::form()
162 public function processForm($element, FormStateInterface $form_state, $form) {
163 // If the form is cached, process callbacks may not have a valid reference
164 // to the entity object, hence we must restore it.
165 $this->entity = $form_state->getFormObject()->getEntity();
171 * Form element #after_build callback: Updates the entity with submitted data.
173 * Updates the internal $this->entity object with submitted values when the
174 * form is being rebuilt (e.g. submitted via AJAX), so that subsequent
175 * processing (e.g. AJAX callbacks) can rely on it.
177 public function afterBuild(array $element, FormStateInterface $form_state) {
178 // Rebuild the entity if #after_build is being called as part of a form
179 // rebuild, i.e. if we are processing input.
180 if ($form_state->isProcessingInput()) {
181 $this->entity = $this->buildEntity($element, $form_state);
188 * Returns the action form element for the current entity form.
190 protected function actionsElement(array $form, FormStateInterface $form_state) {
191 $element = $this->actions($form, $form_state);
193 if (isset($element['delete'])) {
194 // Move the delete action as last one, unless weights are explicitly
196 $delete = $element['delete'];
197 unset($element['delete']);
198 $element['delete'] = $delete;
199 $element['delete']['#button_type'] = 'danger';
202 if (isset($element['submit'])) {
203 // Give the primary submit button a #button_type of primary.
204 $element['submit']['#button_type'] = 'primary';
208 foreach (Element::children($element) as $action) {
209 $element[$action] += [
210 '#weight' => ++$count * 5,
214 if (!empty($element)) {
215 $element['#type'] = 'actions';
222 * Returns an array of supported actions for the current entity form.
224 * @todo Consider introducing a 'preview' action here, since it is used by
227 protected function actions(array $form, FormStateInterface $form_state) {
228 // @todo Consider renaming the action key from submit to save. The impacts
229 // are hard to predict. For example, see
230 // \Drupal\language\Element\LanguageConfiguration::processLanguageConfiguration().
231 $actions['submit'] = [
233 '#value' => $this->t('Save'),
234 '#submit' => ['::submitForm', '::save'],
237 if (!$this->entity->isNew() && $this->entity->hasLinkTemplate('delete-form')) {
238 $route_info = $this->entity->urlInfo('delete-form');
239 if ($this->getRequest()->query->has('destination')) {
240 $query = $route_info->getOption('query');
241 $query['destination'] = $this->getRequest()->query->get('destination');
242 $route_info->setOption('query', $query);
244 $actions['delete'] = [
246 '#title' => $this->t('Delete'),
247 '#access' => $this->entity->access('delete'),
249 'class' => ['button', 'button--danger'],
252 $actions['delete']['#url'] = $route_info;
261 * This is the default entity object builder function. It is called before any
262 * other submit handler to build the new entity object to be used by the
263 * following submit handlers. At this point of the form workflow the entity is
264 * validated and the form state can be updated, this way the subsequently
265 * invoked handlers can retrieve a regular entity object to act on. Generally
266 * this method should not be overridden unless the entity requires the same
267 * preparation for two actions, see \Drupal\comment\CommentForm for an example
268 * with the save and preview actions.
271 * An associative array containing the structure of the form.
272 * @param \Drupal\Core\Form\FormStateInterface $form_state
273 * The current state of the form.
275 public function submitForm(array &$form, FormStateInterface $form_state) {
276 // Remove button and internal Form API values from submitted values.
277 $form_state->cleanValues();
278 $this->entity = $this->buildEntity($form, $form_state);
284 public function save(array $form, FormStateInterface $form_state) {
285 return $this->entity->save();
291 public function buildEntity(array $form, FormStateInterface $form_state) {
292 $entity = clone $this->entity;
293 $this->copyFormValuesToEntity($entity, $form, $form_state);
295 // Invoke all specified builders for copying form values to entity
297 if (isset($form['#entity_builders'])) {
298 foreach ($form['#entity_builders'] as $function) {
299 call_user_func_array($form_state->prepareCallback($function), [$entity->getEntityTypeId(), $entity, &$form, &$form_state]);
307 * Copies top-level form values to entity properties
309 * This should not change existing entity properties that are not being edited
312 * @param \Drupal\Core\Entity\EntityInterface $entity
313 * The entity the current form should operate upon.
315 * A nested array of form elements comprising the form.
316 * @param \Drupal\Core\Form\FormStateInterface $form_state
317 * The current state of the form.
319 protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
320 $values = $form_state->getValues();
322 if ($this->entity instanceof EntityWithPluginCollectionInterface) {
323 // Do not manually update values represented by plugin collections.
324 $values = array_diff_key($values, $this->entity->getPluginCollections());
327 // @todo: This relies on a method that only exists for config and content
328 // entities, in a different way. Consider moving this logic to a config
329 // entity specific implementation.
330 foreach ($values as $key => $value) {
331 $entity->set($key, $value);
338 public function getEntity() {
339 return $this->entity;
345 public function setEntity(EntityInterface $entity) {
346 $this->entity = $entity;
353 public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
354 if ($route_match->getRawParameter($entity_type_id) !== NULL) {
355 $entity = $route_match->getParameter($entity_type_id);
359 // If the entity has bundles, fetch it from the route match.
360 $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
361 if ($bundle_key = $entity_type->getKey('bundle')) {
362 if (($bundle_entity_type_id = $entity_type->getBundleEntityType()) && $route_match->getRawParameter($bundle_entity_type_id)) {
363 $values[$bundle_key] = $route_match->getParameter($bundle_entity_type_id)->id();
365 elseif ($route_match->getRawParameter($bundle_key)) {
366 $values[$bundle_key] = $route_match->getParameter($bundle_key);
370 $entity = $this->entityTypeManager->getStorage($entity_type_id)->create($values);
377 * Prepares the entity object before the form is built first.
379 protected function prepareEntity() {}
382 * Invokes the specified prepare hook variant.
384 * @param string $hook
385 * The hook variant name.
386 * @param \Drupal\Core\Form\FormStateInterface $form_state
387 * The current state of the form.
389 protected function prepareInvokeAll($hook, FormStateInterface $form_state) {
390 $implementations = $this->moduleHandler->getImplementations($hook);
391 foreach ($implementations as $module) {
392 $function = $module . '_' . $hook;
393 if (function_exists($function)) {
394 // Ensure we pass an updated translation object and form display at
395 // each invocation, since they depend on form state which is alterable.
396 $args = [$this->entity, $this->operation, &$form_state];
397 call_user_func_array($function, $args);
405 public function getOperation() {
406 return $this->operation;
412 public function setModuleHandler(ModuleHandlerInterface $module_handler) {
413 $this->moduleHandler = $module_handler;
420 public function setEntityManager(EntityManagerInterface $entity_manager) {
421 $this->entityManager = $entity_manager;
428 public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
429 $this->entityTypeManager = $entity_type_manager;