technology and webhosting news

namespace Drupalmy_project_profileEntityBlockContent;

use DrupalbcaAttributeBundle;
use Drupalmy_project_profileTraitsDescriptionTrait;
use Drupalmy_project_profileTraitsImageTrait;
use Drupalmy_project_profileTraitsTitleTrait;

#[Bundle(entityType: self::ENTITY_TYPE_ID, bundle: self::BUNDLE)]
final class Card extends MyProjectBlockContentBase {

use TitleTrait;
use DescriptionTrait;
use ImageTrait;

public const string BUNDLE = 'card';

}

Let’s set up our Card bundle class:EntityViewBuilders are PHP classes that contain logic on how to build (or render) an entity. Entity types can have custom EntityViewBuilders; for example BlockContent has its own defined in core. These are defined in the view_builder handler in an entity type’s annotation and can also be overridden by using hook_entity_type_alter.

  • Removing the “div soup” of Drupal fields
  • Adding custom classes or attributes to field output
  • Wrapping fields in custom tags (e.g. an h2)

/**
* {@inheritdoc}
*/
public function build(string $viewMode): array {
$build = PintoCard::createFromCardBlock($this);
$image = $this->image->entity;
if ($image) {
   $build->addCacheableDependency($image);
 }
return $build();
}

Let’s look at the Card example:Now, any BlockContent bundle class that implements BuildableEntityInterface and returns TRUE from its shouldBuild method will completely bypass Drupal’s standard entity rendering and instead just return whatever we want from its build method.

  1. Setting up a bundle class. In this example, we will implement it as a Block Content bundle
  2. Using a custom entity view builder
  3. Theming a Card block using Pinto

Bundle classes

The function that drives this is getBuildDefaults so that’s all we need to override.By default, the view builder class takes all of your configuration in an entity view display (i.e. field formatter settings, view modes, etc.) and renders it. We are using a custom view builder class to bypass all of that and simply return a render array via a Pinto object.For this example, a custom view builder for the block content entity type can be as simple as:

  • An Interface per entity type (e.g MyProjectBlockContentInterface)
  • An abstract base class per entity type (e.g. MyProjectBlockContentBase)
  • A Bundle class per bundle
  • Traits and interfaces for any shared fields/logic (e.g. BodyTrait for all bundles that have a Body field)

We also briefly mentioned cacheable metadata in our last post. Since our Pinto object implements CacheableDependencyInterface, we can add that metadata directly to the theme object. For example, you should enhance the bundle class’ build method to add the Image media entity as a cacheable dependency. That way if the media entity is updated, the Card output is invalidated.While there are plenty of modules to alleviate this, it can often mean you have a mix of YAML configuration for markup, preprocess hooks, overridden templates, etc., to pull everything together. Pinto allows you to easily render an entity while reusing your frontender’s perfect template!namespace Drupalmy_project_dsThemeObject;

use DrupalCoreCacheCacheableDependencyInterface;
use Drupalmy_project_profileEntityBlockContentCard as CardBlock;
use Drupalmy_project_dsMyProjectDsMyProjectObjectTrait;
use PintoAttributeThemeDefinition;

#[ThemeDefinition([
'variables' => [
  'title' => '',
  'description' => '',
  'image' => '',
],
])]
final class Card implements CacheableDependencyInterface {

use MyProjectObjectTrait;

private function __construct(
  private readonly string $title,
  private readonly array $image,
  private readonly ?string $description,
) {}

public static function createFromCardBlock(CardBlock $card): static {
  return new static(
    $card->getTitle(),
    $card->getImage(),
    $card->getDescription(),
  );
}

protected function build(mixed $build): mixed {
  return $build + [
    '#title' => $this->title,
    '#description' => $this->description,
    '#image' => $this->image,
  ];
}

}

We have also introduced a factory method createFromCardBlock on the Pinto theme object, which takes the entity and injects its data into the object.That custom interface can then extend BuildableEntityInterface.

  • Title – a plain text field
  • Description – another plain text field
  • Image – a Media reference field.

Custom entity view builder

At PreviousNext, our go-to for implementing bundle classes is the BCA module, which allows you to define a class as a custom Bundle class via an attribute, removing the need for hook_entity_bundle_info_alter.use Drupalmy_project_dsThemeObjectCard as PintoCard;

final class Card extends MyProjectBlockContentBase {
 
 // Trimmed for easy reading.

