Quantcast
Channel: Issues for Drupal core
Viewing all articles
Browse latest Browse all 292268

Add a ParamConverter for upcasting TypedData objects

$
0
0

I've been playing with this around in my head for the past couple of days and after discussions with Kris, Sam, Larry, etc. I came back to the conclusion that we -really- need this. And before the end of next week too. I am not going to go into the argument about why this is so fundamentally important as that has been explained in previous comments already. To summarize: A unified concept for explicitly describing the parameters of a route with metadata buys us generic implementation with all sorts of page manager features that Kris and Sam are working on in princess. Without that, we are making a huge leap backwards to D7 time where every route needs to be introduced to the pager manager through some sort of plugin. I hope everyone understand how much of a win this would be for sitebuilding...

Anyways... After a quick chat with @berdir about typed data and the planned changes that have been discussed at Portland (especially #2002102: Move TypedData primitive types to interfaces) and the LoadableInterface for typed data objects that I proposed over at #1983100: Provide a LoadableInterface for Typed Data objects there is a great opportunity for us to achieve this goal and even address the DX concerns people were having in this issue together. Let me go back in time and explain how we got to where we are now and then outline the idea that I have in mind:

How we ended up here

One of the first patches around param conversion (I think initially written by @katbailey?!) was built around the concept of reflection. While incredibly simple in terms of DX it did have some conceptual flaws and limitations and only covered entity upcasting back then. So we threw it out the window. After that, @Crell and @g.oechsler came up with the concept of "guessing" route arguments based on their naming pattern (e.g. {node} == Node entity). This looked pretty cool initially but turned out to be rather error-prone/unstable as explained in #1943846: Improve ParamConverterManager and developer experience for ParamConverters. So we decided to throw it out the window just as well and agreed that we need explicit declaration of parameter types. This is where we stand now...

However, when I saw how Routes and ParamConverters play together in SensioFrameworkExtraBundle I drooled and started thinking about this again. Now, after speaking with @EclipseGc and @sdboyer and also seeing that @effulgentsia has been more and more +1 on this lately (as well as some other folks, I guess Portland was a big help with that) as well as talking to @Crell last night I sense that it is time to start discussing this issue again and ultimately make a decision.

Pre-requisites for the following concept

Why TypedData?

TypedData is the perfect solution for our parameter conversion problem as we can use it as a generic object factory/object factory location with a unified pattern for defining the parameter types so they can be identified by other sub-systems such as a page manager which would then be capable of fully understanding any defined route without requiring an intermediate plugin layer for every single route definition.

Furthermore it becomes very easy to make custom objects "upcastable" by simply implementing them as typed data objects. And that is pretty much a no-brainer. In many cases this is probably going to be a by-product anyways as many objects will want to be typed data anyways as that buys them integration with other systems as well. Implement it once, integrate with all the things.

TypedData upcasting through Reflection (or other means of type discovery) and explicit parameter definition

Now, how to we implement this without turning route definition DX into a nightmare? The latest patch in this issue is solely based on defining the type and constraints of typed data types. However, as #2002102: Move TypedData primitive types to interfaces has been started lately (and there is already a patch) we will be able to take another look at Reflection to provide an absolutely straight-forward concept for benefiting from Upcasting by simply typehinting the controller arguments with the desired typed data interface as an alternative to manually defining the typed data type in the route options.

The ultimate goal is to end up with a nice typed data definition for each parameter that should be upcast (basically everything except for things that should remain the original input values). The typed data definition can either be manually defined in the route options or automatically discovered by Reflection of the controller or other means during RouteBuildEvent (which then writes the discovered definition into the route options). So whatever scenario applies, in the end we will have the typed data definition in the route options.

How would that work?

First scenario - Reflection

Let's assume we have a route definition that looks something like this:

aggregator_feed_refresh:
  pattern: '/admin/config/services/aggregator/update/{aggregator_feed}'
  defaults:
    _controller: '\Drupal\aggregator\Controller\AggregatorController::feedRefresh'
  requirements:
    _permission: 'administer news feeds'

And a controller that looks something like this:

<?php
 
/**
   * Refreshes a feed, then redirects to the overview page.
   *
   * @param \Drupal\aggregator\FeedInterface $aggregator_feed
   *   An object describing the feed to be refreshed.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request object containing the search string.
   *
   * @return \Symfony\Component\HttpFoundation\RedirectResponse
   *   A redirection to the admin overview page.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
   *   If the query token is missing or invalid.
   */
 
public function feedRefresh(FeedInterface $aggregator_feed, Request $request) {
   
// @todo CSRF tokens are validated in page callbacks rather than access
    //   callbacks, because access callbacks are also invoked during menu link
    //   generation. Add token support to routing: http://drupal.org/node/755584.
   
$token = $request->query->get('token');
    if (!isset(
$token) || !drupal_valid_token($token, 'aggregator/update/'. $aggregator_feed->id())) {
      throw new
AccessDeniedHttpException();
    }
   
// @todo after https://drupal.org/node/1972246 find a new place for it.
   
aggregator_refresh($aggregator_feed);
    return new
RedirectResponse(url('admin/config/services/aggregator', array('absolute'=> TRUE)));
  }
?>

Now, due to the fact that FeedInterface extends EntityInterface we already know that what we have here is an entity. Now, since all entities should define a unique interface like FeedInterface or NodeInterface we can iterate over the $entityManager->getDefinitions() and check (with Reflection) which entity type uses an entity class that implements FeedInterface. This is the entity type we want to upcast to. This obviously only works if the typehinted interface is unique for a given entity type. If it is ambiguous like an EntityInterface typehint or some other, custom SomethingBaseInterface the reflection discovery skips the given route.

Second scenario - Automatic discovery for ambigious or missing typehints

Things like HtmlEntityFormController::content() do not have typehints. However, they make other assumptions. A route definition for an entity form looks something like this:

   pattern: '/foo/{node}/baz'
     defaults:
       _entity_form: 'node'
   // Or like this:
   pattern: '/foo/{node}/baz/edit'
     defaults:
       _entity_form: 'node.edit'

We know that this is an entity form because the controller class is a HtmlEntityFormController. Hence, we can use the same type discovery logic that the controller uses: Retrieve the entity type from $route->getDefault('_enitty_form') by reading it from the string (@see HtmlEntityFormController::getFormObject()). Once retrieved we can, again, write it's typed data definition into the route options. Different discovery, same outcome.

The same process can be applied to basically everything we got. The only criteria we need is that, in the end, we get a nice typed data definition in the route options.

Third scenario - Manual definition in the route options

You might have single, custom routes in your module which do not justify writing a proper parameter type detection service for the RouteBuildEvent. In those cases, it is more convenient to manually define the typed data type for your parameter in the route options. Manual parameter type definitions always overrule the automatic discovery obviously meaning that the ParamConverterManager (or whatever we call the thing that subscribes to the RouteBuildEvent to iterate over each parameter and request type detection from the services that have been registered with it) would simply skip those arguments.

I am very optimistic that this describes a very nice, reliable and developer-friendly solution to our problem. Thoughts?

I would like to bump this issue to critical if we can agree that this is the way to go. I am also going to push Sam and Kris to comment on this once again to express how important this is for what they are building.


Viewing all articles
Browse latest Browse all 292268

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>