According to docs at https://www.drupal.org/node/2310425, we can use the entity name as a route parameter and the entity is magically loaded:

> There is nothing more to provide in the route for the parameter conversion to happen because user is the name of an entity type and it will get automatically get converted into a user object. In the controller, define an argument on the controller method with the same name to get this user object passed in.

Currently, we have:

flag_link_flag.html:
  path: '/flag/flag/{flag_id}/{entity_id}'

Then again, the link controller that has the route (ReloadLinkController in this example) doesn't need the flag entity, because it hands over to FlagService.

But then I don't really follow the reason for the flag/unflag logic to live in FlagService rather than the flag class like it does on 7.

Comments

socketwench’s picture

That was my original plan, but autoloading entities with two consecutive URL parameters was hopelessly broken when I tried it. We would have to change the route to something like /flag/flag/{flag}/flaggable/{entity} to separate the two parameters.

Now if we can get two consecutive parameters to work, sure! If we can simplify the URLs overall, that'd also be great. My prime concern then was to replicate the existing paths in the 7.x module.

joachim’s picture

Is this the sort of explosion you were seeing when you tried it?

Error message
Twig_Error_Runtime: An exception has been thrown during the rendering of a template ("Callable "Drupal\flag\FlaggingAccessController::checkFlag" requires a value for the "$flag" argument.") in "themes/garland/templates/node.html.twig" at line 87. in Twig_Template->displayWithErrorHandling() (line 294 of core/vendor/twig/twig/lib/Twig/Template.php).

Drupal\Component\Utility\ArgumentsResolver->getArgument(Object)
Drupal\Component\Utility\ArgumentsResolver->getArguments(Array)
Drupal\Core\Access\CustomAccessCheck->access(Object, Object, Object)
call_user_func_array(Array, Array)

What I find really difficult in D8 is that I have no idea how I'm meant to debug something like this. I've tried adding debug() statements to ArgumentsResolver::getArguments(), but they totally overload the output because everything calls it.

socketwench’s picture

Something like that. Although at the time I think it was just unresponsive.

berdir’s picture

That error sounds like you just forget to rename flag_id to flag somewhere where that route was used. The way to debug this is search for the route name, which I guess will point you to the link type plugins, which in turn points to http://cgit.drupalcode.org/flag/tree/src/ActionLinkTypeBase.php?h=8.x-4.....

Debugging in those method only helps to figure out the route that is the problem, because the rendering/building happens indirectly, what you need to find is where it is defined initially.

Yes, there's a lot of indirection in place, but to be fair, a considerable amount of that is in flag module itself, in those link plugins, that separates the route name into a separate method, so that route name and parameters aren't in the same place. Which saves a few lines of code and means you have to update a single place instead of 4, but that single place in turn is harder to find. There's always a tradeoff :)

joachim’s picture

Thanks for the hint :)

I'm stuck on whether to pass in a $flag or a flag ID to that. If I pass in a flag, I get this error:

Twig_Error_Runtime: An exception has been thrown during the rendering of a template ("Parameter "flag" for route "/flag/flag/{flag}/{entity_id}" must match "[^/]++" ("" given) to generate a corresponding URL.") in "themes/garland/templates/node.html.twig" at line 87. in Twig_Template->displayWithErrorHandling() (line 294 of core/vendor/twig/twig/lib/Twig/Template.php).

if I pass in an ID, I get this, and it's even more dramatic as the site doesn't even finish rendering:

 Fatal error: Call to a member function hasActionAccess() on a non-object in /Users/joachim/Sites/8-drupal/modules/contrib/flag/src/FlaggingAccessController.php on line 30

I've filed a docs issue in case it is that an object is needed for route parameters. Could you maybe confirm (or deny!) that's the case at #2463507: params for Url::__construct() should state that objects need to be passed in please?

berdir’s picture

ID is correct, sounds like you then actually get further and run into the next problem that you're just not seeing otherwise because of the exception.

