Score:1

How to implement TrustedCallbackInterface

us flag

I'm maintaining a module that I've had a 3rd party dev build for me. I D9 this issue has appeared when using the module that otherwise works in D8 and should be compatible with 8 & 9.

The module creates blocks with event dates pulled from the Songkick service but on D9 pages with a songkick block enabled throws this error:

Drupal\Core\Security\UntrustedCallbackException: Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was _songkick_block_poweredby_prerender. See https://www.drupal.org/node/2966725 in Drupal\Core\Render\Renderer->doTrustedCallback() (line 96 of /var/www/html/web/core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php).

I'm a mere site builder with limited coding experience so I'm looking for help to fix the issue. The error mentions this page explaining the issue.

The code below from my .module files includes the pre_render in mention.

/**
 * Implements hook_block_view_alter().
 */
function songkick_block_content_view_alter(array &$build) {
    $id = $build['#block_content']->id();
  $block = \Drupal\block_content\Entity\BlockContent::load($id);
  $block_type = $block->type[0]->target_id;
  if ($block_type == 'songkick_block') {
        $build['#pre_render'][] = '_songkick_block_poweredby_prerender';
    }
}

/**
 * Get a Upcoming & past events API data.
 */
function _songkick_block_poweredby_prerender(array &$build) {
  $id = $build['#block_content']->id();
  $block = \Drupal\block_content\Entity\BlockContent::load($id);
  $block_type = $block->type[0]->target_id;
  if ($block_type == 'songkick_block') {
    // Get custom Blocks data.
      $artis_id = $block->field_artist_id[0]->value;
      $upcoming_show = $block->field_display_upcoming_shows[0]->value;
      $past_show = $block->field_display_past_shows[0]->value;
      $event_details = $block->field_ticket_button[0]->value;
      $events_data = [
        'event_details' => $event_details,
        'upcoming_show' => $upcoming_show,
        'past_show' => $past_show
      ];
      $events_data = [
          '#theme' => 'songkick_events',
          '#events_data' => $events_data
      ];

        $client = \Drupal::httpClient();
        $api_key = \Drupal::config('songkick.settings')->get('songkick_key');
        $url = 'http://api.songkick.com/api/3.0/artists/';

        // Upcoming event data.
    if ($upcoming_show == '1') {
            $apikey = $url . $artis_id. "/calendar.json" . "?apikey=". $api_key;
            $request = $client->get($apikey);
            $body = $request->getBody()->getContents();
            $AllData = (array)json_decode($body);
            // Last page data
            $per_page = $AllData['resultsPage']->perPage;
            $total_entry = $AllData['resultsPage']->totalEntries;
            $page = (int)($total_entry / $per_page);
            //$mainData = $AllData['resultsPage']->results->event;
            $temp_var = [];
            for ($x = $page; $x >= 0; $x--){ 
                $final_api_key = $apikey . "&page=" . $x;
                $upcoming_event_request = $client->get($final_api_key);
                $upcoming_event_body = $upcoming_event_request->getBody()->getContents();
                $UpcomingEventAllData = json_decode($upcoming_event_body);
                $mainData = array_reverse($UpcomingEventAllData->resultsPage->results->event);

                //$temp_data[] = $mainData;
                foreach ($mainData as $value) {
                    $temp_var[] = $value;
                }

            }
            $upcoming_events_data = [
          '#theme' => 'songkick_events',
          '#upcoming_event' => $temp_var
        ];
    }
    else{
            $upcoming_events_data = [
          '#theme' => 'songkick_events',
          '#upcoming_event' => ''
        ];  
    }

    // Past event data.
    if ($past_show == '1') {
            $apikey = $url . $artis_id. "/gigography.json" . "?apikey=". $api_key;
            $request = $client->get($apikey);
            $body = $request->getBody()->getContents();
            $AllData = (array)json_decode($body);

            // Last page data
            $per_page = $AllData['resultsPage']->perPage;
            $total_entry = $AllData['resultsPage']->totalEntries;
            $page = (int)($total_entry / $per_page);

            $temp_var = [];
            for ($x = $page; $x >= 0; $x--){ 
                $final_api_key = $apikey . "&page=" . $x;
                $past_event_request = $client->get($final_api_key);
                $past_event_body = $past_event_request->getBody()->getContents();
                $PastEventAllData = json_decode($past_event_body);
                $mainData = array_reverse($PastEventAllData->resultsPage->results->event);

                //$temp_data[] = $mainData;
                foreach ($mainData as $value) {
                    $temp_var[] = $value;
                }
            }
            //$final_api_key = $apikey . "&page=" . $page;
            // $past_event_request = $client->get($final_api_key);
            // $past_event_body = $past_event_request->getBody()->getContents();
            // $PastEventAllData = (array)json_decode($past_event_body);
            // $mainData = array_reverse($PastEventAllData['resultsPage']->results->event);
            $past_events_data = [
          '#theme' => 'songkick_events',
          '#past_event' => $temp_var
        ];
    }
    else{
            $past_events_data = [
          '#theme' => 'songkick_events',
          '#past_event' => ''
        ];
    }

    // Merge Upcoming & past evenst data.
    $event_data = array_merge($upcoming_events_data,$past_events_data,$events_data);

    // Return events data.
        return $event_data;
  }
}

