Skip to content

PSR-4 Autoloading in XOOPS 4.0

PSR-4 describes a specification for autoloading classes from file paths. XOOPS 4.0 fully adopts PSR-4, replacing the legacy class loading mechanisms with a standardized, Composer-compatible approach.

  1. Fully Qualified Class Name (FQCN) must have a top-level namespace
  2. Namespace prefixes map to base directories
  3. Subdirectory names correspond to sub-namespace names
  4. File names must match class names with .php extension
Namespace Prefix → Base Directory
Xoops\Module\News\ → modules/news/src/
Class: Xoops\Module\News\Controller\ArticleController
File: modules/news/src/Controller/ArticleController.php
modules/publisher/
├── composer.json # Module-specific dependencies
├── module.json # Module manifest
├── xoops_version.php # Legacy metadata (for compatibility)
├── src/ # PSR-4 autoloaded source
│ ├── Controller/
│ │ ├── ArticleController.php
│ │ └── CategoryController.php
│ ├── Entity/
│ │ ├── Article.php
│ │ └── Category.php
│ ├── Repository/
│ │ ├── ArticleRepository.php
│ │ └── CategoryRepository.php
│ ├── Service/
│ │ ├── ArticleService.php
│ │ └── SearchService.php
│ └── Helper.php
├── class/ # Legacy classes (deprecated)
│ └── Handler/ # Legacy handlers for BC
├── templates/
├── language/
└── assets/

XOOPS 4.0 uses a standardized namespace pattern:

Xoops\Module\{ModuleName}\{Component}\{ClassName}

Examples:

  • Xoops\Module\Publisher\Controller\ArticleController
  • Xoops\Module\Publisher\Entity\Article
  • Xoops\Module\Publisher\Service\ArticleService
  • Xoops\Module\Publisher\Repository\ArticleRepository
{
"name": "xoopsmodules/publisher",
"description": "XOOPS Publisher Module",
"type": "xoops-module",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.4",
"xoops/xoops-core": "^4.0"
},
"autoload": {
"psr-4": {
"Xoops\\Module\\Publisher\\": "src/"
}
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.0"
}
}
{
"name": "xoops/xoops-core",
"description": "XOOPS Content Management System",
"type": "project",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.4",
"psr/http-message": "^2.0",
"psr/container": "^2.0",
"psr/log": "^3.0"
},
"autoload": {
"psr-4": {
"Xoops\\Core\\": "class/Core/",
"Xoops\\Kernel\\": "class/Kernel/",
"Xmf\\": "class/Xmf/"
}
}
}
<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Xoops\Core\View\ViewRendererInterface;
use Xoops\Module\Publisher\Service\ArticleService;
/**
* Article Controller
*
* @package Xoops\Module\Publisher\Controller
*/
class ArticleController
{
public function __construct(
private readonly ArticleService $articleService,
private readonly ViewRendererInterface $view
) {}
public function list(ServerRequestInterface $request): ResponseInterface
{
$page = (int) ($request->getQueryParams()['page'] ?? 1);
$articles = $this->articleService->getPaginated($page);
return $this->view->render('@modules/publisher/article/list', [
'articles' => $articles,
'currentPage' => $page,
]);
}
public function view(ServerRequestInterface $request, int $id): ResponseInterface
{
$article = $this->articleService->findById($id);
if ($article === null) {
throw new NotFoundException('Article not found');
}
return $this->view->render('@modules/publisher/article/view', [
'article' => $article,
]);
}
}
<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher\Entity;
/**
* Article Entity
*
* @package Xoops\Module\Publisher\Entity
*/
class Article
{
public function __construct(
public readonly int $id,
public string $title,
public string $content,
public int $authorId,
public int $categoryId,
public bool $published = false,
public ?\DateTimeImmutable $publishedAt = null,
public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
public ?\DateTimeImmutable $updatedAt = null,
) {}
public function publish(): void
{
$this->published = true;
$this->publishedAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
}
public function unpublish(): void
{
$this->published = false;
$this->publishedAt = null;
$this->updatedAt = new \DateTimeImmutable();
}
public function toArray(): array
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'author_id' => $this->authorId,
'category_id' => $this->categoryId,
'published' => $this->published,
'published_at' => $this->publishedAt?->format('Y-m-d H:i:s'),
'created_at' => $this->createdAt->format('Y-m-d H:i:s'),
'updated_at' => $this->updatedAt?->format('Y-m-d H:i:s'),
];
}
}
<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher\Repository;
use Xoops\Core\Database\ConnectionInterface;
use Xoops\Module\Publisher\Entity\Article;
/**
* Article Repository
*
* @package Xoops\Module\Publisher\Repository
*/
class ArticleRepository implements ArticleRepositoryInterface
{
public function __construct(
private readonly ConnectionInterface $connection
) {}
public function findById(int $id): ?Article
{
$qb = $this->connection->createQueryBuilder();
$qb->select('*')
->from('publisher_articles')
->where('id = :id')
->setParameter('id', $id);
$row = $qb->fetchAssociative();
return $row ? $this->hydrate($row) : null;
}
public function findPublished(int $limit = 10, int $offset = 0): array
{
$qb = $this->connection->createQueryBuilder();
$qb->select('*')
->from('publisher_articles')
->where('published = :published')
->setParameter('published', true)
->orderBy('published_at', 'DESC')
->setMaxResults($limit)
->setFirstResult($offset);
$rows = $qb->fetchAllAssociative();
return array_map([$this, 'hydrate'], $rows);
}
public function save(Article $article): Article
{
$data = $article->toArray();
unset($data['id'], $data['created_at']);
if ($article->id === 0) {
$this->connection->insert('publisher_articles', $data);
$id = (int) $this->connection->lastInsertId();
return new Article($id, ...array_values($data));
}
$this->connection->update('publisher_articles', $data, ['id' => $article->id]);
return $article;
}
private function hydrate(array $row): Article
{
return new Article(
id: (int) $row['id'],
title: $row['title'],
content: $row['content'],
authorId: (int) $row['author_id'],
categoryId: (int) $row['category_id'],
published: (bool) $row['published'],
publishedAt: $row['published_at']
? new \DateTimeImmutable($row['published_at'])
: null,
createdAt: new \DateTimeImmutable($row['created_at']),
updatedAt: $row['updated_at']
? new \DateTimeImmutable($row['updated_at'])
: null,
);
}
}
<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher\Service;
use Xoops\Module\Publisher\Entity\Article;
use Xoops\Module\Publisher\Repository\ArticleRepositoryInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* Article Service
*
* @package Xoops\Module\Publisher\Service
*/
class ArticleService
{
private const ARTICLES_PER_PAGE = 20;
public function __construct(
private readonly ArticleRepositoryInterface $repository,
private readonly EventDispatcherInterface $eventDispatcher
) {}
public function findById(int $id): ?Article
{
return $this->repository->findById($id);
}
public function getPaginated(int $page = 1): array
{
$offset = ($page - 1) * self::ARTICLES_PER_PAGE;
return $this->repository->findPublished(
self::ARTICLES_PER_PAGE,
$offset
);
}
public function publish(int $articleId): Article
{
$article = $this->repository->findById($articleId);
if ($article === null) {
throw new ArticleNotFoundException("Article {$articleId} not found");
}
$article->publish();
$article = $this->repository->save($article);
$this->eventDispatcher->dispatch(
new ArticlePublishedEvent($article->id, $article->authorId)
);
return $article;
}
}
modules/publisher/class/
├── Article.php # class PublisherArticle extends XoopsObject
├── ArticleHandler.php # class PublisherArticleHandler extends XoopsPersistableObjectHandler
└── Category.php

