A non-unique index on (doctor_id, slot_start) plus a count-then-insert check left a TOCTOU race: two concurrent requests could both pass isSlotTaken and both insert. wrapInTransaction alone doesn't stop the phantom under InnoDB REPEATABLE-READ. Add a nullable, unique active_slot_key on Appointment = "doctorId:slotStart" while the booking occupies the slot (pending/confirmed — in lockstep with isSlotTaken); NULL once expired/completed/no_show/cancelled (NULLs don't collide in a MySQL unique index, so released slots rebook freely). bookAtomically now: catches the unique violation -> SlotTakenException, and expires lapsed pendings in-transaction so the ~1-min window before the expiry cron doesn't wrongly block rebooking. All three booking paths (online / my / admin) routed through it. Migration backfills one row per (doctor, slot) — the latest id — so the index builds even on dirty historical data without destructively cancelling bookings. (Backfill surfaced a real pre-existing double-booked slot in dev data.) Regression: tests/Appointment/SlotUniquenessTest. Adjusted the expiry-service test fixture to use distinct slots (one live booking per slot is now enforced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
بازبینی کامل بکاند ClinicPro (معماری/امنیت/Performance/کیفیت/DevOps) — تسکبهتسک با تست
پروژه
clinicpro (Symfony 7.4 / PHP ≥8.2 / Doctrine / MariaDB؛ React 19 admin)
زمینه
پروژه بزرگ است: ۳۸ controller، ۵۵ entity، ~۳۰ دامنه زیر src/. تست تقریباً وجود ندارد (فقط tests/Billing، tests/bootstrap.php)، CI نیست (.github/workflows خالی)، phpstan روی level 5. اخیراً چند باگ prod-only رخ داده (نمونهها پایین) که نشان میدهد بازبینی سیستماتیک لازم است.
این یک ممیزی (audit) کامل است که تسکبهتسک، با تست، و با اجرای ایمن انجام میشود. نه یکجا، نه سطحی.
هدف
Backlog کامل از تسکهای کوچک قابلتست بساز، اولویتبندی کن (Critical→High→Medium→Low)، سپس یکییکی هر تسک را تحلیل/رفع/تست/گزارش کن تا هیچ Critical/High باقی نماند.
قوانین اجرا (الزامی)
- اول کل پروژه را اسکن کن و Backlog بساز (با
TodoWrite— هر تسک یک آیتم). هیچ کدی قبل از Backlog عوض نشود. - فقط یک تسک In Progress در هر لحظه. ترتیب: Critical → High → Medium → Low.
- هر تسک کامل میشود فقط وقتی: fix اعمال شد + تست نوشته شد + تست PASS شد + گزارش ثبت شد.
- برای هر باگ، تست regression بنویس که بدون fix fail و با fix pass شود.
- تغییرات روی برنچ جدا (مثلاً
backend-audit) — prod فعلی (main) دست نخورد. هر چند تسک، commit جدا. - بعد از هر تغییر API → فایل مربوط در
docs/api/آپدیت (قانون استاندارد پروژه). - بعد از تغییر Entity → migration بساز (
doctrine:migrations:diff) و اجرا کن.
محیط و دستورات تست (همه داخل ddev)
ddev exec php bin/phpunit # کل تستها
ddev exec php bin/phpunit tests/Path/To/SomeTest.php # یک فایل
ddev exec php vendor/bin/phpstan analyse # level 5
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console lint:container # سلامت DI
ddev exec php bin/console debug:router | grep api
زیرساخت تست الان ضعیف است. اگر
tests/پایهی کافی (WebTestCase، DB تستی، fixtures) ندارد، اولین تسکهای High باید زیرساخت تست را بسازند (phpunit env، DB تست، factory/fixture سبک، یکApiTestCaseپایه که login/JWT را هندل کند).
وضعیت فعلی (واقعی — بررسیشده)
- دامنهها:
Admin, Appointment, Auth, Blog, Category, Clinic, ClinicInvitation, ClinicService, Config, Dashboard, Doctor, DoctorService, Insurance, Location, Patient, Payment, Rating, Representation, Secretary, Sms, Specialty, Staff, Subscription, Tag, UserProfile, Settlement, Billing. - همه controllerها از
BaseControllerارث میبرند؛ پاسخهاsuccess()/paginated()/error()/validationError(). - خطاها:
AppException(ErrorCodes::ERR_XXX,...)→ExceptionSubscriberتبدیل بهerror(). کد عمومی prod:ERR_INTERNAL_001. - Auth: JWT (lexik) + OAuth
oauth/token؛ firewallpublic_endpoints(stateless) درconfig/packages/security.yaml؛access_controlروی^/api=IS_AUTHENTICATED_FULLY. - Rate limiter:
config/packages/rate_limiter.yaml+enforceLimit()درAuthController/PasswordAuthenticator. - تاریخها: Unix timestamp (نه DateTime).
- لیستهای admin: DQL
getArrayResult()(۱۲ مورد). - ۳۴ entity index صریح دارند، ۲۱ ندارند.
- Cache: redis (
cache.adapter.redis). Messenger:async(redis) +scheduler_default+failed(doctrine).
باگکلاسهای شناختهشده در همین کدبیس (بهعنوان seed برای Backlog)
اینها واقعیاند و باید در Backlog بهصورت تسکهای با تست regression بیایند + مشابهیاب شوند:
- repositoryClass گمشده — قبلاً ۲۵ entity
#[ORM\Entity]بدونrepositoryClassداشتند → روی prod (opcache.preload)getRepository()repo پیشفرض میداد →BadMethodCallExceptionروی متد custom (مثلfindAllActiveBySecretary) → ۵۰۰. رفع شد، ولی تست regression ندارد. تسک: تستی که هر entity دارای custom repo را تأیید کندgetRepository()همان custom class را برمیگرداند (data provider روی همهی entityها). - double-nested response —
$this->success(['data' => ...])→{data:{data:...}}؛ frontend بایدdata?.data?.dataبخواند. تسک: یافتن همهی موارد ناخواستهی nest، یکدستسازی قرارداد، تست پاسخ. - migration روی DB خالی — قبلاً چند migration (drop قبل از create، FK تکراری) روی fresh DB میترکید. تسک: تست/CI که
migrateرا روی DB کاملاً خالی اجرا کند و سبز بماند (smoke). getArrayResult()در لیست admin — برای اجتناب از getter روی entity. تسک: تأیید همهی لیستهای admin این الگو را دارند و N+1 ندارند.
نمونه ابعاد بررسی برای ساخت Backlog (هر کدام به تسکهای کوچک بشکن)
- امنیت: IDOR روی endpointهای دارای
{uuid}/{id}(آیا ownership/scope چک میشود؟ مخصوصاً doctor/clinic/secretary scope و representation)؛ mass-assignment در bodyها؛ نشت اطلاعات در پاسخ خطا؛ صحتpublic_endpoints(هیچ endpoint حساسی اشتباهی public نباشد)؛ rate limit روی login/OTP/reset؛ اعتبارسنجی ورودی (DTO + validator) بهجای آرایهی خام؛ JWT TTL/refresh؛ بررسی SSRF درAPI_IRو callbackهای پرداخت؛ امضا/verify callback درگاه (mellat/sep). - Performance / Doctrine: N+1 (join/fetch)، نبود index روی ستونهای فیلتر/FK پرتکرار (۲۱ entity بدون index صریح)، pagination درست (
paginated()+ count بهینه)، کوئریهای سنگین dashboard/admin، استفادهی درست از redis cache و invalidation. - API design: status codeهای درست (۲۰۰/۲۰۱/۴۰۰/۴۰۱/۴۰۳/۴۲۲/۵۰۰)، یکدستی پاکت پاسخ، ولیدیشن ۴۲۲، نسخهبندی، هماهنگی با
docs/api/*و مصرفکنندهها (nobat724_front/services/*وclinic-pro-tauri/src/service/*). - کیفیت/معماری: controllerهای چاق (منطق به Service منتقل شود)، SOLID، تکرار،
ErrorCodesکامل، exception handling در boundary نه پراکنده. - DevOps/Observability: نبود CI → یک workflow
phpunit + phpstan + migrate-on-empty-db؛ لاگ ساختاریافته (الان monolog نیست — تصمیم بگیر اضافه شود یا نه)؛ healthcheck (/healthهست)؛ بدون secret در repo. - Database integrity: FK/onDelete درست، unique constraintها، nullability، یکدستی timestamp.
وظایف
۱. اسکن و ساخت Backlog (اولین و مهمترین خروجی)
کل src/ را دامنهبهدامنه اسکن کن. برای هر یافته یک تسک کوچک بساز با: عنوان، فایل دقیق، دسته (security/perf/quality/api/db/devops/test)، سطح ریسک، و «چطور تست شود». خروجی = جدول Backlog اولویتبندیشده + TodoWrite.
برای پوشش گسترده، میتوانی اسکن دامنهها را با subagentهای موازی انجام دهی (هر دامنه/بُعد یک agent، خروجی structured)، سپس نتایج را در یک Backlog واحد ادغام و dedup کن. هر یافته باید قبل از تبدیلشدن به fix، با خواندن کد واقعی تأیید (verify) شود تا false-positive نرود.
۲. زیرساخت تست (اگر ناکافی بود — قبل از تسکهای دیگر)
ApiTestCase پایه (WebTestCase + DB تست + helper برای ساخت user/JWT و login)، fixture/factory سبک، تنظیم phpunit.dist.xml برای env تست و DB جدا. خروجی: یک تست سبز نمونه (مثلاً /health و یک endpoint عمومی).
۳..N — اجرای تسکبهتسک
به ترتیب اولویت، برای هر تسک دقیقاً این چرخه:
① تحلیل کد همان تسک ② یافتن bug/smell/security/perf ③ تأیید (verify) یافته با کد واقعی
④ fix ⑤ تست (unit/functional/security + regression برای باگ) ⑥ اجرای تست تا PASS
⑦ phpstan + lint:container سبز ⑧ آپدیت docs/api اگر API عوض شد ⑨ گزارش تسک ⑩ تسک بعدی
خروجی هر تسک
- عنوان · هدف · مشکلات پیداشده · سطح ریسک (Low/Medium/High/Critical) · اصلاحات · تستهای نوشتهشده · نتیجهی تست (PASS/FAIL) · جمعبندی کوتاه.
شرط پایان + گزارش نهایی
پروژه تمام است وقتی: همهی تسکها done، هیچ Critical/High باقی نمانده، همه تستها PASS، هیچ fix بدون تست regression. گزارش نهایی:
- تعداد باگها · تعداد مشکلات امنیتی · تعداد مشکلات performance · میزان پوشش تست (واقعی، با
--coverage-textاگر xdebug/pcov هست) · Production readiness (%) · لیست اولویتبندیشدهی باقیمانده.
نکات مهم (الگوهای پروژه که باید رعایت شوند)
- همه controllerها
BaseController؛ پاسخها فقط باsuccess/paginated/error/validationError. خطاها باAppException + ErrorCodes(پیام فارسی). - لیست admin:
getArrayResult()— getter روی entity در این کوئریها استفاده نشود. - تاریخها Unix timestamp (نه DateTime).
- تغییر Entity → migration؛ تغییر API →
docs/api/*. - تستها داخل ddev اجرا شوند؛ DB تست جدا از DB توسعه باشد (دادهی dev خراب نشود).
- روی برنچ جدا کار شود؛ هیچچیز مستقیم روی main نرود تا review نشود.
- مصرفکنندههای cross-repo (
nobat724_front,clinic-pro-tauri) موقع تغییر قرارداد API لحاظ شوند — در build خطا نمیدهند. - دامنهبودن کار: این یک تلاش بزرگ است؛ اگر یکجلسه تمام نشد، Backlog و وضعیت تسکها باید پایدار بماند (TodoWrite + commitهای متوالی) تا ادامهپذیر باشد.