FieldConfig.php 22.4 KB
Newer Older
1
2
3
4
<?php

/**
 * @file
5
 * Contains \Drupal\field\Entity\FieldConfig.
6
7
 */

8
namespace Drupal\field\Entity;
9

10
use Drupal\Component\Utility\Unicode;
11
use Drupal\Core\Config\Entity\ConfigEntityBase;
12
use Drupal\Core\Entity\EntityStorageInterface;
13
use Drupal\Core\Field\FieldStorageDefinitionInterface;
14
use Drupal\field\FieldException;
15
use Drupal\field\FieldConfigInterface;
16
17
18
19

/**
 * Defines the Field entity.
 *
20
 * @ConfigEntityType(
21
 *   id = "field_config",
22
 *   label = @Translation("Field"),
23
 *   controllers = {
24
 *     "storage" = "Drupal\field\FieldConfigStorage"
25
 *   },
26
 *   config_prefix = "field",
27
28
 *   entity_keys = {
 *     "id" = "id",
29
 *     "label" = "id"
30
31
32
 *   }
 * )
 */
33
class FieldConfig extends ConfigEntityBase implements FieldConfigInterface {
34
35

  /**
36
   * The maximum length of the field name, in characters.
37
38
39
   *
   * For fields created through Field UI, this includes the 'field_' prefix.
   */
40
  const NAME_MAX_LENGTH = 32;
41
42

  /**
43
44
45
46
47
48
49
50
51
52
53
54
   * The field ID.
   *
   * The ID consists of 2 parts: the entity type and the field name.
   *
   * Example: node.body, user.field_main_image.
   *
   * @var string
   */
  public $id;

  /**
   * The field name.
55
56
   *
   * This is the name of the property under which the field values are placed in
57
58
   * an entity: $entity->{$field_name}. The maximum length is
   * Field:NAME_MAX_LENGTH.
59
60
61
62
63
   *
   * Example: body, field_main_image.
   *
   * @var string
   */
64
  public $name;
65

66
67
68
69
70
71
72
  /**
   * The name of the entity type the field can be attached to.
   *
   * @var string
   */
  public $entity_type;

73
74
75
  /**
   * The field type.
   *
76
   * Example: text, integer.
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
   *
   * @var string
   */
  public $type;

  /**
   * The name of the module that provides the field type.
   *
   * @var string
   */
  public $module;

  /**
   * Field-type specific settings.
   *
   * An array of key/value pairs, The keys and default values are defined by the
93
   * field type.
94
95
96
   *
   * @var array
   */
97
  public $settings = array();
98
99
100
101
102

  /**
   * The field cardinality.
   *
   * The maximum number of values the field can hold. Possible values are
103
104
   * positive integers or
   * FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED. Defaults to 1.
105
   *
106
   * @var int
107
   */
108
  public $cardinality = 1;
109
110
111
112
113
114
115
116

  /**
   * Flag indicating whether the field is translatable.
   *
   * Defaults to FALSE.
   *
   * @var bool
   */
117
  public $translatable = FALSE;
118
119
120
121
122
123
124
125
126
127
128
129
130

  /**
   * Flag indicating whether the field is available for editing.
   *
   * If TRUE, some actions not available though the UI (but are still possible
   * through direct API manipulation):
   * - field settings cannot be changed,
   * - new instances cannot be created
   * - existing instances cannot be deleted.
   * Defaults to FALSE.
   *
   * @var bool
   */
131
  public $locked = FALSE;
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148

  /**
   * The custom storage indexes for the field data storage.
   *
   * This set of indexes is merged with the "default" indexes specified by the
   * field type in hook_field_schema() to determine the actual set of indexes
   * that get created.
   *
   * The indexes are defined using the same definition format as Schema API
   * index specifications. Only columns that are part of the field schema, as
   * defined by the field type in hook_field_schema(), are allowed.
   *
   * Some storage backends might not support indexes, and discard that
   * information.
   *
   * @var array
   */
149
  public $indexes = array();
150
151
152
153
154
155
156
157
158
159
160
161
162
163

