Лучшие практики разработки модулей
Этот документ объединяет лучшие практики для разработки высококачественных модулей XOOPS. Следование этим рекомендациям обеспечивает поддерживаемые, безопасные и производительные модули.
Архитектура
Заголовок раздела «Архитектура»Следуйте чистой архитектуре
Заголовок раздела «Следуйте чистой архитектуре»Организуйте код в слои:
src/├── Domain/ # Бизнес-логика, сущности├── Application/ # Варианты использования, сервисы├── Infrastructure/ # База данных, внешние сервисы└── Presentation/ # Контроллеры, шаблоныЕдиная ответственность
Заголовок раздела «Единая ответственность»Каждый класс должен иметь одну причину для изменения:
// Хорошо: Сфокусированные классыclass ArticleRepository { /* только персистентность */ }class ArticleValidator { /* только валидация */ }class ArticleNotifier { /* только уведомления */ }
// Плохо: Класс-монстрclass Article { public function save() { } public function validate() { } public function notify() { } public function generatePDF() { }}Внедрение зависимостей
Заголовок раздела «Внедрение зависимостей»Внедряйте зависимости, не создавайте их:
// Хорошоpublic function __construct( private readonly ArticleRepositoryInterface $repository) {}
// Плохоpublic function __construct() { $this->repository = new ArticleRepository();}Качество кода
Заголовок раздела «Качество кода»Типобезопасность
Заголовок раздела «Типобезопасность»Используйте строгие типы и объявления типов:
<?php
declare(strict_types=1);
final class ArticleService{ public function findById(int $id): ?Article { // ... }
public function create(CreateArticleDTO $dto): Article { // ... }}Обработка ошибок
Заголовок раздела «Обработка ошибок»Используйте исключения надлежащим образом:
// Генерируйте конкретные исключенияthrow new ArticleNotFoundException($id);throw new ValidationException($errors);throw new UnauthorizedException('Cannot edit this article');
// Ловите на подходящем уровнеtry { $article = $service->create($dto);} catch (ValidationException $e) { return $this->renderForm($e->getErrors());} catch (UnauthorizedException $e) { return $this->redirectToLogin();}Безопасность null
Заголовок раздела «Безопасность null»Избегайте null, где это возможно:
// Используйте паттерн null objectpublic function getAuthor(): UserInterface{ return $this->author ?? new AnonymousUser();}
// Используйте паттерн Optional/Maybepublic function findById(int $id): ?Article{ // Явно nullable возврат}База данных
Заголовок раздела «База данных»Используйте Criteria для запросов
Заголовок раздела «Используйте Criteria для запросов»$criteria = new CriteriaCompo();$criteria->add(new Criteria('status', 'published'));$criteria->add(new Criteria('category_id', $categoryId));$criteria->setSort('created_at');$criteria->setOrder('DESC');$criteria->setLimit($limit);
$items = $handler->getObjects($criteria);Экранируйте пользовательский ввод
Заголовок раздела «Экранируйте пользовательский ввод»$sql = sprintf( "SELECT * FROM %s WHERE id = %d AND title = %s", $db->prefix('mymodule_items'), intval($id), $db->quoteString($title));Используйте транзакции
Заголовок раздела «Используйте транзакции»$db->query('START TRANSACTION');
try { $handler->insert($article); $handler->insert($metadata); $db->query('COMMIT');} catch (\Exception $e) { $db->query('ROLLBACK'); throw $e;}Безопасность
Заголовок раздела «Безопасность»Всегда проверяйте входные данные
Заголовок раздела «Всегда проверяйте входные данные»use Xmf\Request;
$id = Request::getInt('id', 0);$title = Request::getString('title', '');$data = Request::getArray('data', []);
// Дополнительная валидацияif (strlen($title) < 5) { throw new ValidationException('Title too short');}Используйте CSRF токены
Заголовок раздела «Используйте CSRF токены»// В форме$form->addElement(new XoopsFormHiddenToken());
// При отправкеif (!$GLOBALS['xoopsSecurity']->check()) { redirect_header('index.php', 3, 'Invalid token');}Проверяйте разрешения
Заголовок раздела «Проверяйте разрешения»if (!$helper->isUserAdmin()) { redirect_header('index.php', 3, _NOPERM);}
if (!$permHandler->isGranted('edit', $categoryId)) { throw new UnauthorizedException();}Производительность
Заголовок раздела «Производительность»Используйте кэширование
Заголовок раздела «Используйте кэширование»$cache = $helper->getCache();$cacheKey = "articles_{$categoryId}_{$limit}";
$articles = $cache->read($cacheKey);if ($articles === false) { $articles = $handler->getArticles($categoryId, $limit); $cache->write($cacheKey, $articles, 3600);}Оптимизируйте запросы
Заголовок раздела «Оптимизируйте запросы»// Используйте индексы// Добавьте в sql/mysql.sql:// INDEX `idx_status_date` (`status`, `created_at`)
// Выбирайте только нужные столбцы$handler->getObjects($criteria, false, true); // asArray = true
// Используйте постраничное отображение$criteria->setLimit($perPage);$criteria->setStart($offset);Тестирование
Заголовок раздела «Тестирование»Напишите модульные тесты
Заголовок раздела «Напишите модульные тесты»public function testCreateArticle(): void{ $repository = $this->createMock(ArticleRepositoryInterface::class); $repository->expects($this->once())->method('save');
$service = new ArticleService($repository); $dto = new CreateArticleDTO('Title', 'Content');
$article = $service->create($dto);
$this->assertInstanceOf(Article::class, $article);}Связанная документация
Заголовок раздела «Связанная документация»- Clean-Code - Принципы чистого кода
- Code-Organization - Структура проекта
- Testing - Руководство по тестированию
- ../02-Core-Concepts/Security/Security-Best-Practices - Руководство по безопасности