Inyección de dependencias en XOOPS
Descripción general
Sección titulada «Descripción general»La inyección de dependencias (DI) es un patrón de diseño que permite que los componentes reciban sus dependencias de fuentes externas en lugar de crearlas internamente. XOOPS 4.0 introduce soporte de contenedor DI compatible con PSR-11.
¿Por qué inyección de dependencias?
Sección titulada «¿Por qué inyección de dependencias?»Sin DI (Acoplamiento fuerte)
Sección titulada «Sin DI (Acoplamiento fuerte)»class ArticleService{ private ArticleRepository $repository; private EventDispatcher $dispatcher;
public function __construct() { // Dependencias duras - difíciles de probar y modificar $this->repository = new ArticleRepository(new XoopsDatabase()); $this->dispatcher = new EventDispatcher(); }}Con DI (Acoplamiento débil)
Sección titulada «Con DI (Acoplamiento débil)»class ArticleService{ public function __construct( private readonly ArticleRepositoryInterface $repository, private readonly EventDispatcherInterface $dispatcher ) {}}Contenedor PSR-11
Sección titulada «Contenedor PSR-11»Uso básico
Sección titulada «Uso básico»use Psr\Container\ContainerInterface;
// Obtener el contenedor$container = \Xmf\Module\Helper::getHelper('mymodule')->getContainer();
// Recuperar un servicio$articleService = $container->get(ArticleService::class);
// Comprobar si existe el servicioif ($container->has(ArticleService::class)) { // Usar el servicio}Configuración del contenedor
Sección titulada «Configuración del contenedor»use Psr\Container\ContainerInterface;
return [ // Instanciación simple de clase ArticleRepository::class => ArticleRepository::class,
// Vinculación de interfaz a implementación ArticleRepositoryInterface::class => ArticleRepository::class,
// Función de fábrica ArticleService::class => function (ContainerInterface $c): ArticleService { return new ArticleService( $c->get(ArticleRepositoryInterface::class), $c->get(EventDispatcherInterface::class) ); },
// Instancia compartida (singleton) 'database' => function (): XoopsDatabase { return XoopsDatabaseFactory::getDatabaseConnection(); },];Registro de servicios
Sección titulada «Registro de servicios»Auto-cableado
Sección titulada «Auto-cableado»// El contenedor resuelve automáticamente las dependencias// cuando hay sugerencias de tipo disponibles
class ArticleController{ public function __construct( private readonly ArticleService $service, private readonly ViewRenderer $renderer ) {}}
// El contenedor crea ArticleController con sus dependencias$controller = $container->get(ArticleController::class);Registro manual
Sección titulada «Registro manual»return [ ArticleService::class => [ 'class' => ArticleService::class, 'arguments' => [ ArticleRepositoryInterface::class, EventDispatcherInterface::class, ], 'shared' => true, // Singleton ],
'article.handler' => [ 'factory' => [ArticleHandlerFactory::class, 'create'], 'arguments' => ['@database'], // Referencia otro servicio ],];Inyección de constructor
Sección titulada «Inyección de constructor»Enfoque preferido
Sección titulada «Enfoque preferido»final class ArticleService{ public function __construct( private readonly ArticleRepositoryInterface $repository, private readonly EventDispatcherInterface $dispatcher, private readonly LoggerInterface $logger ) {}
public function create(CreateArticleDTO $dto): Article { $this->logger->info('Creating article', ['title' => $dto->title]);
$article = Article::create($dto); $this->repository->save($article); $this->dispatcher->dispatch(new ArticleCreatedEvent($article));
return $article; }}Inyección de método
Sección titulada «Inyección de método»Para dependencias opcionales
Sección titulada «Para dependencias opcionales»class ArticleController{ public function __construct( private readonly ArticleService $service ) {}
public function show(int $id, ?CacheInterface $cache = null): Response { $cacheKey = "article_{$id}";
if ($cache && $cached = $cache->get($cacheKey)) { return $this->render($cached); }
$article = $this->service->findById($id);
$cache?->set($cacheKey, $article, 3600);
return $this->render($article); }}Vinculación de interfaz
Sección titulada «Vinculación de interfaz»Definir interfaces
Sección titulada «Definir interfaces»interface ArticleRepositoryInterface{ public function findById(int $id): ?Article; public function save(Article $article): void; public function delete(Article $article): void;}Vincular implementación
Sección titulada «Vincular implementación»return [ ArticleRepositoryInterface::class => XoopsArticleRepository::class,
// O con fábrica ArticleRepositoryInterface::class => function (ContainerInterface $c) { return new XoopsArticleRepository( $c->get('database') ); },];Pruebas con DI
Sección titulada «Pruebas con DI»Burla fácil
Sección titulada «Burla fácil»class ArticleServiceTest extends TestCase{ public function testCreateArticle(): void { // Crear burlas $repository = $this->createMock(ArticleRepositoryInterface::class); $dispatcher = $this->createMock(EventDispatcherInterface::class); $logger = $this->createMock(LoggerInterface::class);
// Inyectar burlas $service = new ArticleService($repository, $dispatcher, $logger);
// Establecer expectativas $repository->expects($this->once())->method('save'); $dispatcher->expects($this->once())->method('dispatch');
// Prueba $dto = new CreateArticleDTO('Title', 'Content'); $article = $service->create($dto);
$this->assertInstanceOf(Article::class, $article); }}Integración heredada de XOOPS
Sección titulada «Integración heredada de XOOPS»Puente entre antiguo y nuevo
Sección titulada «Puente entre antiguo y nuevo»// Obtener servicio del contenedor en código heredadofunction mymodule_get_articles(int $limit): array{ $container = \Xmf\Module\Helper::getHelper('mymodule')->getContainer(); $service = $container->get(ArticleService::class);
return $service->findRecent($limit);}Envolver controladores heredados
Sección titulada «Envolver controladores heredados»return [ 'article.handler' => function () { return xoops_getModuleHandler('article', 'mymodule'); },
ArticleRepositoryInterface::class => function (ContainerInterface $c) { return new LegacyArticleRepository( $c->get('article.handler') ); },];Mejores prácticas
Sección titulada «Mejores prácticas»- Inyectar interfaces - Depender de abstracciones, no de implementaciones
- Inyección de constructor - Preferir constructor sobre inyección de setter
- Responsabilidad única - Cada clase debe tener pocas dependencias
- Evitar conocimiento del contenedor - Los servicios no deberían conocer el contenedor
- Configurar, no codificar - Usar archivos de configuración para el cableado
Documentación relacionada
Sección titulada «Documentación relacionada»- ../07-XOOPS-4.0/Implementation-Guides/PSR-11-Dependency-Injection-Guide - Implementación PSR-11
- ../03-Module-Development/Patterns/Service-Layer - Patrón de servicio
- ../03-Module-Development/Best-Practices/Testing - Pruebas con DI
- ../07-XOOPS-4.0/XOOPS-4.0-Architecture - Descripción general de la arquitectura