  /**
   * Flag indicating whether the field is deleted.
   *
   * The delete() method marks the field as "deleted" and removes the
   * corresponding entry from the config storage, but keeps its definition in
   * the state storage while field data is purged by a separate
   * garbage-collection process.
   *
   * Deleted fields stay out of the regular entity lifecycle (notably, their
   * values are not populated in loaded entities, and are not saved back).
   *
   * @var bool
   */
164
  public $deleted = FALSE;
165
166
167
168
169
170
171
172

  /**
   * The field schema.
   *
   * @var array
   */
  protected $schema;

173
174
175
176
177
178
179
180
181
  /**
   * An array of field property definitions.
   *
   * @var \Drupal\Core\TypedData\DataDefinitionInterface[]
   *
   * @see \Drupal\Core\TypedData\ComplexDataDefinitionInterface::getPropertyDefinitions()
   */
  protected $propertyDefinitions;

182
  /**
183
   * Constructs a FieldConfig object.
184
185
186
187
188
189
   *
   * @param array $values
   *   An array of field properties, keyed by property name. Most array
   *   elements will be used to set the corresponding properties on the class;
   *   see the class property documentation for details. Some array elements
   *   have special meanings and a few are required. Special elements are:
190
   *   - name: required. As a temporary Backwards Compatibility layer right now,
191
   *     a 'field_name' property can be accepted in place of 'id'.
192
   *   - entity_type: required.
193
194
195
   *   - type: required.
   *
   * In most cases, Field entities are created via
196
   * entity_create('field_config', $values)), where $values is the same
197
198
199
200
201
   * parameter as in this constructor.
   *
   * @see entity_create()
   *
   * @ingroup field_crud
202
   */
203
  public function __construct(array $values, $entity_type = 'field_config') {
204
    // Check required properties.
205
    if (empty($values['name'])) {
206
207
      throw new FieldException('Attempt to create an unnamed field.');
    }
208
    if (!preg_match('/^[_a-z]+[_a-z0-9]*$/', $values['name'])) {
209
      throw new FieldException(format_string('Attempt to create a field @field_name with invalid characters. Only lowercase alphanumeric characters and underscores are allowed, and only lowercase letters and underscore are allowed as the first character', array('@field_name' => $values['name'])));
210
    }
211
    if (empty($values['type'])) {
212
      throw new FieldException(format_string('Attempt to create field @field_name with no type.', array('@field_name' => $values['name'])));
213
214
    }
    if (empty($values['entity_type'])) {
215
      throw new FieldException(format_string('Attempt to create a field @field_name with no entity_type.', array('@field_name' => $values['name'])));
216
    }
217
218
219
220

    parent::__construct($values, $entity_type);
  }

221
222
223
224
225
226
227
  /**
   * {@inheritdoc}
   */
  public function id() {
    return $this->entity_type . '.' . $this->name;
  }

228
229
230
  /**
   * {@inheritdoc}
   */
231
  public function toArray() {
232
233
234
235
    $names = array(
      'uuid',
      'status',
      'langcode',
236
237
      'name',
      'entity_type',
238
239
240
241
242
243
244
      'type',
      'settings',
      'module',
      'locked',
      'cardinality',
      'translatable',
      'indexes',
245
      'dependencies',
246
    );
247
248
249
    $properties = array(
      'id' => $this->id(),
    );
250
251
252
253
254
255
256
    foreach ($names as $name) {
      $properties[$name] = $this->get($name);
    }
    return $properties;
  }

