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 protected $entityManager;
45 * The entity type manager.
47 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
49 protected $entityTypeManager;
52 * The entity being used by this form.
54 * @var \Drupal\Core\Entity\EntityInterface
61 public function setOperation($operation) {
62 // If NULL is passed, do not overwrite the operation.
64 $this->operation = $operation;
72 public function getBaseFormId() {
73 // Assign ENTITYTYPE_form as base form ID to invoke corresponding
74 // hook_form_alter(), #validate, #submit, and #theme callbacks, but only if
75 // it is different from the actual form ID, since callbacks would be invoked
77 $base_form_id = $this->entity->getEntityTypeId() . '_form';
78 if ($base_form_id == $this->getFormId()) {
87 public function getFormId() {
88 $form_id = $this->entity->getEntityTypeId();
89 if ($this->entity->getEntityType()->hasKey('bundle')) {
90 $form_id .= '_' . $this->entity->bundle();
92 if ($this->operation != 'default') {
93 $form_id = $form_id . '_' . $this->operation;
95 return $form_id . '_form';
101 public function buildForm(array $form, FormStateInterface $form_state) {
102 // During the initial form build, add this form object to the form state and
103 // allow for initial preparation before form building and processing.
104 if (!$form_state->has('entity_form_initialized')) {
105 $this->init($form_state);
108 // Ensure that edit forms have the correct cacheability metadata so they can
110 if (!$this->entity->isNew()) {
111 \Drupal::service('renderer')->addCacheableDependency($form, $this->entity);
114 // Retrieve the form array using the possibly updated entity in form state.
115 $form = $this->form($form, $form_state);
117 // Retrieve and add the form actions array.
118 $actions = $this->actionsElement($form, $form_state);
119 if (!empty($actions)) {
120 $form['actions'] = $actions;
127 * Initialize the form state and the entity before the first form build.
129 protected function init(FormStateInterface $form_state) {
130 // Flag that this form has been initialized.
131 $form_state->set('entity_form_initialized', TRUE);
133 // Prepare the entity to be presented in the entity form.
134 $this->prepareEntity();
136 // Invoke the prepare form hooks.
137 $this->prepareInvokeAll('entity_prepare_form', $form_state);
138 $this->prepareInvokeAll($this->entity->getEntityTypeId() . '_prepare_form', $form_state);
142 * Gets the actual form array to be built.
144 * @see \Drupal\Core\Entity\EntityForm::processForm()
145 * @see \Drupal\Core\Entity\EntityForm::afterBuild()
147 public function form(array $form, FormStateInterface $form_state) {
148 // Add #process and #after_build callbacks.
149 $form['#process'][] = '::processForm';
150 $form['#after_build'][] = '::afterBuild';
156 * Process callback: assigns weights and hides extra fields.
158 * @see \Drupal\Core\Entity\EntityForm::form()
160 public function processForm($element, FormStateInterface $form_state, $form) {
161 // If the form is cached, process callbacks may not have a valid reference
162 // to the entity object, hence we must restore it.
163 $this->entity = $form_state->getFormObject()->getEntity();
169 * Form element #after_build callback: Updates the entity with submitted data.
171 * Updates the internal $this->entity object with submitted values when the
172 * form is being rebuilt (e.g. submitted via AJAX), so that subsequent
173 * processing (e.g. AJAX callbacks) can rely on it.
175 public function afterBuild(array $element, FormStateInterface $form_state) {
176 // Rebuild the entity if #after_build is being called as part of a form
177 // rebuild, i.e. if we are processing input.
178 if ($form_state->isProcessingInput()) {
179 $this->entity = $this->buildEntity($element, $form_state);
186 * Returns the action form element for the current entity form.
188 protected function actionsElement(array $form, FormStateInterface $form_state) {
189 $element = $this->actions($form, $form_state);
191 if (isset($element['delete'])) {
192 // Move the delete action as last one, unless weights are explicitly
194 $delete = $element['delete'];
195 unset($element['delete']);
196 $element['delete'] = $delete;
197 $element['delete']['#button_type'] = 'danger';
200 if (isset($element['submit'])) {
201 // Give the primary submit button a #button_type of primary.
202 $element['submit']['#button_type'] = 'primary';
206 foreach (Element::children($element) as $action) {
207 $element[$action] += [
208 '#weight' => ++$count * 5,
212 if (!empty($element)) {
213 $element['#type'] = 'actions';
220 * Returns an array of supported actions for the current entity form.
222 * @todo Consider introducing a 'preview' action here, since it is used by
225 protected function actions(array $form, FormStateInterface $form_state) {
226 // @todo Consider renaming the action key from submit to save. The impacts
227 // are hard to predict. For example, see
228 // \Drupal\language\Element\LanguageConfiguration::processLanguageConfiguration().
229 $actions['submit'] = [
231 '#value' => $this->t('Save'),
232 '#submit' => ['::submitForm', '::save'],
235 if (!$this->entity->isNew() && $this->entity->hasLinkTemplate('delete-form')) {
236 $route_info = $this->entity->urlInfo('delete-form');
237 if ($this->getRequest()->query->has('destination')) {
238 $query = $route_info->getOption('query');
239 $query['destination'] = $this->getRequest()->query->get('destination');
240 $route_info->setOption('query', $query);
242 $actions['delete'] = [
244 '#title' => $this->t('Delete'),
245 '#access' => $this->entity->access('delete'),
247 'class' => ['button', 'button--danger'],
250 $actions['delete']['#url'] = $route_info;
259 * This is the default entity object builder function. It is called before any
260 * other submit handler to build the new entity object to be used by the
261 * following submit handlers. At this point of the form workflow the entity is
262 * validated and the form state can be updated, this way the subsequently
263 * invoked handlers can retrieve a regular entity object to act on. Generally
264 * this method should not be overridden unless the entity requires the same
265 * preparation for two actions, see \Drupal\comment\CommentForm for an example
266 * with the save and preview actions.
269 * An associative array containing the structure of the form.
270 * @param \Drupal\Core\Form\FormStateInterface $form_state
271 * The current state of the form.
273 public function submitForm(array &$form, FormStateInterface $form_state) {
274 // Remove button and internal Form API values from submitted values.
275 $form_state->cleanValues();
276 $this->entity = $this->buildEntity($form, $form_state);
282 public function save(array $form, FormStateInterface $form_state) {
283 return $this->entity->save();
289 public function buildEntity(array $form, FormStateInterface $form_state) {
290 $entity = clone $this->entity;
291 $this->copyFormValuesToEntity($entity, $form, $form_state);
293 // Invoke all specified builders for copying form values to entity
295 if (isset($form['#entity_builders'])) {
296 foreach ($form['#entity_builders'] as $function) {
297 call_user_func_array($form_state->prepareCallback($function), [$entity->getEntityTypeId(), $entity, &$form, &$form_state]);
305 * Copies top-level form values to entity properties
307 * This should not change existing entity properties that are not being edited
310 * @param \Drupal\Core\Entity\EntityInterface $entity
311 * The entity the current form should operate upon.
313 * A nested array of form elements comprising the form.
314 * @param \Drupal\Core\Form\FormStateInterface $form_state
315 * The current state of the form.
317 protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
318 $values = $form_state->getValues();
320 if ($this->entity instanceof EntityWithPluginCollectionInterface) {
321 // Do not manually update values represented by plugin collections.
322 $values = array_diff_key($values, $this->entity->getPluginCollections());
325 // @todo: This relies on a method that only exists for config and content
326 // entities, in a different way. Consider moving this logic to a config
327 // entity specific implementation.
328 foreach ($values as $key => $value) {
329 $entity->set($key, $value);
336 public function getEntity() {
337 return $this->entity;
343 public function setEntity(EntityInterface $entity) {
344 $this->entity = $entity;
351 public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
352 if ($route_match->getRawParameter($entity_type_id) !== NULL) {
353 $entity = $route_match->getParameter($entity_type_id);
357 // If the entity has bundles, fetch it from the route match.
358 $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
359 if ($bundle_key = $entity_type->getKey('bundle')) {
360 if (($bundle_entity_type_id = $entity_type->getBundleEntityType()) && $route_match->getRawParameter($bundle_entity_type_id)) {
361 $values[$bundle_key] = $route_match->getParameter($bundle_entity_type_id)->id();
363 elseif ($route_match->getRawParameter($bundle_key)) {
364 $values[$bundle_key] = $route_match->getParameter($bundle_key);
368 $entity = $this->entityTypeManager->getStorage($entity_type_id)->create($values);
375 * Prepares the entity object before the form is built first.
377 protected function prepareEntity() {}
380 * Invokes the specified prepare hook variant.
382 * @param string $hook
383 * The hook variant name.
384 * @param \Drupal\Core\Form\FormStateInterface $form_state
385 * The current state of the form.
387 protected function prepareInvokeAll($hook, FormStateInterface $form_state) {
388 $implementations = $this->moduleHandler->getImplementations($hook);
389 foreach ($implementations as $module) {
390 $function = $module . '_' . $hook;
391 if (function_exists($function)) {
392 // Ensure we pass an updated translation object and form display at
393 // each invocation, since they depend on form state which is alterable.
394 $args = [$this->entity, $this->operation, &$form_state];
395 call_user_func_array($function, $args);
403 public function getOperation() {
404 return $this->operation;
410 public function setModuleHandler(ModuleHandlerInterface $module_handler) {
411 $this->moduleHandler = $module_handler;
418 public function setEntityManager(EntityManagerInterface $entity_manager) {
419 $this->entityManager = $entity_manager;
426 public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
427 $this->entityTypeManager = $entity_type_manager;