Completed
Branch feature/linting (f62599)
by Christopher
02:11
created

FieldValueManager::ensureParagraphEntity()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\paragraphs_editor\EditorFieldValue;
4
5
use Drupal\Core\Entity\EntityFieldManagerInterface;
6
use Drupal\Core\Entity\EntityTypeManagerInterface;
7
use Drupal\Core\Field\FieldConfigInterface;
8
use Drupal\Core\Field\FieldDefinitionInterface;
9
use Drupal\Core\Field\FieldStorageDefinitionInterface;
10
use Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList;
11
use Drupal\paragraphs\ParagraphInterface;
12
13
/**
14
 * Manages the paragraphs editor field values.
15
 */
16
class FieldValueManager implements FieldValueManagerInterface {
17
18
  /**
19
   * The storage plugin for the paragraph entity type.
20
   *
21
   * @var \Drupal\Core\Entity\EntityStorageInterface
22
   */
23
  protected $storage;
24
25
  /**
26
   * The storage plugin for the paragraph type config entity type.
27
   *
28
   * @var \Drupal\Core\Entity\EntityStorageInterface
29
   */
30
  protected $bundleStorage;
31
32
  /**
33
   * The field value manager service for collecting field information.
34
   *
35
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
36
   */
37
  protected $entityFieldManager;
38
39
  /**
40
   * Element definitions for custom elements that can occur in an editor field.
41
   *
42
   * @var array
43
   */
44
  protected $elements;
45
46
  /**
47
   * A static cache of paragraph revisions.
48
   *
49
   * @var \Drupal\paragraphs\ParagraphInterface[]
50
   */
51
  protected $revisionCache = [];
52
53
  /**
54
   * Creates a field value manager object.
55
   *
56
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
57
   *   The field manager service.
58
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
59
   *   The entity type manager service.
60
   * @param array $elements
61
   *   An array of widget binder element definitions.
62
   */
63
  public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, array $elements) {
64
    $this->entityFieldManager = $entity_field_manager;
65
    $this->storage = $entity_type_manager->getStorage('paragraph');
66
    $this->bundleStorage = $entity_type_manager->getStorage('paragraphs_type');
67
    $this->elements = $elements;
68
  }
69
70
  /**
71
   * {@inheritdoc}
72
   */
73
  public function getReferencedEntities(EntityReferenceRevisionsFieldItemList $items) {
74
    $entities = [];
75
    foreach ($items as $item) {
76
      $value = $item->getValue();
77
      if (!empty($value['entity']) && $value['entity'] instanceof ParagraphInterface) {
78
        $entity = $item->entity;
79
      }
80
      elseif ($item->target_revision_id !== NULL) {
81
        if (!empty($this->revisionCache[$item->target_revision_id])) {
82
          $entity = $this->revisionCache[$item->target_revision_id];
83
        }
84
        else {
85
          $entity = $this->storage->loadRevision($item->target_revision_id);
86
          $this->revisionCache[$item->target_revision_id] = $entity;
87
        }
88
      }
89
      $entities[] = $entity;
0 ignored issues
show
Bug introduced by
The variable $entity does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
90
    }
91
    return $entities;
92
  }
93
94
  /**
95
   * {@inheritdoc}
96
   */
97
  public function wrapItems(EntityReferenceRevisionsFieldItemList $items) {
98
    $field_definition = $items->getFieldDefinition();
99
    if (!$this->isParagraphsEditorField($field_definition)) {
100
      throw new \Exception('Attempt to wrap non-paragraphs editor field.');
101
    }
102
103
    // Build a list of refrenced entities and filter out the text entities.
104
    $settings = $field_definition->getThirdPartySettings('paragraphs_editor');
105
    $markup = '';
106
    $entities = [];
107
    $text_entity = NULL;
108
109
    foreach ($this->getReferencedEntities($items) as $entity) {
110
      if ($entity->bundle() == $settings['text_bundle']) {
111
        $markup .= $entity->{$settings['text_field']}->value;
112
        if (!$text_entity) {
113
          $text_entity = $entity;
114
        }
115
      }
116
      else {
117
        $entities[$entity->uuid()] = $entity;
118
      }
119
    }
120
121
    // If there is no text entity we need to create one.
122
    if (!$text_entity) {
123
      $text_entity = $this->ensureParagraphEntity($this->storage->create([
0 ignored issues
show
Documentation introduced by
$this->storage->create(a...ttings['text_bundle'])) is of type object<Drupal\Core\Entity\EntityInterface>, but the function expects a object<Drupal\paragraphs...dValue\EntityInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
124
        'type' => $settings['text_bundle'],
125
      ]));
126
    }