  /**
257
   * Overrides \Drupal\Core\Entity\Entity::preSave().
258
259
260
261
262
   *
   * @throws \Drupal\field\FieldException
   *   If the field definition is invalid.
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   In case of failures at the configuration storage level.
263
   */
264
  public function preSave(EntityStorageInterface $storage) {
265
    // Clear the derived data about the field.
266
    unset($this->schema);
267
268

    if ($this->isNew()) {
269
      $this->preSaveNew($storage);
270
271
    }
    else {
272
      $this->preSaveUpdated($storage);
273
274
275
276
    }
    if (!$this->isSyncing()) {
      // Ensure the correct dependencies are present.
      $this->calculateDependencies();
277
278
    }
  }
279

280
  /**
281
   * Prepares saving a new field definition.
282
   *
283
284
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The entity storage.
285
   *
286
   * @throws \Drupal\field\FieldException If the field definition is invalid.
287
   */
288
   protected function preSaveNew(EntityStorageInterface $storage) {
289
    $entity_manager = \Drupal::entityManager();
290
    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
291

292
293
294
    // Assign the ID.
    $this->id = $this->id();

295
296
    // Field name cannot be longer than FieldConfig::NAME_MAX_LENGTH characters.
    // We use Unicode::strlen() because the DB layer assumes that column widths
297
    // are given in characters rather than bytes.
298
    if (Unicode::strlen($this->name) > static::NAME_MAX_LENGTH) {
299
      throw new FieldException(format_string(
300
301
302
        'Attempt to create a field with an ID longer than @max characters: %name', array(
          '@max' => static::NAME_MAX_LENGTH,
          '%name' => $this->name,
303
304
305
        )
      ));
    }
306

307
308
309
310
    // Disallow reserved field names.
    $disallowed_field_names = array_keys($entity_manager->getBaseFieldDefinitions($this->entity_type));
    if (in_array($this->name, $disallowed_field_names)) {
      throw new FieldException(format_string('Attempt to create field %name which is reserved by entity type %type.', array('%name' => $this->name, '%type' => $this->entity_type)));
311
312
313
    }

    // Check that the field type is known.
314
    $field_type = $field_type_manager->getDefinition($this->type, FALSE);
315
316
317
    if (!$field_type) {
      throw new FieldException(format_string('Attempt to create a field of unknown type %type.', array('%type' => $this->type)));
    }
318
    $this->module = $field_type['provider'];
319

320
321
    // Make sure all settings are present, so that a complete field
    // definition is passed to the various hooks and written to config.
322
     $this->settings += $field_type_manager->getDefaultSettings($this->type);
323

324
325
    // Notify the entity storage.
    $entity_manager->getStorage($this->entity_type)->onFieldCreate($this);
326
  }
327

328
329
330
331
332
333
334
  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    parent::calculateDependencies();
    // Ensure the field is dependent on the providing module.
    $this->addDependency('module', $this->module);
335
336
337
    // Ensure the field is dependent on the provider of the entity type.
    $entity_type = \Drupal::entityManager()->getDefinition($this->entity_type);
    $this->addDependency('module', $entity_type->getProvider());
338
339
340
    return $this->dependencies;
  }

341
  /**
342
   * Prepares saving an updated field definition.
343
   *
344
345
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The entity storage.
346
   */
347
  protected function preSaveUpdated(EntityStorageInterface $storage) {
348
    $module_handler = \Drupal::moduleHandler();
349
    $entity_manager = \Drupal::entityManager();
350
    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
351

352
    // Some updates are always disallowed.
353
    if ($this->type != $this->original->type) {
354
355
      throw new FieldException("Cannot change an existing field's type.");
    }
356
    if ($this->entity_type != $this->original->entity_type) {
357
      throw new FieldException("Cannot change an existing field's entity_type.");
358
359
    }

360
361
362
    // Make sure all settings are present, so that a complete field
    // definition is passed to the various hooks and written to config.
    $this->settings += $field_type_manager->getDefaultSettings($this->type);
363
364

    // See if any module forbids the update by throwing an exception. This
365
366
    // invokes hook_field_config_update_forbid().
    $module_handler->invokeAll('field_config_update_forbid', array($this, $this->original));
367

368
    // Notify the storage. The controller can reject the definition
369
370
    // update as invalid by raising an exception, which stops execution before
    // the definition is written to config.
371
    $entity_manager->getStorage($this->entity_type)->onFieldUpdate($this);
372
  }
373

374
375
376
  /**
   * {@inheritdoc}
   */
377
  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
378
    // Clear the cache.
379
    \Drupal::entityManager()->clearCachedFieldDefinitions();
380
381
382
383
384
385
386
387
388

    if ($update) {
      // Invalidate the render cache for all affected entities.
      $entity_manager = \Drupal::entityManager();
      $entity_type = $this->getTargetEntityTypeId();
      if ($entity_manager->hasController($entity_type, 'view_builder')) {
        $entity_manager->getViewBuilder($entity_type)->resetCache();
      }
    }
