This is a prerequisite to #2937279: [META] Introduce concept of internal resource types.

We need to provide links to related resources to support BC and HATEOAS. However, when resource types may be marked as "internal", those related routes might not be available. Therefore, when we're in the serialization context of the referencing resource type, we need to be able to discover all the resource types to which a field may point and remove those links if all the referenced resource types are internal.

To achieve this, we need to add a discoverability mechanism to ResourceTypes.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

gabesullice created an issue. See original summary.

e0ipso’s picture

Issue summary: View changes
e0ipso’s picture

@gabesullice thanks for creating this.

Wim Leers’s picture

Issue tags: +blocker

Adding blocker tag, since the IS says blocks #2937279: [META] Introduce concept of internal resource types.

gabesullice’s picture

I'm still actively working on this.

All tests should pass and all the big ideas are fleshed out. I know of some code standard violations that I still need to fix. Just don't have enough time left today.

Feel free to look at it for architecture, but please save yourself the time and don't yet look for the little things :)

Or wait... that works too!

gabesullice’s picture

LOL just kidding! I wish all patches we that easy to review. I totally meant to do that.

e0ipso’s picture

Status: Active » Needs review

Kicking off tests and not reviewing until @gabesullice says so.

e0ipso’s picture

Status: Needs review » Needs work

Back to needs work as per #5.

gabesullice’s picture

Status: Needs work » Needs review

Fails look like testbot problems, let's try again

gabesullice’s picture

@e0ipso, this is (really) ready for review.

Status: Needs review » Needs work

The last submitted patch, 10: 2937961-10.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review
FileSize
60.11 KB

Reroll.

Wim Leers’s picture

WOAH 60K patch!

Most important question before I dive into a review (also because it contributes significantly to the patch size): why introduce an interface? We explicitly removed the interface for ResourceType before, because it communicates "you can swap this for your own implementation". It also means we have to commit to that interface, and have to keep supporting it.

I do see that you marked it @deprecated already. But not @internal

But is it even necessary?

gabesullice’s picture

Status: Needs review » Needs work

Given @Wim Leers points about the interface and some chats w/ @e0ipso, I think I need to make a little course adjustment.

Gonna drop the interface and drop the setter injection. That should make this a little smaller in scope.

Back to review soon.

Wim Leers’s picture

👌

gabesullice’s picture

Alright, hopefully this'll cut the patch size (almost) in half and address some of the initial concerns. I'm not sure the interdiff will be all that useful, but including it anyway.

gabesullice’s picture

FileSize
35.66 KB
952 bytes

Missed a hunk in the patch

The last submitted patch, 16: 2937961-16.patch, failed testing. View results

Wim Leers’s picture

