Skip to content

Components

Components are an encapsulation of data which is intended to be transformed and presented to the user as HTML. In Drupal, Pinto outputs the component build as a render array, which are transformed by the Core Renderer into HTML. The asset system renders out CSS and Javascript and attaches it to the outer HTML element.

In frontend terms, Components are always represented as Pinto Components.

In PHP terms, a Pinto Component is always a PHP Class. It could be as simple as a plain class with constructor arguments representing the component slots, a data transfer object (DTO) used for de/serialisation, or even a Drupal entity class or bundle class.

Components may act alone, or be composites combining multiple components into a single re-usable component.

Component

A component comprises:

  • A PHP class
  • Slots
  • An HTML template partial
    In Drupal, the template partial is always a Twig template
  • Optionally
    • CSS assets
    • Javascript assets
    • Dependencies on other components
    • A reference in discovery

PHP Class

Start by creating a PHP class.

php
use Drupal\pinto\Object\DrupalInvokableSlotsTrait;
use Pinto\Attribute\ObjectType\Slots;

#[Slots(bindPromotedProperties: TRUE)]
class MyComponent {

  use DrupalInvokableSlotsTrait;

  public function __construct(
    public string $str,
    public int $number,
  ) {
  }
  
}

Slot definitions

Slots are equivalent to Twig variables, where a component can set a slot value, and it will be made available to a template as a variable.

The most convenient way to use slots is by setting #[Slots(bindPromotedProperties: true)], this option creates slot definitions from promoted properties of a component constructor.

Public arguments are automatically bound, such that if a components builder-method doesn't set a value for a slot, its value will be automatically populated. For private or protected arguments, Pinto does not automatically set the value, so their slot values must be set manually in the builder-method, however they are still made available as slots.

Values of promoted property slots can be conditionally overridden, and can be mixed with defined slots.

php
#[Slots(bindPromotedProperties: true]
final class MyComponent {
  public function __construct(
    public readonly string $slot_1,
  ) {
  }
}

INFO

The constructor may have any visibility when promoted property slots are used.

Example

The following behaves identically to the public example above.

php
#[Slots(bindPromotedProperties: true]
final class MyComponent {
  private function __construct(
    public readonly string $slot_1,
  ) {
  }
}

Manual slots

Slots can be explicitly defined at #[Slots(slots)], instead of relying on promoted properties. Defining slots manually is ideal when the constructor is not an ideal reflection of what the slots should be, such as when an alternative factory methodology is used. Slots can be defined with any combination of string 's, Slot components, enum's, or enum cases.

String and Slot instances

Slots can be defined with string's or Slot components. Slot components are useful as they accept extra information like a default value or binding its value to a class property.

php
#[ObjectType\Slots(slots: [
  'slot_1'
  new Slots\Slot(name: 'slot_2'),
  new Slots\Slot(name: 'slot_3', defaultValue: 3),
  new Slots\Slot(name: 'slot_4', fillValueFromThemeObjectClassPropertyWhenEmpty: 'aClassProperty'),
])]
final class MyComponent {
  public string $aClassProperty;
  
  protected function build(Slots\Build $build): Slots\Build {
    $this->aClassProperty = 'Baz';
    
    return $build
      ->set('slot_1', 'Foo')
      ->set('slot_2', 'Bar');
    }
}

Enum

An enum class-string can be passed along, and all of its cases will be transformed into slots.

Enums are useful when slot definitions need to be shared between components.

php
enum MySlots {
  case SlotA;
  case SlotB;
}
php
#[ObjectType\Slots(slots: [
  MySlots::class, 
])]
final class MyComponent {
  protected function build(Slots\Build $build): Slots\Build {
    return $build
      ->set(MySlots::SlotA, 'Foo') 
      ->set(MySlots::SlotB, 'Bar'); 
  }
}

Enum case

Singular enum instances can be defined if all cases of an enum is excessive.

php
enum MySlots {
  case SlotA;
  case SlotB;
}
php
use Pinto\Attribute\ObjectType;
use Pinto\Slots;
#[ObjectType\Slots(slots: [
  MySlots::SlotA, 
  MySlots::SlotB, 
])]
final class MyComponent {
  protected function build(Slots\Build $build): Slots\Build {
    return $build
      ->set(MySlots::SlotA, 'Foo') 
      ->set(MySlots::SlotB, 'Bar'); 
  }
}

Builder

The build method is used when Pinto needs to automatically render components, such as with nested components, and more comprehensive integrations like layouts, blocks, and themes.

Each component must have one builder method. Implement the PHP __invoke magic method, or by defining a public method and applying the #[Build] attribute.

A builder method at __invoke is automatically added when utilising DrupalInvokableSlotsTrait.

TIP

When a builder method cannot be determined, Pinto will throw a helpful exception.

Builder method determination

