کدنویسی قابلیتهای پایه RCS
همه درباره هوش مصنوعی حرف میزنند. با همان کلاه همیشگیِ توسعهدهنده بدبین روی سرم، مدت زیادی طول کشید تا واقعاً چیزی که ارائه میشد را بپذیرم. مثلاً نسخههای اولیه، تاریخ قطع اطلاعات اینترنتی داشتند.
در همین بازه زمانی کوتاه، چیزهای زیادی تغییر کرده و آنقدر زیاد که حالا بهطور فزایندهای برای کدنویسی روزمره استفاده میشود (خودم هم شاملش هستم). در دنیای PHP، چند انتشار قابل توجه داشتیم که نشان داد چقدر سریع ما به عنوان یک اکوسیستم، توسعه میدهیم و میپذیریم. نیمه دوم سال شاهد اینها بود: بنیاد PHP همراه Symfony فریمورک رسمی MCP را معرفی کرد، LaraCopilot راه افتاد، و Laravel Boost بهعنوان یک سرور MCP تخصصی برای توسعه با هوش مصنوعی عرضه شد.
فابین پوتانسیه، خالق فریمورک Symfony، روز دوم کنفرانس API Platform در شهر Lille را با یک سخنرانی درباره LLMها و توسعه API آغاز کرد؛ و واقعاً چشمگشا بود. LLMها همین حالا وارد قلمرو «تجربه توسعهدهنده» شدهاند، و بنابراین فابین میگوید حالا با یک زنجیره از برچسبهای «Experience» روبهرو هستیم:
-
User Experience
-
Developer Experience
-
Agent Experience
همین حالا یک پیشنهاد روی میز داریم برای llms.txt که به LLMها میگوید Markdownهایی را کجا پیدا کنند که باعث میشود توکنها سریعتر پارس شوند. در آن سخنرانی همچنین به چیزی در Developer Experience اشاره شد که من بهعنوان کسی که در این حوزهام، خوب میشناسمش، اما حالا از همیشه مهمتر شده: پاسخهای API شما باید وقتی چیزی خراب میشود، «اقدام حلکننده» (resolution action) داشته باشند؛ و خراب میشود، و عاملها هنوز هم احتمالاً کارهای عجیبوغریب انجام میدهند.
با توجه به همه اینها، در این مقاله میخواهیم بررسی کنیم هوش مصنوعی چقدر میتواند قابل اعتماد باشد وقتی داریم یک نمونه اولیه Laravel طراحی میکنیم که پیامهای RCS را ارسال و دریافت کند.
اهداف
وقتی یک اپلیکیشن دمو جدید Laravel را شروع میکنم، چند کار «همیشه انجامشدنی» دارم که تقریباً همیشه اجراشان میکنم. این اپ یک endpoint خواهد داشت برای ارسال یک پیام RCS با استفاده از Messages API و یک endpoint برای خواندن پیامهای RCS ورودی. همه اینها مبتنی بر REST API خواهد بود، پس نیازی به starter kitها نیست. روش دستیای که خودم انجام میدادم این است:
-
نوشتن migrations
-
نوشتن models
-
نوشتن database seeders
-
نوشتن DTOها (اگر لازم باشد، معمولاً به عنوان بهترینروش اضافه میشوند)
-
نوشتن controllers
-
نوشتن routes
-
نوشتن یک تست برای یکی از routeها
این الگو را بارها و بارها انجام دادهام و برایم تبدیل به عادت شده. اما زمانبر است، و اینجاست که میخواهم تلاش کنم Cursor AI همه اینها را برایم انجام دهد.
شروع کار
چند چیز لازم داریم. اول، Cursor AI را نصب کنید. Cursor بهجای یک رابط وب برای پرامپت دادن، یک IDE است که از VSCode فورک شده. Cursor تا سقف مشخصی از توکن، رایگان است.
همچنین به PHP و Composer نیاز دارید. برای محیط توسعه، میتوانید از وبسرور داخلی Laravel استفاده کنید، اما من شخصاً طرفدار Herd از Beyond Code هستم تا همه نیازهای وبسرور و دیتابیس را مدیریت کند.
Cursor فعلاً نمیتواند از یک پنجره خالی، محیط Laravel را بالا بیاورد: باید اول کدها را scaffold کنید. میتوانید از Laravel CLI استفاده کنید، اما آن در واقع دور دستور create-project کامپوزر wrapper میزند. برای ساخت کدبیس، با این دستورها شروع میکنیم:
اگر همهچیز درست باشد، چون من از Herd استفاده میکنم، میتوانم بروم به https://my-app.test و صفحه splash لاراول را ببینم.