127
128
    // Reset the text entity markup in case we merged multiple text entities.
129
    $text_entity->{$settings['text_field']}->value = $markup;
130
    if (empty($text_entity->{$settings['text_field']}->format) && !empty($settings['filter_format'])) {
131
      $text_entity->{$settings['text_field']}->format = $settings['filter_format'];
132
    }
133
134
    return new FieldValueWrapper($field_definition, $text_entity, $entities);
0 ignored issues
show
Compatibility introduced by
$field_definition of type object<Drupal\Core\Field...eldDefinitionInterface> is not a sub-type of object<Drupal\Core\Field\FieldConfigInterface>. It seems like you assume a child interface of the interface Drupal\Core\Field\FieldDefinitionInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
135
  }
136
137
  /**
138
   * {@inheritdoc}
139
   */
140
  public function prepareEntityForSave($entity, $new_revision, $langcode) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
141
    $entity->setNewRevision($new_revision);
142
143
    if (isset($langcode) && $entity->get('langcode') != $langcode) {
144
      if ($entity->hasTranslation($langcode)) {
145
        $entity = $entity->getTranslation($langcode);
146
      }
147
      else {
148
        $entity->set('langcode', $langcode);
149
      }
150
    }
151
152
    $entity->setNeedsSave(TRUE);
153
154
    return $entity;
155
  }
156
157
  /**
158
   * {@inheritdoc}
159
   */
160
  public function setItems(EntityReferenceRevisionsFieldItemList $items, array $entities, $new_revision = FALSE, $langcode = NULL) {
161
    $values = [];
162
    $delta = 0;
163
    foreach ($entities as $entity) {
164
      $entity = $this->prepareEntityForSave($entity, $new_revision, $langcode);
165
      $values[$delta]['entity'] = $entity;
166
      $values[$delta]['target_id'] = $entity->id();
167
      $values[$delta]['target_revision_id'] = $entity->getRevisionId();
168
      $delta++;
169
    }
170
171
    $items->setValue($values);
172
    $items->filterEmptyItems();
173
    return $items;
174
  }
175
176
  /**
177
   * {@inheritdoc}
178
   */
179
  public function getTextBundles(array $allowed_bundles = []) {
180
181
    if (!$allowed_bundles) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowed_bundles of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
182
      foreach ($this->bundleStorage->getQuery()->execute() as $name) {
0 ignored issues
show
Bug introduced by
The expression $this->bundleStorage->getQuery()->execute() of type integer|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
183
        $allowed_bundles[$name] = [
184
          'label' => $this->bundleStorage->load($name)->label(),
185
        ];
186
      }
187
    }
188
189
    $bundles = [];
190
    foreach ($allowed_bundles as $name => $type) {
191
      $text_fields = $this->getTextFields($name);
192
      if (count($text_fields) == 1) {
193
        $bundles[$name] = [
194
          'label' => $type['label'],
195
          'text_field' => reset($text_fields),
196
        ];
197
      }
198
    }
199
    return $bundles;
200
  }
201
202
  /**
203
   * {@inheritdoc}
204
   */