Logic for builder method determination can be found at \Pinto\Attribute\Build::buildMethodForThemeObject.

Invokable slots trait

When the \Drupal\pinto\Object\DrupalInvokableSlotsTrait trait is used in a component, a build method is made available. This method is called when the component is invoked.

php
#[Slots(bindPromotedProperties: TRUE)]
class MyComponent {
  use DrupalInvokableSlotsTrait; 
  
  public function __construct(
      public string $slot_1,
  ) {
  }

  protected function build(Slots\Build $build): Slots\Build {
    return $build 
      ->set('slot_1', 'Hello World'); 
  }
}

Build with (new MyComponent())().

Invoke Magic method

When the less opinionated __invoke magic method is implemented with DrupalObjectTrait, you'll need to call out to pintoBuild on your own.

php
#[Slots(bindPromotedProperties: TRUE)]
class MyComponent {
  use DrupalObjectTrait;
  
  public function __construct(
      public string $slot_1,
  ) {
  }
  
  public function __invoke(): mixed {
    return $this->pintoBuild(function (Build $build): Build { 
      return $build
        ->set('slot_a', 'Foo bar.');
    });
  }
}

Build with (new MyObject())().

Custom builder

A custom builder method is useful when a component already uses __invoke for other purposes. The #[Build] attribute must be applied to a public instance method.

In this example, render method is the custom builder.

php
use Pinto\Attribute\Build;

#[Slots(bindPromotedProperties: TRUE)] 
class MyComponent {
  use DrupalObjectTrait;
  
  public function __construct(
      public string $slot_1,
  ) {
  }
  
  #[Build] 
  public function render(): mixed {
    return $this->pintoBuild(function (Build $build): Build {
      return $build
        ->set('slot_a', 'Foo bar.');
    });
  }
}

Build with (new MyComponent())->render().

Setting slot values

Slot values may be set on the build component passed along to the builder-method. This method is used instead of promoted properties when deeper business logic needs to be applied and validated.

php
#[Slots(slots: ['slot_1', 'slot_2'])]
final class MyComponent {
  protected function build(Slots\Build $build): Slots\Build {
    return $build
      ->set('slot_1', 'Foo')
      ->set('slot_2', 'Bar');
  }
}

Slot enums

When slots are defined with enums or enum cases, enum cases may be used directly with set.

php
enum MySlots1 {
  case SlotA;
  case SlotB;
}
enum MySlots2 {
  case SlotC;
}
php
#[Slots(slots: [
  MySlots1::SlotA, 
  MySlots1::SlotB, 
  MySlots2::class, 
])]
final class MyComponent {
  protected function build(Slots\Build $build): Slots\Build {
    return $build
      ->set(MySlots1::SlotA, 'Foo') 
      ->set(MySlots1::SlotB, 'Bar') 
      ->set(MySlots2::SlotC, 'Baz'); 
  }
}

Overriding promoted property slot values

A builder-method may conditionally set the value of a promoted property slot. Values set in the builder override the promoted property value.

php
#[Slots(bindPromotedProperties: true] 
final class MyComponent {
  public function __construct(
    public readonly string $slot_1,
  ) {
  }
  
  protected function build(Slots\Build $build): Slots\Build {
    if (condition) {
      // Conditionally set the value, otherwise slot_1
      // will be set to the value of MyComponent::$slot_1.
      $build->set('slot_1', 'Foo'); 
    }
    
    return $build;
  }
}
php
$instance = new MyComponent('Baz');

In this example, slot_1 will be set to Baz, unless the condition evaluates as true, in case it is set to Foo.

Validation

All slots must have a value, otherwise an exception is thrown. For a slot to be considered as having a value, a slot value must be either explicitly set in the builder-method, automatically via promoted properties, or as a PHP default value. If any slot is missing a value, validation will fail.

Valid slot values include empty-ish values like '' or null.

Validation logic

Validation internals at \Pinto\Attribute\ObjectType\Slots::validateBuild. For validation to work, a components builder-method must use pintoBuild.

Assets

Javascript and CSS assets may be associated with components. Pinto is able to associate assets with components, making use of Drupals' asset and libraries system to create libraries behind the scenes, and adding appropriate #attached render array keys when building.

Local and external assets may be associated, and dependencies on other component assets can be resolved recursively.

See Component Assets for deeper information on assets.

Usage

a component is built, outputting a render array, by executing the builder-method.

php
$component = new MyComponent().

When the __invoke magic method is available. Simply execute using () notation.

php
$build = $component();

Otherwise, if a custom builder is used:

php
$build = $component->build();

Tip: Nested components

When setting values of slots to known Pinto components, the component may be set directly without executing the builder-method.

The return value of the builder-method is usually a render array.

When assets are applied, #attached is populated.