Now you need to update FlaggingAccessController. Right now, you have a $flag_id argument there (which the controller is not passing in, then a recoverable fatal error happens but it continues), you need to change that to FlagInterface $flag and then you don't have to load it anymore.

joachim’s picture

StatusFileSize
new8.29 KB

> Now you need to update FlaggingAccessController. Right now, you have a $flag_id argument there (which the controller is not passing in, then a recoverable fatal error happens but it continues),

I'd made that change already, but I hadn't type-hinted the parameters in the controller methods.

The FlaggingAccessController now says it's not getting any parameter.

I've hacked around that for now, and the flag page at flag/flag/FLAGNAME/1?destination=node crashes with:

> Fatal error: Call to a member function id() on a non-object in modules/contrib/flag/src/Controller/ReloadLinkController.php on line 66

But all the routes in routing.yml have been changed, and I've done drush cr several times.

Is something missing?

Here's a patch of my hacking around so far .... :/

berdir’s picture

+++ b/src/Controller/AJAXLinkController.php
@@ -33,7 +33,9 @@ class AJAXLinkController extends ControllerBase {
-  public function flag($flag_id, $entity_id) {
+  public function flag($flag, $entity_id) {
+    $flag_id = $flag->id();

Entity are only upcasted if you added the type hint, so you need to add that everywhere.

joachim’s picture

Woohoo!!!! I've got it working!! :D

Thank you so much for the help.

I'll clean everything up and post a patch later.

joachim’s picture

Status: Active » Needs review
StatusFileSize
new11.27 KB

All sorted!

There's going to be a fair few follow-ups to this, as a load of methods in various places that expect a flag_id should now really work with a flag entity.

Status: Needs review » Needs work

The last submitted patch, 10: 2461549-10.flag_.route-type-hinting.patch, failed testing.

socketwench’s picture

Nifty!

joachim’s picture

Tests fail though :(
Not sure why yet, as by hand everything works great. I'll run them locally when I get a chance, but that won't be right away, so if anyone else wants a look, feel free to assign the issue :)

martin107’s picture

Just providing information - obtained from a stack trace obtained by running Drupal\flag\Tests\FlagFieldEntryTest

The first fail is the result of a page request to 'node/1' with the expectation of displaying a newly created node.

I will try and track this down some more in a few days when I get more time.. I can't yet explain why the node write/read cycle fails
but tonight I am going to comment on another aspect of it.

The user will occasionally supply an invalid id - which is fine. but our code has a brittle way of dealing with it..

From the stack trace the first argument injected into FieldEntryFormController::getForm() is a boolen (false)

whereas the type hinting is demanding a ( FlaggingInterface $flagging )

The mismatch occurs when FieldEntryFormController::getFlagging() fails to find its target.
and returns the equivalent of reset( "empty array" ) which is understandably false.
As the code as it stands the return type of FieldEntryFormController::getFlagging() is incorrectly annotated and should read.

@return FlaggingInterface|bool
  The flagging or FALSE

Anyway in the spirit of post often -- I hope this was worth reading.

joachim’s picture

Status: Needs work » Needs review
StatusFileSize
new12.28 KB

Thanks for the hints!

I *think* this is now it.

> As the code as it stands the return type of FieldEntryFormController::getFlagging() is incorrectly annotated and should read.

Can you file a new issue for that?

I also spotted this on the way: #2464721: FlaggingConfirmForm::buildForm() parameters should not be optional

socketwench’s picture

Status: Needs review » Needs work

Fails several times for me locally.

FlagConfirmFormTest.php line 151:

Recoverable fatal error: Argument 1 passed to Drupal\flag\FlagService::getFlaggableById() must implement interface Drupal\flag\FlagInterface, null given, called in /home/tess/sites/drupal8/modules/flag/src/Form/FlaggingConfirmForm.php on line 43 and defined in Drupal\flag\FlagService->getFlaggableById() (line 218 of modules/flag/src/FlagService.php).

FlagFieldEntryTest.php line 203:

Symfony\Component\Routing\Exception\MissingMandatoryParametersException: Some mandatory parameters are missing ("flag") to generate a URL for route "/flag/confirm/unflag/{flag}/{entity_id}". in Symfony\Component\Routing\Generator\UrlGenerator->doGenerate() (line 155 of core/vendor/symfony/routing/Symfony/Component/Routing/Generator/UrlGenerator.php).

FlagFieldEntryTest.php line 219:

Symfony\Component\Routing\Exception\MissingMandatoryParametersException: Some mandatory parameters are missing ("flag") to generate a URL for route "/flag/confirm/unflag/{flag}/{entity_id}". in Symfony\Component\Routing\Generator\UrlGenerator->doGenerate() (line 155 of core/vendor/symfony/routing/Symfony/Component/Routing/Generator/UrlGenerator.php

joachim’s picture

Status: Needs work » Needs review
StatusFileSize
new12.88 KB

I think I've found the cause of both of those.

Thanks for running the tests -- dunno why the testbot is being lazy today :/

EDIT Our branch test is failing for some weird reason: https://qa.drupal.org/pifr/test/952508 :(

berdir’s picture

Status Postponed

That means the branch tests aren't passing, so it doesn't run tests for patches. Was an infrastructure problem (git repository checkout failed), happens a lot lately.

The last submitted patch, 15: 2461549-15.flag_.route-type-hinting.patch, failed testing.

martin107’s picture

joachim’s picture

Thanks!

Tests are now passing, but this probably needs human review too.

berdir’s picture

Status: Needs review » Needs work
  1. +++ b/src/Controller/AJAXLinkController.php
    @@ -23,8 +23,8 @@ class AJAXLinkController extends ControllerBase {
    +   * @param FlagInterface $flag
    +   *   The flag entity.
    

    per coding standards, type class names should be fully namespaces in @param and @return.

  2. +++ b/src/Controller/AJAXLinkController.php
    @@ -33,7 +33,9 @@ class AJAXLinkController extends ControllerBase {
    +    $flag_id = $flag->id();
    +
         $flagging = \Drupal::service('flag')->flag($flag_id, $entity_id);
    

    Why don't you just write $flag->id() when passing it to the method? This seems much more complicated to me like this.

  3. +++ b/src/Controller/FieldEntryFormController.php
    @@ -28,7 +29,9 @@ class FieldEntryFormController extends ControllerBase {
    +  public function flag(FlagInterface $flag, $entity_id) {
    +    $flag_id = $flag->id();
    +
         $account = $this->currentUser();
         $flag = Flag::load($flag_id);
    

    Uhm ;)

  4. +++ b/src/Controller/FieldEntryFormController.php
    @@ -82,16 +89,16 @@ class FieldEntryFormController extends ControllerBase {
    -  public function flagTitle($flag_id, $entity_id) {
    -    $flag = \Drupal::service('flag')->getFlagById($flag_id);
    +  public function flagTitle(FlagInterface $flag, $entity_id) {
    +    //$flag = \Drupal::service('flag')->getFlagById($flag_id);
    

    And one more commented out line that can be dropped.

  5. +++ b/src/Controller/FieldEntryFormController.php
    @@ -99,16 +106,16 @@ class FieldEntryFormController extends ControllerBase {
    +    //$flag = \Drupal::service('flag')->getFlagById($flag_id);
    

    Same here.

  6. +++ b/src/Form/FlaggingConfirmForm.php
    @@ -35,10 +36,11 @@ class FlaggingConfirmForm extends ConfirmFormBase {
       public function buildForm(array $form, FormStateInterface $form_state,
    -                            $flag_id = NULL, $entity_id = NULL) {
    +                            FlagInterface $flag = NULL, $entity_id = NULL) {
     
    +    $this->flag = $flag;
    +    $flag_id = $flag->id();
         $flag_service = \Drupal::service('flag');
    

    $flag_id is never used here?

joachim’s picture

Status: Needs work » Needs review
StatusFileSize
new12.9 KB

Thanks!

> per coding standards, type class names should be fully namespaces in @param and @return.

I've spotted quite a few unrelated cases of this -- filed #2465095: classes in @param and @return must be fully-namespaced.

For items 2 and 3, that was deliberate. I plan to follow this up with changing methods in places such as the service to take $flag instead of $flag_id params. Therefore the calls to these are going to be changed soon. So it's less noise now to just change the top of the method to have a $flag_id available, and refactor the methods when the calls they make are changed.

Everything else taken care of.

martin107’s picture

StatusFileSize
new5.27 KB

I wanted to see an interdiff .. might a well post it in public.

martin107’s picture

StatusFileSize
new13.01 KB
new516 bytes

Overall - I like the improvement this patch offers...

Just ironing out a little kink - we were loading a flag object that has already been magically loaded.

martin107’s picture

StatusFileSize
new16.46 KB
new5.48 KB

Tests are now passing, but this probably needs human review too.

I am slowly doing this sorry if it comes out in drips and drabs.

3 observations :-

A) Implicit to these changes is a magic entity load before the constructor is called. - and mostly these changes are justified as the entity is needed anyways to complete the task.

B) We automatically pick some 'reject the request early with a 404' protection if the flag id is invalid. [ which is very good :) ]

C) A common pattern is that the controller extracts the flag id and feeds it into the FlagService::flag()/unflag() methods which load the entity again!

Code changes :-

1) (C) can be used to cut out unnecessary calls to load the flag entity.

2) FIeldEntryFormController::edit()/unflag(); (A) does not apply. So we could revert back to using flag_id and saved a redundant load. I personally value the extra protection from (B) - in short no change.

3) FIeldEntryFormController::getFlagging() has been converted. the entity is pre-loaded and $flag = \Drupal::service('flag')->getFlagById($flag_id); removed
so the only real advantage is (B)

4) Where appropriate I have prefixed $flag to become FlagInterface $flag in a few function arguments.

5) Where appropriate I have fixed up a few function annotations.

6) AJAXLinkController::flag() - I have removed another unnecessary call to flagging->getFlag() as the flag entity is unmodified by the function and the flag input to the function can be used directly in the output.

7) AJAXLinkController::unflag() - flag is unmodified by the unflag operation and so the flag input to the function can be used directly in the output.

8) ReloadLinkController - the input flag entity is used to extract the id which is fed into FlagService::flag() where the id is used to load the flag.. so there is duplication of work.

From the issue summary

But then I don't really follow the reason for the flag/unflag logic to live in FlagService rather than the flag class like it does on 7.

To me (C) and (1) amplify the call to move flag/unflag back into the Flag entity.

There are so many changes here - that this is becoming unreviewable.... so I will move the flag/unflag back in the the Flag entity in my next update early in the week.

PS I was about to file a duplicate of #2465095: classes in @param and @return must be fully-namespaced but caught myself :) So I am linking the issues to stop my brain from doing that again!

Status: Needs review » Needs work

The last submitted patch, 26: 2461549-26.flag_.route-type-hinting.patch, failed testing.

joachim’s picture

> C) A common pattern is that the controller extracts the flag id and feeds it into the FlagService::flag()/unflag() methods which load the entity again!