205
  public function isParagraphsField(FieldDefinitionInterface $field_definition) {
206
    if (!$field_definition instanceof FieldConfigInterface) {
207
      return FALSE;
208
    }
209
210
    if ($field_definition->getType() != 'entity_reference_revisions') {
211
      return FALSE;
212
    }
213
214
    if ($field_definition->getFieldStorageDefinition()->getSetting('target_type') != 'paragraph') {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return !($field_definiti...type') != 'paragraph');.
Loading history...
215
      return FALSE;
216
    }
217
218
    return TRUE;
219
  }
220
221
  /**
222
   * {@inheritdoc}
223
   */
224
  public function isParagraphsEditorField(FieldDefinitionInterface $field_definition) {
225
    if (!static::isParagraphsField($field_definition)) {
226
      return FALSE;
227
    }
228
229
    // We only every allow this widget to be applied to fields that have
230
    // unlimited cardinality. Otherwise we'd have to deal with keeping track of
231
    // how many paragraphs are in the Editor instance.
232
    $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality();
233
    if ($cardinality != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
234
      return FALSE;
235
    }
236
237
    // Make sure it is a pragraphs editor enabled field.
238
    $settings = $field_definition->getThirdPartySettings('paragraphs_editor');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Drupal\Core\Field\FieldDefinitionInterface as the method getThirdPartySettings() does only exist in the following implementations of said interface: Drupal\Core\Field\Entity\BaseFieldOverride, Drupal\Core\Field\FieldConfigBase, Drupal\field\Entity\FieldConfig.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
239
    if (empty($settings['enabled'])) {
240
      return FALSE;
241
    }
242
243
    // Make sure the bundle for storing text is valid.
244
    $text_bundle = $field_definition->getThirdPartySetting('paragraphs_editor', 'text_bundle');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Drupal\Core\Field\FieldDefinitionInterface as the method getThirdPartySetting() does only exist in the following implementations of said interface: Drupal\Core\Field\Entity\BaseFieldOverride, Drupal\Core\Field\FieldConfigBase, Drupal\field\Entity\FieldConfig.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
Unused Code introduced by
$text_bundle is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
245
    $text_field = $field_definition->getThirdPartySetting('paragraphs_editor', 'text_field');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Drupal\Core\Field\FieldDefinitionInterface as the method getThirdPartySetting() does only exist in the following implementations of said interface: Drupal\Core\Field\Entity\BaseFieldOverride, Drupal\Core\Field\FieldConfigBase, Drupal\field\Entity\FieldConfig.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
Unused Code introduced by
$text_field is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
246
247
    return TRUE;
248
  }
249
250
  /**
251
   * {@inheritdoc}
252
   */
253
  public function getTextFields($bundle_name) {
254
    $matches = [];
255
    $field_definitions = $this->entityFieldManager->getFieldDefinitions('paragraph', $bundle_name);
256
    foreach ($field_definitions as $field_definition) {
257
      if ($this->isTextField($field_definition)) {
258
        $matches[] = $field_definition->getName();
259
      }
260
    }
261
    return $matches;
262
  }
263
264
  /**
265
   * {@inheritdoc}
266
   */
267
  public function getElement($element_name) {
268
    return isset($this->elements[$element_name]) ? $this->elements[$element_name] : NULL;
269
  }
270
271
  /**
272
   * {@inheritdoc}
273
   */
274
  public function getAttributeName($element_name, $attribute_name) {
275
    $element = $this->getElement($element_name);
276
    if (!empty($element['attributes'])) {
277
      $map = array_flip($element['attributes']);
278
      $key = !empty($map[$attribute_name]) ? $map[$attribute_name] : NULL;
279
      return $key;
280
    }
281
    else {
282
      return NULL;
283
    }
284
  }
285
286
  /**
287
   * {@inheritdoc}
288
   */
289
  public function getSelector($element_name) {
290
    $element = $this->getElement($element_name);
291
    $selector = !empty($element['tag']) ? $element['tag'] : '';
292
    if (!empty($element['attributes']['class'])) {
293
      $classes = explode(' ', $element['attributes']['class']);
294
      $selector .= '.' . implode('.', $classes);
295
    }
296
    return $selector;
297
  }
298
299
  /**
300
   * Helper function to check if a field is a text field.
301
   *
302
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_config
303
   *   The field to check.
304
   *
305
   * @return bool
306
   *   TRUE if it's a paragraphs editor approved text field, FALSE otherwise.
307
   */
308
  protected function isTextField(FieldDefinitionInterface $field_config) {
309
    return $field_config->getType() == 'text_long';
310
  }
311
312
  /**
313
   * Enforces that an entity is a paragraph entity.
314
   *
315
   * @return \Drupal\paragraphs\ParagraphInterface|null
316
   *   The entity, or NULL if it was not a paragraph.
317
   */
318
  protected function ensureParagraphEntity(EntityInterface $entity) {
319
    return ($entity instanceof ParagraphEntity) ? $entity : NULL;
0 ignored issues
show
Bug introduced by
The class Drupal\paragraphs_editor...ldValue\ParagraphEntity does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
320
  }
321
322
}
323