Hook'lar, B2C Commerce'in kodunuzu kendisine ait olmayan noktalarda çalıştırma yoludur: bir Shopper API isteğinin içinde, sepet hesaplaması sırasında, her storefront isteğinin başında ya da kendi storefront'unuzun tanımladığı bir extension point'te. Production hatalarının önemli bir kısmı da burada yaşar; çünkü hangi implementasyonun hangi sırayla çalıştığı ve bir dönüş değerinin ne yaptığı hook ailesine göre değişir, çoğu eğitim içeriği ise bunları tek bir kural gibi anlatır.
Bu rehber; hook kaydını, çalışma sırasını, dw.system.HookMgr kullanımını, SCAPI ve OCAPI hook'larını, sistem hook'larını, hata yönetimini, performansı ve testi ele alıyor. Cartridge'ların nasıl çalıştığını bildiğinizi varsayar; bilmiyorsanız SFCC cartridge geliştirme rehberiyle başlayın, platformun geneli için de SFCC rehberine bakın.
B2C Commerce'te hook nedir
Hook, bir fonksiyon export eden CommonJS modülüne eşlenmiş, adı olan bir genişletme noktasıdır (extension point). Platform ya da kendi kodunuz extension point'i adıyla çağırır; B2C Commerce kayıtlı modülleri bulur ve çalıştırır. HookMgr referansı bunu "iş mantığında, işlevselliği özelleştirmek için script kaydedebileceğiniz bir extension point" olarak tanımlar.
Üç aile vardır ve aralarındaki farklar ileride önem kazanır:
| Aile | İsim kalıbı | Kim çağırır | Örnek |
|---|---|---|---|
| Shopper API hook'ları (SCAPI ve OCAPI) | dw.ocapi.shop.<resource>.<hook><METHOD> | Platform, bir API isteği sırasında | dw.ocapi.shop.basket.beforePATCH |
| Sistem hook'ları | dw.order.*, dw.system.request.* | Platform ya da HookMgr üzerinden kendi kodunuz | dw.order.calculate |
| Custom hook'lar | Kendi namespace'iniz, örneğin app.* | HookMgr.callHook üzerinden kendi kodunuz | app.payment.processor.default (SFRA) |
Hook'lar SFRA route genişletmesi değildir: server.append bir route'u, hook ise bir iş operasyonunu nerede çalışırsa çalışsın genişletir. SFRA hooks dokümantasyonu, custom bir hook'un hem OCAPI'den hem storefront kodundan çağrılabileceğini belirtir; bu da hook'ları bir SFRA storefront'u ile headless bir kanalın paylaştığı mantığın doğal yeri yapar.
Hook'ları bir cartridge'a kaydetmek
Kayıt üç parçadan oluşur; burada tek bir custom cartridge içinde gösteriliyor:
app_custom_hooks/
package.json
cartridge/
scripts/
hooks.json
hooks/
loyalty.js
product.js
address.js
Adım 1: package.json dosyasını hooks.json dosyasına yönlendirin
Cartridge'ın en üst dizinindeki package.json, hook dosyasını hooks anahtarıyla belirtir. Yol, package.json dosyasının bulunduğu dizine göredir:
{
"hooks": "./cartridge/scripts/hooks.json"
}
Adım 2: hooks.json içinde extension point'leri script'lere eşleyin
hooks dizisindeki her kaydın bir name (extension point) ve bir script (hooks.json dosyasına göre göreli bir yol ya da geçerli herhangi bir modül tanımlayıcısı) alanı vardır:
{
"hooks": [
{ "name": "app.loyalty.orderPlaced", "script": "./hooks/loyalty.js" },
{ "name": "dw.ocapi.shop.product.modifyGETResponse", "script": "./hooks/product.js" },
{ "name": "dw.ocapi.shop.basket.shipment.shipping_address.beforePUT", "script": "./hooks/address.js" }
]
}
Adım 3: fonksiyonu export edin
Platform hook'larında export edilen isim, paket niteliği olmadan hook adıdır: dw.ocapi.shop.basket.shipment.shipping_address.beforePUT, beforePUT olarak export edilir. Custom bir hook'ta fonksiyon adını çağıran taraf callHook metoduna verir; ikisini senkron tutun:
'use strict';
// cartridge/scripts/hooks/loyalty.js
var Logger = require('dw/system/Logger').getLogger('loyalty', 'hooks');
// app.loyalty.orderPlaced implementasyonu; dönüş değeri çağırana gider
exports.orderPlaced = function (order) {
var points = Math.floor(order.totalGrossPrice.value);
Logger.info('Order {0} earns {1} loyalty points', order.orderNo, points);
return { points: points };
};
Cartridge'ın bulunması gereken yer
Kodu yüklemek yetmez. CalculateHooks referansı, hook script'inin bir site cartridge'ı içinde bulunması gerektiğini söyler; yani cartridge, isteği karşılayan sitenin cartridge path listesinde olmalıdır; tüm sitelerin paylaştığı organizasyon düzeyindeki kaynaklar için Salesforce, Business Manager sitesinin kullanılmasını söyler. Bir sitede çalışıp diğerinde çalışmayan bir hook genellikle bir cartridge path farkıdır.
Çalışma sırası ve dönüş değerleri
Kurallar hook ailesine göre değişir; bunları karıştırmak yaygın bir hatadır.
Custom hook'lar: her kayıt çalışır
SFRA hooks dokümantasyonuna göre B2C Commerce, bir extension point için cartridge path üzerindeki tüm cartridge'larda kayıtlı bütün hook'ları cartridge path sırasıyla çalıştırır. Tek bir hooks.json içinde bir extension point için birden fazla modül kaydedebilirsiniz, ama çağrılma sıralarını kontrol edemezsiniz. Tüm modüller ne döndürürse döndürsün çalışır ve çağırana yalnızca son hook'un dönüş değeri ulaşır.
Bunun üç sonucu var:
- Yeniden kaydetmek override etmez. Daha sağdaki bir cartridge
app.validate.basketkaydını yapıyorsa ve sizinki de yapıyorsa, ikisi de çalışır. Controller, template ve script'lerde durum tersidir: en soldaki eşleşme kazanır. - Cartridge path'i yeniden sıralamak çalışma sırasını da değiştirir ve çağıranın hangi implementasyonun dönüş değerini aldığını değiştirebilir.
- Sonuçları dönüş değeri üzerinden toplamayın. Her implementasyonun içine yazdığı bir nesneyi argüman olarak geçin ve
callHookdöndükten sonra onu okuyun.
Shopper API hook'ları: dönüş değeri zinciri bitirir
dw.ocapi.shop.* hook'ları için Commerce API hooks rehberi tersi kuralı koyar: değer döndüren bir hook, o extension point için sistem implementasyonunu ve sonraki tüm kayıtlı hook'ları atlar. Metot referansı bunu "a non-null Status ends the hook execution" (null olmayan bir Status hook yürütmesini bitirir) diye ifade eder.
Varsayılan implementasyonlar gerçek iş yapar. dw.ocapi.shop.basket.items.afterPOST ve dw.ocapi.shop.basket.coupon.afterPOST gibi sepet after* hook'larının varsayılan mantığı örtük olarak dw.order.calculate hook'unu çağırır. items.afterPOST hook'unuzdan new Status(Status.OK) döndürürseniz ürün sepete eklenir ama sepet yeniden hesaplanmaz. Salesforce, işlemi durdurmak istemediğiniz sürece hiçbir şey döndürmemenizi önerir.
| Fonksiyonunuzun dönüşü | Custom hook (app.*) | Shopper API hook'u (dw.ocapi.shop.*) |
|---|---|---|
Hiçbir şey (undefined) | Sonraki kayıt çalışır; çağıran son değeri alır | Sistem implementasyonu ve sonraki kayıtlar yine çalışır |
Status.OK | Sonraki kayıt yine çalışır | Yürütme biter; yeniden hesaplama gibi varsayılan mantık atlanır |
Status.ERROR | Sonraki kayıt yine çalışır; kararı çağıran verir | İşlem durur, transaction geri alınır, HTTP 400 |
| Exception fırlatır | callHook exception'ı çağırana fırlatır | Transaction geri alınır, HTTP 500 |
dw.order.calculate gibi sistem hook'ları, kaydınızın override ettiği bir platform varsayılan implementasyonuyla gelir. Her birini tek bir cartridge'da kaydedin; böylece cevaplanacak bir sıralama sorusu kalmaz.
hasHook göründüğünden daha geniştir
HookMgr.hasHook(extensionPoint), bir hook kayıtlıysa ya da bir sistem varsayılan implementasyonu varsa true döner; bu yüzden hiçbir cartridge kaydetmese bile dw.order.calculate için true döner. Belirli bir fonksiyonun export edilip edilmediğine değil, extension point'in kendisine bakar. Opsiyonel bir custom hook'u güvenle çağırmak için kullanın; cartridge'ınızın doğru bağlandığının kanıtı olarak değil.
dw.system.HookMgr ile hook çağırmak
callHook(extensionPoint, functionName, ...args) hook'un sonucunu döndürür; ne kayıtlı bir hook ne de varsayılan bir implementasyon varsa undefined döner. Hook exception fırlatırsa callHook da fırlatır. Loyalty hook'u için savunmacı bir çağrı noktası:
'use strict';
var HookMgr = require('dw/system/HookMgr');
var Logger = require('dw/system/Logger').getLogger('loyalty', 'hooks');
var LOYALTY_HOOK = 'app.loyalty.orderPlaced';
// Loyalty checkout'u asla bozmamalı; hatalar loglanır, yeniden fırlatılmaz
function notifyOrderPlaced(order) {
if (!HookMgr.hasHook(LOYALTY_HOOK)) {
return null; // bu sitede hiçbir cartridge implemente etmiyor
}
try {
return HookMgr.callHook(LOYALTY_HOOK, 'orderPlaced', order) || null;
} catch (e) {
Logger.error('{0} failed for order {1}: {2}', LOYALTY_HOOK, order.orderNo, e.message);
return null;
}
}
module.exports = { notifyOrderPlaced: notifyOrderPlaced };
Exception'ları yalnızca özellik opsiyonelse yakalayın; aksi halde yayılmalarına izin verin ki transaction geri alınsın. Sistem hook'ları da aynı şekilde çağrılır: HookMgr.callHook('dw.order.calculate', 'calculate', basket).
SCAPI ve OCAPI hook'ları
SCAPI ve OCAPI istekleri için aynı hook'lar çağrılır. Extension point adları tarihsel dw.ocapi.shop.* önekini korur ve Salesforce dokümantasyonu artık OCAPI'nin kendisini deprecated olarak etiketliyor; yeni projeler için bunun ne anlama geldiğini SCAPI ve OCAPI karşılaştırmasında bulabilirsiniz. Tek bir implementasyon iki API'ye de hizmet edebilir; davranışın ayrışması gereken yerde request.isSCAPI() ile dallanın.
Ön koşullar ve kapsam
API hook'larını instance bazında, Business Manager'da Administration > Global Preferences > Feature Switches altında API hook execution seçeneğini açarak etkinleştirin; bunun için Account Manager'da Business Manager Administrator rolü gerekir. Shopper API'lerinin çoğu hook'ları destekler; Shopper Login (SLAS), Experience, Payments, Custom Objects ve Consents desteklemez.
Before, after ve modifyResponse
| Hook tipi | Ne zaman çalışır | Tipik kullanım |
|---|---|---|
before<METHOD> | Platform isteği işlemeden önce | Doğrulama, erişim kontrolleri |
after<METHOD> | İşlemden sonra, yazma transaction'ı içinde | Veri değiştirme; hatası yazmayı geri alması gereken harici çağrılar |
modify<METHOD>Response | Yanıt dönmeden önce | İstemci için c_ property'leri eklemek |
GET istekleri before ve modifyResponse hook'larını destekler; POST, PATCH, PUT ve DELETE bunlara after ekler. Basket payment methods üzerindeki afterGET gibi istisnalar vardır ve bazı adlar _v2 ya da _v3 son eki taşır; bu yüzden adları ve imzaları tahmin etmek yerine Hook Method Details sayfasından alın:
| Extension point | Endpoint (OCAPI yolu) | Argümanlar |
|---|---|---|
dw.ocapi.shop.basket.beforePATCH | PATCH /baskets/{basket_id} | basket, basketInput |
dw.ocapi.shop.basket.shipment.shipping_address.beforePUT | PUT /baskets/{basket_id}/shipments/{shipment_id}/shipping_address | basket, shipment, shippingAddress |
dw.ocapi.shop.order.afterPOST | POST /orders | order |
dw.ocapi.shop.product.modifyGETResponse | GET /products/{id} | scriptProduct, doc |
Doğrulama hook'u
Beklenen hatalar için, makine tarafından okunabilir detaylarla Status.ERROR döndürün:
'use strict';
// cartridge/scripts/hooks/address.js
var Status = require('dw/system/Status');
var addressRules = require('*/cartridge/scripts/helpers/addressRules'); // kendi modülünüz
// dw.ocapi.shop.basket.shipment.shipping_address.beforePUT
exports.beforePUT = function (basket, shipment, shippingAddress) {
if (!addressRules.isDeliverable(shippingAddress)) {
var status = new Status(Status.ERROR, 'ADDRESS_UNDELIVERABLE');
status.addDetail('field', 'shippingAddress');
return status; // HTTP 400, transaction geri alınır
}
// Geçerli: hiçbir şey döndürmeyin, varsayılan implementasyon yine çalışsın
};
SCAPI'de istemci, extension point'i adlandıran ve detaylarınızı statusDetails içinde taşıyan, application/problem+json tipinde bir HTTP 400 yanıtı alır.
Bir modifyResponse hook'u
'use strict';
// cartridge/scripts/hooks/product.js
// dw.ocapi.shop.product.modifyGETResponse: SCAPI ve OCAPI ürün GET isteklerinde çalışır
exports.modifyGETResponse = function (scriptProduct, doc) {
// Yanıta yalnızca c_ önekli custom property'ler eklenebilir
doc.c_hasBrand = !!scriptProduct.brand;
doc.c_isMaster = scriptProduct.master;
// Dönüş değeri yok: null olmayan bir Status hook yürütmesini bitirirdi
};
Bir endpoint için önbellekleme açıksa hook'un sonucu yanıtla birlikte önbelleğe alınır ve kayıt süresi dolana kadar ne sunucu mantığı ne de hook'unuz yeniden çalışır. Dolayısıyla tek bir müşteriye ya da o anki zamana bağlı her şey istek başına yeniden hesaplanmaz; bu etkileşimi SFCC önbellekleme rehberi ele alıyor. Koşullu davranış için custom header değil, c_ önekli query parametreleri (request.httpParameterMap) kullanın: header'lar cache key'in parçası değildir ve yalnızca tanılama amaçlıdır.
Hook'lar arasında veri taşımak
Sepete payment instrument eklemek tek bir istekte dw.ocapi.shop.basket.payment_instrument.beforePOST, afterPOST ve modifyPOSTResponse hook'larını çağırır. Aralarında değer taşımak için istek boyunca yaşayan request.custom nesnesini kullanın. Salesforce'un örneği ödeme sağlayıcısını afterPOST içinde çağırır, böylece bir hata yazma işlemini geri alır; sağlayıcının referansını request.custom içine yazar ve modifyPOSTResponse içinde yanıtın bir c_ property'sine kopyalar.
Karşınıza çıkacak sistem hook'ları
Hesaplama: dw.order.calculate
dw.order.calculate, sepet ve sipariş (yeniden) hesaplamasının tek noktasıdır; dw.order.calculateShipping ve dw.order.calculateTax ise CalculateHooks referansının HookMgr üzerinden çağrılmasını önerdiği alt adımlardır. Platform varsayılanı, deprecated API kullanım loglarına kayıt düşen deprecated dw.ocapi.shop.basket.calculate hook'una geri düşer. Resmi dokümantasyondakiler dahil eski örnekler hâlâ bu adı kaydediyor; siz dw.order.calculate kullanın.
- SCAPI söz konusuysa transaction yok. Salesforce, controller döneminden kalma kodda yaygın olan calculate hook'u içindeki transaction'ların SCAPI'yi bozduğu konusunda uyarır. Hook controller'lara da hizmet ediyorsa
request.isSCAPI()ile dallanın. - Hızlı ve yerel tutun. Otuz sepet ve sipariş hook'unun varsayılan mantığı onu çağırır; buradaki bir uzak çağrı neredeyse her sepet etkileşiminde tekrarlanır.
Ödeme: dw.order.payment.*
PaymentHooks referansı aynı önek altında dw.order.payment.authorize, authorizeCreditCard, capture, refund, reauthorize, validateAuthorization ve releaseAuthorization extension point'lerini tanımlar. authorize(order, paymentDetails) ve authorizeCreditCard(order, paymentDetails, cvn) başarılı bir provizyon için Status.OK, başarısız bir provizyon için Status.ERROR döndürür.
İstek yaşam döngüsü: onRequest ve onSession
dw.system.request.onRequest bir storefront isteği alındığında, dw.system.request.onSession yeni bir storefront oturumu başladığında çalışır. onRequest her storefront isteğinin yolundadır: uzak çağrı yok, ağır sorgu yok.
SFRA custom hook'ları: ödeme işlemcileri ve sepet doğrulama
SFRA, takılıp çıkarılabilir checkout mantığı için custom hook'lar kullanır. Dokümantasyonu app.payment.processor.default, app.payment.processor.basic_credit ve app.validate.basket kaydeden bir hooks.json ile HookMgr.callHook('app.payment.processor.default', 'Handle', ...) çağrısını gösterir: her ödeme işlemcisi (payment processor) için bir extension point, her biri scripts/hooks/payment/processor/ altında.
Bir ödeme entegrasyonu için:
- Yeni bir processor extension point'i kaydedin; mevcut birini yeniden kaydetmeyin.
app.payment.processor.basic_creditkaydını tekrar yapmak ikinci bir implementasyon ekler; temeldekinin yerini almaz. - Base cartridge'ın sözleşmesini aynalayın. Hook adının nasıl türetildiğini ve checkout'un hangi fonksiyonları hangi argümanlarla çağırdığını görmek için kullandığınız
app_storefront_basesürümündeki processor script'lerini ve checkout helper'larını okuyun.
dw.system.Status ile hata yönetimi
Bir dw.system.Status nesnesi new Status(Status.OK) ya da new Status(Status.ERROR, code, message) ile oluşturulur, addDetail(key, value) ile genişletilir ve status.error ile status.code üzerinden okunur. Mesaja göre değil, koda göre dallanın: referans, bir status'u tanımlamak için getCode() metodunu ayırır.
Shopper API hook'larında Status.ERROR HTTP 400 ve rollback, yakalanmamış bir exception ise HTTP 500 ("Hook Execution Error") ve rollback üretir. Beklediğiniz her durum için ilkini döndürün ki istemci ayrıştırılabilir bir hata alsın.
Circuit breaker. Her extension point'in kendi hook circuit breaker'ı vardır. En son 100 çağrının 50'den fazlası başarısız olursa açılır ve o extension point'e yapılan her çağrı 60 saniye boyunca HTTP 503 döner. Ardından half-open durumuna geçer; sonraki 10 çağrının 5'ten fazlası başarısız olursa yeniden açılır. Sık kullanılan bir yolda exception fırlatan bir hook, bir endpoint'i tüm müşteriler için devre dışı bırakabilir.
Hatayı bulmak. Log Center'da category: ( com.demandware.wapi.servlet.ShopRestServlet ) AND stackTrace: ( HookInvocationException ) LCQL sorgusunu çalıştırın ya da başarısız SCAPI isteğinin correlation ID'sinden başlayın. Circuit breaker olayları için HookCircuitBreaker arayın ve stack trace'in Caused by bölümünü okuyun.
Performans ve transaction tuzakları
- Zaman bütçesi. Timeouts and Limits sayfasına göre Shopper API hook yürütmesi varsayılan olarak hook başına 10 saniyedir (aşılırsa HTTP 500), Shopper API istekleri ise varsayılan 10 saniyelik gateway timeout'una tabidir (HTTP 504). İkisi de özelleştirilebilir, ama yavaş bir üçüncü taraf yine de müşterinin bütçesini harcar.
- after hook'larında uzak çağrılar. Bir hata veritabanı yazmasını geri alır, ama uzak sistemdeki yan etkiyi geri almaz. Bu çağrıları idempotent yapın ya da mutabakat için yeterli durumu saklayın.
- Önce sık çalışan yollar.
onRequest,dw.order.calculateve ürün ya da aramamodifyGETResponsehook'ları checkout hook'larından çok daha sık çalışır. Önce onları profilleyin.
Hook'ları test etmek ve hata ayıklamak
Hook script'leri CommonJS modülleridir, dolayısıyla platform dışında unit test yazabilirsiniz. SFRA test dokümantasyonu unit testler için mocha, chai, sinon ve proxyquire'ı listeler. dw/system/Status ve helper modüllerinizi stub'larla değiştirmek için proxyquire kullanın ve her Shopper API hook'u için geçerli yolun undefined döndürdüğünü doğrulayan bir test ekleyin: bu test, code review'da en kolay bozulan kuralı kodlar. Sandbox'ta:
- Kodu debug etmeden önce feature switch'i ve sitenin cartridge path ayarını kontrol edin.
- Bir adı kaydetmeden önce path üzerindeki her
hooks.jsondosyasında arayın; tekrarlanan kayıtlara izin verilir ve hepsi çalışır. Logger.getLogger('loyalty', 'hooks')gibi ayrı bir logger'ı sabit bir önek setiyle kullanın: günlük custom log dosyası üst sınırına ulaşıldığındagetLoggerexception fırlatır.- Hem SCAPI hem OCAPI istemciniz varsa ikisini de test edin;
request.isSCAPI()dalları kolayca test dışı kalır.
SFCC hook kontrol listesi
| Kontrol | Neden önemli |
|---|---|
package.json dosyasında hooks anahtarı var; script yolları hooks.json dosyasına göre | Aksi halde hiçbir şey kaydolmaz ya da çözümlenmez |
| Export edilen adlar hook fonksiyon adlarıyla eşleşiyor | Platform hook'ları adı paket niteliği olmadan çağırır |
| Cartridge, sitenin path'inde (organizasyon düzeyi kod için Business Manager sitesi) | Hook script'leri bir site cartridge'ında olmalı |
| API hook execution feature switch'i açık | Aksi halde SCAPI ve OCAPI hook'ları çalışmaz |
| SCAPI'nin ulaştığı calculate hook'larında transaction yok | SCAPI'yi bozduğu dokümante edilmiş |
| Uzak çağrılar 10 saniyelik hook ve gateway varsayılanlarına sığıyor | Timeout'lar 500 ya da 504 olarak döner |
modifyResponse yalnızca c_ property'leri ekliyor, önbellekli endpoint'lerde istek başına veri eklemiyor | Önbellekten dönen yanıtlar hook'unuzu yeniden çalıştırmaz |
| Extension point, path üzerinde başka bir yerde kayıtlı değil | Custom hook'lar her kaydı çalıştırır |
Önemli çıkarımlar
- Kayıt,
package.json,hooks.jsonve export edilen fonksiyondan oluşur; cartridge sitenin path'inde olmalıdır. - Custom hook'lar her kaydı cartridge path sırasıyla çalıştırır;
callHookyalnızca son değeri döndürür. - Shopper API hook'ları ilk dönen değerde durur; başarıda hiçbir şey döndürmeyin.
- Beklenen hatalar için kodlu
Status.ERRORkullanın; yakalanmamış exception'lar 500'e mal olur ve circuit breaker sayacına yazılır. onRequest, hesaplama ve ürün ya da aramamodifyGETResponsehook'larını sık çalışan yol (hot path) olarak ele alın.
Tekrarlanan kayıtları, artık SCAPI'ye hizmet eden controller dönemi calculate hook'larını ya da sürekli açılan bir circuit breaker'ı denetlemek, sunduğum SFCC mimari ve geliştirme hizmetlerinin bir parçası. İkinci bir göz isterseniz iletişime geçin.
Ek kaynaklar
- SFRA Hooks: kayıt formatı ve birden fazla cartridge'da çalışma sırası.
- Customization with Hooks (SCAPI): before, after ve modifyResponse hook'ları, dönüş değeri kuralları, hatalar ve önbellekleme.
- Hook Method Details: tüm Shopper API extension point'leri ve imzaları.
- Class HookMgr:
callHookvehasHookreferansı.