Эта статья — не сухое руководство, а разбор реальных ошибок, которые возникают при создании CRUD для существующей таблицы в Joomla 5.4.5. Каждая ошибка — это ловушка, в которую легко попасть, если делать компонент «по образцу» без понимания того, как Joomla обрабатывает формы и сохранение.
Контекст
Есть таблица #__example_items с полями:
id— INT, AUTO_INCREMENT, PRIMARY KEYtitle,alias— строкиdescription— текстpriority,is_published,is_processed— числа и булевыitem_type— ENUMparent_id— INT, может быть NULLkeywords— TEXT
В таблице есть уникальный индекс unique_alias по первым 200 символам поля alias. Это важно для понимания первой ошибки.
Компонент com_example имеет namespace MyCompany\Component\Example, административная часть лежит в administrator/components/com_example/.
Ошибка первая: «Duplicate entry» при редактировании
Симптом: открываешь запись на редактирование, меняешь поле, нажимаешь «Сохранить» — получаешь:
Duplicate entry 'sample-alias-value' for key 'example_items.unique_alias'
При этом если ничего не менять и просто нажать «Сохранить» — ошибка та же.
Что происходило
Joomla делала INSERT вместо UPDATE. При INSERT MySQL пытался создать новую запись с тем же alias, что уже есть в базе, и падал на уникальном индексе.
Почему Joomla делала INSERT? Потому что FormController::save() в Joomla 5 берёт id записи из URL, а не из поля jform[id]. В форме action был:
<form action="<?php echo Route::_('index.php?option=com_example&task=item.save'); ?>" ...>
Параметра id в URL не было. FormController считал, что сохраняется новая запись, и передавал в модель $data['id'] = 0. Модель, получив 0, вызывала parent::save(), который делал INSERT.
Исправление
В tmpl/item/edit.php добавьте id в action формы:
<form action="<?php echo Route::_('index.php?option=com_example&task=item.save&id=' . (int) ($this->item->id ?? 0)); ?>" ...>
Теперь FormController видит id в URL, передаёт его в модель, и модель делает UPDATE.
Урок
В Joomla 5 id для сохранения берётся из URL-параметра, а не из скрытого поля формы. Если вы делаете кастомную форму редактирования — всегда добавляйте &id=... в action. Иначе Joomla будет считать, что вы создаёте новую запись.
Ошибка вторая: «Incorrect integer value: '' for column 'parent_id'»
Симптом: после исправления первой ошибки появляется новая:
Incorrect integer value: '' for column 'parent_id' at row 1
Что происходило
Поле parent_id в форме было объявлено как type="text":
<field name="parent_id" type="text" label="COM_EXAMPLE_FIELD_PARENT_ID"/>
Когда поле пустое, Joomla отправляет в POST пустую строку ''. MySQL в строгом режиме (STRICT_TRANS_TABLES) не может преобразовать '' в INT и отклоняет запрос.
Исправление
Замените тип поля на number и задайте default="0":
<field name="parent_id" type="number" default="0" label="COM_EXAMPLE_FIELD_PARENT_ID" min="0" step="1"/>
Теперь при пустом поле Joomla отправляет 0, а не '', и MySQL принимает значение.
Урок
Для числовых полей, которые могут быть пустыми, используйте type="number" с default="0". Не оставляйте type="text" для INT-колонок — пустая строка сломает сохранение в строгом режиме MySQL.
Полный набор файлов для CRUD
Вот эталонный набор, который работает после исправлений.
1. Форма: forms/item.xml
<?xml version="1.0" encoding="UTF-8"?>
<form>
<fieldset name="details">
<field name="id" type="text" readonly="true" class="readonly" label="JGLOBAL_FIELD_ID_LABEL"/>
<field name="title" type="text" label="COM_EXAMPLE_FIELD_TITLE" required="true"/>
<field name="alias" type="text" label="COM_EXAMPLE_FIELD_ALIAS" required="true"/>
<field name="description" type="textarea" label="COM_EXAMPLE_FIELD_DESCRIPTION" rows="15" required="true"/>
<field name="priority" type="number" label="COM_EXAMPLE_FIELD_PRIORITY" default="1" min="1" max="10"/>
<field name="is_published" type="list" label="COM_EXAMPLE_FIELD_IS_PUBLISHED" default="1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="item_type" type="list" label="COM_EXAMPLE_FIELD_ITEM_TYPE" default="manual">
<option value="manual">COM_EXAMPLE_ITEM_TYPE_MANUAL</option>
<option value="processed">COM_EXAMPLE_ITEM_TYPE_PROCESSED</option>
<option value="atomic">COM_EXAMPLE_ITEM_TYPE_ATOMIC</option>
</field>
<field name="parent_id" type="number" default="0" label="COM_EXAMPLE_FIELD_PARENT_ID" min="0" step="1"/>
<field name="is_processed" type="list" label="COM_EXAMPLE_FIELD_IS_PROCESSED" default="0">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="keywords" type="textarea" label="COM_EXAMPLE_FIELD_KEYWORDS" rows="3"/>
</fieldset>
</form>
Ключевые моменты:
id—readonly, но всё равно выводится в форме (Joomla подставит значение из записи).- Числовые поля —
type="number"сdefault. - ENUM —
type="list"с опциями. - Текст —
type="textarea"сrows.
2. Шаблон редактирования: tmpl/item/edit.php
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Router\Route;
HTMLHelper::_('behavior.formvalidator');
?>
<form action="<?php echo Route::_('index.php?option=com_example&task=item.save&id=' . (int) ($this->item->id ?? 0)); ?>" method="post" name="adminForm" id="adminForm" class="form-validate">
<?php echo $this->form->renderField('id'); ?>
<?php echo $this->form->renderField('title'); ?>
<?php echo $this->form->renderField('alias'); ?>
<?php echo $this->form->renderField('description'); ?>
<?php echo $this->form->renderField('priority'); ?>
<?php echo $this->form->renderField('is_published'); ?>
<?php echo $this->form->renderField('item_type'); ?>
<?php echo $this->form->renderField('parent_id'); ?>
<?php echo $this->form->renderField('is_processed'); ?>
<?php echo $this->form->renderField('keywords'); ?>
<input type="hidden" name="task" value="item.save"/>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
Ключевой момент — &id=... в action. Без него Joomla сделает INSERT.
3. Модель: src/Model/ItemModel.php
<?php
namespace MyCompany\Component\Example\Administrator\Model;
use Joomla\CMS\MVC\Model\AdminModel;
class ItemModel extends AdminModel
{
protected $text_prefix = 'COM_EXAMPLE';
public function getForm($data = [], $loadData = true)
{
$form = $this->loadForm(
'com_example.item',
'item',
['control' => 'jform', 'load_data' => $loadData]
);
if (empty($form)) {
return false;
}
return $form;
}
protected function loadFormData()
{
$app = \Joomla\CMS\Factory::getApplication();
$data = $app->getUserState('com_example.edit.item.data', []);
if (empty($data)) {
$data = $this->getItem();
}
return $data;
}
public function save($data)
{
if (!empty($data['id'])) {
$table = $this->getTable();
$table->load((int) $data['id']);
$table->bind($data);
if (!$table->check()) {
$this->setError($table->getError());
return false;
}
if (!$table->store()) {
$this->setError($table->getError());
return false;
}
$this->cleanCache();
return $table->id;
}
return parent::save($data);
}
}
Что здесь важно:
getForm()загружает XML-форму по имениitem.loadFormData()берёт данные из сессии (если было неудачное сохранение) или изgetItem().save()проверяет$data['id']. Если он есть — загружает существующую запись, привязывает данные, проверяет и сохраняет черезstore(). Если нет — вызываетparent::save(), который делает INSERT.
Этот save() — страховка. Даже если id не дойдёт из URL, но будет в $data, модель сделает UPDATE. Но полагаться только на него нельзя: если id нет ни в URL, ни в $data, будет INSERT.
4. Контроллер: src/Controller/ItemController.php
<?php
namespace MyCompany\Component\Example\Administrator\Controller;
use Joomla\CMS\MVC\Controller\FormController;
class ItemController extends FormController
{
protected $option = 'com_example';
protected $view_list = 'items';
}
Ничего лишнего. FormController уже умеет обрабатывать save, apply, cancel. $view_list указывает, куда вернуться после сохранения.
5. Таблица: src/Table/ItemTable.php
<?php
namespace MyCompany\Component\Example\Administrator\Table;
use Joomla\CMS\Table\Table;
use Joomla\Database\DatabaseDriver;
class ItemTable extends Table
{
public function __construct(DatabaseDriver $db)
{
parent::__construct('#__example_items', 'id', $db);
}
}
Ключевой момент: второй аргумент конструктора — имя первичного ключа. Если там опечатка (например, 'ID' вместо 'id'), Joomla не сможет найти запись и сделает INSERT.
6. Список: src/View/Items/HtmlView.php
<?php
namespace MyCompany\Component\Example\Administrator\View\Items;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;
use Joomla\CMS\Language\Text;
class HtmlView extends BaseHtmlView
{
public function display($tpl = null)
{
$this->items = $this->get('Items');
$this->pagination = $this->get('Pagination');
$this->state = $this->get('State');
ToolbarHelper::title(Text::_('COM_EXAMPLE_ITEMS_TITLE'));
ToolbarHelper::addNew('item.add');
ToolbarHelper::editList('item.edit');
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'items.delete');
parent::display($tpl);
}
}
7. Шаблон списка: tmpl/items/default.php
<?php
defined('_JEXEC') or die;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;
?>
<form action="<?php echo Route::_('index.php?option=com_example&view=items'); ?>" method="post" name="adminForm" id="adminForm">
<table class="table table-striped">
<thead>
<tr>
<th width="1%"><?php echo HTMLHelper::_('grid.checkall'); ?></th>
<th><?php echo Text::_('COM_EXAMPLE_FIELD_TITLE'); ?></th>
<th><?php echo Text::_('COM_EXAMPLE_FIELD_ALIAS'); ?></th>
<th width="10%"><?php echo Text::_('COM_EXAMPLE_FIELD_PRIORITY'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $i => $item) : ?>
<tr>
<td><?php echo HTMLHelper::_('grid.id', $i, $item->id); ?></td>
<td>
<a href="/<?php echo Route::_('index.php?option=com_example&task=item.edit&id=' . $item->id); ?>">
<?php echo $this->escape($item->title); ?>
</a>
</td>
<td><?php echo $this->escape($item->alias); ?></td>
<td><?php echo (int) $item->priority; ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php echo $this->pagination->getListFooter(); ?>
<input type="hidden" name="task" value=""/>
<input type="hidden" name="boxchecked" value="0"/>
<?php echo HTMLHelper::_('form.token'); ?>
</form>
Здесь важно:
HTMLHelper::_('grid.checkall')— чекбокс «выбрать все» в заголовке.HTMLHelper::_('grid.id', $i, $item->id)— чекбокс для каждой строки. Без него кнопки «Редактировать» и «Удалить» не работают: Joomla не видит выбранных элементов.- Ссылка на редактирование содержит
&id=....
Отладка: как найти причину, если сохранение не работает
Прежде чем менять код, включите отладку. В админке Joomla: System → Global Configuration → System → Debug System = Yes. После сохранения внизу страницы появится панель с вкладками Request, Session, Queries.
Что смотреть:
- Request → POST — какие данные реально ушли на сервер. Если
jform[id]нет или он пустой — проблема в форме. - Session → registry → com_example → edit → item → data — что Joomla сохранила в сессию после неудачной попытки. Если
data → idравен"0"— значит,idпотерялся до модели. - Queries — какие SQL-запросы выполнились. Если нет
INSERTилиUPDATEв вашу таблицу — значит, сохранение не дошло до базы.
Если отладочная панель не показывает POST (в Joomla 5 она иногда свёрнута), добавьте временную запись в файл прямо в методе save() модели:
file_put_contents('/tmp/debug_save.txt', print_r($data, true));
После сохранения откройте /tmp/debug_save.txt и посмотрите, есть ли в массиве $data ключ id и какое у него значение. Если id нет — проблема в контроллере или форме. Если есть, но не тот — смотрите, что приходит в POST.
Не забудьте удалить отладочную строку после диагностики.
Чек-лист: что проверить, если сохранение не работает
- Action формы содержит
&id=...? Если нет — Joomla сделает INSERT. - Поле
idв XML-форме есть? Оно должно быть, даже еслиreadonly. Joomla подставит значение из записи. - Числовые поля объявлены как
type="number"сdefault="0"? Иначе пустая строка сломает MySQL в строгом режиме. - В
Tableправильно указан первичный ключ? Второй аргумент конструктора. $view_listв контроллере совпадает с именем папки вtmpl/? Например,items→tmpl/items/default.php.- В шаблоне списка есть
grid.idиboxchecked? Иначе кнопки редактирования и удаления не работают. - Кэш автозагрузки сброшен? После правок в PHP-файлах удалите
administrator/cache/autoload_psr4.phpи перезагрузите PHP-FPM.
Почему это неочевидно
Joomla 5 следует MVC, но с несколькими нестандартными решениями:
idдля сохранения берётся из URL, а не из формы.FormControllerне передаётidв модель, если его нет в URL.- MySQL в строгом режиме не прощает пустых строк в INT-колонках.
- Чекбоксы в списке — не украшение, а обязательный элемент для работы тулбара.
Эти вещи не описаны в базовых туториалах по Joomla, и именно на них спотыкаются при создании CRUD для существующих таблиц. Зная их, можно избежать часов отладки.
Автор статьи: https://jedig.ru
Спонсор проекта — магазин ANDROID55: https://mi55.ru
Отдыхайте в Сукко на Чёрном море: https://sukkograd.ru