389
390
391
392
393
  }

  /**
   * {@inheritdoc}
   */
394
  public static function preDelete(EntityStorageInterface $storage, array $fields) {
395
    $state = \Drupal::state();
396
    $instance_storage = \Drupal::entityManager()->getStorage('field_instance_config');
397
398

    // Delete instances first. Note: when deleting a field through
399
    // FieldInstanceConfig::postDelete(), the instances have been deleted already, so
400
401
402
403
404
405
406
    // no instances will be found here.
    $instance_ids = array();
    foreach ($fields as $field) {
      if (!$field->deleted) {
        foreach ($field->getBundles() as $bundle) {
          $instance_ids[] = "{$field->entity_type}.$bundle.{$field->name}";
        }
407
      }
408
409
    }
    if ($instance_ids) {
410
      $instances = $instance_storage->loadMultiple($instance_ids);
411
412
413
      // Tag the objects to preserve recursive deletion of the field.
      foreach ($instances as $instance) {
        $instance->noFieldDelete = TRUE;
414
      }
415
      $instance_storage->delete($instances);
416
    }
417

418
419
420
421
422
    // Keep the field definitions in the state storage so we can use them later
    // during field_purge_batch().
    $deleted_fields = $state->get('field.field.deleted') ?: array();
    foreach ($fields as $field) {
      if (!$field->deleted) {
423
        $config = $field->toArray();
424
425
426
427
428
        $config['deleted'] = TRUE;
        $config['bundles'] = $field->getBundles();
        $deleted_fields[$field->uuid] = $config;
      }
    }
429

430
431
    $state->set('field.field.deleted', $deleted_fields);
  }
432

433
434
435
  /**
   * {@inheritdoc}
   */
436
  public static function postDelete(EntityStorageInterface $storage, array $fields) {
437
438
439
    // Notify the storage.
    foreach ($fields as $field) {
      if (!$field->deleted) {
440
        \Drupal::entityManager()->getStorage($field->entity_type)->onFieldDelete($field);
441
        $field->deleted = TRUE;
442
      }
443
    }
444
445

    // Clear the cache.
446
    \Drupal::entityManager()->clearCachedFieldDefinitions();
447
448
449
  }

  /**
450
   * {@inheritdoc}
451
452
453
   */
  public function getSchema() {
    if (!isset($this->schema)) {
454
      // Get the schema from the field item class.
455
      $class = $this->getFieldItemClass();
456
457
458
      $schema = $class::schema($this);
      // Fill in default values for optional entries.
      $schema += array('indexes' => array(), 'foreign keys' => array());
459
460

      // Check that the schema does not include forbidden column names.
461
      if (array_intersect(array_keys($schema['columns']), static::getReservedColumns())) {
462
        throw new FieldException(format_string('Illegal field type @field_type on @field_name.', array('@field_type' => $this->type, '@field_name' => $this->name)));
463
464
465
466
467
468
469
470
471
472
473
474
      }

      // Merge custom indexes with those specified by the field type. Custom
      // indexes prevail.
      $schema['indexes'] = $this->indexes + $schema['indexes'];

      $this->schema = $schema;
    }

    return $this->schema;
  }

475
476
477
478
479
480
481
  /**
   * {@inheritdoc}
   */
  public function hasCustomStorage() {
    return FALSE;
  }

482
483
484
485
486
487
488
489
490
491
492
493
494
495
  /**
   * {@inheritdoc}
   */
  public function getColumns() {
    $schema = $this->getSchema();
    // A typical use case for the method is to iterate on the columns, while
    // some other use cases rely on identifying the first column with the key()
    // function. Since the schema is persisted in the Field object, we take care
    // of resetting the array pointer so that the former does not interfere with
    // the latter.
    reset($schema['columns']);
    return $schema['columns'];
  }

496
  /**
497
   * {@inheritdoc}
498
499
500
   */
  public function getBundles() {
    if (empty($this->deleted)) {
501
      $map = \Drupal::entityManager()->getFieldMap();
502
503
      if (isset($map[$this->entity_type][$this->name]['bundles'])) {
        return $map[$this->entity_type][$this->name]['bundles'];
504
505
506
507
508
      }
    }
    return array();
  }

