diff --git a/.claude/prompt/fix-prod-soap-extension.md b/.claude/prompt/fix-prod-soap-extension.md new file mode 100644 index 00000000..aa71c131 --- /dev/null +++ b/.claude/prompt/fix-prod-soap-extension.md @@ -0,0 +1,114 @@ +# رفع خطای `Class "SoapClient" not found` در پرداخت ملت (سرور prod) + +## پروژه + +`clinicpro` (backend / infra — Docker image) + +## زمینه + +درگاه ملت (`MellatGateway`) در مسیر prod برای فراخوانی وب‌سرویس بانک از `new \SoapClient(...)` استفاده می‌کند (کلاس PHP از افزونه `ext-soap`). روی محیط لوکال (ddev) افزونه `soap` به‌صورت پیش‌فرض نصب است، پس مشکلی دیده نمی‌شود؛ اما image پرود بر پایه `php:8.2-fpm-alpine` ساخته می‌شود و در `Dockerfile` فقط این افزونه‌ها نصب می‌شوند: + +```dockerfile +docker-php-ext-install pdo_mysql intl opcache +``` + +`soap` نصب نمی‌شود، بنابراین در پرود هنگام initiate کردن پرداخت، fatal می‌شود: + +``` +Payment initiate failed (mellat): Class "SoapClient" not found @ /app/src/Payment/Gateway/MellatGateway.php:70 +context {"orderId":"6","amount":150000} +``` + +## مشکل / هدف + +افزونه `ext-soap` روی image پرود نصب شود تا مسیر prod درگاه ملت (`bpPayRequest` / `bpVerifyRequest` / `bpSettleRequest` / `bpRefundRequest` / `bpReversalRequest`) روی `SoapClient` کار کند. + +نکته مهم: sandbox این مشکل را ندارد چون مسیر sandbox از REST (JSON + Basic Auth) استفاده می‌کند نه SOAP. مشکل فقط در حالت prod (فلگ `mellat_sandbox` غیرفعال) رخ می‌دهد. + +## فایل‌های مرتبط + +| فایل | نقش | +|------|-----| +| `Dockerfile` (stage 3 — runtime) | نصب افزونه‌های PHP؛ باید `soap` اضافه شود | +| `src/Payment/Gateway/MellatGateway.php` | خط ۷۰: `new \SoapClient($this->wsdlUrl(), ...)` — نیازمند `ext-soap` | +| `composer.json` | افزودن `ext-soap` به `require` تا نبود افزونه زودتر (build-time) شناسایی شود | + +## وضعیت فعلی + +`Dockerfile` — بلوک نصب افزونه‌ها در stage runtime (خطوط ~62–73): + +```dockerfile +RUN set -eu; \ + export MAKEFLAGS="-j$(nproc)"; \ + retry() { for i in 1 2 3 4 5; do "$@" && return 0; echo "retry $i: $*"; sleep 5; done; return 1; }; \ + retry apk add --no-cache nginx supervisor icu-libs \ + && retry apk add --no-cache --virtual .build-deps $PHPIZE_DEPS icu-dev \ + && docker-php-ext-install pdo_mysql intl opcache \ + && retry curl -sSLf "https://github.com/phpredis/phpredis/archive/refs/tags/${PHPREDIS_VERSION}.tar.gz" -o /tmp/phpredis.tar.gz \ + && mkdir -p /usr/src/php/ext/redis \ + && tar -xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1 \ + && docker-php-ext-install redis \ + && rm -rf /tmp/phpredis.tar.gz \ + && apk del .build-deps +``` + +`MellatGateway.php:67-78`: + +```php +private function soap(): \SoapClient +{ + if ($this->soap === null) { + $this->soap = new \SoapClient($this->wsdlUrl(), [ + 'trace' => true, + 'exceptions' => true, + 'encoding' => 'UTF-8', + 'connection_timeout' => 10, + ]); + } + return $this->soap; +} +``` + +## وظایف + +### ۱. افزودن `soap` به `docker-php-ext-install` در `Dockerfile` + +افزونهٔ `soap` جزو افزونه‌های bundled خودِ PHP است و نیاز به شبکه (pecl/github) ندارد؛ فقط برای کامپایل به هدرهای `libxml2-dev` نیاز دارد. `libxml2-dev` را به `.build-deps` اضافه کن (تا بعد از build با `apk del .build-deps` پاک شود) و `soap` را به لیست `docker-php-ext-install` اضافه کن. + +نتیجهٔ مطلوب: + +```dockerfile + && retry apk add --no-cache --virtual .build-deps $PHPIZE_DEPS icu-dev libxml2-dev \ + && docker-php-ext-install pdo_mysql intl opcache soap \ +``` + +نکته: `libxml2` (کتابخانهٔ runtime، بدون `-dev`) در image پایهٔ PHP موجود است، پس افزونهٔ کامپایل‌شده در runtime بدون مشکل load می‌شود و نیازی به افزودن آن به لیست runtime نیست. فقط هدرهای `-dev` که در build لازم‌اند در `.build-deps` قرار می‌گیرند. + +### ۲. اعلام نیازمندی در `composer.json` + +`ext-soap` را به بخش `require` اضافه کن تا اگر روی محیطی افزونه نبود، در زمان `composer install` هشدار/خطا داده شود و مشکل زودتر از runtime دیده شود: + +```jsonc +"require": { + // ... موجود + "ext-soap": "*" +} +``` + +توجه: در `Dockerfile` مرحلهٔ vendor از `--ignore-platform-reqs` استفاده می‌کند، پس این افزوده build را نمی‌شکند؛ صرفاً به‌عنوان مستندسازی نیازمندی و اعتبارسنجی در محیط‌های دیگر عمل می‌کند. + +### ۳. تأیید و rebuild + +بعد از تغییر `Dockerfile`، image پرود باید دوباره build/deploy شود (Coolify). برای تأیید نصب افزونه پس از build: + +```bash +php -m | grep -i soap # باید soap را چاپ کند +php -r 'var_dump(class_exists("SoapClient"));' # bool(true) +``` + +## نکات مهم + +- مشکل صرفاً محیطی (نبود افزونه در image پرود) است، نه باگ منطقی در کد `MellatGateway`. کد درست است؛ فقط وابستگی runtime روی پرود غایب بود. +- بعد از deploy، پرداخت واقعی ملت (حالت prod، فلگ `mellat_sandbox=0`) باید یک‌بار تست شود تا مسیر SOAP (`bpPayRequest`) واقعاً پاسخ بگیرد. +- الگوی موجود در `Dockerfile` را رعایت کن: build-deps مجازی که بعد از کامپایل با `apk del .build-deps` حذف می‌شوند تا حجم image کم بماند. `libxml2-dev` را داخل همین گروه بگذار، نه به‌صورت دائمی. +- این تغییر API را عوض نمی‌کند؛ نیازی به به‌روزرسانی `docs/api/payment.md` نیست (فقط اگر رفتار/قرارداد endpoint تغییر کند لازم می‌شد). diff --git a/Dockerfile b/Dockerfile index f052e1c5..61e84ea7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,8 +63,8 @@ RUN set -eu; \ export MAKEFLAGS="-j$(nproc)"; \ retry() { for i in 1 2 3 4 5; do "$@" && return 0; echo "retry $i: $*"; sleep 5; done; return 1; }; \ retry apk add --no-cache nginx supervisor icu-libs \ - && retry apk add --no-cache --virtual .build-deps $PHPIZE_DEPS icu-dev \ - && docker-php-ext-install pdo_mysql intl opcache \ + && retry apk add --no-cache --virtual .build-deps $PHPIZE_DEPS icu-dev libxml2-dev \ + && docker-php-ext-install pdo_mysql intl opcache soap \ && retry curl -sSLf "https://github.com/phpredis/phpredis/archive/refs/tags/${PHPREDIS_VERSION}.tar.gz" -o /tmp/phpredis.tar.gz \ && mkdir -p /usr/src/php/ext/redis \ && tar -xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1 \ diff --git a/composer.json b/composer.json index 7683a246..07ffeef4 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", + "ext-soap": "*", "doctrine/doctrine-bundle": ">=2.18.3", "doctrine/doctrine-migrations-bundle": "*", "doctrine/orm": "^3.6",