Usando la Promoción de Propiedades del Constructor de PHP en Módulos Personalizados de Drupal
PHP 8 introdujo la promoción de propiedades del constructor, una característica que simplifica la definición y asignación de propiedades en las clases al permitir declarar e inicializar propiedades directamente en la firma del constructor. Este tutorial demuestra cómo usar la promoción de propiedades del constructor en módulos personalizados de Drupal (que requieren PHP 8.0+), específicamente para simplificar la inyección de dependencias en tus servicios y controladores. Compararemos el patrón tradicional de Drupal (utilizado en PHP 7 y las primeras versiones de Drupal 9) con el enfoque moderno de PHP 8+, usando ejemplos completos de código para ambos casos. Al finalizar, verás cómo esta sintaxis moderna reduce el código repetitivo, hace que el código sea más claro y se alinea con las mejores prácticas actuales.
Drupal 10 (que requiere PHP 8.1+) ha comenzado a adoptar estas funciones modernas de PHP en el núcleo, por lo que se anima a los desarrolladores de módulos personalizados a hacer lo mismo. Comencemos revisando el patrón tradicional de inyección de dependencias en Drupal y luego lo refactorizaremos utilizando la promoción de propiedades del constructor.
Inyección de dependencias tradicional en Drupal (Pre-PHP 8)
En los servicios y controladores de Drupal, el patrón tradicional para la inyección de dependencias implica tres pasos:
-
Declarar cada dependencia como una propiedad de la clase (generalmente
protected
) con la correspondiente docblock. -
Tipar cada dependencia en los parámetros del constructor y asignarlas a las propiedades de la clase dentro del constructor.
-
Para controladores (y algunas clases de plugins), implementar un método estático
create(ContainerInterface $container)
para recuperar servicios del contenedor de servicios de Drupal e instanciar la clase.
Esto resulta en bastante código repetitivo. Por ejemplo, considera un servicio personalizado simple que necesita la fábrica de configuración y la fábrica de logs. Tradicionalmente, escribirías:
Ejemplo de clase de servicio tradicional
<?php
namespace Drupal\example;
/**
* Servicio de ejemplo que registra el nombre del sitio.
*/
class ExampleService {
/**
* El servicio de la fábrica de configuración.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* El servicio de la fábrica de logs.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* Constructor de un objeto ExampleService.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* La fábrica de configuración.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* La fábrica de logs.
*/
public function __construct(ConfigFactoryInterface $config_factory, LoggerChannelFactoryInterface $logger_factory) {
// Almacena los servicios inyectados.
$this->configFactory = $config_factory;
$this->loggerFactory = $logger_factory;
}
/**
* Registra el nombre del sitio como acción de ejemplo.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Site name: ' . $site_name);
}
}
En el ejemplo anterior, declaramos dos propiedades $configFactory
y $loggerFactory
y las asignamos en el constructor. El servicio correspondiente también debe definirse en el YAML de servicios del módulo (con los servicios que necesita como argumentos), por ejemplo:
# example.services.yml
services:
example.example_service:
class: Drupal\example\ExampleService
arguments:
- '@config.factory'
- '@logger.factory'
Cuando Drupal instancia este servicio, pasará los argumentos configurados al constructor en el orden listado.
Ejemplo de clase de controlador tradicional
Los controladores de Drupal también pueden usar inyección de dependencias. Normalmente, una clase de controlador extiende ControllerBase
(por los métodos de conveniencia) e implementa la inyección de contenedor de Drupal definiendo un método create()
. El método create()
es una factoría que obtiene servicios del contenedor y llama al constructor. Por ejemplo:
<?php
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controlador para rutas de Example.
*/
class ExampleController extends ControllerBase {
/**
* El servicio de gestor de tipos de entidad.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* El servicio de traducción de cadenas.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface
*/
protected $stringTranslation;
/**
* Constructor de un objeto ExampleController.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* El gestor de tipos de entidad.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* El servicio de traducción de cadenas.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
// Recupera los servicios requeridos del contenedor y pásalos al constructor.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Construye una respuesta de página simple.
*/
public function build(): array {
// Ejemplo de uso de los servicios inyectados.
$node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
return [
'#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
];
}
}
Uso de la promoción de propiedades del constructor (PHP 8+) en Drupal
La promoción de propiedades del constructor simplifica el patrón anterior permitiendo declarar y asignar propiedades en un solo paso directamente en la firma del constructor. En PHP 8, puedes anteponer la visibilidad (y otros modificadores como readonly
) a los parámetros del constructor, y PHP creará y asignará automáticamente esas propiedades. Esto significa que ya no necesitas declarar por separado la propiedad ni escribir la asignación dentro del constructor: PHP lo hace por ti.
Es importante destacar que esto es azúcar sintáctica. No cambia cómo funciona la inyección de dependencias en Drupal; simplemente reduce el código que escribes. Todavía registras los servicios en YAML (o dejas que Drupal los autogenere), y para los controladores sigues usando el método factoría create()
(a menos que registres el controlador como un servicio). La diferencia es únicamente en cómo escribes el código de la clase. El resultado es mucho menos código repetitivo, como se demuestra en una incidencia del núcleo de Drupal donde docenas de líneas de declaraciones y asignaciones se redujeron a unas pocas líneas en el constructor.
Vamos a refactorizar nuestros ejemplos usando la promoción de propiedades del constructor.
Clase de servicio con promoción de propiedades del constructor
Así se reescribe ExampleService
usando la sintaxis de propiedades promovidas de PHP 8:
<?php
namespace Drupal\example;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
/**
* Servicio de ejemplo que registra el nombre del sitio (usando promoción de propiedades).
*/
class ExampleService {
/**
* Construye un objeto ExampleService con los servicios inyectados.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* La fábrica de configuración.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
* La fábrica de logs.
*/
public function __construct(
protected ConfigFactoryInterface $configFactory,
protected LoggerChannelFactoryInterface $loggerFactory
) {
// No es necesario cuerpo; las propiedades se asignan automáticamente.
}
/**
* Registra el nombre del sitio como acción de ejemplo.
*/
public function logSiteName(): void {
$site_name = $this->configFactory->get('system.site')->get('name');
$this->loggerFactory->get('example')->info('Site name: ' . $site_name);
}
}
Clase de controlador con promoción de propiedades del constructor
Ahora, considera ExampleController
refactorizado para usar propiedades promovidas:
<?php
namespace Drupal\example\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controlador para rutas Example (usando promoción de propiedades).
*/
final class ExampleController extends ControllerBase {
/**
* Construye un ExampleController.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* El gestor de tipos de entidad.
* @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
* El servicio de traducción de cadenas.
*/
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
private TranslationInterface $stringTranslation
) {
// No es necesario realizar asignaciones; las propiedades se asignan automáticamente.
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
// Pasa los servicios del contenedor al constructor.
return new self(
$container->get('entity_type.manager'),
$container->get('string_translation')
);
}
/**
* Construye una respuesta de página simple.
*/
public function build(): array {
$node_count = $this->entityTypeManager->getStorage('node')->getQuery()->count()->execute();
return [
'#markup' => $this->t('There are @count nodes on the site.', ['@count' => $node_count]),
];
}
}
Ventajas de la promoción de propiedades del constructor
Usar la promoción de propiedades del constructor en tus clases de Drupal ofrece varios beneficios:
-
Menos código repetitivo: Escribes significativamente menos código. No es necesario declarar y asignar propiedades manualmente, lo que puede eliminar muchas líneas especialmente cuando tu clase tiene múltiples dependencias. Esto hace que tus módulos sean más limpios y fáciles de mantener.
-
Código más claro y conciso: Las dependencias de la clase son visibles en un solo lugar — la firma del constructor — en lugar de estar dispersas entre las declaraciones de propiedades y el cuerpo del constructor. Esto mejora la legibilidad y deja claro inmediatamente qué servicios requiere la clase.
-
Menos comentarios de documentación necesarios: Porque las propiedades se declaran con tipos en el constructor, puedes omitir anotaciones
@var
y@param
redundantes para esas propiedades y parámetros del constructor (siempre que el propósito sea obvio por el nombre). El código es en gran medida autoexplicativo. Puedes seguir documentando cualquier cosa no obvia, pero hay menos repetición. -
Sintaxis moderna de PHP: Adoptar la promoción de propiedades significa que tu código se mantiene actualizado con las prácticas modernas de PHP. El núcleo de Drupal 10+ ha empezado a utilizar esta sintaxis para código nuevo, así que usarla en módulos personalizados hace que tu código sea más coherente con el núcleo y los ejemplos de la comunidad. Además, prepara tu base de código para futuras mejoras (por ejemplo, PHP 8.1+ permite usar la palabra clave
readonly
en propiedades promovidas para dependencias verdaderamente inmutables).
El rendimiento y la funcionalidad permanecen igual que con la inyección tradicional: la promoción de propiedades es solo una conveniencia del lenguaje. Sigues teniendo propiedades completamente tipadas que puedes usar en toda la clase (por ejemplo, $this->entityTypeManager
en el controlador del ejemplo). Internamente, el resultado es equivalente al del código más largo; simplemente se logra con menos esfuerzo.
Conclusión
La promoción de propiedades del constructor es una característica simple pero poderosa de PHP 8 que los desarrolladores de Drupal pueden aprovechar para simplificar el desarrollo de módulos personalizados. Al eliminar el código repetitivo, te permite centrarte en lo que realmente hace tu clase en vez del cableado de servicios. Mostramos cómo convertir una clase de servicio típica de Drupal y una clase de controlador para usar propiedades promovidas, y la comparamos con el enfoque tradicional. El resultado es un código más conciso y mantenible sin sacrificar claridad ni funcionalidad. A medida que Drupal avanza con requisitos modernos de PHP, utilizar características como la promoción de propiedades en tus módulos personalizados ayudará a mantener tu código limpio, claro y alineado con las mejores prácticas actuales. Adopta la sintaxis moderna para hacer tu desarrollo en Drupal más fácil y elegante.