509
510
511
  /**
   * {@inheritdoc}
   */
512
  public function getName() {
513
    return $this->name;
514
515
516
517
518
  }

  /**
   * {@inheritdoc}
   */
519
  public function getType() {
520
521
522
523
524
525
    return $this->type;
  }

  /**
   * {@inheritdoc}
   */
526
  public function getSettings() {
527
528
529
530
    // @todo FieldTypePluginManager maintains its own static cache. However, do
    //   some CPU and memory profiling to see if it's worth statically caching
    //   $field_type_info, or the default field and instance settings, within
    //   $this.
531
    $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
532

533
    $settings = $field_type_manager->getDefaultSettings($this->type);
534
    return $this->settings + $settings;
535
536
537
538
539
  }

  /**
   * {@inheritdoc}
   */
540
541
  public function getSetting($setting_name) {
    // @todo See getSettings() about potentially statically caching this.
542
543
    // We assume here that one call to array_key_exists() is more efficient
    // than calling getSettings() when all we need is a single setting.
544
    if (array_key_exists($setting_name, $this->settings)) {
545
546
      return $this->settings[$setting_name];
    }
547
548
549
    $settings = $this->getSettings();
    if (array_key_exists($setting_name, $settings)) {
      return $settings[$setting_name];
550
    }
551
552
553
    else {
      return NULL;
    }
554
555
556
557
558
  }

  /**
   * {@inheritdoc}
   */
559
  public function isTranslatable() {
560
561
562
    return $this->translatable;
  }

563
564
565
566
567
568
569
570
  /**
   * {@inheritdoc}
   */
  public function isRevisionable() {
    // All configurable fields are revisionable.
    return TRUE;
  }

571
572
573
574
575
576
  /**
   * Sets whether the field is translatable.
   *
   * @param bool $translatable
   *   Whether the field is translatable.
   *
577
   * @return $this
578
579
580
581
582
583
   */
  public function setTranslatable($translatable) {
    $this->translatable = $translatable;
    return $this;
  }

584
585
586
587
588
589
590
  /**
   * {@inheritdoc}
   */
  public function getProvider() {
    return 'field';
  }

591
592
593
  /**
   * {@inheritdoc}
   */
594
  public function getLabel() {
595
596
597
598
599
600
    return $this->label();
  }

  /**
   * {@inheritdoc}
   */
601
  public function getDescription() {
602
    return NULL;
603
604
605
606
607
  }

  /**
   * {@inheritdoc}
   */
608
  public function getCardinality() {
609
610
611
612
613
614
    return $this->cardinality;
  }

  /**
   * {@inheritdoc}
   */
615
  public function isRequired() {
616
617
618
    return FALSE;
  }

619
620
621
  /**
   * {@inheritdoc}
   */
622
623
  public function isMultiple() {
    $cardinality = $this->getCardinality();
624
    return ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) || ($cardinality > 1);
625
626
  }

627
628
629
630
631
632
633
  /**
   * {@inheritdoc}
   */
  public function isLocked() {
    return $this->locked;
  }

634
635
  /**
   * {@inheritdoc}
636
637
638
639
640
641
642
   */
  public function getTargetEntityTypeId() {
    return $this->entity_type;
  }

  /**
   * {@inheritdoc}
643
   */
644
  public function isQueryable() {
645
646
647
    return TRUE;
  }

648
649
650
651
652
653
654
655
  /**
   * A list of columns that can not be used as field type columns.
   *
   * @return array
   */
  public static function getReservedColumns() {
    return array('deleted');
  }
656

