1<?php 2 3namespace Drupal\media\Entity; 4 5use Drupal\Core\Entity\EditorialContentEntityBase; 6use Drupal\Core\Entity\EntityStorageInterface; 7use Drupal\Core\Entity\EntityTypeInterface; 8use Drupal\Core\Field\BaseFieldDefinition; 9use Drupal\Core\StringTranslation\StringTranslationTrait; 10use Drupal\media\MediaInterface; 11use Drupal\media\MediaSourceEntityConstraintsInterface; 12use Drupal\media\MediaSourceFieldConstraintsInterface; 13use Drupal\user\EntityOwnerTrait; 14 15/** 16 * Defines the media entity class. 17 * 18 * @todo Remove default/fallback entity form operation when #2006348 is done. 19 * @see https://www.drupal.org/node/2006348. 20 * 21 * @ContentEntityType( 22 * id = "media", 23 * label = @Translation("Media"), 24 * label_singular = @Translation("media item"), 25 * label_plural = @Translation("media items"), 26 * label_count = @PluralTranslation( 27 * singular = "@count media item", 28 * plural = "@count media items" 29 * ), 30 * bundle_label = @Translation("Media type"), 31 * handlers = { 32 * "storage" = "Drupal\media\MediaStorage", 33 * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", 34 * "list_builder" = "Drupal\media\MediaListBuilder", 35 * "access" = "Drupal\media\MediaAccessControlHandler", 36 * "form" = { 37 * "default" = "Drupal\media\MediaForm", 38 * "add" = "Drupal\media\MediaForm", 39 * "edit" = "Drupal\media\MediaForm", 40 * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", 41 * "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm", 42 * }, 43 * "views_data" = "Drupal\media\MediaViewsData", 44 * "route_provider" = { 45 * "html" = "Drupal\media\Routing\MediaRouteProvider", 46 * } 47 * }, 48 * base_table = "media", 49 * data_table = "media_field_data", 50 * revision_table = "media_revision", 51 * revision_data_table = "media_field_revision", 52 * translatable = TRUE, 53 * show_revision_ui = TRUE, 54 * entity_keys = { 55 * "id" = "mid", 56 * "revision" = "vid", 57 * "bundle" = "bundle", 58 * "label" = "name", 59 * "langcode" = "langcode", 60 * "uuid" = "uuid", 61 * "published" = "status", 62 * "owner" = "uid", 63 * }, 64 * revision_metadata_keys = { 65 * "revision_user" = "revision_user", 66 * "revision_created" = "revision_created", 67 * "revision_log_message" = "revision_log_message", 68 * }, 69 * bundle_entity_type = "media_type", 70 * permission_granularity = "bundle", 71 * admin_permission = "administer media", 72 * field_ui_base_route = "entity.media_type.edit_form", 73 * common_reference_target = TRUE, 74 * links = { 75 * "add-page" = "/media/add", 76 * "add-form" = "/media/add/{media_type}", 77 * "canonical" = "/media/{media}/edit", 78 * "collection" = "/admin/content/media", 79 * "delete-form" = "/media/{media}/delete", 80 * "delete-multiple-form" = "/media/delete", 81 * "edit-form" = "/media/{media}/edit", 82 * "revision" = "/media/{media}/revisions/{media_revision}/view", 83 * } 84 * ) 85 */ 86class Media extends EditorialContentEntityBase implements MediaInterface { 87 88 use EntityOwnerTrait; 89 use StringTranslationTrait; 90 91 /** 92 * {@inheritdoc} 93 */ 94 public function getName() { 95 $name = $this->getEntityKey('label'); 96 97 if (empty($name)) { 98 $media_source = $this->getSource(); 99 return $media_source->getMetadata($this, $media_source->getPluginDefinition()['default_name_metadata_attribute']); 100 } 101 102 return $name; 103 } 104 105 /** 106 * {@inheritdoc} 107 */ 108 public function label() { 109 return $this->getName(); 110 } 111 112 /** 113 * {@inheritdoc} 114 */ 115 public function setName($name) { 116 return $this->set('name', $name); 117 } 118 119 /** 120 * {@inheritdoc} 121 */ 122 public function getCreatedTime() { 123 return $this->get('created')->value; 124 } 125 126 /** 127 * {@inheritdoc} 128 */ 129 public function setCreatedTime($timestamp) { 130 return $this->set('created', $timestamp); 131 } 132 133 /** 134 * {@inheritdoc} 135 */ 136 public function getSource() { 137 return $this->bundle->entity->getSource(); 138 } 139 140 /** 141 * Update the thumbnail for the media item. 142 * 143 * @param bool $from_queue 144 * Specifies whether the thumbnail update is triggered from the queue. 145 * 146 * @return \Drupal\media\MediaInterface 147 * The updated media item. 148 * 149 * @internal 150 * 151 * @todo There has been some disagreement about how to handle updates to 152 * thumbnails. We need to decide on what the API will be for this. 153 * https://www.drupal.org/node/2878119 154 */ 155 protected function updateThumbnail($from_queue = FALSE) { 156 $this->thumbnail->target_id = $this->loadThumbnail($this->getThumbnailUri($from_queue))->id(); 157 158 // Set the thumbnail alt. 159 $media_source = $this->getSource(); 160 $plugin_definition = $media_source->getPluginDefinition(); 161 162 $this->thumbnail->alt = ''; 163 if (!empty($plugin_definition['thumbnail_alt_metadata_attribute'])) { 164 $this->thumbnail->alt = $media_source->getMetadata($this, $plugin_definition['thumbnail_alt_metadata_attribute']); 165 } 166 167 return $this; 168 } 169 170 /** 171 * Loads the file entity for the thumbnail. 172 * 173 * If the file entity does not exist, it will be created. 174 * 175 * @param string $thumbnail_uri 176 * (optional) The URI of the thumbnail, used to load or create the file 177 * entity. If omitted, the default thumbnail URI will be used. 178 * 179 * @return \Drupal\file\FileInterface 180 * The thumbnail file entity. 181 */ 182 protected function loadThumbnail($thumbnail_uri = NULL) { 183 $values = [ 184 'uri' => $thumbnail_uri ?: $this->getDefaultThumbnailUri(), 185 ]; 186 187 $file_storage = $this->entityTypeManager()->getStorage('file'); 188 189 $existing = $file_storage->loadByProperties($values); 190 if ($existing) { 191 $file = reset($existing); 192 } 193 else { 194 /** @var \Drupal\file\FileInterface $file */ 195 $file = $file_storage->create($values); 196 if ($owner = $this->getOwner()) { 197 $file->setOwner($owner); 198 } 199 $file->setPermanent(); 200 $file->save(); 201 } 202 return $file; 203 } 204 205 /** 206 * Returns the URI of the default thumbnail. 207 * 208 * @return string 209 * The default thumbnail URI. 210 */ 211 protected function getDefaultThumbnailUri() { 212 $default_thumbnail_filename = $this->getSource()->getPluginDefinition()['default_thumbnail_filename']; 213 return \Drupal::config('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename; 214 } 215 216 /** 217 * Updates the queued thumbnail for the media item. 218 * 219 * @return \Drupal\media\MediaInterface 220 * The updated media item. 221 * 222 * @internal 223 * 224 * @todo If the need arises in contrib, consider making this a public API, 225 * by adding an interface that extends MediaInterface. 226 */ 227 public function updateQueuedThumbnail() { 228 $this->updateThumbnail(TRUE); 229 return $this; 230 } 231 232 /** 233 * Gets the URI for the thumbnail of a media item. 234 * 235 * If thumbnail fetching is queued, new media items will use the default 236 * thumbnail, and existing media items will use the current thumbnail, until 237 * the queue is processed and the updated thumbnail has been fetched. 238 * Otherwise, the new thumbnail will be fetched immediately. 239 * 240 * @param bool $from_queue 241 * Specifies whether the thumbnail is being fetched from the queue. 242 * 243 * @return string 244 * The file URI for the thumbnail of the media item. 245 * 246 * @internal 247 */ 248 protected function getThumbnailUri($from_queue) { 249 $thumbnails_queued = $this->bundle->entity->thumbnailDownloadsAreQueued(); 250 if ($thumbnails_queued && $this->isNew()) { 251 return $this->getDefaultThumbnailUri(); 252 } 253 elseif ($thumbnails_queued && !$from_queue) { 254 return $this->get('thumbnail')->entity->getFileUri(); 255 } 256 257 $source = $this->getSource(); 258 return $source->getMetadata($this, $source->getPluginDefinition()['thumbnail_uri_metadata_attribute']); 259 } 260 261 /** 262 * Determines if the source field value has changed. 263 * 264 * @return bool 265 * TRUE if the source field value changed, FALSE otherwise. 266 * 267 * @internal 268 */ 269 protected function hasSourceFieldChanged() { 270 $source_field_name = $this->getSource()->getConfiguration()['source_field']; 271 $current_items = $this->get($source_field_name); 272 return isset($this->original) && !$current_items->equals($this->original->get($source_field_name)); 273 } 274 275 /** 276 * Determines if the thumbnail should be updated for a media item. 277 * 278 * @param bool $is_new 279 * Specifies whether the media item is new. 280 * 281 * @return bool 282 * TRUE if the thumbnail should be updated, FALSE otherwise. 283 */ 284 protected function shouldUpdateThumbnail($is_new = FALSE) { 285 // Update thumbnail if we don't have a thumbnail yet or when the source 286 // field value changes. 287 return !$this->get('thumbnail')->entity || $is_new || $this->hasSourceFieldChanged(); 288 } 289 290 /** 291 * {@inheritdoc} 292 */ 293 public function preSave(EntityStorageInterface $storage) { 294 parent::preSave($storage); 295 296 // If no thumbnail has been explicitly set, use the default thumbnail. 297 if ($this->get('thumbnail')->isEmpty()) { 298 $this->thumbnail->target_id = $this->loadThumbnail()->id(); 299 } 300 } 301 302 /** 303 * {@inheritdoc} 304 */ 305 public function postSave(EntityStorageInterface $storage, $update = TRUE) { 306 parent::postSave($storage, $update); 307 $is_new = !$update; 308 foreach ($this->translations as $langcode => $data) { 309 if ($this->hasTranslation($langcode)) { 310 $translation = $this->getTranslation($langcode); 311 if ($translation->bundle->entity->thumbnailDownloadsAreQueued() && $translation->shouldUpdateThumbnail($is_new)) { 312 \Drupal::queue('media_entity_thumbnail')->createItem(['id' => $translation->id()]); 313 } 314 } 315 } 316 } 317 318 /** 319 * {@inheritdoc} 320 */ 321 public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { 322 parent::preSaveRevision($storage, $record); 323 324 $is_new_revision = $this->isNewRevision(); 325 if (!$is_new_revision && isset($this->original) && empty($record->revision_log_message)) { 326 // If we are updating an existing media item without adding a 327 // new revision, we need to make sure $entity->revision_log_message is 328 // reset whenever it is empty. 329 // Therefore, this code allows us to avoid clobbering an existing log 330 // entry with an empty one. 331 $record->revision_log_message = $this->original->revision_log_message->value; 332 } 333 334 if ($is_new_revision) { 335 $record->revision_created = self::getRequestTime(); 336 } 337 } 338 339 /** 340 * Sets the media entity's field values from the source's metadata. 341 * 342 * Fetching the metadata could be slow (e.g., if requesting it from a remote 343 * API), so this is called by \Drupal\media\MediaStorage::save() prior to it 344 * beginning the database transaction, whereas static::preSave() executes 345 * after the transaction has already started. 346 * 347 * @internal 348 * Expose this as an API in 349 * https://www.drupal.org/project/drupal/issues/2992426. 350 */ 351 public function prepareSave() { 352 // @todo If the source plugin talks to a remote API (e.g. oEmbed), this code 353 // might be performing a fair number of HTTP requests. This is dangerously 354 // brittle and should probably be handled by a queue, to avoid doing HTTP 355 // operations during entity save. See 356 // https://www.drupal.org/project/drupal/issues/2976875 for more. 357 358 // In order for metadata to be mapped correctly, $this->original must be 359 // set. However, that is only set once parent::save() is called, so work 360 // around that by setting it here. 361 if (!isset($this->original) && $id = $this->id()) { 362 $this->original = $this->entityTypeManager() 363 ->getStorage('media') 364 ->loadUnchanged($id); 365 } 366 367 $media_source = $this->getSource(); 368 foreach ($this->translations as $langcode => $data) { 369 if ($this->hasTranslation($langcode)) { 370 $translation = $this->getTranslation($langcode); 371 // Try to set fields provided by the media source and mapped in 372 // media type config. 373 foreach ($translation->bundle->entity->getFieldMap() as $metadata_attribute_name => $entity_field_name) { 374 // Only save value in entity field if empty. Do not overwrite existing 375 // data. 376 if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $translation->hasSourceFieldChanged())) { 377 $translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name)); 378 } 379 } 380 381 // Try to set a default name for this media item if no name is provided. 382 if ($translation->get('name')->isEmpty()) { 383 $translation->setName($translation->getName()); 384 } 385 386 // Set thumbnail. 387 if ($translation->shouldUpdateThumbnail($this->isNew())) { 388 $translation->updateThumbnail(); 389 } 390 } 391 } 392 } 393 394 /** 395 * {@inheritdoc} 396 */ 397 public function validate() { 398 $media_source = $this->getSource(); 399 400 if ($media_source instanceof MediaSourceEntityConstraintsInterface) { 401 $entity_constraints = $media_source->getEntityConstraints(); 402 $this->getTypedData()->getDataDefinition()->setConstraints($entity_constraints); 403 } 404 405 if ($media_source instanceof MediaSourceFieldConstraintsInterface) { 406 $source_field_name = $media_source->getConfiguration()['source_field']; 407 $source_field_constraints = $media_source->getSourceFieldConstraints(); 408 $this->get($source_field_name)->getDataDefinition()->setConstraints($source_field_constraints); 409 } 410 411 return parent::validate(); 412 } 413 414 /** 415 * {@inheritdoc} 416 */ 417 public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 418 $fields = parent::baseFieldDefinitions($entity_type); 419 $fields += static::ownerBaseFieldDefinitions($entity_type); 420 421 $fields['name'] = BaseFieldDefinition::create('string') 422 ->setLabel(t('Name')) 423 ->setRequired(TRUE) 424 ->setTranslatable(TRUE) 425 ->setRevisionable(TRUE) 426 ->setDefaultValue('') 427 ->setSetting('max_length', 255) 428 ->setDisplayOptions('form', [ 429 'type' => 'string_textfield', 430 'weight' => -5, 431 ]) 432 ->setDisplayConfigurable('form', TRUE) 433 ->setDisplayConfigurable('view', TRUE); 434 435 $fields['thumbnail'] = BaseFieldDefinition::create('image') 436 ->setLabel(t('Thumbnail')) 437 ->setDescription(t('The thumbnail of the media item.')) 438 ->setRevisionable(TRUE) 439 ->setTranslatable(TRUE) 440 ->setDisplayOptions('view', [ 441 'type' => 'image', 442 'weight' => 5, 443 'label' => 'hidden', 444 'settings' => [ 445 'image_style' => 'thumbnail', 446 ], 447 ]) 448 ->setDisplayConfigurable('view', TRUE) 449 ->setReadOnly(TRUE); 450 451 $fields['uid'] 452 ->setLabel(t('Authored by')) 453 ->setDescription(t('The user ID of the author.')) 454 ->setRevisionable(TRUE) 455 ->setDisplayOptions('form', [ 456 'type' => 'entity_reference_autocomplete', 457 'weight' => 5, 458 'settings' => [ 459 'match_operator' => 'CONTAINS', 460 'size' => '60', 461 'autocomplete_type' => 'tags', 462 'placeholder' => '', 463 ], 464 ]) 465 ->setDisplayConfigurable('form', TRUE) 466 ->setDisplayOptions('view', [ 467 'label' => 'hidden', 468 'type' => 'author', 469 'weight' => 0, 470 ]) 471 ->setDisplayConfigurable('view', TRUE); 472 473 $fields['status'] 474 ->setDisplayOptions('form', [ 475 'type' => 'boolean_checkbox', 476 'settings' => [ 477 'display_label' => TRUE, 478 ], 479 'weight' => 100, 480 ]) 481 ->setDisplayConfigurable('form', TRUE); 482 483 $fields['created'] = BaseFieldDefinition::create('created') 484 ->setLabel(t('Authored on')) 485 ->setDescription(t('The time the media item was created.')) 486 ->setTranslatable(TRUE) 487 ->setRevisionable(TRUE) 488 ->setDefaultValueCallback(static::class . '::getRequestTime') 489 ->setDisplayOptions('form', [ 490 'type' => 'datetime_timestamp', 491 'weight' => 10, 492 ]) 493 ->setDisplayConfigurable('form', TRUE) 494 ->setDisplayOptions('view', [ 495 'label' => 'hidden', 496 'type' => 'timestamp', 497 'weight' => 0, 498 ]) 499 ->setDisplayConfigurable('view', TRUE); 500 501 $fields['changed'] = BaseFieldDefinition::create('changed') 502 ->setLabel(t('Changed')) 503 ->setDescription(t('The time the media item was last edited.')) 504 ->setTranslatable(TRUE) 505 ->setRevisionable(TRUE); 506 507 return $fields; 508 } 509 510 /** 511 * Default value callback for 'uid' base field definition. 512 * 513 * @see ::baseFieldDefinitions() 514 * 515 * @deprecated The ::getCurrentUserId method is deprecated in 8.6.x and will 516 * be removed before 9.0.0. 517 * 518 * @return int[] 519 * An array of default values. 520 */ 521 public static function getCurrentUserId() { 522 @trigger_error('The ::getCurrentUserId method is deprecated in 8.6.x and will be removed before 9.0.0.', E_USER_DEPRECATED); 523 return [\Drupal::currentUser()->id()]; 524 } 525 526 /** 527 * {@inheritdoc} 528 */ 529 public static function getRequestTime() { 530 return \Drupal::time()->getRequestTime(); 531 } 532 533} 534