I've tried following the instructions in this comment and have added a file named SongkickBlockPoweredByViewBuilder.php in the src folder with this code:

<?php

namespace Drupal\Songkick;

use Drupal\Core\Security\TrustedCallbackInterface;

/**
 * Provides a trusted callback for the Songkick Poweredby block.
 *
 */
class SongkickBlockPoweredByViewBuilder implements TrustedCallbackInterface {

    /**
     * {@inheritdoc}
     */
    public static function trustedCallbacks() {
     return ['preRender'];    
   }
 
   /**
    * Sets Songkick - #pre_render callback.
    */
   public static function preRender($build) {
     $count = $build['content']['#count'];
     $build['content']['#count_text'] = \Drupal::translation()->formatPlural($count, '(@count)', '(@count)');
     return $build;
   }
 
 }

And I've added the following to my .module file:

use Drupal\Songkick\SongkickBlockPoweredByViewBuilder;

/**
 * Implements TrustedCallbackInterface
 */
function _songkick_block_poweredby_prerender(array &$build, Drupal\Core\Block\BlockPluginInterface $block) {
  $build['#pre_render'][] = [SongkickBlockPoweredByViewBuilder::class, 'preRender'];
}

Then the site throws this fatal error:

Fatal error: Cannot redeclare _songkick_block_poweredby_prerender() (previously declared in /var/www/html/web/modules/contrib/songkick/songkick.module:14) in /var/www/html/web/modules/contrib/songkick/songkick.module on line 51

The fatal error goes away if I remove the declaration in line 14, but the original error then persist. I'm at a loss about how to define the function in line 51 in a way that resolves the issue. Line 51 begins right after this comment in the above excerpt from the .module file:

/*** Get a Upcoming & past events API data. */

Any tips?

cn flag
Drupal is giving you a warning with a specific link that explains the problem. If you don't understand the linked docs-- what is it that you don't understand? And it always helps if you post the actual code that is causing the problem ;)
us flag
Valid points @PatrickKenny, thanks. I've edited the original post to include those things.
Lambic avatar
ph flag
This comment and the reply to it on the issue linked to in the error message contain examples of what you need to do: https://www.drupal.org/node/2966725#comment-13948868
us flag
Thanks a lot @Lambic. I just tried my best at that and I must have done something wrong as it results in a fatal error. I've updated my question with the steps I took. Can I persuade you to take a look?
Lambic avatar
ph flag
Looks like you have the same function defined twice in songkick.module, once at line 14 and again at line 51
us flag
I see, thanks. I've added that detail to the question. Any insights into how I can replace the old declaration (line 51) with the new one (line 14) that implements the TrustedCallbackInterface?
apaderno avatar
us flag
Questions aren't for a back and forth between the user who asks the question and the users who answer it. Once the question is answered, it cannot be edit, if the edit changes the question's meaning, or it adds a follow-up question (*I did as the answer says, but now I have another problem.*)
Score:4
fr flag

You have the right idea, there are just a few details that need to be fixed.

The TrustedCallbackInterface was first added to Drupal 8.8; it didn't exist before Drupal 8.8. While it is optional in Drupal 8.8 and 8.9, it is mandatory in Drupal 9. So your module isn't compatible with Drupal 9 until you make this change.

First, copy the entire body of your original version of _songkick_block_poweredby_prerender() into your public static function preRender($build). That is all that should be in the body of the preRender() method.

Second, delete the function _songkick_block_poweredby_prerender() (both copies) from songkick.module. You no longer need it because you have put that code in Drupal\songkick\SongkickBlockPoweredByViewBuilder.

Now put the use Drupal\songkick\SongkickBlockPoweredByViewBuilder; statement at the top of songkick.module with all the other use statements. Then change this statement:

$build['#pre_render'][] = '_songkick_block_poweredby_prerender';

to point to your new code instead. Like this:

$build['#pre_render'][] = [SongkickBlockPoweredByViewBuilder::class, 'preRender'];

Also note that the correct namespace has a lowercase 's' in 'songkick'. This has to be the same as the module's machine name, and machine names don't allow capital letters. So you need to make sure the namespace declaration in SongkickBlockPoweredByViewBuilder uses the lowercase, and that your use statement in songkick.module uses the lowercase.

Drupal is picky about naming, so also be sure that if your .module file is in songkick/songkick.module, the class SongkickBlockPoweredByViewBuilder is in songkick/src/SongkickBlockPoweredByViewBuilder.php

us flag
Thanks so much. Original error now gone but the block doesn't render the list of events anymore, just the block settings. I've updated the question to reflect the steps taken and the new log reports. Any insights would be massively appreciated.
fr flag
You didn't remove these three lines from your preRender(), so you're returning from the function before your event data code is even run: `$count = $build['content']['#count']; $build['content']['#count_text'] = \Drupal::translation()->formatPlural($count, '(@count)', '(@count)'); return $build;`
fr flag
And at this point, we're debugging your code, which is outside the scope of this issue and not really appropriate for StackExchange.
us flag
Valid point, sorry about that. Anyways, thanks so much for your help. I'll accepting your answer now.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.