That's going to be the subject of one or more follow-up issues. I think tackling it here it too large a change. Can we go back to #25 please?

berdir’s picture

Parts of the last patch are definitely useful and I also tried to point out some of them in my review but it was probably not clear enough in some cases, like 3.

Quite a few controllers now get the flag, then get the ID to load the flag again. We should remove that here IMHO. I don't see why we would have to wait for follow-up's on that, it's something that is introduced here and should be cleaned up here.

I agree that the other part to optimize for not loading the entity is not worth it. in most cases, we will sooner or later need the flag anyway, and it's fairly well cached anyway.

martin107’s picture

So early next week I will start from 25 -

fix 3 is still valid - and can be put in with little fuss.

martin107’s picture

Assigned: Unassigned » martin107
Status: Needs work » Needs review
StatusFileSize
new14.15 KB
new2.06 KB

A smaller set of changes starting from #25.

Only uncontroversial ones... I hope

Reintroduced conversion of FieldEntryFormController:getFlagging which is #23.3
restricting myself to the smallest change possible in the function calling it FieldEntryFormController:edit() and FieldEntryFormController:unflag()

as part of this some annotations and function arguments have changed -- so that looks like #23.4 and #23.5 has also been picked up.

Next week I will resolve

Quite a few controllers now get the flag, then get the ID to load the flag again. We should remove that here IMHO. I don't see why we would have to wait for follow-up's on that, it's something that is introduced here and should be cleaned up here.

