Ana içeriğe atla
OZAN ALTINBAŞ
Geliştirme

SFCC Hook Kullanımı: hooks.json Örnekleriyle Pratik Rehber

Yazar: 13 dk okuma

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ındadw.ocapi.shop.basket.beforePATCH
Sistem hook'larıdw.order.*, dw.system.request.*Platform ya da HookMgr üzerinden kendi kodunuzdw.order.calculate
Custom hook'larKendi namespace'iniz, örneğin app.*HookMgr.callHook üzerinden kendi kodunuzapp.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:

  1. Yeniden kaydetmek override etmez. Daha sağdaki bir cartridge app.validate.basket kaydı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.
  2. 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.
  3. Sonuçları dönüş değeri üzerinden toplamayın. Her implementasyonun içine yazdığı bir nesneyi argüman olarak geçin ve callHook dö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ırSistem implementasyonu ve sonraki kayıtlar yine çalışır
Status.OKSonraki kayıt yine çalışırYürütme biter; yeniden hesaplama gibi varsayılan mantık atlanır
Status.ERRORSonraki kayıt yine çalışır; kararı çağıran verirİşlem durur, transaction geri alınır, HTTP 400
Exception fırlatırcallHook exception'ı çağırana fırlatırTransaction 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 tipiNe zaman çalışırTipik kullanım
before<METHOD>Platform isteği işlemeden önceDoğrulama, erişim kontrolleri
after<METHOD>İşlemden sonra, yazma transaction'ı içindeVeri değiştirme; hatası yazmayı geri alması gereken harici çağrılar
modify<METHOD>ResponseYanı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 pointEndpoint (OCAPI yolu)Argümanlar
dw.ocapi.shop.basket.beforePATCHPATCH /baskets/{basket_id}basket, basketInput
dw.ocapi.shop.basket.shipment.shipping_address.beforePUTPUT /baskets/{basket_id}/shipments/{shipment_id}/shipping_addressbasket, shipment, shippingAddress
dw.ocapi.shop.order.afterPOSTPOST /ordersorder
dw.ocapi.shop.product.modifyGETResponseGET /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_credit kaydı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_base sü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.calculate ve ürün ya da arama modifyGETResponse hook'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.json dosyası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ığında getLogger exception 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

KontrolNeden önemli
package.json dosyasında hooks anahtarı var; script yolları hooks.json dosyasına göreAksi halde hiçbir şey kaydolmaz ya da çözümlenmez
Export edilen adlar hook fonksiyon adlarıyla eşleşiyorPlatform 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çıkAksi halde SCAPI ve OCAPI hook'ları çalışmaz
SCAPI'nin ulaştığı calculate hook'larında transaction yokSCAPI'yi bozduğu dokümante edilmiş
Uzak çağrılar 10 saniyelik hook ve gateway varsayılanlarına sığıyorTimeout'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ğilCustom hook'lar her kaydı çalıştırır

Önemli çıkarımlar

  • Kayıt, package.json, hooks.json ve 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; callHook yalnı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.ERROR kullanın; yakalanmamış exception'lar 500'e mal olur ve circuit breaker sayacına yazılır.
  • onRequest, hesaplama ve ürün ya da arama modifyGETResponse hook'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