657
658
659
  /**
   * Determines whether a field has any data.
   *
660
   * @return bool
661
662
663
   *   TRUE if the field has data for any entity; FALSE otherwise.
   */
  public function hasData() {
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
    return $this->entityCount(TRUE);
  }

  /**
   * Determines the number of entities that have field data.
   *
   * @param bool $as_bool
   *   (Optional) Optimises query for hasData(). Defaults to FALSE.
   *
   * @return bool|int
   *   The number of entities that have field data. If $as_bool parameter is
   *   TRUE then the value will either be TRUE or FALSE.
   */
  public function entityCount($as_bool = FALSE) {
    $count = 0;
    $factory = \Drupal::service('entity.query');
    $entity_type = \Drupal::entityManager()->getDefinition($this->entity_type);
    // Entity Query throws an exception if there is no base table.
    if ($entity_type->getBaseTable()) {
      if ($this->deleted) {
        $query = $factory->get($this->entity_type)
          ->condition('id:' . $this->uuid() . '.deleted', 1);
686
      }
687
688
689
690
691
692
693
694
695
      elseif ($this->getBundles()) {
        $storage_details = $this->getSchema();
        $columns = array_keys($storage_details['columns']);
        $query = $factory->get($this->entity_type);
        $group = $query->orConditionGroup();
        foreach ($columns as $column) {
          $group->exists($this->name . '.' . $column);
        }
        $query = $query->condition($group);
696
      }
697
698
699
700
701
702
703
704
705
706
707

      if (isset($query)) {
        $query
          ->count()
          ->accessCheck(FALSE);
        // If we are performing the query just to check if the field has data
        // limit the number of rows returned by the subquery.
        if ($as_bool) {
          $query->range(0, 1);
        }
        $count = $query->execute();
708
709
710
      }
    }

711
712
713
714
715
716
    if ($as_bool) {
      return (bool) $count;
    }
    else {
      return (int) $count;
    }
717
  }
718
719
720
721
722
723
724
725
726

  /**
   * Implements the magic __sleep() method.
   *
   * Using the Serialize interface and serialize() / unserialize() methods
   * breaks entity forms in PHP 5.4.
   * @todo Investigate in https://drupal.org/node/2074253.
   */
  public function __sleep() {
727
728
    // Only serialize properties from self::toArray().
    return array_keys(array_intersect_key($this->toArray(), get_object_vars($this)));
729
730
731
732
733
734
  }

  /**
   * Implements the magic __wakeup() method.
   */
  public function __wakeup() {
735
736
    // Run the values from self::toArray() through __construct().
    $values = array_intersect_key($this->toArray(), get_object_vars($this));
737
738
739
    $this->__construct($values);
  }

740
741
742
743
744
745
746
  /**
   * {@inheritdoc}
   */
  public function getConstraints() {
    return array();
  }

747
748
749
750
751
752
753
  /**
   * {@inheritdoc}
   */
  public function getConstraint($constraint_name) {
    return NULL;
  }

754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
  /**
   * {@inheritdoc}
   */
  public function getPropertyDefinition($name) {
    if (!isset($this->propertyDefinitions)) {
      $this->getPropertyDefinitions();
    }
    if (isset($this->propertyDefinitions[$name])) {
      return $this->propertyDefinitions[$name];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getPropertyDefinitions() {
    if (!isset($this->propertyDefinitions)) {
      $class = $this->getFieldItemClass();
      $this->propertyDefinitions = $class::propertyDefinitions($this);
    }
    return $this->propertyDefinitions;
  }

  /**
   * {@inheritdoc}
   */
  public function getPropertyNames() {
    return array_keys($this->getPropertyDefinitions());
  }

  /**
   * {@inheritdoc}
   */
  public function getMainPropertyName() {
    $class = $this->getFieldItemClass();
    return $class::mainPropertyName();
  }

  /**
   * Helper to retrieve the field item class.
   */
  protected function getFieldItemClass() {
796
797
798
    $type_definition = \Drupal::typedDataManager()
      ->getDefinition('field_item:' . $this->getType());
    return $type_definition['class'];
799
800
  }

801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
  /**
   * Loads a field config entity based on the entity type and field name.
   *
   * @param string $entity_type_id
   *   ID of the entity type.
   * @param string $field_name
   *   Name of the field.
   *
   * @return static
   *   The field config entity if one exists for the provided field name,
   *   otherwise NULL.
   */
  public static function loadByName($entity_type_id, $field_name) {
    return \Drupal::entityManager()->getStorage('field_config')->load($entity_type_id . '.' . $field_name);
  }

817
}