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.
- The class can be located anywhere. Even in packages/libraries in vendor, outside of
Drupal. - Has a component type attribute tacked on above the
class. The component type may also be associated to the component in Discovery. Usually,#[Slots]is used. - Define slots.
#[Slots(bindPromotedProperties)]to automatically make constructor parameters the slots.#[Slots(slots)]to manually define slots.
- Builder method.
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.
Constructor slots
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.
#[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.
#[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.
#[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.
enum MySlots {
case SlotA;
case SlotB;
}#[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.
enum MySlots {
case SlotA;
case SlotB;
}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.
#[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.
#[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.
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.
#[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.
enum MySlots1 {
case SlotA;
case SlotB;
}
enum MySlots2 {
case SlotC;
}#[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.
#[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;
}
}$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.
$component = new MyComponent().When the __invoke magic method is available. Simply execute using () notation.
$build = $component();Otherwise, if a custom builder is used:
$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.
\print_r($build);
// Array
// (
// [#theme] => MyComponentA
// [#attached] => Array
// (
// [library] => Array
// (
// [0] => pinto/MyComponentA
// )
// )
// [#slot_1] => Hello World
// )In a controller, usage becomes:
namespace Drupal\my_module\Controller;
final class MyController {
public function __invoke(): array {
$component = new MyComponent().
return $component();
}
}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:
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/'
}
}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');
});
}
}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,
) {}
}<div>
{{ slot_1 }}
{{ slot_2 }}
{{ slot_3 }}
{{ slot_4 }}
</div><div>
{{ slot_5 }}
{{ slot_6 }}
</div>Theme definition
When converted to theme definitions, the two components are equivalent to the following hook_theme:
#[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
$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',
];<div>
Hello World
Bar
3
Baz
</div>Object B
$built = (new MyObjectB('Hello World'))();
$built === [
'#theme' => 'MyObjectB',
'#attached' => ['library' => ...],
'#slot_5' => 'Hello World',
'#slot_6' => 6,
];<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.