php
\print_r($build);
// Array
// (
//     [#theme] => MyComponentA
//     [#attached] => Array
//         (
//             [library] => Array
//                 (
//                     [0] => pinto/MyComponentA
//                 )
//         )
//     [#slot_1] => Hello World
// )

In a controller, usage becomes:

php
namespace Drupal\my_module\Controller;

final class MyController {
  public function __invoke(): array {
    $component = new MyComponent().
    return $component();
  }
}
yaml
my_module.my_controller:
  path: '/my-controller'
  defaults:
    _controller: '\Drupal\my_module\Controller\MyController'

Under the hood

Slot concepts are converted to typical Drupal concepts under the hood. Understanding the conversion helps to understand internal mechanics of Pinto, and how Pinto fits in to existing Drupal projects.

Given the following:

php
enum PintoList implements ObjectListInterface {
  #[Definition(MyComponentA::class)]
  case MyComponentA;
  
  #[Definition(MyObjectB::class)]
  case MyObjectB;
  
  public function name(): string {
    // Method override for example purposes only.
    return $this->name;
  }
  
  public function templateDirectory(): string {
    // Method override for example purposes only.
    return '@my_components/templates/';
  }
  
  public function templateName(): string {
    // Method override for example purposes only.
    return $this->name . '-template';
  }
  
  protected function cssDirectory(): string {
    // Method override for example purposes only.
    return '/path/to/module/css/'
  }
  
  protected function jsDirectory(): string {
    // Method override for example purposes only.
    return '/path/to/module/js/'
  }
}
php
use Pinto\Attribute\ObjectType;
use Pinto\Slots;

#[ObjectType\Slots(slots: [
  'slot_1'
  new Slots\Slot(name: 'slot_2'),
  new Slots\Slot(name: 'slot_3', defaultValue: 3),
  new Slots\Slot(name: 'slot_4', fillValueFromThemeObjectClassPropertyWhenEmpty: 'aClassProperty'),
])]
#[Js('script.js')]
#[Css('styles.css')] 
final class MyObjectA {
  public string $aClassProperty;

  public function __construct(
    private readonly string $firstSlotValue,
  ) {
    $this->aClassProperty = 'Baz';
  }

  public function __invoke(): mixed {
    return $this->pintoBuild(function (Build $build): Build {
      return $build
        ->set('slot_1', $this->firstSlotValue)
        ->set('slot_2', 'Bar');
    });
  }
}
php
use Pinto\Attribute\ObjectType\Slots;
#[Slots(bindPromotedProperties: true)]
final class MyObjectB {
  public function __construct(
    public readonly string $slot_5,
    public readonly int $slot_6 = 6,
  ) {}
}
twig
<div>
  {{ slot_1 }}
  {{ slot_2 }}
  {{ slot_3 }}
  {{ slot_4 }}
</div>
twig
<div>
  {{ slot_5 }}
  {{ slot_6 }}
</div>

Theme definition

When converted to theme definitions, the two components are equivalent to the following hook_theme:

php
#[Hook('theme')]
public function hookTheme(): array {
  return [
    // Keys derived from PintoList::name():
    'MyObjectA' => [
      // Value derived from PintoList::templateDirectory():
      'path' => '@my_components/templates/',
      // Value derived from PintoList::templateName():
      'template' => 'MyObjectA-template',
      'variables' => [
        'slot_1' => NULL,
        'slot_2' => NULL,
        // Value derived from `#[Slot(defaultValue)]`:
        'slot_3' => 3,
        'slot_4' => NULL,
      ],
    ],
    
    'MyObjectB' => [
       'path' => '@my_components/templates/',
       'template' => 'MyObjectB-template',
       'variables' => [
         'slot_5' => NULL,
         // Value derived from `public readonly int $slot_6 = 6`:
         'slot_6' => 6,
       ],
    ],
  ];
}

Rendering

When building components, a render array is output and is rendered by Drupal and Twig:

Object A

php
$built = (new MyObjectA('Hello World'))();
$built === [
  '#theme' => 'MyObjectA',
  '#attached' => [
    'library' => [
      'pinto/MyObjectA',
    ],
  ],
  '#slot_1' => 'Hello World',
  '#slot_2' => 'Bar',
  '#slot_3' => 3,
  '#slot_4' => 'Baz',
];
html
<div>
  Hello World
  Bar
  3
  Baz
</div>

Object B

php
$built = (new MyObjectB('Hello World'))();
$built === [
  '#theme' => 'MyObjectB',
  '#attached' => ['library' => ...],
  '#slot_5' => 'Hello World',
  '#slot_6' => 6,
];
html
<div>
  Hello World
  6
</div>
Transformation to render arrays

The values set on \Pinto\Slots\Build in a components builder-method are transformed to a Drupal render array by \Drupal\pinto\Object\PintoToDrupalBuilder::transform.