خب، همهچیز خوب است. وقتش است آن را داخل Cursor وارد کنیم از مسیر File->Open. به پروژه جدید بروید و IDE پیشفرض آماده است.

دادههای من
خب، اول داده. ارسال پیام RCS چیزی در دیتابیس نیاز ندارد، اما من میخواهم پیامهای ورودی را ذخیره کنم. یعنی به یک migration و model نیاز دارم. چون RCS یک OpenAPI Spec کامل دارد که شکل داده را مشخص میکند، میتوانم در پرامپت دقیقاً ساختار داده را بگویم تا OpenAPI را بخواند و این migrations را بنویسد. این هم پرامپت:
Write me a database migration that will create an RCSMessage table that will store incoming RCS webhooks from Vonage. It will need to store the common fields, and then have a relation for each type of RCSMessage:
@https://developer.vonage.com/en/api/messages#webhooks
خب، ببینیم چی درمیآید.

وظیفه را فهمیده و انواع مختلف فایلهای RCS را جدا کرده است. یک نگاه به کد؟
بد نیست. همه جدولها را داخل یک migration آورده، در حالی که اگر خودم بودم احتمالاً برای کنترل دقیقتر، برای هر جدول یک migration جدا میساختم. یک اشتباه بدهی فنیِ واضح اینجاست:
$table->string('rcs_message_type'); // text, image, audio, video, file, location, rich_card, carousel
واقعاً باید enum باشد، یا شاید اصلاً نباید وجود داشته باشد. من با event storeهایی برخورد کردهام که برای زیرنوعها enum دارند و وقتی به چند میلیون ردیف میرسند، اضافه کردن یک enum جدید میتواند دردسر خیلی بزرگی باشد. چون RCS یک فناوری در حال توسعه است، انتظار دارم این اتفاق محتمل باشد. پس راهحل چیست؟ من این فیلد را حذف میکردم و در سطح مدل، روی رابطه معکوس تکیه میکردم. حتی اگر به هر دلیل لازم بود داده خام را بیرون بکشم، چند راه دارم: یا در SQL روی فیلد raw_payload که JSON است query بزنم، یا اگر میخواستم دوباره stitch کنم، از INNER JOIN استفاده کنم.
مدلهای من
چون migrations را دارم، با یک جمله از Cursor خواستم مدلها را بنویسد ببینم چه میکند.
هوم. این کلاس پایه است و الگو درست نیست. برای هر نوع RCS یک رابطه ساخته، بهجای اینکه یک مدل ارثبری/چندریختی داشته باشد. اما در خود subtypeها:
این بیشتر همان چیزی است که انتظار داشتم. مدل فقط فیلدهای همین subtype را دارد و اگر بقیه اطلاعات را بخواهید، میتوانید از موجودیت والد بگیرید. وقتی از Cursor خواستم refactor کند، یک کار جالب انتخاب کرد:
خب، از نظر فنی درست است، اما خیلی مستقیم رفته سمت این راه. ضمن اینکه هنوز بدهی فنیِ کلیدهای entity را دارید. اگر تغییر کنند یا مجبور شوید یکی اضافه کنید چه؟ حالا چند جای مختلف برای فکر کردن دارید.
بهینهترین روش این است که این را یک نوع رابطه Polymorphic در نظر بگیرید. مستندات Laravel دقیقاً توضیح میدهد چطور این نوع رابطه را بسازید. معمولاً وقتی این رابطه بهتر است که بیش از یک فرزند ممکن باشد، اما در اینجا همیشه فقط یک subtype داریم. بنابراین من اینجا امتیاز را مساوی میدهم: migrations قابل قبول است، اما مدلسازی نه.
Seederها
خب، حالا یک بخش سرگرمکننده. با اینکه معماری مدلها واقعاً چیزی نیست که انتخاب کنم، فعلاً با آن پیش میروم. اما آیا Cursor میتواند برای جدولها seeders بسازد که داده ساختگی تولید کنند؟ ازش خواستم Seederها و Factoryهای همراهش را بسازد.
بد نیست، از این جهت که کار را راه میاندازد. تعریفهای factory خیلی سرراست بود، اما خود seeder عملاً از آنها استفاده نمیکند، پس یک پیادهسازی نیمهکاره است که «کار میکند»، اما مطابق کنوانسیونهای Laravel نیست. اینجا هیچ استفادهای از closure برای موجودیتهای مرتبط هم نیست. من امسال در کنفرانس API Platform یک ارائه داشتم که این نوع رابطه را نشان دادم:

این دارد به یک الگوی تکرارشونده اضافه میکند: هوش مصنوعی خیلی از کارهای خستهکننده را انجام میدهد، اما «کمی نادرست» یا «خلاف روشهای opinionated لاراول» انجامشان میدهد. این کنوانسیونها برای عملکرد و مقیاسپذیری عمداً اینطور طراحی شدهاند، پس دور زدنشان یک مسیر تضمینی به سمت بدبختی است.
وقتی seeder را اجرا کنیم چه میشود؟

کنترلرهای من
بخش ۱: RCS ورودی
مثال من برای این endpoint میشود https://localhost:8080/api/webhook. این endpoint باید JSON payload را بگیرد، تشخیص بدهد پیام از چه نوعی است، و بعد موجودیت پایه و subtype را بسازد. پس پرامپت من این است:

حالا اوضاع کثیف میشود. این بخشی از متد خروجی است:
اولین چیزی که اینجا دیدم این است که داریم فعل HTTP در endpoint را نقض میکنیم. من میخواهم این کنترلر با یک POST، یک شیء RCS بسازد. همین الان وارد قلمرو طراحی API مشکوک شدهایم: متد اصلی اینجا updateOrCreate() است. نههه! این کار PATCH است!
قرار نیست کد hydrateSubtype را بگذارم، چون بهشدت بیشازحد مهندسی شده بود و یک switch statement نسبتاً توهینآمیز داشت با تعداد خط بیشتر از هملت.
از نظر معماری، یک حذف بزرگ اینجاست و آن هم چیزی است که حالا به من حس «با هوش مصنوعی مثل یک توسعهدهنده جونیور رفتار کن» میدهد. این endpoint نوشتنی است و بنابراین به اتمیک بودن نیاز دارد. یا کامل انجام میشود یا rollback میکند. وضعیت باید فقط «بهروز شده» یا «بهروز نشده» باشد، با همان روند تکرارپذیر هر بار که replay میکنید (در اینجا اگر دوباره داده را POST کنید، به خاطر داده تکراری شکست میخورد). انتظار ندارم AI دومی را هندل کند، اما اینجا باید دو موجودیت بسازد: موجودیت پایه و subtype. چون کد از قابلیت SQL یعنی BEGIN TRANSACTION استفاده نمیکند، یعنی یک خطا در بخشی از کد باعث ایجاد موجودیت نیمهساخته و عملاً خراب میشود.
اینها همه بحث نظری است (که هدف هم همین است). آیا کار میکند؟

نه. آیا در فایل routing هست؟

بله، یعنی API router داخل اپ bootstrap نشده است. با یک نگاه سریع به فایل bootstrap اپ و:
بله، routeهای API جا افتاده، پس باید دستی اضافهاش کنم. یک درخواست دیگر با HTTPie میزنم و پاسخ ۲۰۱ میگیرم.

هیچ اعتبارسنجیای هم نیست، پس برای تست، یک payload خالی فرستادم و بازم ۲۰۱ برگشت. یعنی واقعاً داخل دیتابیس نوشته شده است.

بد نیست: با خیلی از معماری موافق نیستم و یک مرحله کلیدی یعنی فعال کردن API routes را جا انداخته، اما حداقل کار میکند. اگر بروم به API Reference و یک RCS Location object بردارم و paste کنم، باید یک entity بسازد و داخل rcs_message_locations بنویسد. این هم داده تست که از spec API برداشته شده همراه پاسخ:

و اگر همهچیز درست باشد، باید در دیتابیس ذخیرهاش را ببینیم.

