ข้อจำกัดความรับผิดชอบ:ต่อไปนี้เป็นคำอธิบายว่าฉันเข้าใจรูปแบบ MVC ในบริบทของเว็บแอปพลิเคชันบน PHP ได้อย่างไร ลิงก์ภายนอกทั้งหมดที่ใช้ในเนื้อหาอยู่ที่นั่นเพื่ออธิบายคำศัพท์และแนวคิดและไม่แสดงถึงความน่าเชื่อถือของฉันในเรื่องนี้
สิ่งแรกที่ผมต้องชัดเจนขึ้นคือรูปแบบเป็นชั้น
ประการที่สอง: มีความแตกต่างระหว่างMVC แบบดั้งเดิมและสิ่งที่เราใช้ในการพัฒนาเว็บ ต่อไปนี้เป็นคำตอบที่เก่ากว่าที่ฉันเขียนซึ่งอธิบายสั้น ๆ ว่ามันแตกต่างกันอย่างไร
แบบจำลองไม่ใช่:
โมเดลไม่ใช่คลาสหรืออ็อบเจ็กต์เดี่ยวใด ๆ มันเป็นความผิดพลาดที่เกิดขึ้นบ่อยมาก(ฉันก็ทำเหมือนกันแม้ว่าคำตอบดั้งเดิมจะถูกเขียนเมื่อฉันเริ่มเรียนรู้อย่างอื่น)เพราะกรอบงานส่วนใหญ่จะทำให้ความเข้าใจผิดนี้ผิดไป
ไม่ว่าจะเป็นเทคนิคการทำแผนที่วัตถุเชิงสัมพันธ์ (ORM) หรือนามธรรมของตารางฐานข้อมูล ใครก็ตามที่บอกคุณเป็นอย่างอื่นน่าจะพยายาม'ขาย' ORM ใหม่ล่าสุดหรือกรอบทั้งหมด
แบบจำลองคืออะไร:
ในการปรับ MVC ที่เหมาะสม M จะมีตรรกะทางธุรกิจโดเมนทั้งหมดและModel Layerนั้นส่วนใหญ่ทำจากโครงสร้างสามประเภท:
วัตถุโดเมน
วัตถุโดเมนเป็นภาชนะตรรกะของข้อมูลโดเมนล้วนๆ มันมักจะแสดงถึงหน่วยงานตรรกะในพื้นที่โดเมนปัญหา ปกติจะเรียกว่าตรรกะทางธุรกิจ
นี่คือที่ที่คุณกำหนดวิธีการตรวจสอบข้อมูลก่อนส่งใบแจ้งหนี้หรือคำนวณต้นทุนรวมของการสั่งซื้อ ในเวลาเดียวกัน, วัตถุโดเมนจะสมบูรณ์ไม่ได้ตระหนักถึงการจัดเก็บ - ค่าจากที่ (ฐานข้อมูล SQL, REST API ไฟล์ข้อความ ฯลฯ ) หรือแม้กระทั่งถ้าพวกเขาได้รับการบันทึกไว้หรือดึง
Data Mappers
วัตถุเหล่านี้รับผิดชอบการจัดเก็บเท่านั้น หากคุณเก็บข้อมูลไว้ในฐานข้อมูลนี่จะเป็นที่ที่ SQL อยู่ หรือบางทีคุณอาจใช้ไฟล์ XML เพื่อเก็บข้อมูลและตัวแมปข้อมูลของคุณแยกวิเคราะห์จากและไปยังไฟล์ XML
บริการ
คุณสามารถคิดว่าพวกเขาเป็น "วัตถุระดับที่สูงขึ้นโดเมน" แต่แทนที่จะตรรกะทางธุรกิจบริการมีความรับผิดชอบในการทำงานร่วมกันระหว่างวัตถุโดเมนและMappers โครงสร้างเหล่านี้จบลงด้วยการสร้างส่วนติดต่อ "สาธารณะ" สำหรับการโต้ตอบกับตรรกะทางธุรกิจของโดเมน คุณสามารถหลีกเลี่ยงพวกเขา แต่ในบทลงโทษของการรั่วไหลตรรกะบางโดเมนเข้ามาควบคุม
มีคำตอบที่เกี่ยวข้องกับเรื่องนี้ในคำถามการใช้งาน ACL - อาจเป็นประโยชน์
การสื่อสารระหว่างเลเยอร์โมเดลและส่วนอื่น ๆ ของ MVC สามควรเกิดขึ้นผ่านบริการเท่านั้น การแยกที่ชัดเจนมีประโยชน์เพิ่มเติมบางประการ:
- ช่วยในการบังคับใช้หลักการความรับผิดชอบเดียว (SRP)
- ให้เพิ่มเติม 'ห้องเลื้อย' ในกรณีที่มีการเปลี่ยนแปลงตรรกะ
- ทำให้คอนโทรลเลอร์นั้นง่ายที่สุดเท่าที่จะทำได้
- ให้พิมพ์เขียวที่ชัดเจนหากคุณต้องการ API ภายนอก
วิธีการโต้ตอบกับแบบจำลองหรือไม่?
สิ่งที่ต้องมีก่อน:ดูการบรรยาย"รัฐทั่วโลกและซิงเกิลตัน"และ"อย่ามองหาสิ่งต่าง ๆ !" จาก Clean Code Talks
รับสิทธิ์เข้าถึงอินสแตนซ์ของบริการ
สำหรับทั้งอินสแตนซ์มุมมองและตัวควบคุม (สิ่งที่คุณสามารถโทรได้: "เลเยอร์ UI") เพื่อเข้าถึงบริการเหล่านี้มีวิธีการทั่วไปสองวิธี:
- คุณสามารถฉีดบริการที่จำเป็นใน Constructor ของมุมมองและตัวควบคุมของคุณโดยตรงโดยเฉพาะควรใช้ DI container
- การใช้โรงงานสำหรับบริการเป็นการพึ่งพาที่จำเป็นสำหรับมุมมองและตัวควบคุมทั้งหมดของคุณ
ในขณะที่คุณอาจสงสัยว่าภาชนะ DI เป็นทางออกที่สวยงามกว่ามาก ทั้งสองห้องสมุดที่ผมขอแนะนำให้พิจารณาสำหรับการทำงานนี้จะเป็นแบบสแตนด์อโลน Syfmony ขององค์ประกอบ DependencyInjectionหรือAuryn
ทั้งโซลูชันที่ใช้จากโรงงานและคอนเทนเนอร์ DI จะช่วยให้คุณสามารถแบ่งปันอินสแตนซ์ของเซิร์ฟเวอร์ต่าง ๆ เพื่อใช้ร่วมกันระหว่างตัวควบคุมที่เลือกและมุมมองสำหรับวงจรการตอบสนองคำขอที่กำหนด
การเปลี่ยนแปลงสถานะของแบบจำลอง
ตอนนี้คุณสามารถเข้าถึงเลเยอร์โมเดลในคอนโทรลเลอร์ได้แล้วคุณต้องเริ่มใช้พวกมันจริง:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
ตัวควบคุมของคุณมีงานที่ชัดเจนมาก: รับอินพุตของผู้ใช้และเปลี่ยนสถานะปัจจุบันของตรรกะทางธุรกิจ ในตัวอย่างนี้สถานะที่เปลี่ยนแปลงระหว่างนั้นคือ "ผู้ใช้ที่ไม่ระบุชื่อ" และ "ผู้ใช้ที่ล็อกอิน"
คอนโทรลเลอร์ไม่รับผิดชอบในการตรวจสอบอินพุตของผู้ใช้เนื่องจากเป็นส่วนหนึ่งของกฎเกณฑ์ทางธุรกิจและคอนโทรลเลอร์ไม่ได้เรียกคิวรี่ SQL เช่นเดียวกับที่คุณเห็นที่นี่หรือที่นี่ (โปรดอย่าเกลียดพวกเขาพวกเขาเข้าใจผิดไม่ใช่ปีศาจ)
แสดงผู้ใช้ถึงการเปลี่ยนแปลงสถานะ
ตกลงผู้ใช้เข้าสู่ระบบ (หรือล้มเหลว) ตอนนี้คืออะไร ผู้ใช้ที่กล่าวยังคงไม่รู้ตัว ดังนั้นคุณต้องสร้างการตอบสนองและนั่นคือความรับผิดชอบของมุมมอง
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
ในกรณีนี้มุมมองสร้างหนึ่งในสองคำตอบที่เป็นไปได้โดยขึ้นอยู่กับสถานะปัจจุบันของเลเยอร์โมเดล สำหรับกรณีการใช้งานที่แตกต่างกันคุณจะมีมุมมองในการเลือกเทมเพลตที่แตกต่างเพื่อแสดงผลโดยอ้างอิงจาก "บทความที่เลือกไว้ในปัจจุบัน"
ชั้นนำเสนอจะได้รับจริงค่อนข้างซับซ้อนตามที่อธิบายไว้ที่นี่: เข้าใจ MVC มุมมองใน PHP
แต่ฉันเพิ่งจะสร้าง REST API!
แน่นอนว่ามีสถานการณ์เมื่อนี่คือ overkill
MVC เป็นเพียงการแก้ปัญหาที่เป็นรูปธรรมสำหรับการแยกความกังวลเกี่ยวกับหลักการ MVC แยกส่วนติดต่อผู้ใช้ออกจากตรรกะทางธุรกิจและใน UI จะแยกการจัดการอินพุตของผู้ใช้และการนำเสนอ นี่เป็นสิ่งสำคัญ ในขณะที่คนมักจะอธิบายว่ามันเป็น "สาม" มันไม่ได้สร้างขึ้นจากสามส่วนอิสระ โครงสร้างเป็นดังนี้:
หมายความว่าเมื่อตรรกะของเลเยอร์การนำเสนอของคุณใกล้เคียงกับที่ไม่มีอยู่จริงแนวทางปฏิบัติก็คือการทำให้พวกมันเป็นเลเยอร์เดียว นอกจากนี้ยังสามารถทำให้บางแง่มุมของเลเยอร์โมเดลง่ายขึ้นอย่างมาก
การใช้วิธีการนี้ตัวอย่างการเข้าสู่ระบบ (สำหรับ API) สามารถเขียนเป็น:
public function postLogin(Request $request)
{
$email = $request->get('email');
$data = [
'status' => 'ok',
];
try {
$identity = $this->identification->findIdentityByEmailAddress($email);
$token = $this->identification->loginWithPassword(
$identity,
$request->get('password')
);
} catch (FailedIdentification $exception) {
$data = [
'status' => 'error',
'message' => 'Login failed!',
]
}
return new JsonResponse($data);
}
แม้ว่าสิ่งนี้จะไม่ยั่งยืนเมื่อคุณมีตรรกะที่ซับซ้อนสำหรับการสร้างเนื้อหาการตอบสนองความเรียบง่ายนี้มีประโยชน์มากสำหรับสถานการณ์ที่ไม่สำคัญ แต่ถูกเตือนวิธีนี้จะกลายเป็นฝันร้ายเมื่อพยายามที่จะใช้ในฐานรหัสขนาดใหญ่ที่มีตรรกะการนำเสนอที่ซับซ้อน
วิธีการสร้างแบบจำลอง?
เนื่องจากไม่มีคลาส "โมเดล" เดียว (ดังอธิบายข้างต้น) คุณไม่ได้ "สร้างโมเดล" จริงๆ แต่คุณเริ่มจากการสร้างบริการซึ่งสามารถทำวิธีการบางอย่างได้ แล้วใช้วัตถุโดเมนและMappers
ตัวอย่างของวิธีการบริการ:
ในทั้งสองวิธีข้างต้นมีวิธีการเข้าสู่ระบบนี้สำหรับบริการประจำตัวประชาชน มันจะมีลักษณะเป็นอย่างไร ฉันกำลังใช้ฟังก์ชั่นรุ่นเดียวกันที่ได้รับการแก้ไขเล็กน้อยจากห้องสมุดฉันเขียน .. เพราะฉันขี้เกียจ:
public function loginWithPassword(Identity $identity, string $password): string
{
if ($identity->matchPassword($password) === false) {
$this->logWrongPasswordNotice($identity, [
'email' => $identity->getEmailAddress(),
'key' => $password, // this is the wrong password
]);
throw new PasswordMismatch;
}
$identity->setPassword($password);
$this->updateIdentityOnUse($identity);
$cookie = $this->createCookieIdentity($identity);
$this->logger->info('login successful', [
'input' => [
'email' => $identity->getEmailAddress(),
],
'user' => [
'account' => $identity->getAccountId(),
'identity' => $identity->getId(),
],
]);
return $cookie->getToken();
}
อย่างที่คุณเห็นในระดับความเป็นนามธรรมนี้ไม่มีข้อบ่งชี้ว่าข้อมูลถูกดึงมาจากที่ใด อาจเป็นฐานข้อมูล แต่อาจเป็นเพียงวัตถุจำลองสำหรับการทดสอบ แม้แต่ตัวแมปข้อมูลที่ใช้งานจริงนั้นจะถูกซ่อนอยู่ในprivate
วิธีการของบริการนี้
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
วิธีสร้าง Mappers
ที่จะใช้เป็นนามธรรมของการคงอยู่บนแนวทางความยืดหยุ่นมากที่สุดคือการสร้างที่กำหนดเองทำแผนที่ข้อมูล
จาก: หนังสือPoEAA
ในทางปฏิบัติมีการใช้งานสำหรับการโต้ตอบกับคลาสหรือซูเปอร์คลาสที่เฉพาะเจาะจง ให้บอกว่าคุณมีCustomer
และAdmin
ในรหัสของคุณ (ทั้งสืบทอดมาจากUser
ซูเปอร์คลาส) ทั้งสองอาจจะจบลงด้วยการจับคู่ mapper แยกเนื่องจากพวกเขามีเขตข้อมูลที่แตกต่างกัน แต่คุณจะจบลงด้วยการดำเนินการที่ใช้ร่วมกันและที่ใช้กันทั่วไป ตัวอย่างเช่น: อัปเดตเวลา"ออนไลน์ล่าสุดที่เห็น" และแทนที่จะทำให้ผู้ทำแผนที่ที่มีอยู่มีความเชื่อมั่นมากขึ้นวิธีการที่เป็นประโยชน์มากขึ้นก็คือการมี "User Mapper" ทั่วไปซึ่งอัพเดตเฉพาะการประทับเวลานั้น
ความคิดเห็นเพิ่มเติมบางส่วน:
ตารางและโมเดลฐานข้อมูล
ในขณะที่บางครั้งมีความสัมพันธ์โดยตรง 1: 1: 1 ระหว่างตารางฐานข้อมูล, Domain ObjectและMapperในโครงการขนาดใหญ่อาจมีน้อยกว่าที่คุณคาดไว้:
ข้อมูลที่ใช้โดยวัตถุโดเมนเดียวอาจถูกแมปจากตารางที่แตกต่างกันในขณะที่วัตถุนั้นไม่มีอยู่ในฐานข้อมูล
ตัวอย่าง:หากคุณกำลังสร้างรายงานรายเดือน สิ่งนี้จะรวบรวมข้อมูลจากตารางที่แตกต่างกัน แต่ไม่มีMonthlyReport
ตารางขลังในฐานข้อมูล
Mapperเดียวสามารถส่งผลกระทบต่อหลายตาราง
ตัวอย่าง:เมื่อคุณเก็บข้อมูลจากUser
วัตถุวัตถุโดเมนนี้อาจมีการรวบรวมวัตถุโดเมนอื่น ๆ - Group
อินสแตนซ์ หากคุณดัดแปลงพวกเขาและเก็บUser
ที่Mapper ข้อมูลจะมีการปรับปรุงและ / หรือแทรกรายการในตารางหลาย
ข้อมูลจากวัตถุโดเมนเดียวจะถูกเก็บไว้ในมากกว่าหนึ่งตาราง
ตัวอย่าง:ในระบบขนาดใหญ่ (คิดว่า: เครือข่ายทางสังคมขนาดกลาง) อาจเป็นวิธีที่ใช้ในการจัดเก็บข้อมูลการตรวจสอบผู้ใช้และข้อมูลที่เข้าถึงบ่อยแยกต่างหากจากเนื้อหาขนาดใหญ่ซึ่งไม่ค่อยจำเป็น ในกรณีนี้คุณยังอาจมีUser
ชั้นเดียวแต่ข้อมูลนั้นจะขึ้นอยู่กับว่ามีการดึงรายละเอียดทั้งหมด
สำหรับทุกDomain Objectจะมีผู้ทำแผนที่มากกว่าหนึ่งคน
ตัวอย่าง:คุณมีเว็บไซต์ข่าวที่มีรหัสที่ใช้ร่วมกันสำหรับทั้งสาธารณะและซอฟต์แวร์การจัดการ แต่ในขณะที่อินเทอร์เฟซทั้งสองใช้Article
คลาสเดียวกันการจัดการต้องการข้อมูลเพิ่มเติมที่บรรจุอยู่ในนั้น ในกรณีนี้คุณจะมีผู้ทำแผนที่สองคนแยกกัน: "ภายใน" และ "ภายนอก" แต่ละแบบสอบถามที่มีประสิทธิภาพแตกต่างกันหรือแม้กระทั่งใช้ฐานข้อมูลที่แตกต่างกัน (เช่นในต้นแบบหรือทาส)
มุมมองไม่ใช่แม่แบบ
ดูอินสแตนซ์ใน MVC (หากคุณไม่ได้ใช้รูปแบบ MVP ของรูปแบบ) รับผิดชอบต่อตรรกะเชิงการนำเสนอ ซึ่งหมายความว่าแต่ละมุมมองจะเล่นปาหี่อย่างน้อยสองสามเทมเพลต ได้รับข้อมูลจากModel Layerจากนั้นตามข้อมูลที่ได้รับเลือกแม่แบบและตั้งค่า
ประโยชน์อย่างหนึ่งที่คุณได้รับจากการใช้งานนี้คือ หากคุณสร้างListView
คลาสจากนั้นด้วยรหัสที่ดีคุณสามารถให้คลาสเดียวกันส่งงานนำเสนอรายการผู้ใช้และความคิดเห็นด้านล่างบทความ เพราะทั้งคู่มีตรรกะการนำเสนอเหมือนกัน คุณเพียงแค่สลับเทมเพลต
คุณสามารถใช้เทมเพลต PHP ดั้งเดิมหรือใช้เครื่องมือสร้างเทมเพลตของ บริษัท อื่น นอกจากนี้ยังอาจมีห้องสมุดบุคคลที่สามซึ่งสามารถแทนที่ดูอินสแตนซ์ได้อย่างสมบูรณ์
แล้วคำตอบรุ่นเก่าล่ะ?
สิ่งเดียวที่เปลี่ยนแปลงที่สำคัญคือสิ่งที่เรียกว่ารุ่นในรุ่นเก่าเป็นจริงบริการ ส่วนที่เหลือของ "การเปรียบเทียบของห้องสมุด" นั้นค่อนข้างดี
ข้อบกพร่องเพียงอย่างเดียวที่ฉันเห็นคือมันจะเป็นห้องสมุดที่แปลกจริง ๆ เพราะมันจะคืนข้อมูลให้คุณจากหนังสือ แต่ไม่อนุญาตให้คุณสัมผัสหนังสือเองเพราะมิฉะนั้นสิ่งที่เป็นนามธรรมจะเริ่มเป็น ฉันอาจต้องคิดถึงการเปรียบเทียบที่เหมาะสมกว่านี้
ความสัมพันธ์ระหว่างอินสแตนซ์มุมมองและตัวควบคุมคืออะไร
โครงสร้าง MVC ประกอบด้วยสองชั้น: ui และรุ่น โครงสร้างหลักในเลเยอร์ UIคือมุมมองและตัวควบคุม
เมื่อคุณจัดการกับเว็บไซต์ที่ใช้รูปแบบการออกแบบ MVC วิธีที่ดีที่สุดคือมีความสัมพันธ์แบบ 1: 1 ระหว่างมุมมองและตัวควบคุม แต่ละมุมมองแสดงทั้งหน้าในเว็บไซต์ของคุณและมีตัวควบคุมเฉพาะเพื่อจัดการคำขอขาเข้าทั้งหมดสำหรับมุมมองนั้น
ตัวอย่างเช่นในการเป็นตัวแทนของบทความเปิดคุณจะต้องและ\Application\Controller\Document
\Application\View\Document
นี้จะมีทุกฟังก์ชั่นหลักสำหรับชั้น UI เมื่อมันมาถึงการจัดการกับบทความ(แน่นอนคุณอาจมีบางXHRส่วนประกอบที่ไม่ได้เกี่ยวข้องโดยตรงกับบทความ)