ค่าเริ่มต้นของ Laravel ตั้งมาให้แอปไหนในโลกก็ใช้ได้ เวลาเป็น UTC, log ต่อท้ายไฟล์เดียวไปเรื่อย ๆ และ Eloquent ยอมให้เราเขียนโค้ดหลวม ๆ ได้ ระบบงานในไทยที่ผู้ใช้อยู่ที่เดียว ควรปรับบางค่าตั้งแต่ commit แรก โพสต์นี้รวมห้าอย่างที่เราตั้งในทุกโปรเจกต์ใหม่ ตัวอย่างมาจาก repo ของเว็บนี้เอง
ตั้ง timezone ทั้งในแอปและในฐานข้อมูล
ค่าที่เรามักตั้งกันคือ timezone ใน config/app.php ซึ่งเป็นแค่ครึ่งเดียว อีกครึ่งคือ timezone ของ connection ที่แอปคุยกับฐานข้อมูล
// config/app.php
'timezone' => env('APP_TIMEZONE', 'Asia/Bangkok'),
// config/database.php ใน connection 'pgsql'
'timezone' => env('DB_TIMEZONE', 'Asia/Bangkok'),
// หรือใน connection 'mysql'
'timezone' => env('DB_TIMEZONE', '+07:00'),
timezone ของแอปกำหนดว่า PHP เขียนเวลาลงฐานข้อมูลเป็นเวลาไหน ส่วน timezone ของ connection กำหนดว่า SQL ที่เราเขียนเอง เช่น now() หรือ where created_at >= now() - interval '30 days' คิดว่าตอนนี้กี่โมง ถ้าตั้งแค่ฝั่งแอป ฐานข้อมูลจะใช้ timezone ของเครื่อง server ซึ่งบน VPS ส่วนใหญ่คือ UTC รายงานที่นับยอดด้วย SQL ก็จะคลาดไป 7 ชั่วโมงโดยที่ config ดูถูกทุกอย่าง
ใน PostgreSQL คอลัมน์ timestamp ที่ Laravel สร้างเก็บเวลาแบบไม่มี timezone ติดมา ส่วน MySQL ทำต่างออกไป คอลัมน์ TIMESTAMP แปลงจาก timezone ของ session เป็น UTC ตอนเขียน แล้วแปลงกลับตอนอ่าน ถ้า session ที่เขียนกับที่อ่านตั้งไม่เหมือนกัน เวลาก็เลื่อน
MySQL ยังมีอีกเรื่อง คือชื่อ timezone แบบ Asia/Bangkok ใช้ได้เฉพาะ server ที่โหลดตาราง timezone ไว้แล้ว ซึ่ง MySQL ที่มากับ XAMPP หรือ shared hosting หลายเจ้าไม่ได้โหลดไว้ และจะขึ้น error Unknown or incorrect time zone ทางที่ปลอดภัยกว่าคือ +07:00 ไทยไม่มีการปรับเวลาตามฤดูกาลอยู่แล้ว offset แบบนี้จึงถูกตลอดปี และเราตั้งผ่านแอปได้โดยไม่ต้องขอให้ host แก้ค่าของ server
ตั้งให้เสร็จก่อนรัน migrate ครั้งแรก ถ้ามีข้อมูลเข้ามาแล้วค่อยเปลี่ยน แถวเก่าจะยังเป็นเวลา UTC ต้องเขียน migration แปลงข้อมูลย้อนหลังทุกตาราง
ให้ log เขียนเป็นไฟล์รายวัน
ค่าเริ่มต้นของ Laravel คือ LOG_STACK=single เขียนต่อท้าย storage/logs/laravel.log ไฟล์เดียวไปเรื่อย ๆ ไม่มีอะไรมาตัดให้ เครื่องที่ไม่ได้ติดตั้งใหม่บ่อย ๆ ไฟล์นี้จะโตเป็น GB โดยที่ไม่มีใครเปิดดู บน shared hosting ที่มี quota ของพื้นที่ บางทีเรารู้ตัวก็ตอนเว็บอัปโหลดไฟล์ไม่ได้แล้ว
LOG_STACK=daily
LOG_DAILY_DAYS=14
daily เขียนวันละไฟล์ชื่อ laravel-2026-10-26.log และลบไฟล์ที่เก่ากว่า 14 วันให้เอง ใส่ทั้งใน .env และ .env.example ไม่อย่างนั้นเครื่องใหม่ที่ copy จาก example จะกลับไปเป็น single
ข้อจำกัดของ daily คือจำกัดจำนวนวัน ไม่ได้จำกัดขนาด ถ้าวันไหนมี error วนลูป ไฟล์ของวันนั้นวันเดียวก็โตเป็นหลาย GB ได้ บน server ที่ดูแลเองจึงควรตั้ง logrotate ให้ตัดตามขนาดไว้อีกชั้น และส่ง error ไปเครื่องมืออย่าง Sentry ด้วย จะได้ไม่ต้องรอเปิด log ถึงจะรู้
เปิด shouldBeStrict() ตอนพัฒนา
public function boot(): void
{
Model::shouldBeStrict(! $this->app->isProduction());
}
บรรทัดเดียวนี้เปิดสามอย่าง
preventLazyLoading()โยน exception เมื่อโหลด relation ทีละแถวในลูป ซึ่งเป็นต้นเหตุของ N+1 query หน้าที่วนแสดง 50 แถวแล้วเรียก$post->author->nameจะพังทันทีตอนเราเปิดดูบนเครื่อง แทนที่จะช้าลงเงียบ ๆ บน productionpreventAccessingMissingAttributes()โยน exception เมื่ออ่าน attribute ที่ไม่มี เช่น พิมพ์ชื่อคอลัมน์ผิด หรือselect()มาไม่ครบ ค่าเริ่มต้นจะคืนnullแล้วหน้าเว็บก็แสดงช่องว่างโดยไม่มีอะไรเตือนpreventSilentlyDiscardingAttributes()โยน exception เมื่อfill()ด้วย field ที่ไม่อยู่ใน$fillableแทนการทิ้งค่านั้นไปเงียบ ๆ (โปรเจกต์ที่ใช้Model::unguard()อย่างเว็บนี้จะไม่เจอข้อนี้ เพราะ Laravel ไม่ทิ้ง field ไหนอยู่แล้ว)
ใส่ ! $this->app->isProduction() ไว้เพราะบน production เราไม่อยากให้ผู้ใช้เจอหน้า error เพราะ relation ที่ลืม eager load ข้อผิดพลาดพวกนี้ควรเจอตั้งแต่บนเครื่องเราและใน test ซึ่งได้ผลดีมากตอนให้ AI agent เขียนโค้ด เพราะ test จะ fail ตรงที่ agent ลืม with() แทนที่จะผ่านไปถึงตอน review
กัน migrate:fresh ไว้ทุก environment
DB::prohibitDestructiveCommands();
คำสั่งนี้ห้าม migrate:fresh, migrate:refresh, migrate:reset, migrate:rollback และ db:wipe เมธอดนี้รับค่า boolean ได้ ถ้าส่ง $this->app->isProduction() เข้าไปจะห้ามเฉพาะบน production แต่เราเรียกโดยไม่ส่งอะไรเลย จึงห้ามทุก environment รวมถึงเครื่องเราเองด้วย
php artisan migrate:fresh
WARN This command is prohibited from running in this environment.เหตุผลคือฐานข้อมูลบนเครื่อง dev มักเป็นสำเนาจาก production ที่ดึงลงมาไว้ดูข้อมูลจริง และ AI agent ที่เจอ migration error บางครั้งก็เลือกรัน migrate:fresh เป็นทางลัด พอห้ามไว้ในโค้ด ทั้งคนและ agent ก็ล้างฐานข้อมูลไม่ได้ ต่อให้ลืมอ่านคำสั่งใน CLAUDE.md
สิ่งที่ต้องปรับตามมาคือ RefreshDatabase ใน test ใช้ไม่ได้ เพราะใช้ migrate:fresh อยู่ข้างใน เราจึงเตรียมฐานข้อมูล test จาก snapshot แล้วใช้ DatabaseTransactions แทน และ migrate:rollback ก็ใช้ไม่ได้ด้วย ถ้า migration ผิดให้เขียน migration ใหม่แก้ไปข้างหน้า
เขียนทุก schedule ไว้ใน routes/console.php
crontab ของ server ควรมีแค่บรรทัดเดียวต่อหนึ่งแอป
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
งานที่ตั้งเวลาทั้งหมดอยู่ใน routes/console.php ไม่ว่าจะเป็นคำสั่ง artisan หรือ script ใน repo
Schedule::command('snapshot:create --compress')->dailyAt('03:30');
Schedule::command('model:prune')->daily();
Schedule::command('sources:fetch')->hourlyAt(5)->withoutOverlapping();
ที่ผ่านมาเราก็มักเพิ่มบรรทัดใน crontab ตรง ๆ หรือใน Cron Jobs ของ cPanel เมื่อมีงานใหม่ ซึ่งใช้ได้ แต่บรรทัดพวกนั้นอยู่นอก repo คน review ไม่เห็น test ก็ไม่เห็น และพอย้าย server ก็หายไปทั้งชุด ถ้าเผลอมี schedule:run สองบรรทัดสำหรับแอปเดียว ทุกงานจะรันซ้ำสองรอบ พอทุกอย่างอยู่ใน routes/console.php เราดูรายการทั้งหมดได้ด้วย php artisan schedule:list และเขียน test เช็กว่างานไหนรันตอนไหนได้
งานที่ต้องรันบนเครื่องเดียว ให้ครอบด้วย Schedule::when() ที่อ่านค่าจาก .env ของเครื่องนั้น ดีกว่าเพิ่ม crontab แยกไว้บนเครื่องนั้นเครื่องเดียว
ค่าอื่นที่ตั้งพร้อมกัน
ยังมีอีกค่าที่สำคัญพอกันสำหรับเว็บที่อยู่หลัง Cloudflare หรือ reverse proxy คือ trustProxies ซึ่งถ้าตั้งเป็น * ตามที่ตัวอย่างส่วนใหญ่แนะนำ ใครก็ปลอม IP ของตัวเองได้ เราแยกไปเขียนไว้ในตั้ง trustProxies ให้ Laravel หลัง Cloudflare ส่วนการเขียนค่าพวกนี้ลงไฟล์ให้ AI agent อ่านก่อนเริ่มงาน อยู่ในโพสต์ Laravel Boost กับ MCP ครับ






