<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>Devfavor</title><description>Devfavor – Because Clean Code Deserves Love.</description><link>https://blog.devfavor.com</link><item><title>Test Data Management: จัดการข้อมูลทดสอบอย่างมืออาชีพ</title><link>https://blog.devfavor.com/blog/test-data-management-guide</link><guid isPermaLink="true">https://blog.devfavor.com/blog/test-data-management-guide</guid><description>เคล็ดลับการสร้าง จัดเก็บ และใช้ข้อมูลทดสอบให้ปลอดภัยและมีประสิทธิภาพ</description><pubDate>Mon, 29 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ไม่ว่าเราจะพัฒนาแอปพลิเคชันหรือเว็บไซต์ด้วยภาษาอะไร จุดร่วมที่จำเป็นต้องทำเหมือนกันทั้งหมดก็คือการทดสอบซอฟต์แวร์ และปัญหาที่หลาย ๆ ทีมมักจะเจอเหมือนกันก็คือ
ปัญหาการจัดการข้อมูลทดสอบ เช่น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ใช้ข้อมูลจริงไม่ได้จากปัญหาของ privacy&lt;/li&gt;
&lt;li&gt;ใช้ข้อมูลทดสอบแล้วไม่ครอบคลุมถึง use case จริง&lt;/li&gt;
&lt;li&gt;ข้อมูลทดสอบไม่สอดคล้องกับฟีเจอร์ที่กำลังพัฒนา&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ปัญหาเหล่านี้จะหมดไปหากนักพัฒนามีวิธีการจัดการกับข้อมูลที่ดีเพียงพอ&lt;/p&gt;
&lt;h2&gt;😎 Test Data Management คืออะไร?&lt;/h2&gt;
&lt;p&gt;Test Data Management คือ การสร้าง จัดเก็บ และใช้ข้อมูลทดสอบอย่างเป็นระบบ เพื่อให้การทดสอบซอฟต์แวร์สมจริงและมีประสิทธิภาพมากที่สุด ลดความเสี่ยงจากการใช้ข้อมูลที่ไม่เหมาะสม
และทำให้ทีม QA หรือแม้กระทั่งทีม Dev สามารถทดสอบได้โดยง่าย รวมถึงยังใช้งานร่วมกับ CI/CD ได้อีกด้วย&lt;/p&gt;
&lt;h2&gt;🤔 ปัญหาจากการใช้งานข้อมูลจริงในการทดสอบ&lt;/h2&gt;
&lt;p&gt;ในการทดสอบการทำงานของซอฟต์แวร์ด้วยข้อมูลจริงนั้นมีความสำคัญเป็นอย่างมาก เนื่องจากจะทำให้เราสามารถรับรู้ได้ถึง use case จริงที่อาจเกิดขึ้นในระบบ
ทำให้การทดสอบเกิดประสิทธิภาพใกล้เคียงกับการใช้งานจริง&lt;/p&gt;
&lt;p&gt;แต่ก็มีจุดที่ควรระวังนั่นคือ &lt;code&gt;ความปลอดภัยของข้อมูล&lt;/code&gt; นั่นเอง หากมีการใช้ข้อมูลจริงในการทดสอบและไม่ได้ถูกจัดการอย่างมีประสิทธิภาพ ก็อาจจะเสี่ยงที่ข้อมูลนั้นจะรั่วไหล
จนอาจเกิดปัญหาได้&lt;/p&gt;
&lt;h2&gt;🚀 เทคนิคการจัดการ Test Data อย่างมีประสิทธิภาพ&lt;/h2&gt;
&lt;p&gt;แม้การทดสอบซอฟต์แวร์ในบางครั้งเราอาจจะไม่สามารถนำข้อมูลจริงมาทดสอบได้ทั้งหมด แต่ด้วยเครื่องมือและเทคนิคต่าง ๆ ที่มีอยู่ก็สามารถสร้างสภาพแวดล้อมให้ใกล้เคียงกับข้อมูลจริงแทนได้
(และบางครั้งอาจจะจำลองข้อมูลแปลก ๆ ในการทดสอบได้มากกว่าเสียด้วย) โดยเทคนิคที่แนะนำมีดังนี้&lt;/p&gt;
&lt;h3&gt;🛠️ การสร้างข้อมูลจำลอง&lt;/h3&gt;
&lt;p&gt;สำหรับข้อมูลที่มีความ sensitive หรือข้อมูลที่มีโครงสร้างชัดเจน บางครั้งเราไม่จำเป็นต้องอาศัยข้อมูลจริงในการทดสอบ แต่สามารถจำลองขึ้นมาแทนได้ เช่น การใช้ factory ใน Laravel เป็นต้น&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;User::factory()-&gt;count(100)-&gt;create([
    &apos;email_verified_at&apos; =&gt; now(),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ข้อดีของการจำลองแบบนี้คือ เราสามารถจำลองข้อมูลปริมาณมากได้อย่างรวดเร็ว โดยไม่กระทบกับความเป็นส่วนตัวของผู้ใช้งาน
แต่ก็มีข้อเสียเช่นกัน คือ บางครั้งข้อมูลที่จำลองมาอาจไม่ได้หลากหลายมากพอที่จะสะท้อนถึง use case ที่อาจเกิดขึ้นในระบบจริง&lt;/p&gt;
&lt;p&gt;หากต้องการข้อมูลที่สามารถแยก use case ของระบบได้จริงจะต้องจัดการ factories ให้มีประสิทธิภาพและครอบคลุมมากพอ ซึ่งอาจต้องเป็นการทำงานร่วมกันระหว่าง QA และ Dev&lt;/p&gt;
&lt;h3&gt;📝 การ Masking หรือ Anonymization ข้อมูลจริง&lt;/h3&gt;
&lt;p&gt;เทคนิคนี้จะเป็นการนำข้อมูลจริงมาทดสอบ แต่จะเพิ่มการปกปิดข้อมูลบางอย่างที่เป็นข้อมูล sensitive ของระบบเพื่อให้เกิดความปลอดภัย
โดยที่ข้อมูลนั้นต้องไม่กระทบต่อ use case ของการทดสอบระบบ เช่น&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;UPDATE users SET email = CONCAT(&apos;user_&apos;, id, &apos;@example.com&apos;), phone = &apos;000-000-0000&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากตัวอย่างด้านบนจะเป็นการ Masking อีเมลและหมายเลขโทรศัพท์ของผู้ใช้งานทั้งหมดในระบบ ก่อนจะนำข้อมูลอื่น ๆ ไปทดสอบต่อ&lt;/p&gt;
&lt;h3&gt;🔐 แยก environment สำหรับทดสอบให้ชัดเจน&lt;/h3&gt;
&lt;p&gt;โดยปกติแล้วในการพัฒนาซอฟต์แวร์หรือเว็บไซต์ มักจะมีหลาย environment ขึ้นอยู่กับแต่ละทีม แต่โดยส่วนมากแล้วก็จะแบ่งออกเป็น environment สำหรับ
Dev ทดสอบ , staging สำหรับผู้ใช้งานทดสอบ และ production เป็นอย่างน้อย นั่นทำให้เราต้องจัดการกับข้อมูลที่จะใช้ทดสอบในแต่ละขั้นตอนให้ดี เช่น&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;stages:
  - prepare
  - develop
  - staging
  - deploy

workflow:
  rules:
    - if: &apos;$CI_COMMIT_BRANCH == &quot;develop&quot;&apos;
    - if: &apos;$CI_COMMIT_BRANCH == &quot;staging&quot;&apos;
    - when: never

prepare:
  stage: prepare
  rules:
    - if: &apos;$CI_COMMIT_BRANCH == &quot;develop&quot;&apos;
    - if: &apos;$CI_COMMIT_BRANCH == &quot;staging&quot;&apos;
  script:
    - cp .env.example .env
    - composer install --no-interaction --prefer-dist
    - php artisan key:generate

develop:
  stage: develop
  rules:
    - if: &apos;$CI_COMMIT_BRANCH == &quot;develop&quot;&apos;
  variables:
    APP_ENV: &quot;local&quot;
    TEST_DATA_STRATEGY: &quot;factory&quot;
  script:
    - php artisan migrate:fresh --seed
    - php artisan test --testsuite=Feature

staging:
  stage: staging
  rules:
    - if: &apos;$CI_COMMIT_BRANCH == &quot;staging&quot;&apos;
  variables:
    APP_ENV: &quot;staging&quot;
    TEST_DATA_STRATEGY: &quot;dump&quot;
  before_script:
    - chmod +x scripts/db/pull_prod_dump.sh scripts/db/restore_staging.sh
  script:
    # ดึง production dump + append mask.sql
    - bash scripts/db/pull_prod_dump.sh
    # restore เข้า staging + post-restore adjustments
    - bash scripts/db/restore_staging.sh
    # migrate schema เพิ่มเติม (ถ้ามี) ให้บังคับใช้ใน staging
    - php artisan migrate --force

deploy:
  stage: deploy
  needs: [&quot;staging&quot;]
  rules:
    - if: &apos;$CI_COMMIT_BRANCH == &quot;staging&quot;&apos;
  script:
    - echo &quot;Deploying to staging...&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากตัวอย่างข้างต้น จะเห็นว่าเราได้แบ่ง environment ออกเป็น develop และ staging โดยที่&lt;/p&gt;
&lt;p&gt;ใน develop เราจะใช้ข้อมูลสำหรับทดสอบจาก factory (Laravel) เพื่อให้เกิดความรวดเร็วก่อน&lt;/p&gt;
&lt;p&gt;และเมื่อทดสอบผ่าน เราจะทำการทดสอบที่ staging ต่อด้วยข้อมูลที่ dump มาจาก production และทำการ masking ข้อมูลสำคัญออกไป เพื่อให้การทดสอบครอบคลุมมากยิ่งขึ้น&lt;/p&gt;
&lt;p&gt;เพียงเท่านี้ก็จะสามารถจัดการกับข้อมูลทดสอบได้อย่างมีประสิทธิภาพมากขึ้นแล้ว&lt;/p&gt;
&lt;h2&gt;✨ บทสรุป&lt;/h2&gt;
&lt;p&gt;การทดสอบซอฟต์แวร์ด้วยเทคนิคของ Test Data Management ไม่ใช่เรื่องของเครื่องมือหรือ framework โดยตรง แต่เป็นแนวคิดที่จำเป็นสำหรับทีม QA และทีม Dev ที่จะต้อง
วางแผนและทำงานร่วมกัน เพื่อให้การทดสอบในแต่ละขั้นตอนเป็นไปอย่างมีประสิทธิภาพและครอบคลุม use case ที่อาจเกิดขึ้นและมีโอกาสที่จะทำให้ซอฟต์แวร์เกิดปัญหาขึ้นมาได้&lt;/p&gt;
&lt;p&gt;หากการทดสอบเป็นไปได้ด้วยดี นอกจากจะทำให้คุณภาพของซอฟต์แวร์ดีขึ้นแล้วยังทำให้คุณภาพของทีมสามารถทำงานได้อย่างมั่นใจมากขึ้นอีกด้วย 🤗&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.Dkd5si87.jpg"/><enclosure url="/_astro/cover.Dkd5si87.jpg"/></item><item><title>Git Commit Convention - มาตรฐานการ Commit ที่ทำให้โค้ดอ่านง่ายขึ้น</title><link>https://blog.devfavor.com/blog/git-commit-convention</link><guid isPermaLink="true">https://blog.devfavor.com/blog/git-commit-convention</guid><description>เปลี่ยน Commit Message ธรรมดาให้เป็นระเบียบ เข้าใจง่ายและทำงานเป็นทีมได้ราบรื่นมากยิ่งขึ้น</description><pubDate>Mon, 08 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/packages/pure/components/user&apos;&lt;/p&gt;
&lt;p&gt;การเขียนโค้ดโดยปกติก็ปวดหัวพออยู่แล้ว แต่สิ่งที่ทำให้ปวดหัวถัดไปเมื่อเขียนโค้ดเสร็จก็คือ การเขียน Commit Message นั่นเอง&lt;/p&gt;
&lt;p&gt;ถ้าคุณเคยเจอ Commit Message ที่สั้นมาก ๆ และไม่สื่อความหมาย เช่น update, fix bug, asdf หรือ Commit Message ที่ยาวมาก ๆ ชนิดที่ว่า
เหมือนเขียนโค้ดที่แก้ไขมาใส่ลงไป คงจะพอเห็นภาพของปัญหานี้&lt;/p&gt;
&lt;p&gt;หากโปรเจกต์นี้มีผู้พัฒนาคนเดียวหรือเป็นทีมเล็ก ๆ ก็คงจะไม่มีปัญหาอะไร แต่หากโปรเจกต์มีขนาดใหญ่ขึ้น มีหลาย module เข้ามาเกี่ยวข้อง
หรือมีนักพัฒนาหลายคน การจะไล่ประวัติการแก้ไขของโค้ด (commit history) ก็คงจะไม่ใช่เรื่องง่ายเลยทีเดียว
และจุดนี้เองที่ทำให้ Git Commit Convention เข้ามามีบทบาทเป็นภาษากลางที่ทีมจะได้ใช้สื่อสารร่วมกัน&lt;/p&gt;
&lt;h2&gt;💡 Git Commit Convention คืออะไร&lt;/h2&gt;
&lt;p&gt;Git Commit Convention คือ กติกา/มาตรฐาน ที่ช่วยกำหนดโครงสร้างการเขียน Commit Message ให้สื่อสารชัดเจน เข้าใจง่าย และเป็นระบบ เพื่อให้ทุกคนในทีมสื่อสารตรงกัน&lt;/p&gt;
&lt;h2&gt;📑 ตัวอย่าง Convention ที่นิยมใช้&lt;/h2&gt;
&lt;p&gt;มาตรฐานของ Commit Message นั้นมีได้หลากหลายตั้งแต่มาตรฐานกลางที่ developer ยึดถือร่วมกัน ไปจนกระทั่งมาตรฐานของทีมแบบเฉพาะเจาะจง
โดยทุกรูปแบบมีจุดมุ่งหมายเดียวกันคือ การจัดโครงสร้างของ Commit Message ให้เป็นระเบียบ&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;Conventional Commits&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;เป็นรูปแบบที่ต่อยอดมาจาก &lt;code&gt;Angular Commit Guidelines&lt;/code&gt; โดยมีลักษณะดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;type&gt;[optional scope]: &amp;#x3C;description&gt;

[optional body]

[optional footer(s)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ซึ่งจะมีส่วนประกอบสำคัญอยู่ 2 ส่วน คือ type และ description&lt;/p&gt;
&lt;p&gt;โดยที่ type จะอ้างอิงมาจาก &lt;code&gt;Angular Commit Guidelines&lt;/code&gt; เช่นกัน ซึ่งประกอบด้วย type ที่สำคัญดังนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;build: การแก้ไขที่เกี่ยวกับการ build dependencies ของระบบ&lt;/li&gt;
&lt;li&gt;ci: การแก้ไขที่เกี่ยวกับการทำ CI หรือ deployment&lt;/li&gt;
&lt;li&gt;docs: การอัปเดตเอกสาร&lt;/li&gt;
&lt;li&gt;feat: การเพิ่มฟีเจอร์ใหม่&lt;/li&gt;
&lt;li&gt;fix: การแก้ไขข้อผิดพลาดของระบบ&lt;/li&gt;
&lt;li&gt;perf: การเพิ่ม performance ของระบบ&lt;/li&gt;
&lt;li&gt;refactor: การ refactor code โดยไม่ได้เพิ่มฟีเจอร์หรือแก้ไขข้อผิดพลาดใด ๆ&lt;/li&gt;
&lt;li&gt;style: การปรับแก้ไข code style หรือ format โดยไม่ได้เพิ่มฟีเจอร์หรือแก้ไขข้อผิดพลาดใด ๆ&lt;/li&gt;
&lt;li&gt;test: การเพิ่มหรือแก้ไข tests ของระบบ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ตัวอย่างการ Commit Message ที่อ้างอิงมาตรฐานนี้สามารถเขียนได้ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;feat: allow provided config object to extend other configs

BREAKING CHANGE: `extends` key in config file is now used for extending other config files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จะเห็นว่าเมื่ออ่าน Commit Message จะสามารถเข้าใจการเปลี่ยนแปลงได้โดยคร่าวทันที ทำให้การไล่ประวัติการแก้ไขทำได้ง่ายขึ้นมาก&lt;/p&gt;
&lt;p&gt;สามารถอ่านรายละเอียดเพิ่มเติมได้ที่ &lt;a href=&quot;https://www.conventionalcommits.org/&quot;&gt;Conventional Commits&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;Angular Commit Guidelines&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;เรียกว่าเป็นมาตรฐานต้นแบบสำหรับ Commit Convention เลยก็ว่าได้ มีลักษณะพื้นฐานทั้งหมดเหมือนกับ Conventional Commits
(ซึ่งก็แน่นอนเพราะ Conventional Commits อ้างอิงมาจาก Guidelines นี้) โดยมีลักษณะดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;type&gt;(&amp;#x3C;scope&gt;): &amp;#x3C;short summary&gt;
  │       │             │
  │       │             └─⫸ Summary in present tense. Not capitalized. No period at the end.
  │       │
  │       └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
  │                          elements|forms|http|language-service|localize|platform-browser|
  │                          platform-browser-dynamic|platform-server|router|service-worker|
  │                          upgrade|zone.js|packaging|changelog|docs-infra|migrations|
  │                          devtools
  │
  └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ซึ่งในส่วนของ Commit Scope ตัว Document ของ Angular จะมีการอธิบายเพิ่มเติมเกี่ยวกับ Scope ที่รองรับ ดังนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;animations&lt;/li&gt;
&lt;li&gt;common&lt;/li&gt;
&lt;li&gt;compiler&lt;/li&gt;
&lt;li&gt;compiler-cli&lt;/li&gt;
&lt;li&gt;core&lt;/li&gt;
&lt;li&gt;elements&lt;/li&gt;
&lt;li&gt;forms&lt;/li&gt;
&lt;li&gt;http&lt;/li&gt;
&lt;li&gt;language-service&lt;/li&gt;
&lt;li&gt;platform-browser&lt;/li&gt;
&lt;li&gt;platform-browser-dynamic&lt;/li&gt;
&lt;li&gt;platform-server&lt;/li&gt;
&lt;li&gt;platform-webworker&lt;/li&gt;
&lt;li&gt;platform-webworker-dynamic&lt;/li&gt;
&lt;li&gt;router&lt;/li&gt;
&lt;li&gt;service-worker&lt;/li&gt;
&lt;li&gt;upgrade&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;สำหรับรายละเอียดเพิ่มเติมอื่น ๆ สามารถอ่านได้ที่
&lt;a href=&quot;https://github.com/angular/angular/blob/main/contributing-docs/commit-message-guidelines.md&quot;&gt;Angular Commit Guidelines&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;Custom Team Convention&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ในบางครั้งการพัฒนาภายในทีมอาจต้องการข้อมูลมากขึ้นในการตรวจสอบโค้ดหรือบริหารจัดการทรัพยากร
จึงจำเป็นต้องเพิ่มข้อมูลบางอย่างเข้าไปใน Commit Message เช่น เลขของ task id เป็นต้น เพื่อให้ง่ายต่อการทำ CI/CD
หรือง่ายต่อ development workflow บางอย่าง ตัวอย่าง message เหล่านี้ได้แก่&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TASK-123: fix issue user login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จะเห็นว่า แม้เราจะเพิ่มข้อมูลบางส่วนไปเพื่อให้ง่ายต่อการจัดการแล้ว แต่เราก็ยังคงยึดถือ Conventional Commits บางส่วนได้ทำให้สื่อสารกับนักพัฒนากลุ่มอื่นได้เช่นกัน&lt;/p&gt;
&lt;h2&gt;🛠️ การใช้เครื่องมือช่วยตรวจสอบ Commit Message&lt;/h2&gt;
&lt;p&gt;แม้ว่าจะมีการตกลงกันภายในทีมเป็นอย่างดีแล้ว แต่ในทางปฏิบัติจริงก็อาจมีบางครั้งที่มีการหลงลืมหรือเกิดข้อผิดพลาดในการพิมพ์ Commit Message ขึ้นได้&lt;/p&gt;
&lt;p&gt;ซึ่งในจุดนี้เราสามารถนำเครื่องมือต่าง ๆ เข้ามาช่วยเพื่อให้ทีมสามารถทำงานได้สะดวกขึ้นดังนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;commitlint&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;เป็นเครื่องมือที่ช่วยตรวจสอบ Commit Message ของเราว่าได้พิมพ์ถูกต้องตาม Convention ที่กำหนดหรือไม่
โดยสามารถกำหนดมาตรฐานตาม Conventional Commits ได้เลย หรือจะสร้าง rules โดยเฉพาะของทีมก็ได้&lt;/p&gt;
&lt;p&gt;ดูรายละเอียดการติดตั้ง commitlint ได้ที่ &lt;a href=&quot;https://commitlint.js.org/&quot;&gt;https://commitlint.js.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;หลังจากติดตั้งแล้วให้ลองพิมพ์คำสั่ง&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;update file&quot; | npx commitlint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จะเห็นว่า &lt;code&gt;update file&lt;/code&gt; เป็น Commit Message ที่ไม่ถูกต้องตามมาตรฐาน เมื่อตรวจสอบด้วย commitlint จะได้ผลลัพธ์&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;⧗   input: update file
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;แสดงว่าตัว commitlint ทำงานถูกต้องแล้ว (เพราะตรวจเจอว่า ไม่ได้ใส่ subject และ type มา)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;Git Hook ด้วย Husky&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;หลังจากติดตั้ง commitlint แล้ว ให้เราเชื่อมต่อตัว commitlint และ Git Hook ด้วย Husky&lt;/p&gt;
&lt;p&gt;ซึ่งสามารถติดตั้งได้จาก &lt;a href=&quot;https://typicode.github.io/husky/&quot;&gt;https://typicode.github.io/husky/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;หลังจากติดตั้งเสร็จเรียบร้อยแล้ว ให้เพิ่มคำสั่ง commit-msg เข้าไปดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx --no-install commitlint --edit &quot;$1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;หลังจากนั้นให้เราลองสร้าง commit ที่มีรูปแบบที่ไม่ถูกต้องอีกครั้ง จะเห็นว่า git ของเราจะไม่อนุญาตให้ทำ commit นั้นด้วยข้อความของ commitlint
ที่เราได้ติดตั้งไปก่อนหน้านี้&lt;/p&gt;
&lt;p&gt;เมื่อทุกอย่างติดตั้งเสร็จเรียบร้อยเราสามารถแชร์ config ทั้งหมดนี้ให้ทีมใช้ร่วมกันได้ทันที&lt;/p&gt;
&lt;h2&gt;🎯 บทสรุป&lt;/h2&gt;
&lt;p&gt;การเขียน Commit Message อาจจะดูเป็นเรื่องเล็กน้อยเมื่อเทียบกับการเขียนโค้ดโดยรวม แต่ก็เป็นอีกหนึ่งตัวช่วยให้การทำงานร่วมกันเป็นทีม
เป็นไปอย่างราบรื่นมากยิ่งขึ้นไม่แพ้มาตรฐานอื่นเลย อีกทั้งยังเป็นเรื่องที่สามารถเริ่มทำได้ง่ายโดยไม่กระทบภาพรวมของทีมอีกด้วย 🥰&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.BAuITnoG.jpg"/><enclosure url="/_astro/cover.BAuITnoG.jpg"/></item><item><title>Laravel API Resource Patterns - ยกระดับการจัดการ Response ให้ทรงพลัง</title><link>https://blog.devfavor.com/blog/laravel-api-resource-patterns</link><guid isPermaLink="true">https://blog.devfavor.com/blog/laravel-api-resource-patterns</guid><description>เปลี่ยน Resource ธรรมดาให้กลายเป็น Layer ที่ชัดเจน สวยงาม และยืดหยุ่น</description><pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;หากเราต้องการเขียน API ด้วย Laravel ตัว framework ของ Laravel เองก็มีเครื่องมืออำนวยความสะดวกให้เราใช้อยู่แล้วนั่นก็คือ API Resource
ซึ่งทำให้เราสามารถจัดการกับ API response ได้อย่างง่ายดาย&lt;/p&gt;
&lt;p&gt;แต่เมื่อระบบของเราใหญ่ขึ้น มีความซับซ้อนขึ้น รวมถึงความสัมพันธ์ระหว่าง Model เองก็ตาม การใช้ Laravel API Resource โดยปกติอาจจะไม่เพียงพอต่อความต้องการ
จนทำให้โค้ดเกิดความซับซ้อนขึ้นได้&lt;/p&gt;
&lt;p&gt;บทความนี้จะพาคุณมาทำความรู้จักกับการใช้งาน API Resource ร่วมกับ design pattern อื่น ๆ ที่มักพบในการเขียน Laravel
เพื่อให้ API response ของเรามีความยืดหยุ่นและทรงพลังมากยิ่งขึ้น&lt;/p&gt;
&lt;h2&gt;🎨 Presenter / Transformer ตัวช่วยแปลงค่า แยก logic ให้ชัดเจน&lt;/h2&gt;
&lt;p&gt;บางครั้งใน API Resource ที่เขียนอาจไม่ได้มีแค่การส่งค่าเพียงอย่างเดียว แต่ยังมี business logic แฝงอยู่ใน response ที่ส่งค่ากลับไปด้วย
หากเรานำ business logic เหล่านี้เขียนไว้ใน Resource ตรง ๆ ก็อาจจะส่งผลเสียให้ business logic เหล่านี้ไม่สามารถ reuse ได้
และอาจเกิด duplicate code ในกรณีที่มีการส่งค่าลักษณะคล้ายกันซ้ำ ๆ&lt;/p&gt;
&lt;p&gt;ในกรณีนี้เราจะใช้ Layer ของ Presenter / Transformer มาช่วยในการแปลงค่าบางอย่างของ model หรือ collection ก่อนที่จะส่งค่ากลับ เช่น&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class UserPresenter {
    public function transform(User $user): array {
        return [
            &apos;id&apos; =&gt; $user-&gt;id,
            &apos;full_name&apos; =&gt; &quot;{$user-&gt;first_name} {$user-&gt;last_name}&quot;,
            &apos;status&apos; =&gt; $user-&gt;is_active ? &apos;active&apos; : &apos;inactive&apos;,
        ];
    }
}

class UserResource extends JsonResource {
    public function toArray($request) {
        return (new UserPresenter())-&gt;transform($this-&gt;resource);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากตัวอย่างจะเห็นว่าเราสามารถแยก business logic บางอย่างออกมาจาก Resource ได้ และยังสามารถนำ logic เหล่านี้ไป reuse ใช้กับส่วนอื่น ๆ ในโปรเจกต์ได้อีกด้วย&lt;/p&gt;
&lt;h2&gt;🧩 Contextual Resource เปลี่ยน Resource ไปตาม context&lt;/h2&gt;
&lt;p&gt;บางครั้งเราอาจต้องการ API ที่มีลักษณะคล้ายกันหรือดึงข้อมูลแบบเดียวกัน แต่อาจมีค่าบางอย่างเปลี่ยนไปตาม context ของระบบที่เรียกใช้ เช่น
permission ของ user หรือ module ที่เรียกใช้งาน เป็นต้น การแสดงผลเหล่านี้แม้จะมีความใกล้เคียงกันแต่ก็อาจมีบางส่วนแตกต่างกันตาม context
เราจึงสามารถประยุกต์ใช้ API Resource ของเราได้ ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class UserResource extends JsonResource {
    protected $context;

    public function __construct($resource, $context = []) {
        parent::__construct($resource);
        $this-&gt;context = $context;
    }

    public function toArray($request) {
        $data = [
            &apos;id&apos; =&gt; $this-&gt;id,
            &apos;name&apos; =&gt; $this-&gt;name,
        ];

        if ($this-&gt;context[&apos;role&apos;] === &apos;admin&apos;) {
            $data[&apos;email&apos;] = $this-&gt;email;
            $data[&apos;last_login&apos;] = $this-&gt;last_login;
        }

        return $data;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จะเห็นว่าเมื่อ context เปลี่ยน ค่าบางอย่างของ API Resource จะเปลี่ยนไป แต่ค่าหลัก ๆ ยังคงอยู่เหมือนเดิม ทำให้ผลลัพธ์มีความยืดหยุ่นมากยิ่งขึ้น&lt;/p&gt;
&lt;h2&gt;📦 Response Envelope Pattern หุ้มทุก Response ด้วยรูปแบบเดียวกัน&lt;/h2&gt;
&lt;p&gt;กรณีที่ระบบที่พัฒนามีนักพัฒนาหลายคน และเป็นระบบใหญ่ การที่ทุก API Resource จะมีมาตรฐานเดียวกันเป็นสิ่งที่ดี แต่หากทุกคนต้องมาคอยเขียน response structure
ให้เหมือนกัน แม้จะมีเอกสารกลางคอยควบคุมแต่ก็คงเป็นเรื่องยุ่งยากอยู่ดี การมีตัวช่วยห่อหุ้ม response ไว้จะช่วยให้การจัดการง่ายขึ้นอย่างมาก&lt;/p&gt;
&lt;p&gt;สมมุติว่าในโปรเจกต์ของเราต้องการ response structure ที่มีโครงสร้างแบบนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;status&quot;: &quot;success&quot;,
  &quot;data&quot;: {...},
  &quot;meta&quot;: {
    &quot;request_id&quot;: &quot;xxx&quot;,
    &quot;timestamp&quot;: 123456
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;สิ่งที่เราต้องการคือ BaseResource ที่ช่วยห่อหุ้มโครงสร้างดังกล่าวและส่งข้อมูลเพียงแค่ data ที่ต้องการ ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class BaseResource {
    public static function success($data, $meta = []) {
        return response()-&gt;json([
            &apos;status&apos; =&gt; &apos;success&apos;,
            &apos;data&apos; =&gt; $data,
            &apos;meta&apos; =&gt; array_merge($meta, [
                &apos;request_id&apos; =&gt; Str::uuid(),
                &apos;timestamp&apos; =&gt; now()-&gt;timestamp,
            ]),
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากนั้นเมื่อเรียกใช้งาน BaseResource ที่ตั้งค่าไว้ก็จะทำให้ response ของเรามีโครงสร้างพื้นฐานตามที่กำหนด&lt;/p&gt;
&lt;h2&gt;📝 Polymorphic Resource เมื่อ Resource เปลี่ยนตาม Model&lt;/h2&gt;
&lt;p&gt;แม้ Resource ของ Laravel โดยปกติจะแยกตาม Model อยู่แล้ว แต่เราก็สามารถออกแบบโครงสร้างของ Resource ใหม่ให้สามารถใช้ Resource ร่วมกัน
แต่แบ่งหน้าที่การจัดการ format ของ Model ไปที่ Presenter ได้ เช่น&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ContentResource extends JsonResource {
    public function toArray($request) {
        return match (get_class($this-&gt;resource)) {
            Post::class =&gt; (new PostPresenter())-&gt;transform($this-&gt;resource),
            Video::class =&gt; (new VideoPresenter())-&gt;transform($this-&gt;resource),
            Podcast::class =&gt; (new PodcastPresenter())-&gt;transform($this-&gt;resource),
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากตัวอย่างจะเห็นว่า ทั้ง Post, Video และ Podcast เป็น Resource ที่อยู่ในกลุ่มเดียวกัน และมักจะมี logic ในการ response ที่คล้ายกัน
แต่ก็อาจจะมีบาง attributes ที่ต้องการรายละเอียดต่างกัน ในจุดนี้เราสามารถประยุกต์ใช้ Presenter เข้ามาช่วยได้ขึ้นอยู่กับสถานการณ์&lt;/p&gt;
&lt;h2&gt;🚀 Caching Layer ให้ response ได้เร็วขึ้น&lt;/h2&gt;
&lt;p&gt;บางครั้ง API ที่เรียกไม่ได้เปลี่ยนแปลงข้อมูลบ่อย การใช้ cache เข้ามาช่วยเป็นเทคนิคพื้นฐานที่ช่วยให้ Laravel สามารถรองรับ traffic ที่สูงขึ้นได้
และในกรณีนี้เราสามารถนำ cache มาใช้ได้ตั้งแต่ Resource Layer เลย&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class CachedResource extends JsonResource {
    public function toArray($request) {
        return Cache::remember(&quot;resource:{$this-&gt;id}&quot;, 60, function() {
            return parent::toArray($request);
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;เพียงเท่านี้ API ของเราก็สามารถรองรับ traffic ที่มากขึ้นได้แล้ว&lt;/p&gt;
&lt;h2&gt;🛠️ Repository Pattern Integration จัดการ Model อย่างมีประสิทธิภาพ&lt;/h2&gt;
&lt;p&gt;แม้ตัวของ Resource เองจะจัดการ Model relations ได้อยู่แล้ว แต่เมื่อระบบใหญ่ขึ้น เรามักจะใช้ Repository Pattern เข้ามาช่วยจัดการโครงสร้างของ Model อยู่แล้ว&lt;/p&gt;
&lt;p&gt;เราจึงสามารถนำ Repository Pattern นี้มาต่อยอดใช้กับ API Resource ได้เลยโดยที่ไม่ต้องให้ Resource จัดการ Model relations เอง&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$users = $this-&gt;userRepository-&gt;getWithRelations();
return UserResource::collection($users);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ด้วย Pattern นี้จะทำให้เราสามารถนำ Repository Pattern มา reuse ใช้ได้อย่างมีประสิทธิภาพมากยิ่งขึ้น&lt;/p&gt;
&lt;h2&gt;🎯 บทสรุป&lt;/h2&gt;
&lt;p&gt;แม้ Laravel Resource จะเป็นเครื่องมือที่ทรงพลังในตัวเองอยู่แล้ว แต่จะเห็นได้ว่าเรายังสามารถประยุกต์และแก้ไขเพิ่มเติมบางอย่าง
เพื่อให้การพัฒนาสามารถทำได้ง่ายและยืดหยุ่นมากยิ่งขึ้น อีกทั้งยังช่วยเพิ่ม performance ให้แก่ระบบอีกด้วย&lt;/p&gt;
&lt;p&gt;ซึ่งสิ่งที่นำมาประยุกต์ใช้นั้น ก็ล้วนแล้วแต่เป็นพื้นฐานในการเขียน Laravel ทั้งสิ้น ตัวแอดมินเองหวังว่าบทความนี้จะช่วยให้ผู้อ่านเห็นประโยชน์ของ
การออกแบบโค้ดในเชิงพื้นฐานและการประยุกต์ใช้เพื่อต่อยอดให้เกิดประโยชน์สูงที่สุด ☕&lt;/p&gt;
&lt;p&gt;ขอให้มีความสุขกับการเขียนโค้ด 🥰&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.DhwTvF4z.jpg"/><enclosure url="/_astro/cover.DhwTvF4z.jpg"/></item><item><title>Laravel API Versioning - ดีไซน์ API ให้รองรับอนาคต</title><link>https://blog.devfavor.com/blog/laravel-api-versioning</link><guid isPermaLink="true">https://blog.devfavor.com/blog/laravel-api-versioning</guid><description>การวางแผนการจัดการ API ไม่ใช่เรื่องไกลตัว แต่เป็นเรื่องที่ต้องวางแผนจัดการตั้งแต่แรก เพื่อรองรับการเปลี่ยนแปลงในระยะยาว</description><pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;หากคุณเป็น developer ที่เขียน Laravel อยู่แล้ว การจะสร้าง API ขึ้นมาใช้งานคงไม่ใช่เรื่องยากและถ้าผู้ใช้งานเป็น developer ในทีมเดียวกันด้วยแล้วก็คงจะไม่เกิดปัญหาอะไร
แต่ถ้า API ที่เราเขียน จำเป็นต้องให้บริการแก่ client หลายคนหรือหลายฝ่าย เช่น web frontend หรือ mobile application อาจมีโอกาสที่ client ทั้ง 2 นี้ต้องการข้อมูลที่แตกต่างกัน&lt;/p&gt;
&lt;p&gt;หากเราอัปเดต Version ของ API ผิดพลาด ผลก็คืออาจทำให้ client ที่ใช้งานอยู่บางส่วนพังได้ทันทีโดยไม่รู้ตัว และคงจะเป็นเรื่องยากยิ่งกว่าเดิมแน่ ๆ หากระบบเรามีความซับซ้อนมากยิ่งขึ้น
โอกาสที่การแก้ไขของเราจะทำให้ client พังก็ยิ่งมีมากขึ้นไปอีก นั่นทำให้การวางแผนเรื่อง API Versioning จึงเป็นเรื่องที่สำคัญและไม่ควรมองข้าม&lt;/p&gt;
&lt;h2&gt;💡 API Versioning คืออะไร&lt;/h2&gt;
&lt;p&gt;API Versioning คือ การกำหนดรุ่น หรือ Version ให้กับ API แต่ละเส้นของเรา เพื่อให้ผู้ใช้งานสามารถแยกการใช้งานและกำหนดคุณลักษณะของ client ที่เรียกใช้แยกออกจากกัน
เช่น web frontend หรือ mobile application เป็นต้น เพื่อให้ client แต่ละตัวสามารถทำงานได้ต่อเนื่องแม้ว่าทาง backend จะมีการอัปเดตเวอร์ชันนั่นเอง&lt;/p&gt;
&lt;h2&gt;📑 รูปแบบของ API Versioning&lt;/h2&gt;
&lt;p&gt;การกำหนดเวอร์ชันให้กับ API สามารถทำได้หลายรูปแบบ เช่น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;URI Versioning&lt;/em&gt;&lt;/strong&gt; - เป็นการกำหนดเวอร์ชันของ API ผ่าน path ของ endpoint เช่น&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;/api/v1/users&lt;/code&gt; และ &lt;code&gt;/api/v2/users&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;Header Versioning&lt;/em&gt;&lt;/strong&gt; - เป็นการกำหนดเวอร์ชันของ API ผ่าน request header เช่น&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;X-API-Version: 2&lt;/code&gt; และ &lt;code&gt;Accept: application/vnd.example.v2+json&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;Query Parameter&lt;/em&gt;&lt;/strong&gt; - เป็นการกำหนดเวอร์ชันของ API ผ่าน query parameters ของ endpoint เช่น&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;/api/users?version=1&lt;/code&gt; และ &lt;code&gt;/api/users?version=2&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;จะเห็นว่าแต่ละวิธีที่เลือกใช้ก็จะมีข้อดีและข้อเสียแตกต่างกัน ขึ้นอยู่กับว่าผู้พัฒนาต้องการผลลัพธ์แบบใด&lt;/p&gt;
&lt;h2&gt;🛠️ การออกแบบ API Versioning บน Laravel&lt;/h2&gt;
&lt;p&gt;สำหรับในบทความนี้ แอดมินจะขอแนะนำวิธีการบริหารจัดการ API Versioning ในรูปแบบของ URI Versioning ซึ่งเป็นรูปแบบที่ผู้อ่านน่าจะพบเห็นได้ทั่วไป
และตัว Laravel เองก็มีฟีเจอร์สำหรับแยก prefix และ group ของ API อยู่แล้วจึงทำให้สามารถบริหารจัดการได้ง่าย&lt;/p&gt;
&lt;p&gt;โดยหัวข้อที่เกี่ยวกับการพัฒนาแบ่งออกเป็นส่วน ๆ ดังนี้&lt;/p&gt;
&lt;h3&gt;การใช้ Route Group และ Namespace&lt;/h3&gt;
&lt;p&gt;ในขั้นแรกของการจัดการ API Versioning เราต้องทำการแยก route ของ API แต่ละเวอร์ชันออกจากกันเสียก่อน
และให้ API แต่ละเวอร์ชันเรียกผ่าน Controller ที่ Namespace ตรงกับเวอร์ชันของ API ตัวอย่างเช่น&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Route::prefix(&apos;v1&apos;)-&gt;group(function () {
    Route::get(&apos;/users&apos;, [App\Http\Controllers\V1\UserController::class, &apos;index&apos;]);
});

Route::prefix(&apos;v2&apos;)-&gt;group(function () {
    Route::get(&apos;/users&apos;, [App\Http\Controllers\V2\UserController::class, &apos;index&apos;]);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากตัวอย่างจะเห็นว่า เรามี endpoint 2 เส้นคือเส้น v1 และ v2 ซึ่งถูกบริหารจัดการด้วย Controller ที่แตกต่างกัน&lt;/p&gt;
&lt;h3&gt;การใช้ Service Layer เพื่อลดความซ้ำซ้อนของโค้ดใน Controller&lt;/h3&gt;
&lt;p&gt;หากเราแยก Controller ออกเป็น 2 เวอร์ชันแล้ว อาจมีโค้ดบางส่วนที่เหมือนเดิมซึ่งทำให้ยากต่อการบำรุงรักษาในอนาคต
ในจุดนี้เราจะใช้ Service Layer เข้ามาช่วยลดความซ้ำซ้อนของโค้ดบางส่วนลง เช่น&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class UserService {
    public function getUsers() {
        return User::all();
    }
}

// Controller V1
class UserControllerV1 extends Controller {
    public function index(UserService $service) {
        // Business logic for V1
        return response()-&gt;json($service-&gt;getUsers());
    }
}

// Controller V2
class UserControllerV2 extends Controller {
    public function index(UserService $service) {
        // Business logic for V2
        return response()-&gt;json($service-&gt;getUsers());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากตัวอย่างจะเห็นว่า แม้เราจะแยก Controller ออกเป็น 2 ไฟล์แล้วก็ตาม แต่การใช้ Service Layer เข้ามาช่วยจะทำให้โค้ดภายใน Controller ทั้ง 2 ไฟล์นี้
แทบจะไม่มีความซ้ำซ้อนกันอยู่เลย สิ่งที่แตกต่างกันมีเพียงโครงสร้างที่สำคัญเท่านั้นเอง&lt;/p&gt;
&lt;h2&gt;✔️ Best Practices และแนวทางที่ควรทำ&lt;/h2&gt;
&lt;p&gt;แม้ว่าการแยก API Versioning ออกเป็นหลายเวอร์ชัน จะเป็นสิ่งที่ดีและควรทำ แต่หากไม่บริหารจัดการให้ดีก็อาจทำให้เกิดปัญหาที่คาดไม่ถึงขึ้นมาในภายหลังได้
สำหรับ Best Practices ที่ดีของการจัดการ API Versioning ที่แอดมินแนะนำว่าควรทำควบคู่กันไปมีดังนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;เปลี่ยนเวอร์ชัน เมื่อมี breaking changes เท่านั้น&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;แม้ว่าการเปลี่ยนเวอร์ชันใน Laravel จะสามารถทำได้โดยง่าย แต่หากเราต้องเปลี่ยนเวอร์ชันทุกครั้งที่มีการแก้ไขเล็ก ๆ น้อย ๆ ก็อาจทำให้โค้ดของเราบวมขึ้นมาได้
และจะกลายเป็นว่าเวอร์ชันที่เพิ่มขึ้นมานั้นทำให้ยากต่อการบำรุงรักษาเสียเอง&lt;/p&gt;
&lt;p&gt;ซึ่งในจุดนี้แอดมินแนะนำว่าให้เปลี่ยนเวอร์ชันเมื่อโครงสร้างของ API เปลี่ยน เช่น request parameters หรือ JSON response เท่านั้นก็เพียงพอ
(หรือการ breaking changes อื่น ๆ ตามที่ทีมตกลงร่วมกัน)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;เขียนเอกสารกำกับให้ชัดเจน&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;กรณีที่ API มีหลายเส้นและหลายเวอร์ชัน อาจทำให้ client หรือผู้ใช้งานเกิดความสับสนถึงโครงสร้างของ request และ response ที่อาจเปลี่ยนแปลงไปได้
เราจึงควรมีเอกสารกลางไว้กำกับคุณสมบัติของ API แต่ละเวอร์ชันเพื่อให้การสื่อสารระหว่างผู้พัฒนา API และผู้ใช้งานมีความชัดเจนมากยิ่งขึ้น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;วางแผน Deprecation&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;บางครั้ง API เวอร์ชันเก่าที่โครงสร้างไม่รองรับแล้วและจำเป็นต้องหยุดให้บริการลงด้วยเหตุผลต่าง ๆ เช่น ความปลอดภัยหรือ Performance ของ software
ในกรณีนี้ผู้พัฒนาควรวางแผน Deprecation และแจ้งผู้ใช้งานก่อนที่จะหยุดให้บริการ เพื่อให้ผู้ใช้งานสามารถปรับปรุง website หรือ application
ให้รองรับกับ API เวอร์ชันปัจจุบันได้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;วางแผน Test API แต่ละเวอร์ชันแยกกัน&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;แม้การทำงานของ API แต่ละเวอร์ชันจะคล้ายกัน แต่เมื่อมีการพัฒนาก็ย่อมมีความแตกต่างกันอย่างแน่นอน (ยิ่งรวมกับว่าเราแยก API เมื่อ breaking changes)
ทำให้การทดสอบ API แต่ละเวอร์ชันควรทำแยกกัน เพื่อให้มั่นใจได้ว่า API แต่ละเวอร์ชันยังคงสามารถทำงานได้อย่างถูกต้องแม้จะมีการเพิ่มเติมหรือแก้ไขใด ๆ&lt;/p&gt;
&lt;h2&gt;👩‍💻 บทสรุป&lt;/h2&gt;
&lt;p&gt;แม้ว่าเรื่อง API Versioning จะดูเป็นเรื่องเล็กน้อยแต่ก็เป็นสิ่งสำคัญที่จะช่วยให้ระบบสามารถพัฒนาและเปลี่ยนแปลงได้โดยไม่กระทบผู้ใช้เดิม และเมื่อวางแผนอย่างเป็นระบบตั้งแต่ต้น ก็จะช่วยลดปัญหาในอนาคต และสร้างความมั่นใจให้ทั้งทีมพัฒนาและผู้ใช้งาน API อีกด้วย&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.qV9c8MYF.jpg"/><enclosure url="/_astro/cover.qV9c8MYF.jpg"/></item><item><title>สร้าง Component ครั้งเดียว ใช้ได้ทุกที่ – ลดความซ้ำซ้อน เพิ่มประสิทธิภาพงานพัฒนา</title><link>https://blog.devfavor.com/blog/create-component-once-use-anywhere</link><guid isPermaLink="true">https://blog.devfavor.com/blog/create-component-once-use-anywhere</guid><description>เรียนรู้แนวทางออกแบบ Reusable Component ที่ทำให้โค้ดสะอาด ลดการซ้ำซ้อน และปรับตัวได้กับทุกโปรเจกต์</description><pubDate>Thu, 21 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;นักพัฒนาหลายคนอาจคุ้นเคยกับการเขียนโค้ด UI อยู่แล้ว เช่น ปุ่มล็อกอิน ปุ่มบันทึก หรือกล่องแสดงข้อความ เป็นต้น
UI เหล่านี้มักจะมีดีไซน์ที่คล้ายกัน แต่ต่างกันเล็กน้อย เช่น สีหรือขนาด ซึ่งเราก็สามารถแก้ไขค่าเหล่านั้นได้ตรง ๆ อย่างไม่ยากเย็น
แต่เมื่อโปรเจกต์เราใหญ่ขึ้น ความซ้ำซ้อนเหล่านี้จะกลายเป็นภาระหนักในการบำรุงรักษา และเป็นแหล่งที่มาของ bug ได้ง่าย&lt;/p&gt;
&lt;p&gt;เพื่อแก้ปัญหาเหล่านี้ แนวคิด Reusable Component จึงมีความสำคัญมากในงานพัฒนา ไม่ว่าจะเป็นการเขียนด้วย React, Vue, Angular
หรือแม้แต่ Laravel ที่มี Blade Components ก็ตาม การสร้างเพียงครั้งเดียวแต่สามารถนำไปใช้ซ้ำในหลายที่ ไม่เพียงช่วยลดภาระ แต่ยังทำให้ทีมทำงานอย่างมีระบบและคุณภาพมากยิ่งขึ้น&lt;/p&gt;
&lt;p&gt;ในบทความนี้ เราจะมาดูกันว่า Reusable Component คืออะไร หลักการออกแบบที่ดีควรเป็นแบบไหน รวมถึงวิธีการสร้างจริง
พร้อมทั้งตัวอย่างโค้ด และแนวทางปฏิบัติที่ช่วยให้คุณสร้างโค้ดที่สะอาดและมีประสิทธิภาพต่อไป&lt;/p&gt;
&lt;h2&gt;💡 Reusable Component คืออะไร&lt;/h2&gt;
&lt;p&gt;Reusable Component คือ Component สำหรับแสดงผลต่อผู้ใช้งาน (UI Component) ที่สามารถนำไปใช้ซ้ำได้ในหลาย ๆ ที่ โดยไม่ต้องเขียนโค้ดใหม่ทุกครั้ง
เช่น ปุ่ม (Button), ฟอร์มกรอกข้อมูล (Form Field), การ์ด (Card), หรือ Modal เป็นต้น&lt;/p&gt;
&lt;h2&gt;🛠️ หลักการออกแบบ&lt;/h2&gt;
&lt;p&gt;ในการออกแบบ Reusable Component ที่ดี คือการออกแบบ Component โดยคำนึงถึงหลักการต่อไปนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Single Responsibility Principle (SRP)&lt;/em&gt;&lt;/strong&gt; - แต่ละ Component ควรทำหน้าที่เฉพาะ เช่น ปุ่ม (Button)
ควรทำหน้าที่สำหรับแสดงผลและรับ event กด เท่านั้น ไม่ควรผูกกับ logic อื่นที่ไม่เกี่ยวข้อง&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Configurable&lt;/em&gt;&lt;/strong&gt; - Component ควรออกแบบให้สามารถปรับแต่งได้ เช่น ผ่าน Props, Slot, Parameter หรือ Attribute โดยไม่ต้องเขียนใหม่&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Composable&lt;/em&gt;&lt;/strong&gt; - Component ควรจะสามารถนำมาประกอบกับ Component อื่นได้โดยง่าย เช่น ฟอร์มหนึ่งชุดอาจประกอบด้วย Component Input, Label และ Button&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Consistency&lt;/em&gt;&lt;/strong&gt; - Component ควรสร้างความสม่ำเสมอให้กับ UI ทั้งระบบ เพื่อประสบการณ์ของผู้ใช้ที่ดี&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;⚠️ ปัญหาที่เกิดขึ้นเมื่อไม่ใช้ Reusable Component&lt;/h2&gt;
&lt;p&gt;จากข้อดีต่าง ๆ ที่ได้เกริ่นไปแล้ว หากระบบที่พัฒนาอยู่ไม่ได้คำนึงถึงหลักการ Reusable Component แล้วอาจเกิดปัญหาขึ้นได้ดังนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;โค้ดซ้ำซ้อน (Duplication Code)&lt;/em&gt;&lt;/strong&gt; - มีโค้ดชุดเดียวกันซ้ำในหลายไฟล์ ทำให้โปรเจกต์ใหญ่ขึ้นโดยไม่จำเป็น&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;บำรุงรักษายาก (Maintenance Overhead)&lt;/em&gt;&lt;/strong&gt; - หากต้องเปลี่ยนสีหรือรูปแบบปุ่มทั้งโปรเจกต์ ต้องแก้ทุกไฟล์ที่เคยใช้ ทำให้มีโอกาสตกหล่นหรือผิดพลาด&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;เกิดความไม่สอดคล้องกันของ UI (Inconsistency UI)&lt;/em&gt;&lt;/strong&gt; - UI บางจุดอาจเกิดความผิดพลาดให้มีขนาดหรือสีที่ต่างกัน ทำให้เกิดความสับสนต่อผู้ใช้งาน&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;em&gt;ข้อเสียต่อการพัฒนาระบบ&lt;/em&gt;&lt;/strong&gt; - การไม่ออกแบบ Reusable Component อาจทำให้การพัฒนาใช้เวลามากกว่าที่ควรจะเป็น หรืออาจเกิดความผิดพลาดได้ง่ายกว่า&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;🚀 ตัวอย่างการออกแบบ Reusable Component&lt;/h2&gt;
&lt;p&gt;สมมุติว่าเราต้องการสร้าง UI ของปุ่มขึ้นมา 1 ปุ่มเพื่อใช้งาน โดยใช้ CSS framework เป็น tailwindcss เราอาจคำนึงถึงโอกาสที่ปุ่มนี้จะถูกเรียกใช้ในจุดอื่นและวางโครงสร้างไว้ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;type ButtonProps = {
  variant?: &quot;primary&quot; | &quot;secondary&quot;;
  size?: &quot;sm&quot; | &quot;md&quot; | &quot;lg&quot;;
  children: React.ReactNode;
};

export const Button = ({
  variant = &quot;primary&quot;,
  size = &quot;md&quot;,
  children,
}: ButtonProps) =&gt; {
  const base = &quot;rounded px-4 py-2 font-semibold&quot;;
  const variants = {
    primary: &quot;bg-blue-500 text-white&quot;,
    secondary: &quot;bg-gray-200 text-black&quot;,
  };
  const sizes = {
    sm: &quot;text-sm py-1&quot;,
    md: &quot;text-base py-2&quot;,
    lg: &quot;text-lg py-3&quot;,
  };
  return (
    &amp;#x3C;button className={`${base} ${variants[variant]} ${sizes[size]}`}&gt;
      {children}
    &amp;#x3C;/button&gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากตัวอย่างจะเห็นว่าปุ่มที่เราสร้างขึ้น สามารถกำหนดสีหรือขนาดผ่าน Props ได้เลยโดยไม่ต้องแก้ไขโค้ดอีก หรือหากต้องการปรับปรุงเพิ่มเติมในอนาคตก็สามารถทำได้โดยง่าย&lt;/p&gt;
&lt;h2&gt;🎯 เพิ่มประสิทธิภาพการใช้งานด้วย CVA (Class Variance Authority)&lt;/h2&gt;
&lt;p&gt;CVA (Class Variance Authority) คือ utility function สำหรับจัดการ className ของ Tailwind ที่มี variants (เช่น intent, size, state) ได้อย่างเป็นระบบและปลอดภัยกว่าเขียน string ตรงๆ
หากต้องการใช้งาน CVA ก็สามารถทำได้โดยง่ายดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &quot;react&quot;;
import { cva, type VariantProps } from &quot;class-variance-authority&quot;;

const button = cva(&quot;rounded px-4 py-2 font-semibold&quot;, {
  variants: {
    intent: {
      primary: &quot;bg-blue-500 text-white&quot;,
      secondary: &quot;bg-gray-200 text-black&quot;,
    },
    size: {
      sm: &quot;text-sm&quot;,
      md: &quot;text-base&quot;,
      lg: &quot;text-lg&quot;,
    },
  },
  defaultVariants: {
    intent: &quot;primary&quot;,
    size: &quot;md&quot;,
  },
});

export interface ButtonProps
  extends Omit&amp;#x3C;React.ButtonHTMLAttributes&amp;#x3C;HTMLButtonElement&gt;, &quot;disabled&quot;&gt;,
    VariantProps&amp;#x3C;typeof button&gt; {}

export const Button: React.FC&amp;#x3C;ButtonProps&gt; = ({
  className,
  intent,
  size,
  ...props
}) =&gt; &amp;#x3C;button className={button({ intent, size, className })} {...props} /&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;เมื่อต้องการใช้งาน เราก็เพียงแค่เรียกผ่าน Props ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;#x3C;Button intent=&quot;primary&quot;&gt;Save&amp;#x3C;/Button&gt;
&amp;#x3C;Button intent=&quot;secondary&quot; size=&quot;sm&quot;&gt;Cancel&amp;#x3C;/Button&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;☕ สรุปส่งท้าย&lt;/h2&gt;
&lt;p&gt;จากตัวอย่างจะเห็นว่า Reusable Component ไม่เพียงแค่ประหยัดเวลาในการเขียนโค้ด แต่คือแนวคิดที่ทำให้ทีมทำงานอย่างเป็นระบบ
มีความสอดคล้อง และบำรุงรักษาง่ายขึ้น โดยยึดหลักการสำคัญในการออกแบบที่ดี
ซึ่งผลลัพธ์ปลายทางจะเห็นว่าโค้ดของโปรเจกต์สะอาดขึ้น และทีมจะสามารถทำงานได้อย่างมีประสิทธิภาพมากขึ้นอย่างแน่นอน 🥰&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.D2MhaR58.jpg"/><enclosure url="/_astro/cover.D2MhaR58.jpg"/></item><item><title>การใช้งาน Eloquent Relationships - เพื่อดึงข้อมูลใน Laravel อย่างมีประสิทธิภาพ</title><link>https://blog.devfavor.com/blog/laravel-eloquent-relationships</link><guid isPermaLink="true">https://blog.devfavor.com/blog/laravel-eloquent-relationships</guid><description>บทความนี้จะพาคุณไปทำความรู้จักกับ Eloquent Relationships ประเภทต่าง ๆ พร้อมตัวอย่างโค้ดที่สามารถนำไปปรับใช้งานต่อได้จริง</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;🤔 ทำไม Relationships ถึงสำคัญ&lt;/h2&gt;
&lt;p&gt;ในการเขียนโปรแกรมด้วย Laravel เราสามารถดึงข้อมูลจากฐานข้อมูลได้หลากหลายวิธีมาก ไม่ว่าจะเป็นการเรียกผ่าน Facade DB ตรง ๆ หรือการใช้ Model ช่วย Query ก็ตาม
แต่อีก 1 วิธีที่เราสามารถใช้เรียกข้อมูลได้เหมือนกันก็คือการดึงผ่าน Eloquent Relationships นั่นเอง&lt;/p&gt;
&lt;p&gt;การดึงข้อมูลด้วย Eloquent Relationships นั้นจะเป็นการดึงข้อมูลที่มีความสัมพันธ์ซึ่งกันและกัน โดยมีข้อดีคือ เราไม่ต้องคอยเขียนโค้ดเพื่อบริหารจัดการความสัมพันธ์ทุกครั้งที่เรียกใช้
ขอเพียงเราสร้างและประกาศความสัมพันธ์ไว้ก่อน เวลาเรียกใช้ก็เพียงแค่ดึงผ่านตัว Model เท่านั้น ทำให้การเขียนโค้ดของเรามีประสิทธิภาพมากยิ่งขึ้น
นอกจากนี้ การดึงข้อมูลในรูปแบบของ Relationships ยังมีส่วนช่วยในการมองภาพของระบบและทำความเข้าใจกับ Business Logic ของ Model นั้น ๆ ได้ดียิ่งขึ้นอีกด้วย&lt;/p&gt;
&lt;h2&gt;💻 พื้นฐาน Eloquent Relationships&lt;/h2&gt;
&lt;p&gt;การจะดีไซน์ Eloquent Relationships นอกจากจะต้องรู้พื้นฐานของภาษา PHP และ Laravel แล้ว ยังต้องเข้าใจความสัมพันธ์ในเชิงของฐานข้อมูลควบคู่กันไปด้วย
เพื่อให้การดีไซน์ Relationships เกิดประสิทธิภาพสูงสุดและมีความถูกต้องใชเชิง Logic โดยในบทความนี้จะขอปูพื้นฐานคำศัพท์บางคำที่อาจต้องใช้ในการทำความเข้าใจไว้ก่อน
เพื่อเสริมความเข้าใจให้มากยิ่งขึ้น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Pivot Table&lt;/em&gt; - คือตารางกลาง ที่ใช้สำหรับเชื่อมความสัมพันธ์แบบ Many-to-Many โดยจะเก็บ foreign key ของทั้ง 2 Model ไว้ใช้สำหรับดึงข้อมูล&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Eager Loading&lt;/em&gt; - คือการดึงข้อมูลจากความสัมพันธ์พร้อมกันล่วงหน้า เพื่อป้องกันการ Query ที่เยอะเกินจำเป็น นอกจากนี้ยังช่วยป้องกันปัญหา N+1 อีกด้วย (ไว้ในโอกาศหน้าจะมาเล่าเกี่ยวกับปัญหา N+1 อีกที)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Polymorphic&lt;/em&gt; - หรือ Polymorphism หมายถึง การมีหลายรูปแบบ ซึ่งในกรณีของ Eloquent Relationships นี้คำว่า Polymorphic มีความหมายเฉพาะตัวคือ
การที่ Model หนึ่งสามารถมีความสัมพันธ์แบบ One-to-Many กับหลาย Model ได้ (หลายรูปแบบ) ผ่าน field เดียวกัน โดยไม่ต้องเพิ่ม foreign key&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ในความเป็นจริงแล้วคำศัพท์เหล่านี้มีรายละเอียดเฉพาะตัวอีกมาก แต่ในที่นี้ขอให้เข้าใจพื้นฐานโดยคร่าวก่อน ก็เพียงพอที่จะดีไซน์ความสัมพันธ์ได้อย่างมีประสิทธิภาพแล้ว&lt;/p&gt;
&lt;h2&gt;📝 ประเภทของ Eloquent Relationships ใน Laravel&lt;/h2&gt;
&lt;p&gt;ใน Laravel เราจะพบกับความสัมพันธ์ และ Eloquent Relationships ของ Model ดังต่อไปนี้&lt;/p&gt;
&lt;h3&gt;One-to-One (hasOne / belongsTo)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับ Model เดียวในตาราง B&lt;/p&gt;
&lt;h3&gt;One-to-Many (hasMany / belongsTo)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับหลาย Model (หนึ่ง Model ขึ้นไป) ในตาราง B&lt;/p&gt;
&lt;h3&gt;Many-to-Many (belongsToMany)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง B ได้มากกว่า 1 Model โดยผ่านตารางกลางที่เป็น Pivot Table&lt;/p&gt;
&lt;h3&gt;Has One Through (hasOneThrough)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง C ได้ 1 Model ผ่านตัวกลางที่เป็นตาราง B&lt;/p&gt;
&lt;h3&gt;Has Many Through (hasManyThrough)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง C ได้มากกว่า 1 Model ผ่านตัวกลางที่เป็นตาราง B&lt;/p&gt;
&lt;h3&gt;Polymorphic One-to-One (morphOne)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับ Model เดียวในตาราง B แบบ Polymorphic&lt;/p&gt;
&lt;h3&gt;Polymorphic One-to-Many (morphMany)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับ Model ในตาราง B มากกว่า 1 Model แบบ Polymorphic&lt;/p&gt;
&lt;h3&gt;Polymorphic Many-to-Many (morphToMany)&lt;/h3&gt;
&lt;p&gt;เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง B ได้มากกว่า 1 Model แบบ Polymorphic&lt;/p&gt;
&lt;p&gt;จะสังเกตว่าจริง ๆ แล้วรูปแบบความสัมพันธ์ของ Eloquent Relationships จะเหมือนกับความสัมพันธ์ของฐานข้อมูลเลย
เพียงแค่ตัว Laravel นั้นจะเพิ่มฟังก์ชันการใช้งานให้เรา เพื่อให้เราเรียกใช้งานได้สะดวกขึ้นเท่านั้นเอง&lt;/p&gt;
&lt;h2&gt;👩‍💻 ตัวอย่างฐานข้อมูล เตรียม Migration และ Model ให้พร้อม&lt;/h2&gt;
&lt;p&gt;หลังจากที่อ่านและทำความเข้าใจกันไปคร่าว ๆ แล้วเชื่อว่าผู้อ่านคงจะเห็นภาพของประโยชน์ของ Eloquent Relationships มากยิ่งขึ้น แต่เพียงเท่านั้นยังไม่พอ&lt;/p&gt;
&lt;p&gt;การจะนำไปใช้ในโปรเจกต์จริงได้ต้องลองดูตัวอย่างโค้ดจริง ๆ ควบคู่กันไปด้วย ซึ่งบทความนี้ก็เตรียมตัวอย่างไว้ให้อย่างครบถ้วน ไปลองดูกันเลย&lt;/p&gt;
&lt;p&gt;ก่อนอื่นให้เตรียมฐานข้อมูลสำหรับใช้ในการ Query ให้พร้อมโดยการพิมพ์คำสั่งดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan make:migration create_countries_table
php artisan make:migration create_users_table
php artisan make:migration create_profiles_table
php artisan make:migration create_roles_table
php artisan make:migration create_role_user_table
php artisan make:migration create_posts_table
php artisan make:migration create_comments_table
php artisan make:migration create_videos_table
php artisan make:migration create_photos_table
php artisan make:migration create_tags_table
php artisan make:migration create_taggables_table
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;หลังจากสร้างไฟล์ Migration แล้วให้จัดการ fields ของแต่ละตารางดังนี้&lt;/p&gt;
&lt;p&gt;&lt;code&gt;create_countries_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;countries&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;string(&apos;name&apos;)-&gt;unique();
        $table-&gt;string(&apos;iso2&apos;, 2)-&gt;unique();
        $table-&gt;timestamps();
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_users_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;users&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;foreignId(&apos;country_id&apos;)-&gt;nullable()-&gt;constrained()-&gt;nullOnDelete();
        $table-&gt;string(&apos;name&apos;);
        $table-&gt;string(&apos;email&apos;)-&gt;unique();
        $table-&gt;timestamp(&apos;email_verified_at&apos;)-&gt;nullable();
        $table-&gt;string(&apos;password&apos;);
        $table-&gt;rememberToken();
        $table-&gt;timestamps();

        $table-&gt;index(&apos;country_id&apos;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_profiles_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;profiles&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;foreignId(&apos;user_id&apos;)-&gt;unique()-&gt;constrained()-&gt;cascadeOnDelete();
        $table-&gt;text(&apos;bio&apos;)-&gt;nullable();
        $table-&gt;string(&apos;phone&apos;)-&gt;nullable();
        $table-&gt;timestamps();
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_roles_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;roles&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;string(&apos;name&apos;)-&gt;unique();
        $table-&gt;string(&apos;label&apos;)-&gt;nullable();
        $table-&gt;timestamps();
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_role_user_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;role_user&apos;, function (Blueprint $table) {
        $table-&gt;foreignId(&apos;role_id&apos;)-&gt;constrained()-&gt;cascadeOnDelete();
        $table-&gt;foreignId(&apos;user_id&apos;)-&gt;constrained()-&gt;cascadeOnDelete();
        $table-&gt;foreignId(&apos;assigned_by&apos;)-&gt;nullable()-&gt;constrained(&apos;users&apos;)-&gt;nullOnDelete();
        $table-&gt;timestamp(&apos;expires_at&apos;)-&gt;nullable();
        $table-&gt;timestamps();

        $table-&gt;primary([&apos;role_id&apos;, &apos;user_id&apos;]);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_posts_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;posts&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;foreignId(&apos;user_id&apos;)-&gt;constrained()-&gt;cascadeOnDelete();
        $table-&gt;string(&apos;title&apos;);
        $table-&gt;text(&apos;body&apos;)-&gt;nullable();
        $table-&gt;enum(&apos;status&apos;, [&apos;draft&apos;,&apos;published&apos;])-&gt;default(&apos;draft&apos;);
        $table-&gt;timestamps();

        $table-&gt;index(&apos;user_id&apos;);
        $table-&gt;index(&apos;status&apos;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_comments_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;comments&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;foreignId(&apos;post_id&apos;)-&gt;constrained()-&gt;cascadeOnDelete();
        $table-&gt;foreignId(&apos;user_id&apos;)-&gt;nullable()-&gt;constrained()-&gt;nullOnDelete();
        $table-&gt;text(&apos;content&apos;);
        $table-&gt;timestamps();

        $table-&gt;index([&apos;post_id&apos;, &apos;user_id&apos;]);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_videos_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;videos&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;foreignId(&apos;user_id&apos;)-&gt;nullable()-&gt;constrained()-&gt;nullOnDelete();
        $table-&gt;string(&apos;title&apos;);
        $table-&gt;string(&apos;url&apos;);
        $table-&gt;timestamps();

        $table-&gt;index(&apos;user_id&apos;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_photos_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;photos&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;morphs(&apos;imageable&apos;); // imageable_type, imageable_id (indexed)
        $table-&gt;string(&apos;path&apos;);
        $table-&gt;string(&apos;alt&apos;)-&gt;nullable();
        $table-&gt;timestamps();
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_tags_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;tags&apos;, function (Blueprint $table) {
        $table-&gt;id();
        $table-&gt;string(&apos;name&apos;)-&gt;unique();
        $table-&gt;timestamps();
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;create_taggables_table&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function up(): void {
    Schema::create(&apos;taggables&apos;, function (Blueprint $table) {
        $table-&gt;foreignId(&apos;tag_id&apos;)-&gt;constrained()-&gt;cascadeOnDelete();
        $table-&gt;morphs(&apos;taggable&apos;); // taggable_type, taggable_id
        $table-&gt;foreignId(&apos;added_by&apos;)-&gt;nullable()-&gt;constrained(&apos;users&apos;)-&gt;nullOnDelete();
        $table-&gt;timestamps();

        $table-&gt;index([&apos;tag_id&apos;, &apos;taggable_type&apos;, &apos;taggable_id&apos;], &apos;taggables_full_index&apos;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากนั้นสั่ง&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;เป็นอันเรียบร้อยสำหรับการเตรียมฐานข้อมูล จากนั้นให้เตรียม Model ต่อ โดยการพิมพ์คำสั่งดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan make:model Country
php artisan make:model User
php artisan make:model Profile
php artisan make:model Role
php artisan make:model Post
php artisan make:model Comment
php artisan make:model Video
php artisan make:model Photo
php artisan make:model Tag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากนั้นแก้ไขไฟล์ของ Model แต่ละไฟล์ดังนี้&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Country.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Country extends Model
{
    protected $fillable = [&apos;name&apos;, &apos;iso2&apos;];

    public function users() {
        return $this-&gt;hasMany(User::class);
    }

    public function posts() {
        return $this-&gt;hasManyThrough(Post::class, User::class);
    }

    public function latestPost() {
        return $this-&gt;hasOneThrough(Post::class, User::class)-&gt;latestOfMany();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;User.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class User extends Authenticatable
{
    use HasFactory, Notifiable;

    public function country() { return $this-&gt;belongsTo(Country::class); }

    public function profile() { return $this-&gt;hasOne(Profile::class); }

    public function posts() { return $this-&gt;hasMany(Post::class); }

    public function videos() { return $this-&gt;hasMany(Video::class); }

    public function roles() { return $this-&gt;belongsToMany(Role::class)-&gt;withTimestamps()-&gt;withPivot([&apos;assigned_by&apos;, &apos;expires_at&apos;]); }

    public function photo() { return $this-&gt;morphOne(Photo::class, &apos;imageable&apos;); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Profile.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Profile extends Model
{
    protected $fillable = [&apos;user_id&apos;, &apos;bio&apos;, &apos;phone&apos;];

    public function user() {
        return $this-&gt;belongsTo(User::class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Role.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Role extends Model
{
    protected $fillable = [&apos;name&apos;,&apos;label&apos;];

    public function users() {
        return $this-&gt;belongsToMany(User::class)-&gt;withTimestamps();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Post.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Post extends Model
{
    protected $fillable = [&apos;user_id&apos;,&apos;title&apos;,&apos;body&apos;,&apos;status&apos;];

    public function user() { return $this-&gt;belongsTo(User::class); }

    public function comments() { return $this-&gt;hasMany(Comment::class); }

    public function photos() { return $this-&gt;morphMany(Photo::class, &apos;imageable&apos;); }

    public function tags() { return $this-&gt;morphToMany(Tag::class, &apos;taggable&apos;)-&gt;withPivot([&apos;added_by&apos;]); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Comment.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Comment extends Model
{
    protected $fillable = [&apos;post_id&apos;,&apos;user_id&apos;,&apos;content&apos;];

    public function post() { return $this-&gt;belongsTo(Post::class); }
    public function user() { return $this-&gt;belongsTo(User::class); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Video.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Video extends Model
{
    protected $fillable = [&apos;user_id&apos;,&apos;title&apos;,&apos;url&apos;];

    public function user() { return $this-&gt;belongsTo(User::class); }

    public function tags() { return $this-&gt;morphToMany(Tag::class, &apos;taggable&apos;)-&gt;withPivot([&apos;added_by&apos;]); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Photo.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Photo extends Model
{
    protected $fillable = [&apos;path&apos;,&apos;alt&apos;];

    public function imageable() {
        return $this-&gt;morphTo();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Tag.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Tag extends Model
{
    protected $fillable = [&apos;name&apos;];

    public function posts() { return $this-&gt;morphedByMany(Post::class, &apos;taggable&apos;); }

    public function videos() { return $this-&gt;morphedByMany(Video::class, &apos;taggable&apos;); }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;หลังจากเพิ่ม Model เสร็จเรียบร้อย มาลองดูตัวอย่างการดึงข้อมูลแบบต่าง ๆ ได้เลย&lt;/p&gt;
&lt;h2&gt;🛠️ ตัวอย่างการดึงและอัปเดตข้อมูลแบบต่าง ๆ ผ่าน Eloquent Relationships&lt;/h2&gt;
&lt;p&gt;หลังจากที่เตรียมไฟล์ Model เสร็จเรียบร้อย ก็ได้เวลาลองใช้งาน Eloquent Relationships โดยจะแบ่งเป็นประเภทดังนี้&lt;/p&gt;
&lt;h3&gt;ตัวอย่าง One-to-One (hasOne / belongsTo)&lt;/h3&gt;
&lt;p&gt;ตัวอย่างการดึงข้อมูลแบบ One-to-One ผ่านฟังก์ชัน hasOne จะเป็นการเรียกข้อมูล Profile ของ User คนนั้น ๆ&lt;/p&gt;
&lt;p&gt;โดยที่หากเราต้องการแก้ไขข้อมูล Profile ของ User เราสามารถทำได้ง่าย ๆ ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Read
$profile = $user-&gt;profile;
$owner   = $profile-&gt;user;

// Create
$user-&gt;profile()-&gt;create([
  &apos;bio&apos; =&gt; &apos;I love Laravel&apos;,
  &apos;phone&apos; =&gt; &apos;0812345678&apos;,
]);

// Update
$user-&gt;profile-&gt;update([&apos;phone&apos; =&gt; &apos;0888888888&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ตัวอย่าง One-to-Many (hasMany / belongsTo)&lt;/h3&gt;
&lt;p&gt;สำหรับในตัวอย่างนี้ เราจะมาดูที่ความสัมพันธ์ของ User และ Post โดยที่ User 1 คน สามารถเขียนได้หลาย Post&lt;/p&gt;
&lt;p&gt;สามารถใช้ Eloquent Relationships เรียกข้อมูลได้ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Read
$posts = $user-&gt;posts;
$owner = $post-&gt;user;

// Create
$user-&gt;posts()-&gt;createMany([
  [&apos;title&apos; =&gt; &apos;A&apos;, &apos;status&apos; =&gt; &apos;draft&apos;],
  [&apos;title&apos; =&gt; &apos;B&apos;, &apos;status&apos; =&gt; &apos;published&apos;],
]);

// Update
$user-&gt;posts()-&gt;where(&apos;status&apos;, &apos;draft&apos;)-&gt;update([&apos;status&apos; =&gt; &apos;published&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ตัวอย่าง Many-to-Many (belongsToMany)&lt;/h3&gt;
&lt;p&gt;ตัวอย่าง Many-to-Many เราจะใช้ความสัมพันธ์ระหว่าง User และ Role โดยการดึงข้อมูลผ่านตาราง Pivot ชื่อ role_user โดยสามารถดึงข้อมูลได้โดยใช้ฟังก์ชันดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Read
$roles = $user-&gt;roles;

// Create
$user-&gt;roles()-&gt;attach($roleId, [
  &apos;assigned_by&apos; =&gt; auth()-&gt;id(),
  &apos;expires_at&apos;  =&gt; now()-&gt;addMonth(),
]);

// Update
$user-&gt;roles()-&gt;updateExistingPivot($roleId, [
  &apos;expires_at&apos; =&gt; now()-&gt;addMonths(3),
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ตัวอย่าง Has One/Many Through (hasOneThrough / hasManyThrough)&lt;/h3&gt;
&lt;p&gt;ในตัวอย่างนี้ เราจะลองดึงข้อมูลของ Post ภายใต้ Country ผ่านทาง Model ตัวกลางคือ User ดังตัวอย่างต่อไปนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Read
$posts = $country-&gt;posts;
$latest = $country-&gt;latestPost()-&gt;latestOfMany()-&gt;first();

// Create
$user = $country-&gt;users()-&gt;firstOrFail();
$user-&gt;posts()-&gt;create([&apos;title&apos; =&gt; &apos;From Country&apos;, &apos;status&apos; =&gt; &apos;published&apos;]);

// Update
$country-&gt;posts()-&gt;where(&apos;status&apos;, &apos;draft&apos;)-&gt;update([&apos;status&apos; =&gt; &apos;published&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ตัวอย่าง Polymorphic One-to-One / One-to-Many (morphOne / morphMany)&lt;/h3&gt;
&lt;p&gt;การดึงข้อมูลแบบ Polymorphic ในตัวอย่างนี้ จะใช้ Model Photo โดยที่ ทั้ง User และ Post สามารถมี Photo ได้ทั้งคู่&lt;/p&gt;
&lt;p&gt;แต่เราจะใช้คุณสมบัติของ Polymorphic ในการจัดการ Photo แยกออกจากกัน โดยที่ User จะมี 1 Photo และ Post มีมากกว่า 1 Photo&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Read
$photo = $user-&gt;photo;
$photos = $post-&gt;photos;

// Create
$user-&gt;photo()-&gt;create([
  &apos;path&apos; =&gt; &apos;avatars/u1.jpg&apos;,
  &apos;alt&apos;  =&gt; &apos;User avatar&apos;,
]);

$post-&gt;photos()-&gt;createMany([
  [&apos;path&apos; =&gt; &apos;posts/1/cover.jpg&apos;, &apos;alt&apos; =&gt; &apos;Cover&apos;],
  [&apos;path&apos; =&gt; &apos;posts/1/detail.jpg&apos;, &apos;alt&apos; =&gt; &apos;Detail&apos;],
]);

// Update
$photo-&gt;update([&apos;alt&apos; =&gt; &apos;Main Cover&apos;]);

$post-&gt;photos()-&gt;whereNull(&apos;alt&apos;)-&gt;update([&apos;alt&apos; =&gt; &apos;N/A&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ตัวอย่าง Polymorphic Many-to-Many (morphToMany / morphedByMany)&lt;/h3&gt;
&lt;p&gt;สำหรับตัวอย่างสุดท้ายจะเป็นการใช้ Polymorphic แบบ Many-to-Many กับตาราง Tag โดยที่ทั้ง Post และ Video สามารถมีได้หลาย Tag สามารถเขียนโค้ดได้ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Read
$postTags = $post-&gt;tags;
$videoTags = $video-&gt;tags;

$postsOfTag  = $tag-&gt;posts;
$videosOfTag = $tag-&gt;videos;

// Create
$post-&gt;tags()-&gt;attach($tagId);
$post-&gt;tags()-&gt;attach([1 =&gt; [&apos;added_by&apos; =&gt; auth()-&gt;id()], 3 =&gt; []]);

// Update
$post-&gt;tags()-&gt;updateExistingPivot($tagId, [&apos;added_by&apos; =&gt; auth()-&gt;id()]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;📚 สรุปและแนวทางต่อยอด&lt;/h2&gt;
&lt;p&gt;จากตัวอย่างจะเห็นว่า หากเราสามารถใช้ Eloquent Relationships ได้อย่างถูกต้อง จะทำให้โค้ดสั้นขึ้นและอ่านง่ายขึ้นมาก
ทำให้เราบริหารจัดการความสัมพันธ์ของ Model ได้ดียิ่งขึ้น แต่ทั้งนี้ในการใช้งานโปรเจกต์จริง อาจมีเงื่อนไขที่ซับซ้อนมากกว่านี้อีกมาก
ซึ่งใน Document ของ Laravel ก็ได้มีการอัปเดตอยู่อย่างสม่ำเสมอ ดังนั้นหากต้องการใช้งาน Eloquent Relationships
ให้ชำนาญจะต้องศึกษาเอกสารควบคู่ไปด้วย เพียงเท่านี้เราก็สามารถบริหารจัดการโค้ดของเราให้เป็นระเบียบและพร้อมเรียกใช้ข้อมูลได้แล้ว 🥰&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.BdcYM5dJ.jpg"/><enclosure url="/_astro/cover.BdcYM5dJ.jpg"/></item><item><title>การเขียน Action Pattern - เพื่อเพิ่มคุณภาพของโค้ดใน Laravel</title><link>https://blog.devfavor.com/blog/laravel-action-pattern</link><guid isPermaLink="true">https://blog.devfavor.com/blog/laravel-action-pattern</guid><description>การแยกไฟล์ Class ด้วย Action Pattern โดยให้ 1 Class มี 1 หน้าที่ด้วย Laravel เพื่อเพิ่มคุณภาพของโค้ด</description><pubDate>Tue, 22 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;❓ Action Pattern คืออะไร?&lt;/h2&gt;
&lt;p&gt;ถ้าพูดถึง Code Pattern หรือ Design Pattern ในการเขียน Laravel แล้ว หลาย ๆ คนคงจะคุ้นเคยกับ Service Pattern หรือพวก Facade มากกว่า
แต่เชื่อว่าหลายคนคงจะไม่คุ้นกับคำว่า Action Pattern สักเท่าไหร่ ซึ่งก็ไม่ใช่เรื่องแปลกอะไรเพราะใน document หลักของ Laravel แทบจะไม่พูดถึงคำนี้เลย
แต่จริง ๆ แล้ว Action Pattern นั้นวนเวียนอยู่รอบตัวเราอยู่แล้ว เพียงแต่เราอาจจะแค่ยังไม่เคยทำความรู้จักกับมันจริง ๆ เท่านั้นเอง&lt;/p&gt;
&lt;p&gt;Action Pattern คือ Design Pattern แบบหนึ่ง ที่มีแนวคิดให้ 1 Class มีหน้าที่เพียง 1 อย่างเท่านั้น (Single Responsibility)
เพื่อลดความซับซ้อนของโค้ดให้สามารถบริหารจัดการได้ง่ายขึ้น โดย Class ที่เขียนให้รองรับ Action Pattern นั้นมักจะมีเพียง 1 method เท่านั้น เช่น handle() หรือ execute() เป็นต้น&lt;/p&gt;
&lt;h2&gt;✨ ประโยชน์ของ Action Pattern&lt;/h2&gt;
&lt;p&gt;แม้ว่าโดย concept ของตัว Action Pattern นั้นจะบอกเพียงแค่ว่าให้ 1 Class มีหน้าที่เพียง 1 อย่างเท่านั้น แต่ในความเป็นจริงแล้ว เรามักจะใช้ประโยชน์จาก Action Pattern
นี้เพื่อแยก business logic ออกมาจาก Laravel Lifecycle ปกติเสียมากกว่า เพื่อให้โค้ดในส่วนที่เป็น business logic นั้นสามารถปรับปรุงแก้ไขและดูแลรักษาได้โดยง่ายนั่นเอง&lt;/p&gt;
&lt;p&gt;ซึ่งประโยชน์ของการแยก business logic ออกมานั้นสามารถสรุปออกมาได้เป็นหัวข้อคร่าว ๆ ดังนี้&lt;/p&gt;
&lt;h3&gt;&lt;em&gt;Single Responsibility&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;การเขียนโค้ดโดยยึดหลัก Single Responsibility Principle (SRP) ทำให้เราสามารถจัดระเบียบโค้ดได้ดีขึ้น ลดการผูกกันของโค้ดที่ไม่เกี่ยวข้องกัน
เพื่อให้ developer สามารถแก้ไขหรือเพิ่มเติมได้ง่าย โดยที่เราจะสามารถมั่นใจได้ว่าสิ่งที่แก้ไขไปจะไม่ไปกระทบกับ business ส่วนอื่นที่ไม่เกี่ยวข้องกัน&lt;/p&gt;
&lt;h3&gt;&lt;em&gt;Reusable&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;ตัว Class ที่ถูกเขียนแบบ Action Pattern สามารถนำไปใช้ได้อย่างหลากหลาย ไม่ว่าจะเป็นใน Controller เองหรือจะใช้ใน Command, Queue หรือ Event ก็สามารถนำไปต่อยอดได้อย่างง่ายดาย&lt;/p&gt;
&lt;h3&gt;&lt;em&gt;Test Friendly&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;การแยก business logic ออกมาจาก Laravel Lifecycle ปกติทำให้เราสามารถเขียน test ได้แม่นยำขึ้น โดยสามารถเขียน test แยกการทำงานที่อาจเกิดขึ้นใน request เดียว
ออกเป็นการ test งานย่อยแต่ละอย่างแทน ทำให้สามารถวิเคราะห์ปัญหาและแก้ไขได้อย่างตรงจุดและรวดเร็ว&lt;/p&gt;
&lt;h3&gt;&lt;em&gt;Dev Friendly&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;การเขียนโดยใช้ Action Pattern นอกจากจะดีในแง่ของระบบแล้วยังดีต่อตัวของ developer เองอีกด้วย เพราะไม่ว่าจะเป็น การ refactor code, การทำ code review
หรือแม้แต่การ merge code ภายในทีม ตัว Action Pattern เองก็มีส่วนช่วยให้กิจกรรมเหล่านี้สามารถทำได้ง่าย ทำให้การบริหารจัดการกิจกรรมภายในทีมสามารถทำได้ง่ายยิ่งขึ้น&lt;/p&gt;
&lt;h2&gt;🚀 ลงมือทำจริง ลดความอ้วนให้กับ Controller แบบดั้งเดิม&lt;/h2&gt;
&lt;p&gt;หลังจากที่อ่านแนวคิดและประโยชน์มาเรียบร้อยแล้ว ตอนนี้คงได้เวลาที่จะต้องลงมือทำจริงกันสักที อย่ารอช้ามาลองดูตัวอย่างการใช้งานกันเลย&lt;/p&gt;
&lt;p&gt;สมมุติว่าเรามีไฟล์ Controller ดังนี้&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app\Http\Controllers\CheckoutController.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmedMail;

class CheckoutController extends Controller
{
    public function store(Request $request)
    {
        /* 1. Validate */
        $validated = $request-&gt;validate([
            &apos;customer_id&apos; =&gt; [&apos;required&apos;, &apos;exists:customers,id&apos;],
            &apos;items&apos;       =&gt; [&apos;required&apos;, &apos;array&apos;, &apos;min:1&apos;],
            &apos;items.*.id&apos;  =&gt; [&apos;required&apos;, &apos;exists:products,id&apos;],
            &apos;items.*.qty&apos; =&gt; [&apos;required&apos;, &apos;integer&apos;, &apos;min:1&apos;],
        ]);

        /* 2. Business logic + DB transaction */
        /*
         *
         * Multiple lines of Business logic
         *
         */
        $order = Order::insert($validated);

        /* 3. Side-effect : broadcast */
        /*
         *
         * Multiple lines of Side-effect
         *
         */
        Mail::to($order-&gt;customer-&gt;email)-&gt;send(new OrderConfirmedMail($order));

        /** 4. Response */
        return response()-&gt;json($order, 201);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จะเห็นว่า method store ของ CheckoutController มีจำนวนบรรทัดที่เยอะมากเพราะในการ Checkout และสร้าง Order มักจะมี business logic ที่ซับซ้อน&lt;/p&gt;
&lt;p&gt;หากเราลองใช้แนวคิดของ Action Pattern เพื่อแยก business logic ออกมาจะทำให้ Controller ของเรามีหน้าที่แค่ รับ Request (และ validate data)
และ Response กลับเพียงเท่านั้น ซึ่งเราสามารถแยก business logic ออกมาได้โดยการเพิ่มไฟล์ Action Class ดังนี้&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app\Actions\CreateOrder.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php

namespace App\Actions;

use App\Models\Order;

class CreateOrder
{
    public function handle($validated)
    {
        /* 2. Business logic + DB transaction */
        /*
         *
         * Multiple lines of Business logic
         *
         */
        $order = Order::insert($validated);

        return $order;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;app\Actions\SendOrderConfirmedMail.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php

namespace App\Actions;

use Illuminate\Support\Facades\Mail;
use App\Mail\OrderConfirmedMail;

class SendOrderConfirmedMail
{
    public function handle($order)
    {
        /* 3. Side-effect : broadcast */
        /*
         *
         * Multiple lines of Side-effect
         *
         */
        Mail::to($order-&gt;customer-&gt;email)-&gt;send(new OrderConfirmedMail($order));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;และใน Controller จะเหลือเพียงแค่&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app\Http\Controllers\CheckoutController.php&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;#x3C;?php

namespace App\Http\Controllers;

use App\Actions\CreateOrder;
use Illuminate\Http\Request;

class CheckoutController extends Controller
{
    public function store(Request $request)
    {
        /* 1. Validate */
        $validated = $request-&gt;validate([
            &apos;customer_id&apos; =&gt; [&apos;required&apos;, &apos;exists:customers,id&apos;],
            &apos;items&apos;       =&gt; [&apos;required&apos;, &apos;array&apos;, &apos;min:1&apos;],
            &apos;items.*.id&apos;  =&gt; [&apos;required&apos;, &apos;exists:products,id&apos;],
            &apos;items.*.qty&apos; =&gt; [&apos;required&apos;, &apos;integer&apos;, &apos;min:1&apos;],
        ]);

        /* 2. Business logic + DB transaction */
        $order = app(CreateOrder::class)-&gt;handle($request-&gt;validated());

        /* 3. Side-effect : broadcast */
        app(CreateOrder::class)-&gt;handle($order);

        /** 4. Response */
        return response()-&gt;json($order, 201);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;สังเกตว่าเราได้ย้ายในส่วนของ business logic ทั้งหมด (การสร้าง Order และการส่งอีเมล) ไปยัง Action แทน
และให้ตัว Controller ทำหน้าที่เพียงแค่รับและส่ง Request และ Response เท่านั้น
ทำให้การแก้ไข business logic ของการสร้าง Order และส่งอีเมลสามารถนำไป reuse ต่อได้ง่ายและยังสามารถปรับปรุงโค้ดต่อได้ง่ายอีกด้วย 🌟&lt;/p&gt;
&lt;h2&gt;🤔 Action Pattern VS Service Pattern เลือกใช้อย่างไร&lt;/h2&gt;
&lt;p&gt;ใน document ของ Laravel เองจะมีการพูดถึง Service Pattern อยู่แล้วและในการบริหารจัดการโค้ดก็สามารถใช้ Service Pattern ได้อยู่แล้วเช่นกัน
แล้วทีนี้เราจะเลือกอย่างไรว่าเมื่อไหร่ควรใช้ Service Pattern และเมื่อไหร่ควรใช้ Action Pattern มาลองพิจารณาจากประเด็นเหล่านี้กัน&lt;/p&gt;
&lt;h3&gt;แนวคิดในการ Design&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Action Pattern&lt;/em&gt; - จะมอง Class ในแง่ของ Behavioral หรือ พฤติกรรม มากกว่า ทำให้ชื่อ Class มักเป็นกิริยา เช่น SendMailAction หรือ CreateOrderAction เป็นต้น&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Service Pattern&lt;/em&gt; - จะมอง Class ในแง่ของ Structural หรือ โครงสร้าง มากกว่า ทำให้ชื่อ Class มักเป็นคำนามแต่แยกการกระทำด้วย method ข้างใน เช่น Order::create() เป็นต้น&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;ขนาดของทีม&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Action Pattern&lt;/em&gt; - จะมีจำนวนไฟล์เยอะขึ้น แยกการทำงานออกเป็นไฟล์ที่ชัดเจน ลดการ conflict ของ developer ในการแก้ไข&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Service Pattern&lt;/em&gt; - จะมีจำนวนไฟล์น้อยกว่า แต่ใน 1 ไฟล์จะมีความสัมพันธ์กันของ method&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;คุณสมบัติของ OOP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Action Pattern&lt;/em&gt; - จะมี method เดียวในการเขียนฟังก์ชันการทำงาน&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Service Pattern&lt;/em&gt; - จะสามารถดึงคุณสมบัติของการเขียนโปรแกรมแบบ OOP มาใช้ได้อย่างมีประสิทธิภาพกว่า&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;เมื่อพิจารณาจากความแตกต่างกันระหว่าง Action Pattern และ Service Pattern จะเห็นว่าทั้งคู่มีส่วนช่วยในการเพิ่มคุณภาพของโค้ดเหมือนกัน
แต่จะแตกต่างกันที่รายละเอียดและแนวคิดในการนำไปใช้งานมากกว่า ทั้งนี้เราสามารถเลือกใช้งาน Pattern เหล่านี้ได้ตามความเหมาะสมของสถานการณ์ขึ้นอยู่กับการตกลงกันภายในทีม
โดยที่เราไม่จำเป็นต้องยึดรูปแบบใดรูปแบบหนึ่งเท่านั้น 🤩&lt;/p&gt;
&lt;h2&gt;🐥 ติดปีกให้ Action Pattern ด้วย lorisleiva/laravel-actions&lt;/h2&gt;
&lt;p&gt;สำหรับใครที่อ่านมาถึงตรงนี้ แล้วเริ่มจะซื้อแนวคิดของ Action Pattern แล้ว แอดมินขอแนะนำ plugin ทิ้งท้ายไว้สัก 1 ตัวเพื่อให้สามารถนำ Action Pattern
ไปต่อยอดและได้เห็นไอเดียและวิธีการใช้งานที่หลากหลายยิ่งขึ้น โดย plugin ที่ว่านี้สามารถดูเพิ่มเติมได้ที่&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.laravelactions.com/&quot;&gt;https://www.laravelactions.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;โดยที่ plugin ตัวนี้จะช่วยให้ Class ที่เราเขียนขึ้นสามารถนำไป reuse ใช้งานได้ง่ายยิ่งขึ้น ไม่ว่าจะเป็น Controller, Job หรือ Listener ก็ตาม&lt;/p&gt;
&lt;p&gt;ตัวอย่างโค้ดคร่าว ๆ จากในเว็บไซต์มีดังนี้&lt;/p&gt;
&lt;p&gt;&lt;code&gt;app\Actions\PublishANewArticle&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class PublishANewArticle
{
    use AsAction;

    public function handle(User $author, string $title, string $body): Article
    {
        return $author-&gt;articles()-&gt;create([
            &apos;title&apos; =&gt; $title,
            &apos;body&apos; =&gt; $body,
        ]);
    }

    public function asController(Request $request): ArticleResource
    {
        $article = $this-&gt;handle(
            $request-&gt;user(),
            $request-&gt;get(&apos;title&apos;),
            $request-&gt;get(&apos;body&apos;),
        );

        return new ArticleResource($article);
    }

    public function asListener(NewProductReleased $event): void
    {
        $this-&gt;handle(
            $event-&gt;product-&gt;manager,
            $event-&gt;product-&gt;name . &apos; Released!&apos;,
            $event-&gt;product-&gt;description,
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จะเห็นว่าหากเราวางแนวคิดของ Action Pattern ไว้ตั้งแต่แรก เราสามารถเขียน 1 Class ที่ทำงานเพียงอย่างเดียวแล้วนำไป reuse ใช้ได้โดยง่าย
โดยที่ไม่ต้องมีไฟล์ Controller หรือ Listener มา wrap การทำงานเพิ่มไปอีก ทำให้จำนวนไฟล์และโค้ดที่เราเขียนสะอาดมากยิ่งขึ้น
(แต่ไม่แนะนำกับระบบใหญ่ ๆ ที่ควรจะวางโครงสร้างให้ดี ไม่เช่นนั้นอาจจะเจอปัญหาในการ refactor ทีหลังได้)&lt;/p&gt;
&lt;p&gt;สำหรับบทความนี้แอดมินขอแนะนำแนวคิดและไอเดียของการใช้ Action Pattern ใน Laravel ไว้เพียงเท่านี้ก่อน
หากมีโอกาสจะมาแนะนำวิธีการใช้งาน plugin เพิ่มเติมในโอกาสต่อไป 🛠️&lt;/p&gt;
&lt;p&gt;ขอให้สนุกกับการเรียนรู้และการเขียนโค้ด 🥰&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.DO8ypBkY.jpg"/><enclosure url="/_astro/cover.DO8ypBkY.jpg"/></item><item><title>อ่านข้อมูล จดบันทึก สรุปรายงาน - กับเลขาส่วนตัว NotebookLM</title><link>https://blog.devfavor.com/blog/introduce-to-notebooklm</link><guid isPermaLink="true">https://blog.devfavor.com/blog/introduce-to-notebooklm</guid><description>ต้องอ่านไฟล์เอกสารหลายร้อยหน้า? ต้องจดบันทึกการประชุมหลายชั่วโมง? สิ่งเหล่านี้จะไม่ใช่เรื่องยากอีกต่อไปเมื่อเรามีเลขาส่วนตัวที่ชื่อ &quot;NotebookLM&quot; จัดการให้</description><pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;📚 งานประจำวัน ที่ต้องเจอ(เกือบ)ทุกวัน&lt;/h2&gt;
&lt;p&gt;แม้ว่าเราจะอยู่ใน role ของ developer (หรือ role ไหน ๆ ก็ตาม) แอดมินเชื่อว่าเกือบทุกคนน่าจะเคยเจอกับสถานการณ์ที่เราต้องมานั่งจัดการกับงานเอกสารต่าง ๆ มากมาย
ไม่ว่าจะเป็นการอ่านหรือสรุปรวบรวมเอกสารคู่มือหลายร้อยหน้า หรือการสรุปบันทึกการประชุมต่าง ๆ ซึ่งเป็นงานแฝงที่เกิดขึ้นโดยที่งานเหล่านี้อาจจะไม่ใช่งานหลักของเราจริง ๆ&lt;/p&gt;
&lt;p&gt;บ่อยครั้งที่งานเหล่านี้ก็กินเวลาทำงานของเรามากจนเกินไป จนทำให้ productivity ที่ควรจะเกิดขึ้นถูกขัดจังหวะอย่างหลีกเลี่ยงไม่ได้
และบางครั้งก็ถึงกับทำให้ developer หลาย ๆ คนเกิดอาการ burnout กับงานเอกสารเหล่านี้กันเลยทีเดียว 😵&lt;/p&gt;
&lt;h2&gt;📝 ทางออกของปัญหากับเลขาส่วนตัว NotebookLM&lt;/h2&gt;
&lt;p&gt;แต่ในวันนี้ ปัญหาที่ว่ามานี้จะหมดไปทันที เมื่อ Google ออกผลิตภัณฑ์ใหม่ที่ชื่อว่า NotebookLM ออกมา&lt;/p&gt;
&lt;p&gt;NotebookLM เป็นเครื่องมือที่เปรียบเสมือนกับ &quot;ผู้ช่วยอัจฉริยะ&quot; ที่จะมาช่วยจัดการงานเอกสารของเราในแบบที่มีความเฉพาะเจาะจงมากกว่าการใช้ AI อื่น ๆ ทั่วไป
โดยสิ่งที่ทำให้ NotebookLM โดดเด่นกว่า AI tool ตัวอื่น ๆ ก็คือฟีเจอร์ที่น่าสนใจเหล่านี้&lt;/p&gt;
&lt;h2&gt;🛠️ ฟีเจอร์หลัก ๆ ที่น่าสนใจ&lt;/h2&gt;
&lt;p&gt;NotebookLM มีฟีเจอร์พื้นฐานที่ผู้ช่วย AI ของเราควรจะมีอย่างครบถ้วน แต่สิ่งที่ทำให้ NotebookLM กลายเป็น &quot;ผู้ช่วยอัจฉริยะ&quot; นั้นมาจากฟีเจอร์สำคัญเหล่านี้&lt;/p&gt;
&lt;h3&gt;📂 อัปโหลดแหล่งที่มาได้&lt;/h3&gt;
&lt;p&gt;NotebookLM จะไม่นำคำถามของเราไปค้นหาคำตอบจากอินเทอร์เน็ตเหมือนกับ search engine อื่น ๆ แต่จะหาคำตอบจากแหล่งข้อมูลที่เราได้อัปโหลดไปเท่านั้น เช่น
ไฟล์เอกสาร ไฟล์ PDF ไฟล์เสียงหรือแม้แต่ไฟล์วิดีโอก็ตาม เอกสารเหล่านี้จะกลายเป็นแหล่งข้อมูลที่เฉพาะเจาะจงและ NotebookLM จะทำการอ่านและหาคำตอบมาตอบคำถามของเรา&lt;/p&gt;
&lt;h3&gt;✅ การตอบคำถามที่น่าเชื่อถือ&lt;/h3&gt;
&lt;p&gt;เนื่องจาก NotebookLM บังคับให้ผู้ใช้งานต้องอัปโหลดแหล่งที่มาของข้อมูลก่อน ทำให้คำตอบที่ได้มานั้นสามารถระบุแหล่งอ้างอิงของคำตอบได้ โดยที่
NotebookLM จะตอบคำถามของเราโดยการแนบแหล่งอ้างอิงในคำตอบให้เราด้วย เช่น คำตอบที่ตอบออกมานี้ มาจากเอกสารไหน หน้าไหน บรรทัดไหน เป็นต้น
ซึ่งเราสามารถกดเข้าไปดูที่เอกสารต้นฉบับเพื่อตรวจสอบความถูกต้องหรืออ่านข้อมูลเพิ่มเติมได้ทันที&lt;/p&gt;
&lt;h3&gt;📊 การนำเสนอที่น่าสนใจ&lt;/h3&gt;
&lt;p&gt;ข้อนี้เองเป็นจุดที่ทำให้แอดมินหลงรัก NotebookLM ทันทีหลังจากที่ได้ทดลองใช้ เพราะมันไม่ได้เพียงแค่ตอบคำถามของเราเพียงอย่างเดียวเท่านั้น
แต่ NotebookLM ยังสามารถนำเสนอข้อมูลของเราในรูปแบบต่าง ๆ ได้อีกด้วย เช่น การสรุปข้อมูลเป็นโน้ตย่อหรือแม้แต่การทำแผนผังความคิด (mindmap) ก็ตาม&lt;/p&gt;
&lt;h2&gt;🚀 เริ่มต้นใช้งาน NotebookLM&lt;/h2&gt;
&lt;p&gt;หลังจากที่ทำความรู้จักกันมาพอสมควรแล้ว น่าจะได้เวลาอันสมควรแล้วที่เราจะมาลองใช้งานจริง ๆ กันสักที ก่อนอื่นเข้าไปที่เว็บไซต์
&lt;a href=&quot;https://notebooklm.google.com/&quot;&gt;https://notebooklm.google.com/&lt;/a&gt; จะพบกับเว็บไซต์นี้&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/notebooklm.BjsAQi_4_Y2Wf4.webp&quot; alt=&quot;notebooklm&quot;&gt;&lt;/p&gt;
&lt;p&gt;จากนั้นกดปุ่ม &lt;code&gt;สร้าง Notebook ใหม่&lt;/code&gt; เพื่อเข้าสู่ระบบ&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/upload.DMUPO54p_XjyuN.webp&quot; alt=&quot;upload&quot;&gt;&lt;/p&gt;
&lt;p&gt;เมื่อกดเข้าสู่ระบบมาแล้ว หน้าจอจะแสดง popup ให้ผู้ใช้งานอัปโหลดแหล่งที่มาเข้าไปก่อน โดยในที่นี้แอดมินจะทดลองอัปโหลดไฟล์ PDF ไฟล์หนึ่งชื่อ
&quot;FREE n8n Beginner Course QWERTY School&quot; ซึ่งเป็น PDF ที่แจกฟรีสำหรับศึกษาเรื่องเกี่ยวกับ n8n นั่นเอง มาดูกันว่าตัว NotebookLM จะเข้าใจเอกสารนี้ว่าอย่างไร
และเราจะได้อะไรจากการอัปโหลดเอกสารนี้บ้าง&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/example-pdf.BbmZljcw_212wzu.webp&quot; alt=&quot;example-pdf&quot;&gt;&lt;/p&gt;
&lt;p&gt;รอสักครู่ให้ NotebookLM อ่านข้อมูลทั้งหมดเสร็จก่อน จากนั้นหน้าจอจะแสดงข้อมูลสรุปคร่าว ๆ ของไฟล์ออกมา&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/upload-finished.mMKIPEGJ_ZQkAPL.webp&quot; alt=&quot;upload-finished&quot;&gt;&lt;/p&gt;
&lt;p&gt;ถ้าขึ้นข้อความแบบนี้แสดงว่าเลขาของเราอ่านข้อมูลที่เราส่งให้เสร็จเรียบร้อยแล้ว&lt;/p&gt;
&lt;h2&gt;💬 ลองถามข้อมูลกับเลขา&lt;/h2&gt;
&lt;p&gt;หลังจากที่สรุปไฟล์เสร็จเรียบร้อยแล้ว ในขั้นตอนนี้เราสามารถเริ่มพูดคุยกับ AI เพื่อสอบถามข้อมูลเกี่ยวกับเอกสารของเราได้แล้ว เช่น ถ้าแอดมินอยากรู้ว่า
&quot;โครงสร้างข้อมูลใน n8n เป็นอย่างไร&quot; ก็สามารถพิมพ์ถามไปได้เลย ตัว NotebookLM ก็จะหาคำตอบมาให้เราแบบนี้&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/chat.D23KSfuY_ZVtoFK.webp&quot; alt=&quot;chat&quot;&gt;&lt;/p&gt;
&lt;h2&gt;📖 สร้างโน้ตสรุปรายงานโดยย่อ&lt;/h2&gt;
&lt;p&gt;หากเอกสารมีจำนวนหลายร้อยหน้า การมานั่งอ่านทั้งหมดคงจะไม่ใช่งานที่น่าสนุกแน่ คงจะดีกว่าถ้าเรารู้ข้อมูลสรุปคร่าว ๆ ก่อน จากนั้นค่อยไปหาข้อมูลละเอียดตามหัวข้ออีกครั้ง
ซึ่งเลขาคนนี้ก็จัดการให้ได้ง่าย ๆ อีกเช่นเคย ด้วยการกดไปที่ปุ่ม &lt;code&gt;เอกสารสรุป&lt;/code&gt; จากนั้นรอไม่นาน สรุปเอกสารของเราก็จะออกมาหน้าตาประมาณนี้&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/summarize.BZCnnKxQ_19FOqR.webp&quot; alt=&quot;summarize&quot;&gt;&lt;/p&gt;
&lt;h2&gt;📈 สร้างแผนผังความคิด (mindmap)&lt;/h2&gt;
&lt;p&gt;แต่ถ้าหากว่าเอกสารมีจำนวนหน้าเยอะจริง ๆ แล้วล่ะก็ การอ่านสรุปแบบข้อความก็ไม่ต่างจากการฝืนกินอาหารที่มีประโยชน์โดยไม่ผ่านการปรุง
จะดีกว่าไหมถ้าเราลองปรุงแต่งมันสักหน่อยเพื่อให้อาหารของเราย่อยง่ายขึ้นด้วยการสร้างเป็นแผนผังความคิด (mindmap) แทน
หากต้องการปรุงอาหารแบบนี้ก็ไม่ใช่เรื่องยาก เพียงแค่กดที่ปุ่ม &lt;code&gt;แผนผังความคิด&lt;/code&gt; จากนั้นก็จิบกาแฟรอเหมือนเดิม ไม่นานเราก็จะได้แผนผังสรุปดังนี้&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/mindmap.Ci96dkej_Z1LuXPP.webp&quot; alt=&quot;mindmap&quot;&gt;&lt;/p&gt;
&lt;p&gt;ซึ่งโดยส่วนตัวแล้วแอดมินรู้สึกว่ามันอ่านง่ายกว่าการอ่าน text ยาว ๆ มาก ๆ แม้จะสรุปมาแล้วก็ตาม การทำแผนผังแบบนี้จะช่วยให้เราสืบค้นและอ่านได้ง่ายยิ่งขึ้นไปอีก
นอกจากนี้ยังมีประโยชน์ในการเรียนรู้อะไรสักอย่างอีกด้วย&lt;/p&gt;
&lt;h2&gt;📢 ไม่อยากอ่านอะไรแล้ว ให้เลขาอ่านให้ฟังเลยแล้วกัน&lt;/h2&gt;
&lt;p&gt;ฟีเจอร์นี้คือที่สุดของการสรุปแล้ว เพราะไม่ว่าจะสรุปออกมาเป็นแบบไหน จะเป็นข้อความหรือแผนผังความคิดก็ตาม สุดท้ายยังไงก็ต้องอ่านอยู่ดี แต่ NotebookLM สามารถลดขั้นตอนนั้นให้เราได้ด้วยการอ่านให้เราฟังแทน โดยให้เราดูที่ section &lt;code&gt;การสนทนาแบบเจาะลึก&lt;/code&gt; จากนั้นกดที่ปุ่ม &lt;code&gt;สร้าง&lt;/code&gt; บนหน้าจอ&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/generate-voice.BHrSE_iY_Z1Sg9XQ.webp&quot; alt=&quot;generate-voice&quot;&gt;&lt;/p&gt;
&lt;p&gt;ระบบจะทำการประมวลผล จากนั้นก็จิบกาแฟอีกแก้วหนึ่งเพื่อรอการประมวลผลสักครู่ ไม่นานก็เสร็จเรียบร้อย ตอนนี้เราสามารถกดฟังข้อความได้แล้ว&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/generate-voice-finished.Ar8c240H_1JqTCe.webp&quot; alt=&quot;generate-voice-finished&quot;&gt;&lt;/p&gt;
&lt;p&gt;ในการฟังข้อความจะไม่ได้ให้ความรู้สึกเหมือนมีคนอ่านให้ฟัง แต่จะได้ความรู้สึกเหมือนกับมีพิธีกร 2 คนมาเล่าเรื่องหรือพูดคุยเกี่ยวกับเนื้อหานั้นให้เราฟังมากกว่า
ซึ่งทำให้เราสามารถฟังได้เพลิน ๆ โดยไม่รู้สึกเบื่อเลย&lt;/p&gt;
&lt;h2&gt;👩‍💻 ลองเพิ่มแหล่งข้อมูลและถามต่อ&lt;/h2&gt;
&lt;p&gt;สำหรับแพลนฟรีไม่มีค่าใช้จ่าย แหล่งที่มาของข้อมูลจะเพิ่มได้สูงสุด 50 แหล่งต่อ 1 โปรเจกต์ ซึ่งแอดมินก็คิดว่าเพียงพอแล้วสำหรับในโปรเจกต์ที่ไม่ใหญ่มาก&lt;/p&gt;
&lt;p&gt;โดยเราสามารถเพิ่มแหล่งที่มาที่เกี่ยวข้องกับเรื่องนี้ได้อีก ไม่ว่าจะเป็น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ลิงก์ข้อมูลอื่น ๆ&lt;/li&gt;
&lt;li&gt;ข้อความที่บันทึกมา&lt;/li&gt;
&lt;li&gt;อัปโหลดจาก Google Drive&lt;/li&gt;
&lt;li&gt;ข้อความที่ถูกสรุปมาจากตัว NotebookLM เอง เป็นต้น&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ยิ่งมีแหล่งข้อมูลมากเท่าไหร่ เลขาของเราคนนี้ก็จะยิ่งมีความแม่นยำขึ้นมากเท่านั้น เพียงแค่นี้งานเอกสารประจำวันที่เคยกินเวลางานของเราไปก็สามารถจัดการได้โดยง่ายแล้ว 😎 ☕&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.CDYqp64l.jpg"/><enclosure url="/_astro/cover.CDYqp64l.jpg"/></item><item><title>สร้าง Static Site Blog ง่าย ๆ - ด้วย Astro framework</title><link>https://blog.devfavor.com/blog/static-site-blog-with-astro</link><guid isPermaLink="true">https://blog.devfavor.com/blog/static-site-blog-with-astro</guid><description>สร้างเว็บบล็อกของตัวเองเป็น Static Site ได้ง่าย ๆ ไม่ต้องมี backend ไม่ต้องมี database และ build ขึ้น vercel ได้เลย</description><pubDate>Wed, 25 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;@/packages/pure/components/user&apos;&lt;/p&gt;
&lt;h2&gt;❓ Astro framework คืออะไร ?&lt;/h2&gt;
&lt;p&gt;Astro framework เป็น web framework ตัวหนึ่ง ที่มีแนวคิดการทำงานแบบ content-driven เหมาะสำหรับการนำมาทำเว็บไซต์ที่เน้นการอ่านและการแสดงผลเนื้อหาเป็นหลัก เช่น เว็บบล็อก หรือ เว็บไซต์ของบริษัท เป็นต้น&lt;/p&gt;
&lt;p&gt;โดยที่ตัว Astro มีจุดเด่นในเรื่องของ &quot;ความเร็ว&quot; เป็นหลัก ไม่ว่าจะเป็น ความเร็วในการเริ่มต้นใช้งานที่สามารถเริ่มเรียนรู้ได้อย่างง่ายดาย และความเร็วของเว็บไซต์ที่ทำให้เว็บของคุณมี SEO ที่ดีและเป็นที่รักของ search engine อีกด้วย&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/astro.R3bIOHLJ_Z15OvQJ.webp&quot; alt=&quot;astro&quot;&gt;&lt;/p&gt;
&lt;h2&gt;🚀 ทำไม Astro ถึงเร็ว ?&lt;/h2&gt;
&lt;p&gt;เหตุผลที่ทำให้ Astro นั้นเร็วกว่า framework เจ้าดังอื่น ๆ นั้นเป็นเพราะว่าตัว Astro จะ build เว็บไซต์ของเราเป็น static HTML ก่อนที่จะเสิร์ฟถึงผู้ใช้งานนั่นเอง
ซึ่งตัว static HTML นี้ทาง Astro เคลมว่าเป็น Zero Javascript หรือก็คือจะไม่มี JavaScript ที่ใช้สำหรับ render HTML เลย จะมีก็แต่ JavaScript ที่เป็นฟังก์ชันการทำงานจริงอยู่เท่านั้น
ต่างจาก framework เจ้าดังอื่น ๆ ที่ยังคงใช้ JavaScript ในการ render HTML อยู่&lt;/p&gt;
&lt;p&gt;ซึ่งตัว Astro มีการบอกเอาไว้ในหน้าเว็บไซต์ของตัวเองว่า ประสบการณ์การใช้งานเว็บไซต์ที่ถูกสร้างด้วย Astro นั้นดีกว่า framework ตัวอื่น ๆ อยู่พอสมควร&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/astro-speed.zIbug1S4_bVcTL.webp&quot; alt=&quot;astro-speed&quot;&gt;&lt;/p&gt;
&lt;h2&gt;🖥️ เกริ่นนำพอแล้ว เริ่มเลยดีกว่า&lt;/h2&gt;
&lt;p&gt;หลังจากที่ทำความรู้จักและเข้าใจข้อดีของ Astro กันมาพอสมควรแล้ว เพื่อไม่ให้เป็นการเสียเวลา เรามาเริ่มการสร้างเว็บบล็อกของเราด้วย Astro กันเลยดีกว่า&lt;/p&gt;
&lt;p&gt;ในการติดตั้ง Astro สามารถทำได้หลายวิธีตามเอกสาร ซึ่งในบทความนี้จะขอใช้ tool มาตรฐานคือ npm ในการติดตั้งก่อน แต่หากผู้ใช้งานมี tool อื่นติดตั้งอยู่แล้วก็สามารถเลือกใช้งานได้ตามสะดวก&lt;/p&gt;
&lt;p&gt;เริ่มต้นติดตั้ง Astro ด้วยการพิมพ์คำสั่งง่าย ๆ ดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create astro@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;เมื่อพิมพ์คำสั่งจบ ตัว Astro จะให้เราตั้งชื่อโปรเจกต์ของเราและตั้งค่าเริ่มต้นอีกนิดหน่อย ให้เราตั้งชื่อโปรเจกต์ตามที่ต้องการได้เลย&lt;/p&gt;
&lt;p&gt;หลังจากนั้นตัว Astro จะดาวน์โหลดไฟล์โค้ดและ packages ทั้งหมดที่ต้องใช้งานเก็บไว้ที่โฟลเดอร์ชื่อโปรเจกต์ตามที่เราตั้ง หลังจากที่ npm ทำการติดตั้งเสร็จสิ้น ให้เราเข้าไปที่พาธของโปรเจกต์และพิมพ์คำสั่งเพื่อเริ่มใช้งานได้เลย&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;รอสักครู่ให้ระบบเริ่มทำงาน จากนั้นเปิด browser และเข้าไปที่ URL &lt;code&gt;http://localhost:4321&lt;/code&gt; ได้เลย เพียงเท่านี้เราก็จะได้เว็บไซต์เริ่มต้นสำหรับใช้งาน Astro มาใช้งานแล้ว 😎&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/astro-default.CtWA8leV_Z1gkNwv.webp&quot; alt=&quot;astro-default&quot;&gt;&lt;/p&gt;
&lt;h2&gt;📝 เริ่มต้นเขียนบทความแรกที่ src / pages&lt;/h2&gt;
&lt;p&gt;โดยพื้นฐานแล้ว Astro จะมีระบบ routing ของเว็บไซต์แบบ file-base routing คือ เมื่อเราสร้างไฟล์หรือโฟลเดอร์ชื่ออะไร เราก็สามารถเข้าสู่เว็บไซต์ด้วยการพิมพ์ชื่อไฟล์หรือโฟลเดอร์นั้น ๆ ได้เลย เช่น&lt;/p&gt;
&lt;p&gt;หากเราต้องการเพิ่มหน้าสำหรับทดสอบสร้างบทความ และอยากให้มี URL ของเว็บไซต์เป็น &lt;code&gt;/hello-world&lt;/code&gt; เราต้องสร้างไฟล์ &lt;code&gt;index.astro&lt;/code&gt; ไว้ที่โฟลเดอร์ &lt;code&gt;src/pages/hello-world/index.astro&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;จากนั้นให้ลองเข้าเว็บไซต์เดิมอีกครั้งด้วย URL &lt;code&gt;http://localhost:4321/hello-world&lt;/code&gt; หน้าเว็บก็จะแสดงเนื้อหาตามที่เราเขียนไว้&lt;/p&gt;
&lt;h2&gt;🎨 เพิ่มความสวยงามให้เว็บไซต์ด้วย Astro Theme&lt;/h2&gt;
&lt;p&gt;แม้เว็บไซต์ตั้งต้นของ Astro จะสวยงามและน่าใช้อยู่แล้ว แต่ตัว Astro เองก็ยังมีธีมให้เราเลือกใช้อีกมากมายเพื่อให้เราไม่ต้องเริ่มต้นเขียน CSS เองตั้งแต่ศูนย์ และทำให้เราสามารถโฟกัสไปที่การเขียนเนื้อหาได้อย่างเต็มที่&lt;/p&gt;
&lt;p&gt;ซึ่งธีมของ Astro นั้นจะมีทั้งธีมที่เป็น official ของทาง Astro เองและธีมที่นักพัฒนาคนอื่น ๆ สร้างมาให้เราใช้อยู่มากมาย
โดยเราสามารถดูรายชื่อธีมแบบ official ได้ที่ &lt;a href=&quot;https://github.com/withastro/astro/tree/main/examples&quot;&gt;https://github.com/withastro/astro/tree/main/examples&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;ในบทความนี้เราจะทดลองสร้างเว็บไซต์ที่เป็นเว็บบล็อกกัน ดังนั้นเราสามารถเลือกธีมที่เป็น template ของ blog ได้เลย โดยในการสร้างโปรเจกต์เราต้องเพิ่มคำสั่งเข้าไปดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create astro@latest -- --template blog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;เพียงเท่านี้เราก็จะได้โปรเจกต์ตั้งต้นที่มีหน้าตาเป็นเว็บบล็อกให้ใช้งานแล้ว&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/astro-blog.Bt8Nauyg_Z155ure.webp&quot; alt=&quot;astro-blog&quot;&gt;&lt;/p&gt;
&lt;p&gt;แต่ถ้าหากว่าธีมนี้ยังไม่สวยถูกใจ เราก็สามารถเลือกธีมอื่น ๆ เพิ่มเติมได้ที่ &lt;a href=&quot;https://astro.build/themes&quot;&gt;https://astro.build/themes&lt;/a&gt; จะพบธีมให้เลือกมากมายทั้งแบบฟรีและเสียเงิน&lt;/p&gt;
&lt;p&gt;สมมุติว่าเราสนใจธีมที่มีชื่อว่า bookworm ซึ่งมี repository อยู่ที่ &lt;a href=&quot;https://github.com/themefisher/bookworm-light-astro&quot;&gt;https://github.com/themefisher/bookworm-light-astro&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;เราก็สามารถติดตั้ง Astro ผ่านการระบุ template ได้เช่นกัน โดยการพิมพ์คำสั่งดังนี้&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create astro@latest -- --template themefisher/bookworm-light-astro
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;จากนั้นก็เพียงแต่ทำแบบเดิมในการติดตั้ง เพียงเท่านี้เราก็สามารถเริ่มต้นใช้งาน Astro ด้วยธีมที่สวยงามได้แล้ว&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/astro-bookworm.DHpVXbjY_1kSjFE.webp&quot; alt=&quot;astro-bookworm&quot;&gt;&lt;/p&gt;
&lt;h2&gt;⚙️ การปรับแต่งอื่น ๆ ที่สำคัญ&lt;/h2&gt;
&lt;p&gt;แม้การแก้ไขเนื้อหาใน Astro จะทำได้ง่าย ๆ ผ่าน md ไฟล์ แต่นอกจากนี้เรายังสามารถปรับแต่งส่วนอื่น ๆ ได้อีก เช่น การแก้ไข SEO บางอย่างหรือเพิ่มเติมรายละเอียดเล็ก ๆ น้อย ๆ ตามแต่ธีมนั้น ๆ จะมีมาให้&lt;/p&gt;
&lt;p&gt;ซึ่งในการแก้ไขนี้เราสามารถแก้ได้ที่ไฟล์ &lt;code&gt;astro.config.mjs&lt;/code&gt; ซึ่งการแก้ไขไฟล์นี้จะขึ้นอยู่กับธีมที่เลือกใช้ด้วย ดังนั้นก่อนแก้ไขขอให้ศึกษาโครงสร้างของธีมและเอกสารของธีมนั้น ๆ อีกครั้งเพื่อที่จะได้ไม่เกิดข้อผิดพลาดขึ้น&lt;/p&gt;
&lt;h2&gt;🌐 build เว็บไซต์ขึ้น vercel เพื่อให้เพื่อน ๆ ได้ชม&lt;/h2&gt;
&lt;p&gt;หลังจากที่เราเขียนบล็อกเสร็จเรียบร้อย สิ่งถัดไปที่ต้องทำก็คือการนำบล็อกที่เราเขียนอัปโหลดขึ้นเว็บไซต์จริงเพื่อให้เราสามารถแชร์บทความนี้ไปยังเพื่อน ๆ ของเราได้&lt;/p&gt;
&lt;p&gt;ในเมื่อตัว Astro สามารถ build เว็บของเราเป็น static HTML แล้วเราก็สามารถเลือก cloud เจ้าดังได้มากมายที่สามารถอัปโหลด static HTML ได้แบบฟรี ๆ โดยในบทความนี้เราจะลอง build และอัปโหลดขึ้นบน vercel กัน&lt;/p&gt;
&lt;p&gt;ก่อนอื่นเราต้องสมัครใช้งาน vercel ให้เรียบร้อยก่อน หลังจาก login เข้ามาแล้วให้ทำการผูก repository ที่เรา push code ของโปรเจกต์ขึ้นไป จากนั้นก็เลือก repository ที่ต้องการและกด build ได้เลย&lt;/p&gt;
&lt;p&gt;จากนั้นก็เพียงแค่รอจนกว่าหน้าจอเว็บไซต์จะแสดงข้อความผลลัพธ์จากการ build เสร็จสิ้น&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.devfavor.com/_astro/vercel.KK1FwMNd_Z1iwE63.webp&quot; alt=&quot;vercel&quot;&gt;&lt;/p&gt;
&lt;p&gt;หากหน้าเว็บไซต์ขึ้นว่า build success แบบนี้ก็หมายความว่าบล็อกของเราพร้อมที่จะออนไลน์ให้เพื่อน ๆ ได้อ่านกันแล้ว เราสามารถคัดลอก URL ที่ vercel แสดงผลอยู่บนหน้าจอแล้วส่งต่อได้เลย&lt;/p&gt;
&lt;h2&gt;👨‍💻 ทักษะพื้นฐานยังสำคัญ&lt;/h2&gt;
&lt;p&gt;แม้ Astro จะเตรียมข้อมูลให้เราพร้อมสรรพแล้ว แต่หากเราต้องการที่จะแก้ไขเว็บไซต์ให้ได้อย่างมีประสิทธิภาพจริง ๆ ก็ต้องไม่มองข้ามทักษะในการเขียนโปรแกรมโดยเด็ดขาด&lt;/p&gt;
&lt;p&gt;ซึ่งใน Astro เองจะมี syntax ของ Javascript, JSX, CSS หรือแม้แต่การเขียนเนื้อหาใน markdown เองก็ตาม สิ่งเหล่านี้ถือเป็นพื้นฐานในการปรับแต่งและแก้ไขเว็บทั้งสิ้น&lt;/p&gt;
&lt;p&gt;ไว้ในโอกาสถัดไปแอดมินจะมาแนะนำเทคนิคการเขียนและการปรับแต่ง Astro ให้ได้ดั่งใจกัน สำหรับวันนี้ ขอให้ทุกคนสนุกกับการเขียนบล็อกของตัวเองให้เต็มที่ได้เลย 🥰&lt;/p&gt;</content:encoded><h:img src="/_astro/cover.y0yz8RML.jpg"/><enclosure url="/_astro/cover.y0yz8RML.jpg"/></item><item><title>Hello World, From Devfavor - บันทึกเล่มแรกของนักพัฒนา</title><link>https://blog.devfavor.com/blog/hello-world-from-devfavor</link><guid isPermaLink="true">https://blog.devfavor.com/blog/hello-world-from-devfavor</guid><description>ยินดีต้อนรับสู่ Devfavor — พื้นที่เล็ก ๆ สำหรับบันทึกเรื่องราวการเดินทางของนักพัฒนาเว็บไซต์คนหนึ่ง ที่ชื่นชอบในการเขียนโค้ดและการเรียนรู้</description><pubDate>Sun, 22 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;☕ ก่อนอื่นต้องขอกล่าวสวัสดีอย่างเป็นทางการ...&lt;/h2&gt;
&lt;p&gt;แอดมินขอกล่าวสวัสดีทุกท่าน และขอขอบคุณที่แวะเวียนผ่านมาที่มุมเล็ก ๆ แห่งนี้ เพราะนี่คือบทความแรกของ Devfavor บล็อกที่อยากจะเป็นแหล่งความรู้ แหล่งพูดคุยและแหล่งพักผ่อนหย่อนใจสำหรับนักพัฒนาทั้งหลายที่กำลังเดินทางอยู่ในขณะนี้&lt;/p&gt;
&lt;h2&gt;☕ เราคือใคร ?&lt;/h2&gt;
&lt;p&gt;เราไม่ใช่บริษัทใหญ่โต ไม่ใช่ influencer ชื่อดัง แต่เราเป็นเพียงนักพัฒนาเว็บไซต์คนหนึ่งที่ชื่นชอบในเรื่องราวเกี่ยวกับเทคโนโลยีและการเขียนโปรแกรม โดยหวังว่าสักวันหนึ่ง โปรแกรมที่เราเขียนจะเป็นประโยชน์ต่อโลกใบนี้&lt;/p&gt;
&lt;p&gt;นอกจากงานหลักอย่างการพัฒนาเว็บไซต์ซึ่งเป็นซึ่งเป็นงานที่เราหลงใหลแล้ว ในเวลาว่าง เรายังมีงานอดิเรกอื่น ๆ ที่ช่วยเติมพลังใจและจุดประกายไอเดียใหม่ให้เราอยู่เสมอ&lt;/p&gt;
&lt;p&gt;บางวันเราอาจจะใช้เวลาลองทำโปรเจกต์เล็ก ๆ สักโปรเจกต์ อาจเป็น tool เล็ก ๆ ที่คิดว่าน่าจะมีประโยชน์ในเวลานั้น หรือฟีเจอร์สนุก ๆ ที่อยากรู้ว่าภาษาที่ใช้อยู่จะทำได้ไหม ซึ่งโปรเจกต์เหล่านี้มักจะกลายเป็นสนามซ้อมที่ดีสำหรับเรียนรู้เทคโนโลยีใหม่ ๆ อยู่เสมอ&lt;/p&gt;
&lt;p&gt;บางวันเราอาจจะอยากพักสายตาด้วยการจิบกาแฟแล้วนั่งอ่านข่าวสารในวงการ dev จากนักพัฒนาเก่ง ๆ คนอื่น เพื่อเติมความรู้และแรงบันดาลใจ&lt;/p&gt;
&lt;p&gt;บางวันเราอาจจะพักผ่อนด้วยการเล่นเกมที่ชอบกับเพื่อน ๆ แต่ถึงอย่างนั้นก็ยังไม่พ้นที่จะเอาไอเดียจากเกมเหล่านั้นมาพูดคุยกันอย่างสนุกสนานอยู่ดี&lt;/p&gt;
&lt;p&gt;ในขณะที่บางวันก็เหนื่อยกว่าจะทำอะไรไหว การพักผ่อนอย่างเต็มที่จึงเป็นเรื่องสำคัญที่หลีกเลี่ยงไม่ได้สำหรับชาว dev อย่างเรา (และสำหรับทุกคน)&lt;/p&gt;
&lt;p&gt;และทั้งหมดนี้คือสิ่งที่เราเป็นและทำอย่างสม่ำเสมอตลอดมา หวังว่าทุกท่านที่แวะเวียนผ่านมาจะได้รู้จักเรามากขึ้น ❤️&lt;/p&gt;
&lt;h2&gt;☕ ทำไมถึงเริ่มเขียน blog ?&lt;/h2&gt;
&lt;p&gt;เพราะเราเชื่อว่า การเรียนรู้ที่ดีที่สุดคือการลงมือทำจริง หากเราเรียนรู้อะไรสักอย่างจากเพียงแค่ในตำราเราจะไม่มีทางเห็นปัญหาต่าง ๆ ที่อาจเกิดขึ้นในการทำงาน และมันคงจะยิ่งดีขึ้นไปอีกถ้าเราได้นำปัญหาหรือผลลัพธ์จากการทำสิ่งนั้นมาแบ่งปัน&lt;/p&gt;
&lt;p&gt;ในการทำงานที่ผ่านมาก่อนหน้านี้ เราเองก็เหมือนหลาย ๆ คน ที่ต้องเจอกับปัญหาเฉพาะหน้าต่าง ๆ หรือเรื่องที่ไม่เคยรู้มาก่อน แต่พอผ่านการหาข้อมูล ทดลอง และแก้ไขไปทีละขั้นตอน สิ่งเหล่านั้นก็ค่อย ๆ สะสมกลายเป็นประสบการณ์และความรู้ที่ติดตัวเราไว้เสมอ&lt;/p&gt;
&lt;p&gt;พอประสบการณ์เหล่านี้มีมากขึ้น ก็เกิดความคิดขึ้นมาว่า &quot;จะดีไหมหากเราได้มีโอกาศนำสิ่งเหล่านี้ออกมาแบ่งปัน&quot; และเป็นพื้นที่สำหรับพูดคุยหรือแชร์เทคนิค มุมมองต่าง ๆ จากคนที่เคยเจอแล้วออกไป แทนที่จะเก็บไว้คนเดียว&lt;/p&gt;
&lt;p&gt;และด้วยความคิดนี้เอง บล็อกเล็ก ๆ แห่งนี้จึงได้ถือกำเนิดขึ้น 🎉 ซึ่งบอกได้เลยว่า แม้แต่บล็อกนี้เอง ตอนที่ลงมือทำก็ยังได้เรียนรู้เทคนิคใหม่ ๆ เช่นกัน (ไว้จะมาเล่าให้ฟังในบทความถัดไปน้า)&lt;/p&gt;
&lt;p&gt;และนี่เองคือจุดเริ่มต้นของพื้นที่เล็ก ๆ แห่งนี้ หวังว่าจะเป็นพื้นที่ที่อบอุ่นและเป็นประโยชน์ต่อทุก ๆ คนนะ 😊&lt;/p&gt;
&lt;h2&gt;☕ จะได้เจอกับบทความแบบไหน ?&lt;/h2&gt;
&lt;p&gt;ในบล็อกนี้คุณจะได้เจอกับบทความที่หลากหลายเกี่ยวกับการพัฒนาเว็บไซต์ทั้งหมด ตามประสบการณ์ของเราที่ผ่านมา ไม่ว่าจะเป็น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;h3&gt;เทคนิคการพัฒนาเว็บไซต์&lt;/h3&gt;
ไม่ว่าจะเป็นการใช้งาน Framework ยอดนิยมทั้ง Frontend และ Backend, การใช้งาน infrastructure เพื่อการ deploy หรือการทดสอบระบบด้วยเครื่องมือต่าง ๆ เพื่อให้ระบบมีความเสถียรและปลอดภัย&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;ประสบการณ์การทำงานจริงที่อยากแบ่งปัน&lt;/h3&gt;
เรื่องราวที่พบเจอ ปัญหาที่ไม่คาดคิด หรือแนวทางในการเพิ่ม productivity ต่าง ๆ ที่ได้จากการศึกษาและลองผิดลองถูกมา&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;ข่าวสารหรือบทความต่าง ๆ ที่เป็นประโยชน์&lt;/h3&gt;
ไม่ว่าจะเป็น AI ที่กำลังมาแรงหรือเกร็ดความรู้ต่าง ๆ ที่บังเอิญผ่านไปเจอ เราจะรวบรวมไว้ให้ได้แวะเวียนมาอ่านกัน&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ซึ่งเอาเข้าจริงแล้วต้องบอกว่า บล็อกนี้ไม่ได้มีหัวข้อที่ชัดเจน แต่จะเน้นเขียนตามอารมณ์และประสบการณ์ของเรามากกว่า แต่ขอสัญญาว่าทุก ๆ บทความจะถูกเขียนผ่านความตั้งใจและเป็นประโยชน์ต่อผู้อ่านแน่นอน 🙏&lt;/p&gt;
&lt;h2&gt;☕ ถ้าใครผ่านมาแล้วอยากคุย&lt;/h2&gt;
&lt;p&gt;เราเชื่อว่าการพูดคุยกันระหว่างนักพัฒนา (หรือใครก็ตามที่หลงมาเจอบล็อกนี้) เป็นสิ่งที่ดีและมีพลังอย่างไม่น่าเชื่อต่อการทำงานและการแก้ปัญหาต่าง ๆ&lt;/p&gt;
&lt;p&gt;ไม่ว่าคุณจะมีคำถาม มีไอเดีย อยากเล่าเรื่องราวของตัวเอง หรือมีหัวข้อไหนที่อยากให้เราเขียนออกมาเป็นบทความ ก็สามารถพูดคุยกันได้ตามช่องทางต่าง ๆ ของเราได้เลย เรายินดีที่จะรับฟังและพูดคุยกับทุก ๆ คนจริง ๆ&lt;/p&gt;
&lt;p&gt;เราเชื่อว่าบล็อกนี้จะพัฒนาและมีความหมายมากยิ่งขึ้น ถ้าเราได้พูดคุยกัน&lt;/p&gt;
&lt;p&gt;สุดท้ายนี้ เราขอขอบคุณทุกท่านที่แวะเข้ามาในพื้นที่เล็ก ๆ แห่งนี้จริง ๆ ขอให้กาแฟดี ๆ ช่วยเติมพลังและทำให้ทุกวันของคุณกลายเป็นวันที่ดี แล้วเจอกันในบทความถัดไป... 🥰&lt;/p&gt;</content:encoded><h:img src="/_astro/cover._7NclrlN.jpg"/><enclosure url="/_astro/cover._7NclrlN.jpg"/></item></channel></rss>