Keep legacy classes but create new PSR-4 classes:

// modules/publisher/src/Entity/Article.php (new)
namespace Xoops\Module\Publisher\Entity;
class Article { /* ... */ }
modules/publisher/src/Repository/LegacyArticleRepository.php
namespace Xoops\Module\Publisher\Repository;
use Xoops\Module\Publisher\Entity\Article;
class LegacyArticleRepository implements ArticleRepositoryInterface
{
private \PublisherArticleHandler $handler;
public function __construct()
{
$helper = \Xoops\Module\Publisher\Helper::getInstance();
$this->handler = $helper->getHandler('Article');
}
public function findById(int $id): ?Article
{
$obj = $this->handler->get($id);
if ($obj === false) {
return null;
}
return $this->convertToEntity($obj);
}
private function convertToEntity(\PublisherArticle $obj): Article
{
return new Article(
id: (int) $obj->getVar('articleid'),
title: $obj->getVar('title'),
content: $obj->getVar('body'),
// ... map other fields
);
}
}
// Register based on configuration
$container->set(ArticleRepositoryInterface::class, function($c) {
$useLegacy = $c->get('config')->get('publisher.use_legacy_handler');
return $useLegacy
? new LegacyArticleRepository()
: new ArticleRepository($c->get('database'));
});
include/common.php
<?php
// Load Composer autoloader
require_once XOOPS_ROOT_PATH . '/vendor/autoload.php';
// The autoloader handles all PSR-4 namespaced classes automatically
// No manual require_once needed for classes following PSR-4

XOOPS core registers module namespaces automatically:

// Automatic registration for all modules
// Xoops\Module\{ModuleName}\ → modules/{modulename}/src/
$loader = require XOOPS_ROOT_PATH . '/vendor/autoload.php';
// Register active modules
foreach ($activeModules as $module) {
$namespace = 'Xoops\\Module\\' . ucfirst($module->dirname) . '\\';
$path = XOOPS_ROOT_PATH . '/modules/' . $module->dirname . '/src/';
if (is_dir($path)) {
$loader->addPsr4($namespace, $path);
}
}
// Correct: ArticleController.php contains only ArticleController
// Wrong: Multiple classes in one file
modules/publisher/src/Controller/Admin/ArticleController.php
// Class: Xoops\Module\Publisher\Controller\Admin\ArticleController
<?php
declare(strict_types=1);
namespace Xoops\Module\Publisher\Service;
TypeConventionExample
ClassPascalCaseArticleController
MethodcamelCasefindById()
PropertycamelCase$articleService
ConstantUPPER_SNAKEMAX_ARTICLES

PHPStorm automatically detects composer.json and configures autoloading.

Install PHP Intelephense extension and ensure composer.json is present.

.vscode/settings.json
{
"intelephense.environment.includePaths": [
"vendor",
"class"
]
}
  • PSR Standards Overview
  • Migration Guide
  • XOOPS 4.0 Specification

#xoops-4.0 #psr-4 #autoloading #namespaces #composer