التحقق من قيم حقول النموذج وتسطيحها مع الحفاظ على حالتها التفاعلية
لمحة سريعة
قسم بعنوان «لمحة سريعة»نادرًا ما يسطّح مسار نماذج إنتاجي المدخلات الخام مباشرة. أولًا، تحقق من كل قيمة. ثم قرر أي الحقول تصبح دائمة وأيها يبقى قابلًا للتحرير. تمنحك نواة NextPDF لبنتين أساسيتين لهذا العمل: سمة الإنشاء HasFormFields، التي تكتب قيمة الحقل في المستند أثناء إنشائه، وflattenForms()، التي تثبّت كل حقل ضمن رسوميات الصفحة الساكنة وتزيل النموذج التفاعلي.
تربط هذه الوصفة هاتين اللبنتين بخطوة تحقق على طبقة التطبيق تتركها النواة لك عمدًا. ستنفذ ما يلي:
- تحقق من خريطة قيم وفق قواعد خاصة بكل حقل قبل إنشاء أي حقل، بحيث لا تصل قيمة غير صالحة إلى المستند أبدًا.
- ابنِ مجموعة بيانات واحدة متحقَّقًا منها، ثم أصدرها مرتين — مرة مسطّحة (نسخة مقفلة للقراءة فقط) ومرة تفاعلية (نسخة قابلة للتحرير) — بحيث تنتقل حالة الحقل نفسها عبر كلا الخرجين.
المتطلبات المسبقة: تثبيت حزمة نواة NextPDF (composer require nextpdf/core)، والاطلاع على إنشاء نموذج PDF وتعبئته مسبقًا وتسطيح حقول النموذج، إذ تجمع هذه الوصفة آليات إنشاء الحقول والتسطيح الواردة فيهما.
حدود النطاق. تعمل دالة
flattenForms()في النواة على مستوى المستند بأكمله: فهي تسطّح كل حقل أو لا تسطّح أيًّا منها. لا تتضمن واجهة برمجة النماذج في النواة مفتاحًا عامًا للتسطيح خاصًا بكل حقل ولا أداة مدمجة للتحقق من القيم. لذلك فإن “تسطيح بعضها، والإبقاء على غيرها قابلًا للتحرير” يحدث في طبقة التطبيق: تحقق مرة واحدة، ثم اعرض مجموعة البيانات نفسها المتحقَّق منها في مستندين. توثّق هذه الوصفة هذا النمط؛ وهي لا تضيف آلية من النواة خاصة بكل حقل.
التثبيت
قسم بعنوان «التثبيت»composer require nextpdf/coreلا تحتاج إلى أي امتداد إضافي. تأتي سمة إنشاء النماذج وأداة التسطيح كلتاهما ضمن النواة.
نظرة عامة على المفاهيم
قسم بعنوان «نظرة عامة على المفاهيم»يخزّن حقل نموذج Acrobat (AcroForm) قيمته الحالية في المُدخل V ضمن قاموس الحقل الخاص به. تقرأ flattenForms() قيمة V لكل حقل وتعرضها في دفق محتوى الصفحة المالكة — فتتحول حقول النص إلى نص BT ... Tj ... ET، وتتحول مربعات الاختيار وأزرار الانتقاء إلى مسارات مرسومة، وتعرض حقول الاختيار العنصر المحدد فيها. ثم تزيل المُدخل /AcroForm من الفهرس. والنتيجة نموذج غير تفاعلي: تمثيل ساكن للحقول يظهر بشكل متطابق في أي قارئ، دون الحاجة إلى أي قدرة على تعبئة النماذج (ISO 32000-2 12.7).
تُشكّل حقيقتان النمط الإنتاجي:
-
لا تتحقق النواة من قيم الحقول. فكل طريقة إنشاء (
textField()وcomboBox()وcheckBox()وما إلى ذلك) تكتب أي قيمة تمرّرها مباشرةً فيV. إن تنسيق البريد الإلكتروني، والانتماء إلى الخيارات المسموح بها، ووجود الحقول المطلوبة، كلها من مسؤوليات التطبيق. تحقق قبل الإنشاء، وأخفق بسرعة عند حدوث مخالفة بدلًا من إصدار مستند مثبَّتة فيه قيمة غير صالحة. -
التسطيح لا رجعة فيه ويشمل المستند بأكمله. بعد استدعاء
flattenForms()وsave()، تصبح الحقول رسوميات ساكنة. وللاحتفاظ بنسخة قابلة للتحرير أيضًا، لا تُلغِ التسطيح — بل اعرض مجموعة البيانات المتحقَّق منها مرة ثانية دون استدعاءflattenForms(). تنطلق النسختان كلتاهما من القيم المتحقَّق منها نفسها، فتحمل النسخة المقفلة والنسخة القابلة للتحرير حالة حقول متطابقة.
ملف تعريف قابلية إعادة الإنتاج هو structural: يحمل كل مستند مصفوفة /ID في المقطورة، وتطبّعها تمريرة لاحقة قبل المقارنة بين تشغيلين.
سطح واجهة برمجة التطبيقات
قسم بعنوان «سطح واجهة برمجة التطبيقات»NextPDF\Core\Document (عبر NextPDF\Core\Concerns\HasFormFields):
textField(string $name, float $x, float $y, float $w, float $h, string $default = '', array $options = []): static— لإنشاء حقل نصي تكون قيمته فيdefault.comboBox(string $name, float $x, float $y, float $w, float $h, array $items, string $selected = ''): static— لإنشاء قائمة منسدلة يكون عنصرها المحدد فيselected.checkBox(string $name, float $x, float $y, float $size, bool $checked = false): static— لإنشاء مربع اختيار تكون حالته فيchecked.flattenForms(): static— لتثبيت قيمة كل حقل في محتوى الصفحة الساكن وإسقاط AcroForm. وهي عملية لا أثر لها عند عدم وجود أي حقول. تفوّض العملية داخليًا إلىNextPDF\Form\FormFlattener.
NextPDF\Core\Concerns\HasOutput:
save(string $path): void— لبناء ملف PDF وكتابته. تطرحNextPDF\Exception\InvalidConfigExceptionعندما يكون مسار الإخراج غلاف دفق، أو يحتوي على بايت فارغ، أو يسمّي دليلًا أصلًا غير موجود.
أداة التحقق في الأمثلة أدناه هي شيفرة تطبيق تملكها أنت، وليست رمزًا من النواة. لا تتضمن النواة واجهة برمجة للتحقق من القيم، لذلك تظهر خطوة التحقق صراحةً هنا.
عيّنة شيفرة — بداية سريعة
قسم بعنوان «عيّنة شيفرة — بداية سريعة»يتحقق هذا التدفق المبسّط من خريطة قيم، وينشئ منها ثلاثة حقول، ثم يسطّح نسخة مقفلة واحدة ويحفظها.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
/** @var array<string, string> $input Untrusted value map (e.g. from a request). */$input = [ 'full_name' => 'Ada Lovelace', 'country' => 'Taiwan',];
$allowedCountries = ['United Kingdom', 'Taiwan', 'Japan'];
// Validate before authoring: core writes whatever you pass, so reject early.if (trim($input['full_name']) === '') { throw new InvalidArgumentException('full_name must not be empty.');}if (filter_var($input['email'], FILTER_VALIDATE_EMAIL) === false) { throw new InvalidArgumentException('email is not a valid address.');}if (!in_array($input['country'], $allowedCountries, true)) { throw new InvalidArgumentException('country is not an allowed option.');}
$doc = Document::createStandalone();$doc->setTitle('Validated Form (locked copy)');$doc->addPage();
$doc->textField(name: 'full_name', x: 20, y: 30, w: 90, h: 8, default: $input['full_name']);$doc->textField(name: 'email', x: 20, y: 45, w: 90, h: 8, default: $input['email']);$doc->comboBox( name: 'country', x: 20, y: 60, w: 90, h: 8, items: $allowedCountries, selected: $input['country'],);
// Whole-document flatten: every field becomes static graphics.$doc->flattenForms();
$doc->save(__DIR__ . '/registration-locked.pdf');echo "Wrote registration-locked.pdf\n";عيّنة شيفرة — للإنتاج
قسم بعنوان «عيّنة شيفرة — للإنتاج»يفصل تدفق الإنتاج التحقق عن العرض. يتحقق FieldRuleSet المُحدَّد النوع من خريطة القيم مرة واحدة ويعيد مجموعة بيانات متحقَّقًا منها. تنشئ دالة مساعدة واحدة، هي renderForm()، الحقول وتُشغَّل مرتين — مع التسطيح للنسخة المقفلة، ومن دونه للنسخة القابلة للتحرير. تأتي النسختان كلتاهما من القيم المتحقَّق منها نفسها، فتنتقل الحالة التفاعلية عبرهما. تقرأ منظومة الاختبار مسارات الإخراج من NEXTPDF_COOKBOOK_LOCKED_OUTPUT وNEXTPDF_COOKBOOK_EDITABLE_OUTPUT.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\InvalidConfigException;
/** * A single validated form record. Immutable: once constructed, every value has * already passed the rule set, so the rendering step cannot re-introduce a bad * value. This is the "validate once, render many" boundary. */final readonly class ValidatedRegistration{ /** * @param non-empty-string $fullName * @param non-empty-string $email * @param non-empty-string $country */ public function __construct( public string $fullName, public string $email, public string $country, public bool $newsletter, ) {}}
/** * Application-layer field validation. Core performs no value validation, so the * rules live here. Each method throws on the first violation; the caller maps * the exception to an HTTP 422 or a user-facing message. */final class FieldRuleSet{ /** @var list<non-empty-string> */ private const array ALLOWED_COUNTRIES = ['United Kingdom', 'Taiwan', 'Japan', 'Germany'];
/** * @param array<string, string|bool> $input Untrusted value map. * * @throws InvalidArgumentException When any field fails its rule. */ public function validate(array $input): ValidatedRegistration { $fullName = $this->requireNonEmpty($input, 'full_name'); $email = $this->requireEmail($input, 'email'); $country = $this->requireAllowed($input, 'country', self::ALLOWED_COUNTRIES);
$newsletter = $input['newsletter'] ?? false; if (!is_bool($newsletter)) { throw new InvalidArgumentException('newsletter must be a boolean.'); }
return new ValidatedRegistration( fullName: $fullName, email: $email, country: $country, newsletter: $newsletter, ); }
/** * @param array<string, string|bool> $input * * @return non-empty-string * * @throws InvalidArgumentException */ private function requireNonEmpty(array $input, string $key): string { $value = $input[$key] ?? ''; if (!is_string($value) || trim($value) === '') { throw new InvalidArgumentException(sprintf('%s must not be empty.', $key)); }
return $value; }
/** * @param array<string, string|bool> $input * * @return non-empty-string * * @throws InvalidArgumentException */ private function requireEmail(array $input, string $key): string { $value = $input[$key] ?? ''; if (!is_string($value) || $value === '' || filter_var($value, FILTER_VALIDATE_EMAIL) === false) { throw new InvalidArgumentException(sprintf('%s is not a valid email address.', $key)); }
return $value; }
/** * @param array<string, string|bool> $input * @param list<non-empty-string> $allowed * * @return non-empty-string * * @throws InvalidArgumentException */ private function requireAllowed(array $input, string $key, array $allowed): string { $value = $input[$key] ?? ''; if (!is_string($value) || !in_array($value, $allowed, true)) { throw new InvalidArgumentException(sprintf('%s is not an allowed option.', $key)); }
return $value; }
/** @return list<non-empty-string> */ public function allowedCountries(): array { return self::ALLOWED_COUNTRIES; }}
/** * Author the same field layout from one validated record. When $flatten is * true, every field is baked into static page content and the AcroForm is * dropped; when false, the fields stay interactive and editable. Both paths * start from identical values, preserving the form's state across copies. */function renderForm(ValidatedRegistration $record, FieldRuleSet $rules, bool $flatten): Document{ $doc = Document::createStandalone(); $doc->setTitle($flatten ? 'Customer Registration (locked)' : 'Customer Registration (editable)'); $doc->addPage();
$doc->setFont('helvetica', 'B', 18); $doc->cell(0, 12, 'Customer Registration', newLine: true); $doc->ln(4);
$leftMargin = 15.0; $fieldX = 70.0; $fieldW = 120.0; $fieldH = 8.0; $rowSpacing = 12.0; $y = 40.0;
$doc->setFont('helvetica', '', 10);
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Full Name:'); $doc->textField(name: 'full_name', x: $fieldX, y: $y, w: $fieldW, h: $fieldH, default: $record->fullName); $y += $rowSpacing;
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Email:'); $doc->textField(name: 'email', x: $fieldX, y: $y, w: $fieldW, h: $fieldH, default: $record->email); $y += $rowSpacing;
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Country:'); $doc->comboBox( name: 'country', x: $fieldX, y: $y, w: $fieldW, h: $fieldH, items: $rules->allowedCountries(), selected: $record->country, ); $y += $rowSpacing;
$doc->setXY($leftMargin, $y); $doc->cell(50, $fieldH, 'Newsletter:'); $doc->checkBox(name: 'newsletter', x: $fieldX, y: $y, size: 5, checked: $record->newsletter);
if ($flatten) { // Whole-document flatten: locked, read-only copy. $doc->flattenForms(); }
return $doc;}
/** @var array<string, string|bool> $input Untrusted value map (request payload). */$input = [ 'full_name' => 'Ada Lovelace', 'country' => 'Taiwan', 'newsletter' => true,];
$rules = new FieldRuleSet();
try { // Validate once. A violation aborts before any document is built. $record = $rules->validate($input);
// Render the validated dataset twice: locked, then editable. $locked = renderForm($record, $rules, flatten: true); $editable = renderForm($record, $rules, flatten: false);
$lockedPath = getenv('NEXTPDF_COOKBOOK_LOCKED_OUTPUT') ?: __DIR__ . '/registration-locked.pdf'; $editablePath = getenv('NEXTPDF_COOKBOOK_EDITABLE_OUTPUT') ?: __DIR__ . '/registration-editable.pdf';
$locked->save($lockedPath); $editable->save($editablePath);
echo "Wrote locked and editable registration copies\n";} catch (InvalidArgumentException $e) { // Validation failure: a bad value never reached the document. fwrite(STDERR, 'Form validation failed: ' . $e->getMessage() . "\n"); exit(1);} catch (InvalidConfigException $e) { // Output failure: bad path, missing directory, or stream wrapper. fwrite(STDERR, sprintf( 'PDF save failed for key "%s": %s' . "\n", $e->getConfigKey(), $e->getMessage(), )); exit(1);}الخرج المتوقع:
Wrote locked and editable registration copiesتُفتح النسخة المقفلة دون أي حقول تفاعلية — فالقيم رسوميات ساكنة. وتُفتح النسخة القابلة للتحرير وقد عُبّئت مسبقًا بالقيم نفسها، ويبقى كل حقل قابلًا للتحرير. وتعكس النسختان مجموعة البيانات الواحدة المتحقَّق منها.
الحالات الحدية والمزالق
قسم بعنوان «الحالات الحدية والمزالق»- تحقق قبل الإنشاء، لا بعده. تكتب طرق الإنشاء قيمتك في
Vحرفيًا. لا يوجد خطّاف في النواة يرفض قيمة مشوّهة عندsave()، لذلك فإن أي قيمة تتجاوز التحقق تُثبَّت في النسخة المسطّحة دون أي مسار للاسترداد. flattenForms()إما الكل أو لا شيء. فهي تسطّح كل حقل في المستند. للإبقاء على بعض الحقول قابلًا للتحرير، اعرض مستندًا ثانيًا دون استدعاء التسطيح، كما تفعل عيّنة الإنتاج — ولا تتوقع مفتاحًا خاصًا بكل حقل في واجهة برمجة النواة.- ترتيب الاستدعاء. أنشئ كل الحقول أولًا، ثم استدعِ
flattenForms()، ثمsave(). إن استدعاءflattenForms()دون أي حقول عملية آمنة لا أثر لها؛ أما استدعاؤها بعدsave()فلا يؤثر في البايتات المكتوبة فعلًا. - يجب أن تكون أسماء الحقول فريدة. يصبح حقلان يتشاركان الاسم نفسه حقلًا منطقيًا واحدًا بقيمة مشتركة في القارئات المتوافقة. تحقق من تفرّد الأسماء عندما تكون مدفوعة بالبيانات.
- صدقية مربع الاختيار عند التسطيح. يرسم مربع الاختيار المسطّح علامة الاختيار عندما تكون قيمته
YesأوOnأو1أوtrue؛ أما القيمة الفارغة أوOffفترسم المربع فقط. مرّر قيمة منطقية حقيقية إلىcheckBox()بحيث تطابق الحالة المعروضة قيمتك المتحقَّق منها. - لا تُسطَّح حقول التوقيع أبدًا. إن سطح حقل
/Sigهو المظهر الناتج من حمولة توقيعه، وليس قيمة قابلة لإعادة العرض، لذلك تتخطّاه أداة التسطيح. سطّح قبل التوقيع، لا بعده أبدًا. - النسخة القابلة للتحرير تبقى قابلة للتحرير. يعني الحفاظ على الحالة في النسخة القابلة للتحرير أن المستلِم يستطيع تغييرها. عامِل النسخة المقفلة بوصفها السجل المرجعي، والنسخة القابلة للتحرير بوصفها مسودة عمل.
الأداء
قسم بعنوان «الأداء»التحقق خطّي مع عدد الحقول ويُشغَّل مرة واحدة لكل مجموعة بيانات. ينشئ كل عرض تعليق أداة واحدًا، إضافةً إلى مظهر، لكل حقل. تضيف خطوة التسطيح كتلة محتوى محدودة لكل حقل. يؤدي عرض مجموعة البيانات مرتين إلى مضاعفة تكلفة كل مستند تقريبًا، ومع ذلك تبقى ضمن ميزانية 1500 ms / 64 MB بهامش واسع للنماذج التي تضم بضع مئات من الحقول. إذا كنت تحتاج إلى خرج واحد فقط، فاعرض مرة واحدة وتخطَّ التمريرة الثانية.
ملاحظات أمنية
قسم بعنوان «ملاحظات أمنية»- تحقق من المدخلات غير الموثوقة عند الحدود. يُفرض تنسيق البريد الإلكتروني، والانتماء إلى الخيارات المسموح بها، ووجود الحقول المطلوبة، في شيفرة التطبيق لأن النواة لا تفرضها. اهرب أو طبّع أي قيمة مشتقة من مدخلات غير موثوقة قبل وصولها إلى حقل، لأن النواة تكتبها حرفيًا في المستند.
- التسطيح ليس تحكمًا في الوصول. إن القيمة المسطّحة غير قابلة للتحرير في القارئ العادي، لكنها تبقى ظاهرة في محتوى الصفحة وقابلة للاستخراج بأي أداة نصية. لا تعامل التسطيح بوصفه تنقيحًا أو حمايةً للقيم الحساسة.
- النسخة القابلة للتحرير تحمل البيانات نفسها. وزّعها فقط على الأطراف المسموح لها برؤية تلك القيم وتغييرها. عندما يكون المحتوى حساسًا، ادمج أيًّا من النسختين مع التشفير مع الأذونات، ولاحظ التحذير المتعلق بتعاون القارئ الموضّح هناك: لا تفرض بتات الأذونات قيودًا على القراءة.
- أخفق مغلقًا. تخرج عيّنة الإنتاج بقيمة غير صفرية عند فشل التحقق أو الإخراج بدلًا من كتابة مستند جزئي أو غير صالح. لا تبتلع هذه الاستثناءات أبدًا.
المطابقة
قسم بعنوان «المطابقة»| العبارة | المواصفة | البند | reference_id |
|---|---|---|---|
| النموذج المسطّح هو تمثيل غير تفاعلي (ساكن) للحقول. | ISO 32000-2 | 12.7 |
ينتج NextPDF البنية الساكنة الموصوفة في البند المُستشهَد به؛ وهو لا يؤكد مطابقة شاملة لـ ISO 32000-2. إن قواعد التحقق في هذه الوصفة سياسة تطبيق، وليست متطلب مطابقة للمعيار.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- إنشاء نموذج PDF وتعبئته مسبقًا — لإنشاء الحقول وتعيين قيمها الأولية.
- تسطيح حقول النموذج — التسطيح على مستوى المستند بأكمله الذي تبني عليه هذه الوصفة.
- معالجة الأخطاء بتسلسل استثناءات NextPDF الهرمي — لالتقاط حالات الفشل بالدقة المناسبة.
- التشفير مع الأذونات — لإضافة السرية عندما تكون بيانات النموذج حساسة.
- وحدة النماذج — مرجع حقول النموذج.