 /**
  * {@inheritdoc}
  */
 public function build(string $viewMode): array {
   return PintoCard::createFromCardBlock($this)();
 }

}

use Drupalmy_project_profileHandlerMyProjectBlockContentViewBuilder;

/**
* Implements hook_entity_type_alter().
*/
function my_project_profile_entity_type_alter(array &$entity_types): void {
 /** @var DrupalCoreEntityContentEntityType $blockContentDefinition */
 $blockContentDefinition = $entity_types['block_content'];
 // Override view builder class.
 $blockContentDefinition->setViewBuilderClass(MyProjectBlockContentViewBuilder::class);
}

We’ll continue our Card component example from the previous post and cover:Back to our Card example. It was extending a custom base class MyProjectBlockContentBase. That class may look something like this:NOTE: I won’t go into too much detail about how the interfaces, base classes, and traits are set up. There are plenty of examples of how you might write these. Check out the change record for some basic examples! In our last post, we discussed Pinto concepts and how to use Theme objects to encapsulate theming logic in a central place for a component. Next, we’ll apply that knowledge to theming an entity. This will demonstrate the power of Pinto and how it will dramatically improve the velocity of delivering new components. Now we just need an alter hook to wire things up:

BuildableEntityInterface

In case you’re not aware, Drupal introduced the concept of Bundle classes almost three years ago. They essentially allow business logic for each bundle to be encapsulated in its own PHP class and benefit from regular PHP concepts such as code sharing via Traits, Interfaces, etc.Now, all we need to do is implement the build method on our BlockContent bundle classes.One of the hardest things about theming Drupal is outputting markup that matches your design system. Our standard setup on projects is:The shouldBuild method is an optional implementation detail, but it is nice if you have multiple view modes for a bundle, which need to have differing logic. For example, you might have a media_library view mode that you want to continue to use Drupal’s standard rendering.namespace Drupalmy_project_profileHandler;

use DrupalCoreCacheCacheableMetadata;
use DrupalCoreEntityEntityInterface;
use Drupalblock_contentBlockContentViewBuilder;
use Drupalmy_project_profileEntityInterfaceBuildableEntityInterface;

class MyProjectBlockContentViewBuilder extends BlockContentViewBuilder {

/**
 * {@inheritdoc}
 */
public function getBuildDefaults(EntityInterface $entity, $view_mode) {
  $build = parent::getBuildDefaults($entity, $view_mode);

  if (!$entity instanceof BuildableEntityInterface || !$entity->shouldBuild($view_mode)) {
    return $build;
  }

  $cache = CacheableMetadata::createFromRenderArray($build);
  $build = $entity->build($view_mode);
  $cache->merge(CacheableMetadata::createFromRenderArray($build))
    ->applyTo($build);

  return $build;
}

}

In our case, each trait is a getter/setter pair for each of our fields required to build our Card component: I can’t overstate how much this has accelerated our backend development. My latest project utilised Pinto from the very beginning, and it has made theming the entire site extremely fast and even… fun! 😀namespace Drupalmy_project_profileEntityInterface;

/**
* Interface for entities which override the view builder.
*/
interface BuildableEntityInterface {

/**
 * Default method to build an entity.
 */
public function build(string $viewMode): array;

/**
 * Determine if the entity should be built for the given view mode.
 */
public function shouldBuild(string $viewMode): bool;

}

It is generally not recommended to use this for Node since you’re more likely to get value out of something like Layout Builder for rendering nodes. Those layouts would then have block content added to them, which in turn will be rendered via this method.My preferred approach is to have a directory structure that matches the entity type located inside the project’s profile (e.g. src/Entity/BlockContent/Card.php. Feel free to set this up however you like. For example, some people may prefer to separate entity types into different modules.New bundles are simple to implement. All that’s needed is to click together the fields in the UI to build the content model, add the new Theme object, and wire that together with a bundle class.For example:This is what the fully implemented Pinto object would look likenamespace Drupalmy_project_profileEntityBlockContent;

use Drupalblock_contentBlockContentTypeInterface;
use Drupalblock_contentEntityBlockContent;

abstract class MyProjectBlockContentBase extends BlockContent implements MyProjectBlockContentInterface {

/**
 * {@inheritdoc}
 */
public function shouldBuild(string $viewMode): bool {
  return TRUE;
}

}

We need to cover a few more concepts and set things up to pull this all together. Once set up, new bundles or entity types can be added with ease.

Similar Posts