I hope this is acceptable -- I will back out anything you gauge inappropriate.

joachim’s picture

Status: Needs review » Needs work

> Reintroduced conversion of FieldEntryFormController:getFlagging

I was actually pondering moving getFlagging() to the service, since getFlaggings() lives there, but I haven't filed an issue for that yet. So best left out of this issue.

> Quite a few controllers now get the flag, then get the ID to load the flag again. We should remove that here IMHO.

Yup, now you mention it, I do see a few places such as AJAXLinkController::flag() where there's a load of the flag which is now pointless.

socketwench’s picture

C) A common pattern is that the controller extracts the flag id and feeds it into the FlagService::flag()/unflag() methods which load the entity again!

There are flagByObject()/unflagByObject() methods on FlagService. We should be calling those when we have the obejcts in question rather than calling flag() or unflag(). All flag() and unflag() do is load the objects and call the zzzzByObject() method anyways.

If only PHP supported proper method overloading (http://php.net/manual/en/language.oop5.overloading.php) we wouldn't have this problem, we could just have the same method name with different arguments. Since I don't want to give up type hinting, a different named method is what was needed.

joachim’s picture

> There are flagByObject()/unflagByObject() methods on FlagService. We should be calling those when we have the obejcts in question rather than calling flag() or unflag(). All flag() and unflag() do is load the objects and call the zzzzByObject() method anyways

Once this is in, I think we should kill flag() and unflag() and rename flagByObject()/unflagByObject() to take their place.

... or alternatively, move flag() and unflag() to the flag entity, as suggested in #26.

martin107’s picture

Assigned: martin107 » Unassigned
StatusFileSize
new13.29 KB
new842 bytes

@socketwench - Thanks for filing those issues. Your wording is much more eloquent that what I had in mind :)

Leaving ANY issue relating to getFlagging and flag/unflag alone - well what remains if I have looked carefully enough is only the fix to AJAXLinkController - a small step from #25

[Changes to AJAXLinkController will be picked up elsewhere]

I hope I am not being too reductionist but is this issue now complete?

martin107’s picture

Status: Needs work » Needs review
joachim’s picture

Yup, looks like it's all done to me.

socketwench’s picture

Status: Needs review » Reviewed & tested by the community

Me too! Thank you so much for working on this!

  • martin107 authored f1a3478 on 8.x-4.x
    Issue #2461549 by joachim, martin107: Updated to use entity type hinting...
socketwench’s picture

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

Now that I have a moment to think about it, I think the problem was that I was trying to autoload both the flag and the flaggable in my original design. It didn't occur to me to only autoload the flag.

joachim’s picture

Great work everyone!

> I think the problem was that I was trying to autoload both the flag and the flaggable in my original design. It didn't occur to me to only autoload the flag.

Baby steps :) We *could* autoload the flaggable too: #2465505: use the EntityConverter to load the flaggable entity in the routes.

The other follow-up is #2465865: Change FlagService::flag() and ::unflag() to use objects to discourage use of IDs.

Status: Fixed » Closed (fixed)

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