Problem/Motivation

This is a follow-up for #2910682: Mark serializer encoders @internal.

Using encoder/normalizer services directly is not supported, one must use the serializer service, which will then call the appropriate encoder/normalizer services.

However, it is valid to inject a normalizer into another normalizer. Therefore we should only disallow code that does \Drupal::service('some normalizer') or $this->container->get('some normalizer').

Proposed resolution

Mark all encoder and normalizer services private. Which is what Symfony's serialization component should've been doing all along…

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#3 2920536-3.patch4.76 KBWim Leers
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Wim Leers’s picture

Issue summary: View changes

Imported relevant information from parent issue.

Wim Leers’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
4.76 KB

Please credit @damiankloip, who wrote most of this at #2910682-21: Mark serializer encoders @internal.

borisson_’s picture

Status: Needs review » Reviewed & tested by the community

This has tests that are very explicit and they clearly indicate that the functionality is working. This looks like a good way to make this internal functionality not exposed to the outside.

xjm credited Grayside.

xjm’s picture

Status: Reviewed & tested by the community » Needs review

Can we have Grayside's feedback over here as well on whether this resolves his concern?

Because this does look like a BC break. As such, it'd also need a change record.

Wim Leers’s picture

Thanks for crediting Grayside, whose feedback was indeed instrumental. Please also credit @damiankloip as requested in #3, because the patch itself was actually mostly written by him. I just split it off from #2910682: Mark serializer encoders @internal per the reviewers' request.

Wim Leers’s picture

#2910682: Mark serializer encoders @internal landed in the mean time, yay!

Grayside’s picture

I'm not sure if I understand the implications of private services well enough to say whether it's a use case problem.

I've read http://symfony.com/blog/new-in-symfony-3-2-improved-private-services and http://symfony.com/doc/current/service_container.html#public-versus-priv..., and if I understand correctly, our goal in making these services private is primarily to avoid them being accessed by static code, but not to prevent their use and abuse when injected as a service. I guess the intention is to inhibit the misuse of normalizers in procedural code?

The case I raised in the previous issue would have developers that need to do awkward things in custom formats with a pattern such as the following pseudo-code:

my_hal/my_hal.services.yml

services:
  serializer.normalizer.entity_reference_item.my_hal:
    class: Drupal\my_hal\Normalizer\EntityReferenceItemNormalizer
    arguments: ['@rest.link_manager', '@serializer.entity_resolver', '@serializer.normalizer.field_item.my_hal']
    tags:
      - { name: normalizer, priority: 10 }

my_hal/src/Normalizer/EntityReferenceItemNormalizer

class EntityReferenceItemNormalizer extends HalEntityReferenceItemNormalizer {

  public function __construct($arg1, $arg2, FieldItemNormalizer $normalizer) {
    $this->fieldItemNormalizer = $normalizer;
   }

  /**
   * {@inheritdoc}
   */
  public function normalize($field_item, $format = NULL, array $context = []) {
    $target_entity = $field_item->get('entity')->getValue();
    if ($this->shouldEmbedAsGenericField($target_entity)) {
      return $this->fieldItemNormalizer->normalize($field_item, $format, $context);
    }
    return parent::normalize($field_item, $format, $context);
  }
}

If that is correct, then marking these private is fine from a pure capabilities standpoint for developers to effectively vary serialization formats. However, anyone that's previously used \Drupal::service('serializer.normalizer.field_item.my_hal') as an ugly hack will of course be burned by an incompatibility. I don't see a way around that here without either adding a B/C layer or deciding that this is an exception case because clearly using the static wrapper to abuse the normalization process was not intended.

Also, as I noted in the other issue, I have seen a developer use a complex contrib encoder/decoder to do some data parsing where all they cared about was correctly deriving an intermediate, unclassed data structure. It's less clear in my mind that encoders/decoders should be locked to the serialization system. Is this primarily to ensure higher priority encoders for a given format will be loaded properly for all calling code? That said, if you do need to use an encoder outside serialization, it doesn't seem that big a problem to insist they register the class as a custom service.

Wim Leers’s picture

if I understand correctly, our goal in making these services private is primarily to avoid them being accessed by static code, but not to prevent their use and abuse when injected as a service. I guess the intention is to inhibit the misuse of normalizers in procedural code?

Correct!

If that is correct, then marking these private is fine from a pure capabilities standpoint for developers to effectively vary serialization formats.

Correct!

However, anyone that's previously used \Drupal::service('serializer.normalizer.field_item.my_hal') as an ugly hack will of course be burned by an incompatibility.

These services were never meant to be used directly. Could you? Yes, because it's just another service. Not every service is an actual supported API though.
You're absolutely right this is a hard line to walk: either we make things better/more explicit for the future and break perhaps a few custom pieces of code in the process. Or we choose to not break this in the process, but then we end up never making the future better.

Also, as I noted in the other issue, I have seen a developer use a complex contrib encoder/decoder to do some data parsing where all they cared about was correctly deriving an intermediate, unclassed data structure. It's less clear in my mind that encoders/decoders should be locked to the serialization system.

They should use $this->serialize->decode() for this purpose. There's no need to call encoders/decoders direclty.

Is this primarily to ensure higher priority encoders for a given format will be loaded properly for all calling code?

Yes.

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community
larowlan’s picture

@Grayside - are you happy with the responses from @Wim at #10

Grayside’s picture

Yes, that response clarifies the goals of this change and confirms my code in #9 is the intended direction for edge case private normalizer use.

Wim Leers’s picture

Thanks, @Grayside!

larowlan’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Needs change record

Can we get a change record here, I suspect this might cause some issues for people who're using them incorrectly.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs change record +maintainability

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

  • catch committed efd626d on 8.6.x
    Issue #2920536 by Wim Leers, Grayside: Force all serializer encoders +...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.6.x, thanks!

This missed the alpha, so leaving it out of 8.5.x for now, will update the change notice when I publish it.

Wim Leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.