Кастомные хуки Payload CMS
Хуки в Payload — основной механизм расширения бизнес-логики. Они выполняются на определённых этапах жизненного цикла документа: до/после чтения, создания, изменения, удаления. Хуки — async-функции TypeScript с полной типизацией.
Типы хуков коллекций
// collections/Orders.ts
const Orders: CollectionConfig = {
slug: 'orders',
hooks: {
beforeOperation: [/* валидация до операции */],
beforeValidate: [/* изменение данных до валидации */],
beforeChange: [/* трансформация данных */],
afterChange: [/* side effects после сохранения */],
beforeRead: [/* фильтрация данных при чтении */],
afterRead: [/* обогащение данных после чтения */],
beforeDelete: [/* проверки перед удалением */],
afterDelete: [/* cleanup после удаления */],
},
}
Хуки beforeChange: трансформация данных
import type { CollectionBeforeChangeHook } from 'payload/types'
const generateOrderNumber: CollectionBeforeChangeHook = async ({
data,
req,
operation,
}) => {
if (operation === 'create') {
// Генерация номера заказа
const count = await req.payload.count({ collection: 'orders' })
data.orderNumber = `ORD-${String(count + 1).padStart(6, '0')}`
// Установить автора
if (req.user) {
data.createdBy = req.user.id
}
// Timestamp
data.createdAt = new Date().toISOString()
}
return data
}
const validateStock: CollectionBeforeChangeHook = async ({ data, req }) => {
// Проверить наличие товаров перед созданием заказа
for (const item of data.items || []) {
const product = await req.payload.findByID({
collection: 'products',
id: item.product,
})
if (product.stock < item.quantity) {
throw new Error(`Товар "${product.name}": недостаточно на складе`)
}
}
return data
}
Хуки afterChange: side effects
import type { CollectionAfterChangeHook } from 'payload/types'
const sendOrderConfirmation: CollectionAfterChangeHook = async ({
doc,
operation,
req,
}) => {
if (operation === 'create') {
// Отправить email через сервис
await emailService.send({
to: doc.customerEmail,
subject: `Заказ #${doc.orderNumber} принят`,
template: 'order-confirmation',
data: { order: doc },
})
}
if (operation === 'update' && doc.status === 'shipped') {
await emailService.send({
to: doc.customerEmail,
subject: `Заказ #${doc.orderNumber} отправлен`,
template: 'order-shipped',
data: { order: doc, trackingNumber: doc.trackingNumber },
})
}
}
const revalidateCache: CollectionAfterChangeHook = async ({ doc }) => {
// Инвалидация Next.js ISR кэша
const paths = [`/products/${doc.slug}`, '/products']
await Promise.all(
paths.map(path =>
fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/revalidate`, {
method: 'POST',
headers: { 'x-revalidate-secret': process.env.REVALIDATE_SECRET! },
body: JSON.stringify({ path }),
})
)
)
}
const syncWithCRM: CollectionAfterChangeHook = async ({ doc, operation }) => {
if (operation === 'create') {
// Создать сделку в CRM
await crmClient.deals.create({
title: `Заказ #${doc.orderNumber}`,
amount: doc.total,
contactEmail: doc.customerEmail,
})
}
}
Хуки afterRead: обогащение данных
import type { CollectionAfterReadHook } from 'payload/types'
const addComputedFields: CollectionAfterReadHook = async ({ doc }) => {
// Вычислить итоговые поля, которые не хранятся в БД
if (doc.items) {
doc.subtotal = doc.items.reduce(
(sum: number, item: any) => sum + item.price * item.quantity,
0
)
doc.totalItems = doc.items.length
}
return doc
}
Хуки beforeDelete: защита от удаления
import type { CollectionBeforeDeleteHook } from 'payload/types'
const preventDeleteWithOrders: CollectionBeforeDeleteHook = async ({
id,
req,
}) => {
// Запретить удаление клиента с активными заказами
const orders = await req.payload.find({
collection: 'orders',
where: {
and: [
{ customer: { equals: id } },
{ status: { not_in: ['completed', 'cancelled'] } },
],
},
limit: 1,
})
if (orders.totalDocs > 0) {
throw new Error('Нельзя удалить клиента с активными заказами')
}
}
Хуки Global
const Settings: GlobalConfig = {
slug: 'settings',
hooks: {
afterChange: [
async ({ doc }) => {
// При изменении настроек — инвалидировать весь сайт
await fetch('/api/revalidate?path=/', { method: 'POST' })
},
],
},
}
Переиспользование хуков
// hooks/shared/timestamps.ts
import type { CollectionBeforeChangeHook } from 'payload/types'
export const setTimestamps: CollectionBeforeChangeHook = ({ data, operation }) => {
if (operation === 'create') {
data.createdAt = new Date().toISOString()
}
data.updatedAt = new Date().toISOString()
return data
}
// В коллекциях:
hooks: {
beforeChange: [setTimestamps, ...otherHooks],
}
Обработка ошибок в хуках
const validateHook: CollectionBeforeChangeHook = async ({ data }) => {
try {
await externalValidationService.validate(data)
} catch (error) {
if (error instanceof ValidationError) {
// Payload покажет ошибку в admin UI
throw new Error(`Validation failed: ${error.message}`)
}
// Логировать неожиданные ошибки, не блокировать сохранение
console.error('External validation error:', error)
}
return data
}
Сроки
Набор хуков для бизнес-логики одной коллекции (email-уведомления, CRM-синхронизация, валидация) — 1–2 дня.







