การใช้งานระดับ Production — fallback, การวัดและส่งข้อมูล (telemetry), การจัดเก็บถาวร และการป้องกัน
ภาพรวมโดยย่อ
หัวข้อที่มีชื่อว่า “ภาพรวมโดยย่อ”หน้านี้ครอบคลุมประเด็นด้าน Production สี่เรื่องที่อยู่นอกเหนือการ render พื้นฐาน ได้แก่ fallback ภายในเครื่อง การวัดและส่งข้อมูล (telemetry) จาก edge การจัดเก็บถาวรบน Cloudflare R2 และเลเยอร์การป้องกัน application programming interface (API) สำหรับคำขอขาเข้า แต่ละหัวข้อสอดคล้องกับพฤติกรรมของคลาสที่ตรวจสอบยืนยันแล้ว
fallback ภายในเครื่อง
หัวข้อที่มีชื่อว่า “fallback ภายในเครื่อง”เมื่อไม่สามารถเข้าถึง Worker ได้และ fallbackToLocal เป็น true bridge จะมอบหมายการ render ให้ renderer ภายในเครื่อง จัดเตรียม renderer นี้ผ่าน LocalRendererFactoryInterface bridge จะสร้าง renderer แบบ lazy ดังนั้น create() ของ factory จะทำงานเฉพาะบนเส้นทาง fallback เท่านั้น
<?php
declare(strict_types=1);
use NextPDF\Cloudflare\Contract\LocalRendererFactoryInterface;use NextPDF\Cloudflare\Contract\LocalRendererInterface;
final class ArtisanLocalRendererFactory implements LocalRendererFactoryInterface{ public function __construct( private readonly \NextPDF\Artisan\ChromeHtmlRenderer $chrome, ) {}
public function create(): LocalRendererInterface { return new readonly class($this->chrome) implements LocalRendererInterface { public function __construct( private \NextPDF\Artisan\ChromeHtmlRenderer $chrome, ) {}
/** @param array<string, mixed> $options */ public function render(string $html, array $options = []): string { // Delegate to the local Chrome renderer; return raw PDF bytes. return $this->chrome->renderToString($html, $options); } }; }}เชื่อมต่อ factory เข้ากับ renderer:
use NextPDF\Cloudflare\CloudflareHtmlRenderer;
$renderer = new CloudflareHtmlRenderer( config: $config, httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory, logger: $logger, localRendererFactory: new ArtisanLocalRendererFactory($chrome), responseFactory: $httpFactory,);เมื่อ fallback ทำงาน renderLocation ของผลลัพธ์จะเป็นสตริงตามตัวอักษร local และ heightPt จะเป็น 0.0 เส้นทางภายในเครื่องจะไม่รายงานตำแหน่ง edge หรือความสูงที่วัดได้ bridge จะส่งความกว้างที่ร้องขอไปยัง renderer ภายในเครื่องผ่านคีย์ตัวเลือก widthPt
ตรรกะการตัดสินใจของ fallback
หัวข้อที่มีชื่อว่า “ตรรกะการตัดสินใจของ fallback”อ่านโดยตรงจาก CloudflareHtmlRenderer:
| สถานการณ์ | ผลลัพธ์ |
|---|---|
การตั้งค่าไม่สมบูรณ์ fallbackToLocal: false | CloudflareNotAvailableException |
การตั้งค่าไม่สมบูรณ์ fallbackToLocal: true เชื่อมต่อ factory แล้ว | render ภายในเครื่อง |
| Worker โยน transport error โดยเปิดใช้งาน fallback และเชื่อมต่อ factory แล้ว | render ภายในเครื่อง บันทึก log ที่ระดับ warning จากนั้น info |
| Worker โยน error โดยเปิดใช้งาน fallback ติดตั้ง Artisan แล้ว แต่ ไม่มี factory | CloudflareNotAvailableException ที่ระบุชื่อ factory ที่ขาดหายไป |
| Worker โยน error โดยเปิดใช้งาน fallback แต่ ไม่ได้ ติดตั้ง Artisan | CloudflareNotAvailableException ที่ระบุชื่อแพ็กเกจที่ขาดหายไป |
| Worker คืนค่า Hypertext Transfer Protocol (HTTP) error / body ที่ผิดรูปแบบ | CloudflareRenderException ไม่ ทำ fallback เลย |
แถวสุดท้ายเป็นจุดสำคัญ Worker ที่คืนค่า error ถือเป็นความล้มเหลวในการ render ไม่ใช่ความล้มเหลวด้านการเข้าถึง bridge จะโยน error นั้นซ้ำเพื่อให้โค้ดของคุณแยกแยะระหว่าง render ที่เสียหายกับ edge ที่ไม่สามารถเข้าถึงได้
การวัดและส่งข้อมูล (telemetry) จาก edge
หัวข้อที่มีชื่อว่า “การวัดและส่งข้อมูล (telemetry) จาก edge”การ render ที่สำเร็จทุกครั้งบนเส้นทาง binary จะมีข้อมูล telemetry จาก response header:
$result = $renderer->render($html);
$logger->info('edge render', [ 'edge' => $result->renderLocation, // e.g. 'TPE', 'NRT' 'render_time_ms' => $result->renderTimeMs, 'content_px' => $result->contentHeightPx, 'pdf_bytes' => $result->size(),]);renderer จะอ่าน renderLocation จาก response header CF-Ray และใช้ส่วนที่อยู่หลังเครื่องหมายยัติภังค์ตัวสุดท้าย สำหรับ CF-Ray: 8abc123def456-TPE ตำแหน่งคือ TPE หากไม่มี header นี้ ตำแหน่งจะเป็นสตริงว่าง สำหรับเส้นทาง response แบบ JavaScript Object Notation (JSON) ค่าจะมาจากฟิลด์ renderLocation ใน JSON แทน ให้ถือว่าค่าเหล่านี้เป็นสัญญาณ observability จาก Worker ไม่ใช่การรับประกันของแพลตฟอร์ม
การจัดเก็บถาวรบน R2
หัวข้อที่มีชื่อว่า “การจัดเก็บถาวรบน R2”R2ArchiveManager อัปโหลดไบต์ของ Portable Document Format (PDF) ไปยัง Cloudflare R2 ผ่าน API ที่รองรับ Amazon Simple Storage Service (S3) และลงนามคำขอด้วย Amazon Web Services (AWS) Signature V4
use NextPDF\Cloudflare\R2ArchiveConfig;use NextPDF\Cloudflare\R2ArchiveManager;
$r2 = new R2ArchiveManager( config: new R2ArchiveConfig( bucketName: 'pdf-archive', accountId: getenv('CF_ACCOUNT_ID') ?: '', accessKeyId: getenv('R2_ACCESS_KEY_ID') ?: '', secretAccessKey: getenv('R2_SECRET_ACCESS_KEY') ?: '', pathPrefix: 'invoices/', ), httpClient: $httpClient, requestFactory: $httpFactory, streamFactory: $httpFactory,);
$upload = $r2->upload($result->pdfData, 'invoice-2026-0042.pdf', [ 'tenant' => 'acme',]);
if (!$upload->success) { $logger->error('r2 upload failed', ['error' => $upload->error]);}พฤติกรรมที่ตรวจสอบยืนยันแล้วจาก R2ArchiveManager และ R2ObjectKey:
- ระบบจะแบ่งพาร์ติชัน object key ตามวันที่ในรูปแบบ:
<pathPrefix><Y>/<m>/<d>/<sanitized-filename>ตัวอย่างเช่นinvoices/2026/05/18/invoice-2026-0042.pdf - ชื่อไฟล์จะถูก sanitize:
basename()จะลบ path traversal ออก จากนั้น null byte และอักขระควบคุม (\x00–\x1f,\x7f) จะถูกตัดออก หากผลลัพธ์ว่างเปล่า จะใช้ชื่อdocument.pdfแทน - เมตาดาทาที่กำหนดเองจะถูกส่งในรูปแบบ header
x-amz-meta-<lowercased-key>และรวมอยู่ในชุด signed-header ของ V4 - ไฟล์ที่มีขนาดใหญ่กว่า
maxFileSizeBytes(ค่าเริ่มต้น104857600) จะถูกปฏิเสธก่อนส่งคำขอใด ๆ และคืนค่าR2UploadResultที่มีsuccess: false R2UploadResult::isValid()กำหนดให้ต้องมีsuccesskeyที่ไม่ว่าง และetagที่ไม่ว่าง
URL ดาวน์โหลดแบบ pre-signed
หัวข้อที่มีชื่อว่า “URL ดาวน์โหลดแบบ pre-signed”$url = $r2->generateSignedUrl('invoices/2026/05/18/invoice-2026-0042.pdf', 900);generateSignedUrl() สร้าง URL แบบ GET ที่ลงนามผ่าน query ด้วย AWS Signature V4 พร้อมค่า X-Amz-Expires ที่คุณควบคุมได้ (ค่าเริ่มต้น 3600 วินาที) canonical request ใช้ค่า sentinel ของ content-hash แบบ UNSIGNED-PAYLOAD URL สำหรับอ่านที่ลงนามผ่าน query ใช้รูปแบบนี้เพราะ body ไม่ได้เป็นส่วนหนึ่งของคำขอที่ลงนาม เนื้อหาส่วนนี้อธิบายพฤติกรรมการลงนามที่แพ็กเกจนำไปใช้งานจริงตามที่อ่านได้จาก R2ArchiveManager เอกสารบริการของ Amazon เป็นผู้กำหนด AWS Signature Version 4 ไม่ใช่มาตรฐานจาก standards development organization (SDO) จึงไม่มีการอ้างอิงข้อกำหนดเชิงบรรทัดฐานไว้ที่นี่ access key ของ object เป็น #[SensitiveParameter] อย่าให้ปรากฏใน log
URL สาธารณะ
หัวข้อที่มีชื่อว่า “URL สาธารณะ”R2UploadResult::publicUrl($customDomain) จะคืนค่าเป็นสตริงว่างเมื่อคุณไม่ระบุโดเมน หรือ https://<domain>/<key> เมื่อคุณระบุ เมธอดจะเพิ่ม scheme แบบ Hypertext Transfer Protocol Secure (HTTPS) เมื่อโดเมนที่ระบุไม่มี scheme เมธอดนี้ไม่ได้ทำให้ bucket ส่วนตัวกลายเป็นสาธารณะ ซึ่งยังคงเป็นเรื่องของการตั้งค่า R2 bucket
การป้องกัน API สำหรับคำขอขาเข้า
หัวข้อที่มีชื่อว่า “การป้องกัน API สำหรับคำขอขาเข้า”ApiProtection คือเลเยอร์ที่คุณนำมาใช้กับคำขอ render ที่เข้ามายัง PHP gateway หน้า Worker เลเยอร์นี้ตรวจสอบตามลำดับที่กำหนดไว้ตายตัว: API key จากนั้นขนาด payload จากนั้น rate limit
use NextPDF\Cloudflare\ApiKeyValidator;use NextPDF\Cloudflare\ApiProtection;use NextPDF\Cloudflare\ApiProtectionConfig;
$protection = new ApiProtection( config: new ApiProtectionConfig( maxRequestsPerMinute: 30, maxRequestsPerHour: 500, maxPayloadSizeBytes: 5_000_000, requireApiKey: true, ), keyValidator: new ApiKeyValidator([getenv('GATEWAY_API_KEY') ?: '']),);
$decision = $protection->checkRequest( clientId: $clientIp, payloadSize: strlen($requestBody), apiKey: $request->getHeaderLine('X-Api-Key'),);
if (!$decision->allowed) { http_response_code(429); foreach ($decision->toHeaders() as $name => $value) { header("{$name}: {$value}"); } echo $decision->denialReason; exit;}พฤติกรรมที่ตรวจสอบยืนยันแล้ว:
- ลำดับคือ API key → ขนาด payload → rate limit การตรวจสอบรายการแรกที่ล้มเหลวจะหยุดทันทีพร้อมระบุ
denialReasonที่เฉพาะเจาะจง ApiKeyValidator::validate()ใช้hash_equals()สำหรับการเปรียบเทียบแบบ timing-safe และปฏิเสธคีย์ที่ว่างเปล่าvalidateHashed()เปรียบเทียบกับแฮช Secure Hash Algorithm 256-bit (SHA-256) สำหรับการจัดเก็บคีย์แบบ at-rest พารามิเตอร์ของคีย์มี#[SensitiveParameter]กำกับไว้- store สำหรับ rate-limit เป็นแบบ in-memory ต่อโปรเซส store จะติดตามหน้าต่างแบบต่อนาที (
rateLimitWindowSecondsค่าเริ่มต้น60) และหน้าต่างแบบต่อชั่วโมง (คงที่3600วินาที) store จะไม่คงอยู่ข้าม worker หรือการ restart หากต้องการแชร์ลิมิตข้ามโปรเซส ให้วาง shared store ไว้ด้านหน้า ApiProtectionResult::toHeaders()จะเพิ่มX-Content-Type-Options: nosniffและX-Frame-Options: DENYเสมอ และผสาน header ของ rate-limit (X-RateLimit-Remaining,X-RateLimit-ResetรวมถึงRetry-Afterเมื่อถูกปฏิเสธ)
render แล้วจึงลงนาม
หัวข้อที่มีชื่อว่า “render แล้วจึงลงนาม”bridge นี้ไม่ลงนาม PDF หากต้องการสร้าง pipeline การลงนามระดับ Production ให้ render ที่ edge จากนั้นลงนามไบต์ที่ได้รับกลับมาด้วย engine:
render()→CloudflareRenderResult::$pdfData(ผลลัพธ์)- ส่ง
$pdfDataให้กับnextpdf/core(หรือ NextPDF Pro สำหรับการลงนาม PDF Advanced Electronic Signatures (PAdES) B-B) โปรไฟล์ long-term-validation เป็นความสามารถระดับ Enterprise ส่วน core bridge นี้ไม่ได้อ้างว่ารองรับความสามารถทั้งสองอย่าง
เก็บขั้นตอนการลงนามไว้ในโปรเซสของคุณเอง เพื่อไม่ให้คีย์สำหรับลงนามข้ามขอบเขตของ edge เลย
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- /integrations/cloudflare/security-and-operations/ — การทำ pinning การป้องกัน server-side request forgery (SSRF) การหมุนเวียน secret และคู่มือการปฏิบัติงาน
- /integrations/cloudflare/troubleshooting/ — แคตตาล็อกของรูปแบบความล้มเหลว
- /integrations/cloudflare/configuration/ — ทุกฟิลด์และค่าเริ่มต้น