annotate core/modules/quickedit/src/QuickEditController.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\quickedit;
Chris@0 4
Chris@0 5 use Drupal\Core\Controller\ControllerBase;
Chris@18 6 use Drupal\Core\Entity\EntityRepositoryInterface;
Chris@0 7 use Drupal\Core\Form\FormState;
Chris@0 8 use Drupal\Core\Render\RendererInterface;
Chris@14 9 use Drupal\Core\TempStore\PrivateTempStoreFactory;
Chris@0 10 use Symfony\Component\DependencyInjection\ContainerInterface;
Chris@0 11 use Symfony\Component\HttpFoundation\JsonResponse;
Chris@0 12 use Symfony\Component\HttpFoundation\Request;
Chris@0 13 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Chris@0 14 use Drupal\Core\Ajax\AjaxResponse;
Chris@0 15 use Drupal\Core\Entity\EntityInterface;
Chris@18 16 use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
Chris@0 17 use Drupal\quickedit\Ajax\FieldFormCommand;
Chris@0 18 use Drupal\quickedit\Ajax\FieldFormSavedCommand;
Chris@0 19 use Drupal\quickedit\Ajax\FieldFormValidationErrorsCommand;
Chris@0 20 use Drupal\quickedit\Ajax\EntitySavedCommand;
Chris@0 21
Chris@0 22 /**
Chris@0 23 * Returns responses for Quick Edit module routes.
Chris@0 24 */
Chris@0 25 class QuickEditController extends ControllerBase {
Chris@0 26
Chris@0 27 /**
Chris@0 28 * The PrivateTempStore factory.
Chris@0 29 *
Chris@14 30 * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
Chris@0 31 */
Chris@0 32 protected $tempStoreFactory;
Chris@0 33
Chris@0 34 /**
Chris@0 35 * The in-place editing metadata generator.
Chris@0 36 *
Chris@0 37 * @var \Drupal\quickedit\MetadataGeneratorInterface
Chris@0 38 */
Chris@0 39 protected $metadataGenerator;
Chris@0 40
Chris@0 41 /**
Chris@0 42 * The in-place editor selector.
Chris@0 43 *
Chris@0 44 * @var \Drupal\quickedit\EditorSelectorInterface
Chris@0 45 */
Chris@0 46 protected $editorSelector;
Chris@0 47
Chris@0 48 /**
Chris@0 49 * The renderer.
Chris@0 50 *
Chris@0 51 * @var \Drupal\Core\Render\RendererInterface
Chris@0 52 */
Chris@0 53 protected $renderer;
Chris@0 54
Chris@0 55 /**
Chris@18 56 * The entity display repository service.
Chris@18 57 *
Chris@18 58 * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
Chris@18 59 */
Chris@18 60 protected $entityDisplayRepository;
Chris@18 61
Chris@18 62 /**
Chris@18 63 * The entity repository.
Chris@18 64 *
Chris@18 65 * @var \Drupal\Core\Entity\EntityRepositoryInterface
Chris@18 66 */
Chris@18 67 protected $entityRepository;
Chris@18 68
Chris@18 69 /**
Chris@0 70 * Constructs a new QuickEditController.
Chris@0 71 *
Chris@14 72 * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
Chris@0 73 * The PrivateTempStore factory.
Chris@0 74 * @param \Drupal\quickedit\MetadataGeneratorInterface $metadata_generator
Chris@0 75 * The in-place editing metadata generator.
Chris@0 76 * @param \Drupal\quickedit\EditorSelectorInterface $editor_selector
Chris@0 77 * The in-place editor selector.
Chris@0 78 * @param \Drupal\Core\Render\RendererInterface $renderer
Chris@0 79 * The renderer.
Chris@18 80 * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
Chris@18 81 * The entity display repository service.
Chris@18 82 * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
Chris@18 83 * The entity repository.
Chris@0 84 */
Chris@18 85 public function __construct(PrivateTempStoreFactory $temp_store_factory, MetadataGeneratorInterface $metadata_generator, EditorSelectorInterface $editor_selector, RendererInterface $renderer, EntityDisplayRepositoryInterface $entity_display_repository, EntityRepositoryInterface $entity_repository) {
Chris@0 86 $this->tempStoreFactory = $temp_store_factory;
Chris@0 87 $this->metadataGenerator = $metadata_generator;
Chris@0 88 $this->editorSelector = $editor_selector;
Chris@0 89 $this->renderer = $renderer;
Chris@18 90 if (!$entity_display_repository) {
Chris@18 91 @trigger_error('The entity_display.repository service must be passed to QuickEditController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
Chris@18 92 $entity_display_repository = \Drupal::service('entity_display.repository');
Chris@18 93 }
Chris@18 94 $this->entityDisplayRepository = $entity_display_repository;
Chris@18 95 if (!$entity_repository) {
Chris@18 96 @trigger_error('The entity.repository service must be passed to QuickEditController::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED);
Chris@18 97 $entity_repository = \Drupal::service('entity.repository');
Chris@18 98 }
Chris@18 99 $this->entityRepository = $entity_repository;
Chris@0 100 }
Chris@0 101
Chris@0 102 /**
Chris@0 103 * {@inheritdoc}
Chris@0 104 */
Chris@0 105 public static function create(ContainerInterface $container) {
Chris@0 106 return new static(
Chris@14 107 $container->get('tempstore.private'),
Chris@0 108 $container->get('quickedit.metadata.generator'),
Chris@0 109 $container->get('quickedit.editor.selector'),
Chris@18 110 $container->get('renderer'),
Chris@18 111 $container->get('entity_display.repository'),
Chris@18 112 $container->get('entity.repository')
Chris@0 113 );
Chris@0 114 }
Chris@0 115
Chris@0 116 /**
Chris@0 117 * Returns the metadata for a set of fields.
Chris@0 118 *
Chris@0 119 * Given a list of field quick edit IDs as POST parameters, run access checks
Chris@0 120 * on the entity and field level to determine whether the current user may
Chris@0 121 * edit them. Also retrieves other metadata.
Chris@0 122 *
Chris@0 123 * @return \Symfony\Component\HttpFoundation\JsonResponse
Chris@0 124 * The JSON response.
Chris@0 125 */
Chris@0 126 public function metadata(Request $request) {
Chris@0 127 $fields = $request->request->get('fields');
Chris@0 128 if (!isset($fields)) {
Chris@0 129 throw new NotFoundHttpException();
Chris@0 130 }
Chris@0 131 $entities = $request->request->get('entities');
Chris@0 132
Chris@0 133 $metadata = [];
Chris@0 134 foreach ($fields as $field) {
Chris@0 135 list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode('/', $field);
Chris@0 136
Chris@0 137 // Load the entity.
Chris@18 138 if (!$entity_type || !$this->entityTypeManager()->getDefinition($entity_type)) {
Chris@0 139 throw new NotFoundHttpException();
Chris@0 140 }
Chris@18 141 $entity = $this->entityTypeManager()->getStorage($entity_type)->load($entity_id);
Chris@0 142 if (!$entity) {
Chris@0 143 throw new NotFoundHttpException();
Chris@0 144 }
Chris@0 145
Chris@0 146 // Validate the field name and language.
Chris@0 147 if (!$field_name || !$entity->hasField($field_name)) {
Chris@0 148 throw new NotFoundHttpException();
Chris@0 149 }
Chris@0 150 if (!$langcode || !$entity->hasTranslation($langcode)) {
Chris@0 151 throw new NotFoundHttpException();
Chris@0 152 }
Chris@0 153
Chris@0 154 $entity = $entity->getTranslation($langcode);
Chris@0 155
Chris@0 156 // If the entity information for this field is requested, include it.
Chris@0 157 $entity_id = $entity->getEntityTypeId() . '/' . $entity_id;
Chris@0 158 if (is_array($entities) && in_array($entity_id, $entities) && !isset($metadata[$entity_id])) {
Chris@0 159 $metadata[$entity_id] = $this->metadataGenerator->generateEntityMetadata($entity);
Chris@0 160 }
Chris@0 161
Chris@0 162 $metadata[$field] = $this->metadataGenerator->generateFieldMetadata($entity->get($field_name), $view_mode);
Chris@0 163 }
Chris@0 164
Chris@0 165 return new JsonResponse($metadata);
Chris@0 166 }
Chris@0 167
Chris@0 168 /**
Chris@0 169 * Returns AJAX commands to load in-place editors' attachments.
Chris@0 170 *
Chris@0 171 * Given a list of in-place editor IDs as POST parameters, render AJAX
Chris@0 172 * commands to load those in-place editors.
Chris@0 173 *
Chris@0 174 * @return \Drupal\Core\Ajax\AjaxResponse
Chris@0 175 * The Ajax response.
Chris@0 176 */
Chris@0 177 public function attachments(Request $request) {
Chris@0 178 $response = new AjaxResponse();
Chris@0 179 $editors = $request->request->get('editors');
Chris@0 180 if (!isset($editors)) {
Chris@0 181 throw new NotFoundHttpException();
Chris@0 182 }
Chris@0 183
Chris@0 184 $response->setAttachments($this->editorSelector->getEditorAttachments($editors));
Chris@0 185
Chris@0 186 return $response;
Chris@0 187 }
Chris@0 188
Chris@0 189 /**
Chris@0 190 * Returns a single field edit form as an Ajax response.
Chris@0 191 *
Chris@0 192 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@0 193 * The entity being edited.
Chris@0 194 * @param string $field_name
Chris@0 195 * The name of the field that is being edited.
Chris@0 196 * @param string $langcode
Chris@0 197 * The name of the language for which the field is being edited.
Chris@0 198 * @param string $view_mode_id
Chris@0 199 * The view mode the field should be rerendered in.
Chris@0 200 * @param \Symfony\Component\HttpFoundation\Request $request
Chris@0 201 * The current request object containing the search string.
Chris@0 202 *
Chris@0 203 * @return \Drupal\Core\Ajax\AjaxResponse
Chris@0 204 * The Ajax response.
Chris@0 205 */
Chris@0 206 public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view_mode_id, Request $request) {
Chris@0 207 $response = new AjaxResponse();
Chris@0 208
Chris@0 209 // Replace entity with PrivateTempStore copy if available and not resetting,
Chris@0 210 // init PrivateTempStore copy otherwise.
Chris@0 211 $tempstore_entity = $this->tempStoreFactory->get('quickedit')->get($entity->uuid());
Chris@0 212 if ($tempstore_entity && $request->request->get('reset') !== 'true') {
Chris@0 213 $entity = $tempstore_entity;
Chris@0 214 }
Chris@0 215 else {
Chris@0 216 $this->tempStoreFactory->get('quickedit')->set($entity->uuid(), $entity);
Chris@0 217 }
Chris@0 218
Chris@0 219 $form_state = (new FormState())
Chris@0 220 ->set('langcode', $langcode)
Chris@0 221 ->disableRedirect()
Chris@0 222 ->addBuildInfo('args', [$entity, $field_name]);
Chris@0 223 $form = $this->formBuilder()->buildForm('Drupal\quickedit\Form\QuickEditFieldForm', $form_state);
Chris@0 224
Chris@0 225 if ($form_state->isExecuted()) {
Chris@0 226 // The form submission saved the entity in PrivateTempStore. Return the
Chris@0 227 // updated view of the field from the PrivateTempStore copy.
Chris@0 228 $entity = $this->tempStoreFactory->get('quickedit')->get($entity->uuid());
Chris@0 229
Chris@0 230 // Closure to render the field given a view mode.
Chris@0 231 $render_field_in_view_mode = function ($view_mode_id) use ($entity, $field_name, $langcode) {
Chris@0 232 return $this->renderField($entity, $field_name, $langcode, $view_mode_id);
Chris@0 233 };
Chris@0 234
Chris@0 235 // Re-render the updated field.
Chris@0 236 $output = $render_field_in_view_mode($view_mode_id);
Chris@0 237
Chris@0 238 // Re-render the updated field for other view modes (i.e. for other
Chris@0 239 // instances of the same logical field on the user's page).
Chris@0 240 $other_view_mode_ids = $request->request->get('other_view_modes') ?: [];
Chris@0 241 $other_view_modes = array_map($render_field_in_view_mode, array_combine($other_view_mode_ids, $other_view_mode_ids));
Chris@0 242
Chris@0 243 $response->addCommand(new FieldFormSavedCommand($output, $other_view_modes));
Chris@0 244 }
Chris@0 245 else {
Chris@0 246 $output = $this->renderer->renderRoot($form);
Chris@0 247 // When working with a hidden form, we don't want its CSS/JS to be loaded.
Chris@0 248 if ($request->request->get('nocssjs') !== 'true') {
Chris@0 249 $response->setAttachments($form['#attached']);
Chris@0 250 }
Chris@0 251 $response->addCommand(new FieldFormCommand($output));
Chris@0 252
Chris@0 253 $errors = $form_state->getErrors();
Chris@0 254 if (count($errors)) {
Chris@0 255 $status_messages = [
Chris@17 256 '#type' => 'status_messages',
Chris@0 257 ];
Chris@0 258 $response->addCommand(new FieldFormValidationErrorsCommand($this->renderer->renderRoot($status_messages)));
Chris@0 259 }
Chris@0 260 }
Chris@0 261
Chris@0 262 return $response;
Chris@0 263 }
Chris@0 264
Chris@0 265 /**
Chris@0 266 * Renders a field.
Chris@0 267 *
Chris@0 268 * If the view mode ID is not an Entity Display view mode ID, then the field
Chris@0 269 * was rendered using a custom render pipeline (not the Entity/Field API
Chris@0 270 * render pipeline).
Chris@0 271 *
Chris@0 272 * An example could be Views' render pipeline. In that case, the view mode ID
Chris@0 273 * would probably contain the View's ID, display and the row index.
Chris@0 274 *
Chris@0 275 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@0 276 * The entity being edited.
Chris@0 277 * @param string $field_name
Chris@0 278 * The name of the field that is being edited.
Chris@0 279 * @param string $langcode
Chris@0 280 * The name of the language for which the field is being edited.
Chris@0 281 * @param string $view_mode_id
Chris@0 282 * The view mode the field should be rerendered in. Either an Entity Display
Chris@0 283 * view mode ID, or a custom one. See hook_quickedit_render_field().
Chris@0 284 *
Chris@0 285 * @return \Drupal\Component\Render\MarkupInterface
Chris@0 286 * Rendered HTML.
Chris@0 287 *
Chris@0 288 * @see hook_quickedit_render_field()
Chris@0 289 */
Chris@0 290 protected function renderField(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
Chris@18 291 $entity_view_mode_ids = array_keys($this->entityDisplayRepository->getViewModes($entity->getEntityTypeId()));
Chris@0 292 if (in_array($view_mode_id, $entity_view_mode_ids)) {
Chris@18 293 $entity = $this->entityRepository->getTranslationFromContext($entity, $langcode);
Chris@0 294 $output = $entity->get($field_name)->view($view_mode_id);
Chris@0 295 }
Chris@0 296 else {
Chris@0 297 // Each part of a custom (non-Entity Display) view mode ID is separated
Chris@0 298 // by a dash; the first part must be the module name.
Chris@0 299 $mode_id_parts = explode('-', $view_mode_id, 2);
Chris@0 300 $module = reset($mode_id_parts);
Chris@0 301 $args = [$entity, $field_name, $view_mode_id, $langcode];
Chris@0 302 $output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args);
Chris@0 303 }
Chris@0 304
Chris@0 305 return $this->renderer->renderRoot($output);
Chris@0 306 }
Chris@0 307
Chris@0 308 /**
Chris@0 309 * Saves an entity into the database, from PrivateTempStore.
Chris@0 310 *
Chris@0 311 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@0 312 * The entity being edited.
Chris@0 313 *
Chris@0 314 * @return \Drupal\Core\Ajax\AjaxResponse
Chris@0 315 * The Ajax response.
Chris@0 316 */
Chris@0 317 public function entitySave(EntityInterface $entity) {
Chris@0 318 // Take the entity from PrivateTempStore and save in entity storage.
Chris@0 319 // fieldForm() ensures that the PrivateTempStore copy exists ahead.
Chris@0 320 $tempstore = $this->tempStoreFactory->get('quickedit');
Chris@0 321 $tempstore->get($entity->uuid())->save();
Chris@0 322 $tempstore->delete($entity->uuid());
Chris@0 323
Chris@0 324 // Return information about the entity that allows a front end application
Chris@0 325 // to identify it.
Chris@0 326 $output = [
Chris@0 327 'entity_type' => $entity->getEntityTypeId(),
Chris@17 328 'entity_id' => $entity->id(),
Chris@0 329 ];
Chris@0 330
Chris@0 331 // Respond to client that the entity was saved properly.
Chris@0 332 $response = new AjaxResponse();
Chris@0 333 $response->addCommand(new EntitySavedCommand($output));
Chris@0 334 return $response;
Chris@0 335 }
Chris@0 336
Chris@0 337 }