آها! حالا باید دستی درستکاری کنیم. رکورد را نوشته، اما اولاً latitude و longitude را استخراج نکرده، و حالا مشخص شده فیلدها در migration اولیه اشتباه هستند، چون name و address نیستند. پس باید migration را اصلاح کنم، بعد هم سراغ controller بروم.
خب، این اولین مشکل است. فیلدهای خیالی location.name و location.address را به entity اضافه کرده، بعلاوه location.latitude باید location.lat باشد. تفسیر جالبی از AI. قبلاً هم گفتم، اما این داخل یک switch بزرگ بود و اصولاً باید تلاش کرد هرگز چنین چیزی ننویسید. همچنین احتمالاً بحث میکنم که آوردن کتابخانه Arr برای استخراج فیلدها واقعاً لازم نیست، نه $lat و $lng. در تقریباً همه کدبیسهای PHP که کار کردهام، نامگذاری متغیرها صریح و بلند است، نه اینها که بیشتر شبیه Golang هستند.
حذف فیلدهای بلااستفاده، و اصلاح داده مختصات هم در منطق کنترلر و هم در migration باید انجام شود، و وبهوک را دوباره ارسال کردم. دیباگ بیشتر (احتمالاً دارید الگو را میبینید) نشان میدهد وقتی میخواهد $messageContent را بیرون بکشد، این کار را میکند:
دو مشکل دارد: اول اینکه message یا rcs کلیدهای payload نیستند، پس محتوا همیشه null میشود. دوم اینکه خب، اصولاً این متغیر لازم نیست. من خود $payload را دارم که آرایه JSON body است.
باز درستش کردیم، چی میشود؟
اسکرینشات نشان میدهد یک موجودیت location درست در دیتابیس ذخیره شده
Location درست، بالاخره
اوف، بالاخره. بیشتر از چیزی که انتظار داشتم طول کشید. ببینیم پاسخهای API برای واکشی پیامها را چطور هندل میکند.
پاسخ API
وقت آن است کمی از این دادهها را بیرون بکشیم. ما Laravel API Resourceها را نداریم که وقتی میخواهید یک لایه اضافی کنترل تغییرات (mutation control) بین دیتابیس و endpoint داشته باشید، بهترینروش است. در پرامپت میخواهم یک endpoint جدید که همه موجودیتهای RCS text را برگرداند.

خب، ببینیم چه کرده.
این هم کنترلر. به نظرم تقریباً در اولین تلاش درست زده (هرچند pagination نخواستم، پس هرگز این کار را نکنید که همه چیز را یکجا بکشید بیرون). وقتی endpoint را میزنم چه میشود؟

خوب. relation را لود کرده و داده درست را برگردانده. برای این مورد، من حرفی ندارم. خوشحالم حداقل بخشی از این آزمایش روان پیش رفت.
نتیجهگیری
نتیجهگیری مهمترین برداشت اینجاست، چون عملاً نقطه شروعی است که بعداً بر اساسش مقالههای دیگری مینویسم برای بهتر کردن تجربه توسعهدهنده در PHP با هوش مصنوعی (و SDKهای Vonage). من کورکورانه واردش شدم و فقط حداقل ابزارها را استفاده کردم (Cursor با Claude 4.5 و GPT5). از این تجربه، دو نکته مهم وجود دارد:
-
هوش مصنوعی در PHP، حتی در سادهترین setup، باید طوری با آن برخورد شود که انگار یک توسعهدهنده جونیور برایتان کار میکند. بعضی وقتها خروجی درست میدهد، اما در کنوانسیونها و روشهای opinionated لاراول مشکل دارد. این کنوانسیونها عمداً برای عملکرد و مقیاسپذیری طراحی شدهاند، پس دور زدنشان یک مسیر تضمینی به سمت بدبختی است.
-
اینکه داده را وارد یک API کنید و بعد دوباره بیرون بکشید، زمان بسیار بیشتری برای دیباگ و اصلاح کد تولیدشده گرفت، نسبت به نوشتن معمولیِ خود کد.
وقتی خودتان را به حداقل ابزارها محدود میکنید، کمک گرفتن از AI برای یادگیری Laravel یا PHP سخت میشود. ابزارهای بهتر به AI بدهید، تجربه بهطور چشمگیری بهتر میشود. تیم هسته Symfony اوایل امسال سرور رسمی PHP Model Context Protocol (MCP) را منتشر کرد و پروژه Laravel Boost هم همین حالا در مراحل ابتدایی توسعه است. هر دوی اینها به عامل هوش مصنوعی زمینه حیاتی اضافه میکنند. در مقاله بعدی، همین کدبیس را برمیداریم و میبینیم این ابزارها چطور تجربه توسعهدهنده را تغییر میدهند.
