Tuyên bố miễn trừ trách nhiệm: sau đây là mô tả về cách tôi hiểu các mẫu giống như MVC trong ngữ cảnh của các ứng dụng web dựa trên PHP. Tất cả các liên kết bên ngoài được sử dụng trong nội dung đều có để giải thích các thuật ngữ và khái niệm và không ngụ ý uy tín của riêng tôi về chủ đề này.
Điều đầu tiên mà tôi phải làm rõ là: mô hình là một lớp .
Thứ hai: có một sự khác biệt giữa MVC cổ điển và những gì chúng ta sử dụng trong phát triển web. Đây là một chút câu trả lời cũ hơn mà tôi đã viết, mô tả ngắn gọn về sự khác biệt của chúng.
Mô hình là gì KHÔNG:
Mô hình không phải là một lớp hoặc bất kỳ đối tượng nào. Đó là một lỗi rất phổ biến (tôi cũng vậy, mặc dù câu trả lời ban đầu được viết khi tôi bắt đầu học theo cách khác) , bởi vì hầu hết các khuôn khổ đều duy trì quan niệm sai lầm này.
Đây không phải là một kỹ thuật Ánh xạ quan hệ đối tượng (ORM) cũng không phải là sự trừu tượng hóa của các bảng cơ sở dữ liệu. Bất cứ ai nói với bạn khác rất có thể đang cố gắng 'bán' một ORM hoàn toàn mới hoặc toàn bộ khung.
Mô hình là gì:
Trong sự thích nghi MVC thích hợp, M chứa tất cả các logic kinh doanh tên miền và các mẫu lớp là chủ yếu được làm từ ba loại cấu trúc:
Đối tượng miền
Một đối tượng miền là một thùng chứa logic của thông tin miền thuần túy; nó thường đại diện cho một thực thể logic trong không gian miền vấn đề. Thường được gọi là logic kinh doanh .
Đây sẽ là nơi bạn xác định cách xác thực dữ liệu trước khi gửi hóa đơn hoặc để tính tổng chi phí của đơn hàng. Đồng thời, các Đối tượng Miền hoàn toàn không biết về lưu trữ - không phải từ đâu (cơ sở dữ liệu SQL, API REST, tệp văn bản, v.v.) cũng như ngay cả khi chúng được lưu hoặc truy xuất.
Trình ánh xạ dữ liệu
Những đối tượng này chỉ chịu trách nhiệm cho việc lưu trữ. Nếu bạn lưu trữ thông tin trong cơ sở dữ liệu, đây sẽ là nơi SQL sống. Hoặc có thể bạn sử dụng tệp XML để lưu trữ dữ liệu và Trình ánh xạ dữ liệu của bạn đang phân tích cú pháp từ và sang tệp XML.
Dịch vụ
Bạn có thể coi chúng là "Đối tượng miền cấp cao hơn", nhưng thay vì logic nghiệp vụ , Dịch vụ chịu trách nhiệm tương tác giữa Đối tượng miền và Trình ánh xạ . Các cấu trúc này cuối cùng tạo ra một giao diện "công khai" để tương tác với logic nghiệp vụ miền. Bạn có thể tránh chúng, nhưng với hình phạt rò rỉ một số logic miền vào Bộ điều khiển .
Có một câu trả lời liên quan đến chủ đề này trong câu hỏi triển khai ACL - nó có thể hữu ích.
Giao tiếp giữa lớp mô hình và các phần khác của bộ ba MVC chỉ nên diễn ra thông qua Dịch vụ . Sự tách biệt rõ ràng có một vài lợi ích bổ sung:
- nó giúp thực thi nguyên tắc trách nhiệm duy nhất (SRP)
- cung cấp thêm 'phòng ngọ nguậy' trong trường hợp logic thay đổi
- giữ cho bộ điều khiển càng đơn giản càng tốt
- đưa ra một kế hoạch chi tiết rõ ràng, nếu bạn cần một API bên ngoài
Làm thế nào để tương tác với một mô hình?
Điều kiện tiên quyết: xem các bài giảng "Nhà nước và người độc thân toàn cầu" và "Đừng tìm kiếm những điều!" từ các cuộc thảo luận về mã sạch.
Đạt được quyền truy cập vào các trường hợp dịch vụ
Đối với cả phiên bản Chế độ xem và Trình điều khiển (cái mà bạn có thể gọi: "Lớp UI") để truy cập các dịch vụ này, có hai cách tiếp cận chung:
- Bạn có thể tiêm trực tiếp các dịch vụ cần thiết vào các hàm tạo của các khung nhìn và bộ điều khiển của bạn, tốt nhất là sử dụng bộ chứa DI.
- Sử dụng một nhà máy cho các dịch vụ như một sự phụ thuộc bắt buộc cho tất cả các quan điểm và bộ điều khiển của bạn.
Như bạn có thể nghi ngờ, container DI là một giải pháp thanh lịch hơn rất nhiều (trong khi không phải là dễ nhất cho người mới bắt đầu). Hai thư viện mà tôi khuyên bạn nên xem xét cho chức năng này sẽ là thành phần DependencyInjection độc lập của Syfmony hoặc Auryn .
Cả hai giải pháp sử dụng nhà máy và bộ chứa DI sẽ cho phép bạn cũng chia sẻ các phiên bản của các máy chủ khác nhau được chia sẻ giữa bộ điều khiển được chọn và chế độ xem cho một chu kỳ đáp ứng yêu cầu đã cho.
Thay đổi trạng thái của mô hình
Bây giờ bạn có thể truy cập vào lớp mô hình trong bộ điều khiển, bạn cần bắt đầu thực sự sử dụng chúng:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
Bộ điều khiển của bạn có một nhiệm vụ rất rõ ràng: lấy đầu vào của người dùng và, dựa trên đầu vào này, thay đổi trạng thái logic kinh doanh hiện tại. Trong ví dụ này, các trạng thái được thay đổi giữa là "người dùng ẩn danh" và "người dùng đã đăng nhập".
Trình điều khiển không chịu trách nhiệm xác thực đầu vào của người dùng, vì đó là một phần của quy tắc kinh doanh và trình điều khiển chắc chắn không gọi các truy vấn SQL, giống như những gì bạn sẽ thấy ở đây hoặc ở đây (xin đừng ghét chúng, chúng bị nhầm lẫn, không phải là xấu xa).
Hiển thị cho người dùng thay đổi trạng thái.
Ok, người dùng đã đăng nhập (hoặc thất bại). Giờ thì sao?Người dùng cho biết vẫn không biết về nó. Vì vậy, bạn cần phải thực sự tạo ra một phản hồi và đó là trách nhiệm của một quan điểm.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
Trong trường hợp này, khung nhìn tạo ra một trong hai phản hồi có thể, dựa trên trạng thái hiện tại của lớp mô hình. Đối với trường hợp sử dụng khác, bạn sẽ có chế độ xem chọn các mẫu khác nhau để kết xuất, dựa trên thứ gì đó như "bài viết được chọn hiện tại".
Lớp trình bày thực sự có thể trở nên khá phức tạp, như được mô tả ở đây: Hiểu về khung nhìn MVC trong PHP .
Nhưng tôi chỉ đang tạo một API REST!
Tất nhiên, có những tình huống, khi đây là một quá mức cần thiết.
MVC chỉ là một giải pháp cụ thể cho nguyên tắc Tách biệt các mối quan tâm . MVC tách giao diện người dùng khỏi logic nghiệp vụ và trong giao diện người dùng, nó phân tách xử lý đầu vào của người dùng và bản trình bày. Điều này là rất quan trọng. Mặc dù mọi người thường mô tả nó như là một "bộ ba", nhưng nó không thực sự được tạo thành từ ba phần độc lập. Cấu trúc giống như thế này:
Điều đó có nghĩa là, khi logic của lớp trình bày của bạn gần với không tồn tại, cách tiếp cận thực tế là giữ chúng dưới dạng một lớp. Nó cũng có thể đơn giản hóa một số khía cạnh của lớp mô hình.
Sử dụng phương pháp này, ví dụ đăng nhập (đối với API) có thể được viết là:
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);
}
Mặc dù điều này không bền vững, khi bạn có logic phức tạp để hiển thị phần thân phản hồi, việc đơn giản hóa này rất hữu ích cho các tình huống tầm thường hơn. Nhưng được cảnh báo , cách tiếp cận này sẽ trở thành một cơn ác mộng, khi cố gắng sử dụng trong các cơ sở mã lớn với logic trình bày phức tạp.
Làm thế nào để xây dựng mô hình?
Vì không có một lớp "Model" nào (như đã giải thích ở trên), nên bạn thực sự không "xây dựng mô hình". Thay vào đó, bạn bắt đầu từ việc tạo Dịch vụ , có thể thực hiện một số phương pháp nhất định. Và sau đó thực hiện các đối tượng miền và Mappers .
Một ví dụ về phương thức dịch vụ:
Trong cả hai cách tiếp cận ở trên đều có phương thức đăng nhập này cho dịch vụ nhận dạng. Nó thực sự sẽ trông như thế nào. Tôi đang sử dụng một phiên bản sửa đổi một chút của cùng chức năng từ một thư viện mà tôi đã viết .. vì tôi lười biếng:
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();
}
Như bạn có thể thấy, ở mức độ trừu tượng này, không có dấu hiệu cho thấy dữ liệu được lấy từ đâu. Nó có thể là một cơ sở dữ liệu, nhưng nó cũng có thể chỉ là một đối tượng giả cho mục đích thử nghiệm. Ngay cả các trình ánh xạ dữ liệu, thực sự được sử dụng cho nó, cũng bị ẩn đi trong các private
phương thức của dịch vụ này.
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
Cách tạo ánh xạ
Để thực hiện một sự trừu tượng của sự kiên trì, trên các phương pháp linh hoạt nhất là tạo ra các trình ánh xạ dữ liệu tùy chỉnh .
Từ: sách PoEAA
Trong thực tế, chúng được thực hiện để tương tác với các lớp hoặc siêu lớp cụ thể. Hãy nói rằng bạn có Customer
và Admin
trong mã của bạn (cả hai đều thừa hưởng từ một User
siêu lớp). Cả hai có thể sẽ có một trình ánh xạ phù hợp riêng biệt, vì chúng chứa các trường khác nhau. Nhưng bạn cũng sẽ kết thúc với các hoạt động được chia sẻ và thường được sử dụng. Ví dụ: cập nhật thời gian "nhìn thấy lần cuối trực tuyến" . Và thay vì làm cho các trình ánh xạ hiện tại trở nên phức tạp hơn, cách tiếp cận thực tế hơn là có một "Trình ánh xạ người dùng" chung, chỉ cập nhật dấu thời gian đó.
Một số ý kiến bổ sung:
Bảng và mô hình cơ sở dữ liệu
Mặc dù đôi khi có mối quan hệ 1: 1: 1 trực tiếp giữa bảng cơ sở dữ liệu, Đối tượng miền và Mapper , trong các dự án lớn hơn, nó có thể ít phổ biến hơn bạn mong đợi:
Thông tin được sử dụng bởi một Đối tượng Miền duy nhất có thể được ánh xạ từ các bảng khác nhau, trong khi bản thân đối tượng đó không có sự tồn tại trong cơ sở dữ liệu.
Ví dụ: nếu bạn đang tạo báo cáo hàng tháng. Điều này sẽ thu thập thông tin từ các bảng khác nhau, nhưng không có MonthlyReport
bảng phép thuật trong cơ sở dữ liệu.
Một Mapper duy nhất có thể ảnh hưởng đến nhiều bảng.
Ví dụ: khi bạn đang lưu trữ dữ liệu từ User
đối tượng, Đối tượng miền này có thể chứa bộ sưu tập các đối tượng miền khác - Group
phiên bản. Nếu bạn thay đổi chúng và lưu trữ User
, Data Mapper sẽ phải cập nhật và / hoặc chèn các mục trong nhiều bảng.
Dữ liệu từ một Đối tượng Miền duy nhất được lưu trữ trong nhiều bảng.
Ví dụ: trong các hệ thống lớn (nghĩ: mạng xã hội cỡ trung bình), có thể thực tế khi lưu trữ dữ liệu xác thực người dùng và dữ liệu thường được truy cập tách biệt khỏi khối nội dung lớn hơn, điều này hiếm khi được yêu cầu. Trong trường hợp đó, bạn vẫn có thể có một User
lớp duy nhất , nhưng thông tin chứa trong đó sẽ phụ thuộc vào việc liệu các chi tiết đầy đủ có được tìm nạp hay không.
Đối với mỗi Đối tượng miền, có thể có nhiều hơn một trình ánh xạ
Ví dụ: bạn có một trang web tin tức với một mã được chia sẻ cho cả phần mềm quản lý và công khai. Nhưng, trong khi cả hai giao diện sử dụng cùng một Article
lớp, quản lý cần nhiều thông tin hơn trong đó. Trong trường hợp này, bạn sẽ có hai trình ánh xạ riêng biệt: "nội bộ" và "bên ngoài". Mỗi thực hiện các truy vấn khác nhau, hoặc thậm chí sử dụng các cơ sở dữ liệu khác nhau (như trong chủ hoặc nô lệ).
Một khung nhìn không phải là một mẫu
Xem các phiên bản trong MVC (nếu bạn không sử dụng biến thể MVP của mẫu) chịu trách nhiệm về logic trình bày. Điều này có nghĩa là mỗi Chế độ xem thường sẽ tung hứng ít nhất một vài mẫu. Nó lấy dữ liệu từ Lớp mẫu và sau đó, dựa trên thông tin nhận được, chọn một mẫu và đặt các giá trị.
Một trong những lợi ích bạn có được từ việc này là khả năng sử dụng lại. Nếu bạn tạo một ListView
lớp, sau đó, với mã được viết tốt, bạn có thể có cùng một lớp bàn giao việc trình bày danh sách người dùng và nhận xét bên dưới một bài viết. Bởi vì cả hai đều có logic trình bày giống nhau. Bạn chỉ cần chuyển mẫu.
Bạn có thể sử dụng các mẫu PHP gốc hoặc sử dụng một số công cụ tạo khuôn mẫu của bên thứ ba. Cũng có thể có một số thư viện của bên thứ ba, có thể thay thế hoàn toàn các phiên bản View .
Còn phiên bản cũ của câu trả lời thì sao?
Thay đổi lớn duy nhất là, cái được gọi là Model trong phiên bản cũ, thực sự là một Dịch vụ . Phần còn lại của "thư viện tương tự" theo kịp khá tốt.
Lỗ hổng duy nhất mà tôi thấy là đây sẽ là một thư viện thực sự kỳ lạ, bởi vì nó sẽ trả về cho bạn thông tin từ cuốn sách, nhưng không cho phép bạn chạm vào cuốn sách, bởi vì nếu không thì sự trừu tượng sẽ bắt đầu "rò rỉ". Tôi có thể phải nghĩ về một sự tương tự phù hợp hơn.
Mối quan hệ giữa các phiên bản View và Trình điều khiển là gì?
Cấu trúc MVC bao gồm hai lớp: ui và model. Các cấu trúc chính trong lớp UI là khung nhìn và bộ điều khiển.
Khi bạn đang làm việc với các trang web sử dụng mẫu thiết kế MVC, cách tốt nhất là có mối quan hệ 1: 1 giữa các khung nhìn và bộ điều khiển. Mỗi chế độ xem đại diện cho toàn bộ một trang trong trang web của bạn và nó có một bộ điều khiển chuyên dụng để xử lý tất cả các yêu cầu đến cho chế độ xem cụ thể đó.
Ví dụ, để đại diện cho một bài viết đã mở, bạn sẽ có \Application\Controller\Document
và \Application\View\Document
. Điều này sẽ chứa tất cả các chức năng chính cho lớp UI, khi nói đến việc xử lý các bài viết (tất nhiên bạn có thể có một số thành phần XHR không liên quan trực tiếp đến bài viết) .