Much better! Almost 50% smaller patch, nice!

  1. +++ b/src/ResourceType/ResourceType.php
    @@ -15,32 +22,64 @@ namespace Drupal\jsonapi\ResourceType;
    +   * @var \Drupal\Core\Entity\EntityTypeInterface|\Drupal\Core\Entity\FieldableEntityInterface
    

    These interfaces are for very different things. I think the latter is the wrong one?

  2. +++ b/src/ResourceType/ResourceType.php
    @@ -145,21 +184,140 @@ class ResourceType {
    +   * The return value from this method is a multi-dimensional array keyed by the
    +   * public field name, where each value is an array of relatable resource
    +   * for that particular field. There may be duplicates across keys, but not
    +   * per key.
    +   *
    +   * @return array
    +   *   The relatable resource types, keyed by relationship field names.
    

    I'd expect the paragraph above the @return to be part of the docs under @return.

  3. +++ b/src/ResourceType/ResourceType.php
    @@ -145,21 +184,140 @@ class ResourceType {
    +  public function getRelatableResourceTypes() {
    

    As a non-native speaker, I wonder if relatable is the correct adjective here?

    relatable |rəˈlādəb(ə)l|
    adjective
    1 able to be related to something else: the growth of the welfare state will be clearly relatable to the growth of democracy.
    2 enabling a person to feel that they can relate to someone or something: Mary-Kate's problems make her more relatable.
    

    I instantly think of the second meaning, but it's of course the first meaning. Sounds right.

  4. +++ b/src/ResourceType/ResourceType.php
    @@ -145,21 +184,140 @@ class ResourceType {
    +      throw new \LogicException("calculateRelatableResourceTypes must be called before getting relatable resource types.");
    

    Why can't we call this automatically?

  5. +++ b/src/ResourceType/ResourceType.php
    @@ -145,21 +184,140 @@ class ResourceType {
    +  public function getRelatableResourceTypesByField($field_name) {
    +    if (!isset($this->relatableResourceTypes)) {
    +      throw new \LogicException("calculateRelatableResourceTypes() must be called before getting relatable resource types.");
    +    }
    +    return isset($this->relatableResourceTypes[$field_name]) ?
    +      $this->relatableResourceTypes[$field_name] :
    +      [];
    +  }
    

    I'd expect this to just call getRelateableResourcetTypes() rather than duplicating some of the logic.

  6. +++ b/src/ResourceType/ResourceType.php
    @@ -145,21 +184,140 @@ class ResourceType {
    +  public function calculateRelatableResourceTypes(ResourceTypeRepositoryInterface $resource_type_repository) {
    

    Per the above, IMHO this should be protected.

  7. +++ b/tests/src/Kernel/Controller/EntityResourceTest.php
    @@ -872,8 +873,15 @@ class EntityResourceTest extends JsonapiKernelTestBase {
    +    $resource_type = $this->prophesize(ResourceType::class);
    +    $resource_type->getEntityTypeId()->willReturn($entity_type_id);
    +    $resource_type->getBundle()->willReturn($bundle);
    +    $resource_type->getTypeName()->willReturn($entity_type_id . '--' . $bundle);
    +    $resource_type->includeCount()->willReturn(TRUE);
    +    $resource_type->getInternalName(Argument::type("string"))->willReturnArgument(0);
    +
         return new EntityResource(
    -      new ResourceType($entity_type_id, $bundle, NULL),
    +      $resource_type->reveal(),
    

    This feels like a regression. ResourceType used to be pure value objects. Now they no longer are, so we need to resort to mocking.

    This is the consequence of injecting services into ResourceType.

    Have you considered turning a ResourceTypeRepository into a factory (or even introducing a separate factory), so that all logic can be contained there, and ResourceType can continue to be a value object?

    Then we can have a kernel test for the factory, rather than for the value object. All tests that merely consume ResourceType value objects could then remain far simpler.

    It would also keep the codebase easier to reason about.

gabesullice’s picture

FileSize
40.15 KB
15.6 KB

Whew, I had no idea this issue would end up being such a time sink. I'm not surprised though, the issue of related resource types has eaten up a ton of brain power in every other issue that runs into it (see the FieldResolver). Hopefully this will set us up to solve those issues for good though, so I think it's worth really getting this right :)

I was able to completely revert all the unit test changes and restore most of the ResourceType class to its original form. Now it only has the addition of the three needed methods for the related resource type stuff. Calculating the related resource types has been moved to the repository. AND I think we've cut the patch in half again!

1. Fixed.
2. Done.
3. 👍
4. We can only calculate and set the related resources after all the resource type have been defined. If we were to calculate them when the resource type is first constructed and the resource type has a reference to a not-yet-defined type, then an exception will be thrown as the referenced resource type can't be found.
5. Good call.
6. Moved this into the repository and made it protected.
7. I tried twice at making a factory and then deleted it again. For the same reasons as number 4 above, it's very hard to avoid a circular dependency where the resource repository relies on the factory and the factory relies on the repository.

Anticipating a question:

While I would have liked to avoid the setRelatedResourceTypes call, I can't possibly think of a way to do this so that we don't hit the dependency problem described above. I'm open to suggestions, but given that this is an internal class, I think the \LogicException is probably tolerable.

Wim Leers’s picture

Whew, I had no idea this issue would end up being such a time sink. I'm not surprised though, the issue of related resource types has eaten up a ton of brain power in every other issue that runs into it (see the FieldResolver). Hopefully this will set us up to solve those issues for good though, so I think it's worth really getting this right :)

+1 — and think of how much time getting this right (or at least better) will save us once this is in core!

I think we've cut the patch in half again!

You've been exponentially decreasing the size of the patch … could you do the same with Drupal core please? :P


OMG THIS BECAME 100X SIMPLER, love it now! I have only 3 nits. Then I will RTBC, and will assign to @e0ipso for final review + commit.

  1. +++ b/src/ResourceType/ResourceType.php
    @@ -158,8 +158,56 @@ class ResourceType {
         $this->deserializationTargetClass = $deserialization_target_class;
    -
         $this->typeName = sprintf('%s--%s', $this->entityTypeId, $this->bundle);
    

    Supernit: unnecessary change.

  2. +++ b/src/ResourceType/ResourceType.php
    @@ -158,8 +158,56 @@ class ResourceType {
    +   * Get all resource types with which the given field may have a relationship.
    +   *
    +   * Gets all the JSON API resource types that may be related by the given,
    +   * public field name.
    +   *
    

    I don't see much additional information in the second sentence here? Can we remove it? Or merge it into the @return docs?

  3. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -104,4 +122,91 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +  public function calculateRelatableResourceTypes(ResourceType $resource_type) {
    

    Should be protected — and AFAICT you even meant it to be :)

gabesullice’s picture

FileSize
15.35 KB
1.58 KB

Supernit: unnecessary change.

Jeeze, I can't get anything past you two.

You've been exponentially decreasing the size of the patch … could you do the same with Drupal core please

My first order of business would be to purge all the superfluous newlines.

2. Removed.

3. Eagle eye. Good catch.

Wim Leers’s picture

Assigned: gabesullice » e0ipso
Status: Needs review » Reviewed & tested by the community
e0ipso’s picture

This looks good to me. Some clarifications before committing.

  1. +++ b/src/ResourceType/ResourceTypeRepository.php
    @@ -104,4 +122,91 @@ class ResourceTypeRepository implements ResourceTypeRepositoryInterface {
    +    $entity_type_id = $item_definition->getSetting('target_type');
    +    $handler_settings = $item_definition->getSetting('handler_settings');
    

    Can we ensure that all relationship fields follow this pattern? Probably not. This will work with entity reference (and cousins).

    Should we document this contract somehow?

  2. +++ b/tests/src/Kernel/Context/FieldResolverTest.php
    @@ -16,7 +16,13 @@ use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
    +    'user',
    

    Why this addition?

gabesullice’s picture

Issue summary: View changes

1. That's a really good question and it brings up a deeper point... We really only support entity reference fields and subclasses of it. See the following from EntityResource.php:

  /**
   * ...
   * @return bool
   *   Returns TRUE, if entity field is EntityReferenceItem.
   */
  protected function isRelationshipField($entity_field) {
    $class = $this->pluginManager->getPluginClass($entity_field->getDataDefinition()->getType());
    return ($class == EntityReferenceItem::class || is_subclass_of($class, EntityReferenceItem::class));
  }

The code in this patch is a little more liberal than that already. Do we ever want to support reference types that aren't descendants of EntityReferenceItem? My guess is that the answer is going to be yes. I just don't know how we should do that without examples of such a field type.

TBH, I think this is really begging for an interface in core that would do a lot of the work in this patch for us. ($field_definition->getReferenceableEntityTypes() or something like that. The space is just too unexplored for us to introduce that I think. Questions like, "what about a reference field which isn't constrained by an entity type?" come up. As far as we're concerned, that would be perfectly fine since we've flattened entity type + bundles into "resource types" already.

This is a really long winded of saying, maybe the best way to handle this is simply to do nothing and simply wait for the bug report/support request that says "my custom field type isn't handled as a relationship, why?"

2. Simply: "because the test breaks without it". More thoroughly: because entity_test_with_bundle has an entity reference field user_id on it (I went down a rabbit hole trying to find our where this comes from with no luck) so that when we calculate the "relatable" resource types, we end up trying to get a definition for 'user' that doesn't exist and breaks the test.

Wim Leers’s picture

TBH, I think this is really begging for an interface in core that would do a lot of the work in this patch for us.

This is a really long winded of saying, maybe the best way to handle this is simply to do nothing and simply wait for the bug report/support request that says "my custom field type isn't handled as a relationship, why?"

Yes, the metadata infrastructure for "field that references other entities" is completely missing from core, we can't fix that. The only choice we have is to support entity_reference fields, until core provides richer metadata.

Wim Leers’s picture

Assigned: e0ipso » Unassigned

Given this is blocking a whole bunch of next steps, and @e0ipso already said in #24:

This looks good to me. Some clarifications before committing.

… implying that this was ready, definitely no major concerns, with just some clarifications desired, which @gabesullice posted in #25, I think it's time to get this in to unblock those next steps.

  • Wim Leers committed 843b6b2 on 8.x-1.x authored by gabesullice
    Issue #2937961 by gabesullice, Wim Leers, e0ipso: ResourceType should...
Wim Leers’s picture

Status: Reviewed & tested by the community » Fixed
Wim Leers’s picture

(Commit was delayed because git.drupal.org was down, I could not push!)

e0ipso’s picture

@gabesullice this one gets me excited! It makes me wonder if the resource type should also be aware of the attributes. Would that make the schema generation for Schemata super simple?

gabesullice’s picture

I don't know enough about schemata to answer that, but if it would, I'd be happy to work on that.

Status: Fixed » Closed (fixed)

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