Сквозной сценарий CRUD (список + создание/редактирование + удаление) на примере сущности Note в гипотетическом модуле MyModule. Живой рабочий пример с тем же набором слоёв — страница «Шаблоны» модуля dle_faker (TemplatesPage.php, Ajax/CreateTemplateHandler.php, Ajax/DeleteTemplateHandler.php).
AbstractEntity уже даёт $id, createdAt/updatedAt/creator/lastEditor с автозаполнением через beforeSave() — свои модели не должны дублировать эти поля/геттеры (см. AbstractEntity).
AbstractRepository уже даёт findFiltered() (фильтр + пагинация + сортировка), saveEntity(), deleteEntity(), distinctColumnValues()/columnBounds() для фильтров (см. AbstractRepository).
<?phpdeclare(strict_types=1);namespaceDevCraft\Modules\MyModule\Ajax;useDevCraft\Core\Application;useDevCraft\Core\Http\AjaxRequest;useDevCraft\Core\Http\JsonResponse;useDevCraft\Core\Interfaces\AjaxHandlerInterface;useDevCraft\Core\Interfaces\ResponseInterface;useDevCraft\Modules\MyModule\Models\Note;useDevCraft\Modules\MyModule\Repositories\NoteRepository;finalclassSaveNoteHandlerimplementsAjaxHandlerInterface{publicfunctionhandle(AjaxRequest$request):ResponseInterface{$data=$request->data;if(trim((string)($data['title']??''))===''){returnJsonResponse::fail(__('Ошибка'),__('Заголовок не может быть пустым'),'validation',422,['fields'=>['title'=>__('Поле обязательно')],]);}$database=Application::instance()->database();$id=(int)($data['id']??0);/** @var NoteRepository $repository */$repository=$database->repository(Note::class);$note=$id>0?$repository->findOneById($id):NULL;$note??=newNote();$note->title=trim((string)$data['title']);$note->body=(string)($data['body']??'');$id>0?$database->update($note):$database->create($note);returnJsonResponse::toast(__('Заметка сохранена'),['saved'=>true,'id'=>$note->id(),'redirect'=>'?mod=my_module&action=notes',]);}}
AdminLink::page() регистрирует и пункт меню, и маршрут action=notes → NotesPage; ajax.methods — карту method => Handler для devcraft/ajax.php (единая точка входа AJAX, см. манифест модуля).
Отправка формы — через DevCraft.Ajax.post('save_note', data) (см. frontend/templates.md), обработчик ответа читает JsonResponse (toast/fail) и делает redirect, если он присутствует в data.