면책 조항 : 다음은 PHP 기반 웹 응용 프로그램의 맥락에서 MVC와 같은 패턴을 이해하는 방법에 대한 설명입니다. 내용에 사용 된 모든 외부 링크는 용어와 개념을 설명하기 위해 있으며 주제에 대한 내 자신의 신뢰성을 암시 하지 않습니다 .
가장 먼저 정리해야 할 것은 모델은 레이어 입니다.
둘째 : 클래식 MVC 와 웹 개발에 사용하는 것에는 차이 가 있습니다. 여기에 내가 쓴 오래된 답변이 있는데, 어떻게 다른지 간단히 설명합니다.
모델이 아닌 것 :
모델은 클래스 또는 단일 객체가 아닙니다. 대부분의 프레임 워크가 이러한 오해를 영속시키기 때문에 (나도 그렇게 배우기 시작했을 때 원래의 대답이 쓰여졌지만) 그렇게 하는 것은 매우 일반적인 실수 입니다.
ORM (Object-Relational Mapping) 기술이나 데이터베이스 테이블의 추상화도 아닙니다. 달리 말하면 누구나 다른 새로운 ORM 또는 전체 프레임 워크 를 '판매' 하려고 할 것 입니다.
모델이란?
적절한 MVC 적응에서 M은 모든 도메인 비즈니스 로직을 포함하며 모델 계층 은 주로 세 가지 유형의 구조로 구성됩니다.
도메인 객체
도메인 객체는 순수한 도메인 정보의 논리적 컨테이너입니다. 일반적으로 문제점 도메인 공간의 논리 엔티티를 나타냅니다. 일반적으로 비즈니스 로직 이라고합니다 .
여기에서 송장을 보내기 전에 데이터의 유효성을 검사하거나 주문의 총 비용을 계산하는 방법을 정의 할 수 있습니다. 동시에, 도메인 객체가 저장 전혀 모르고있다 -도에서 경우 (SQL 데이터베이스, REST API를, 텍스트 파일 등)도 심지어는 경우 가 저장하거나 검색하세요.
데이터 매퍼
이러한 객체는 스토리지에만 책임이 있습니다. 데이터베이스에 정보를 저장하면 SQL이있는 곳이됩니다. 또는 XML 파일을 사용하여 데이터를 저장하고 데이터 매퍼 가 XML 파일에서 구문 분석하고 있습니다.
서비스
이를 "상위 레벨 도메인 오브젝트"로 생각할 수 있지만 비즈니스 로직 대신 서비스 는 도메인 오브젝트 와 맵퍼 간의 상호 작용을 담당합니다 . 이러한 구조는 결국 도메인 비즈니스 로직과 상호 작용하기위한 "공용"인터페이스를 작성합니다. 이를 피할 수는 있지만 일부 도메인 로직을 컨트롤러 로 유출하는 경우가 있습니다 .
ACL 구현 질문 에이 주제에 대한 관련 답변이 있습니다. 유용 할 수 있습니다.
모델 계층과 MVC 트라이어드의 다른 부분 간의 통신은 서비스를 통해서만 이루어져야 합니다 . 명확한 분리는 몇 가지 추가 이점이 있습니다.
- 단일 책임 원칙 (SRP) 을 시행하는 데 도움이됩니다.
- 논리가 변경되는 경우 추가 '흔들기 방'제공
- 컨트롤러를 최대한 간단하게 유지
- 외부 API가 필요한 경우 명확한 청사진을 제공합니다.
모델과 상호 작용하는 방법?
전제 조건 : "글로벌 스테이트 및 싱글 톤" 및 "사물을 찾지 마십시오!" 강의 시청 클린 코드 대화에서.
서비스 인스턴스에 대한 액세스 권한 얻기
이러한 서비스에 액세스하기 위해 View 및 Controller 인스턴스 ( "UI 계층"이라고 부름)에 대해 두 가지 일반적인 접근 방식이 있습니다.
- DI 컨테이너를 사용하여 뷰 및 컨트롤러의 생성자에 필요한 서비스를 직접 삽입 할 수 있습니다.
- 모든 뷰 및 컨트롤러에 대한 필수 종속성으로 서비스 팩토리를 사용합니다.
의심 할 수 있듯이 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')
);
}
컨트롤러는 매우 명확한 작업을 수행합니다. 사용자 입력을 취하고이 입력을 기반으로 비즈니스 로직의 현재 상태를 변경합니다. 이 예에서 사이에 변경된 상태는 "익명 사용자"및 "로그인 한 사용자"입니다.
Controller는 사용자 입력의 유효성을 검사 할 책임이 없습니다. 비즈니스 규칙의 일부이고 Controller가 여기 또는 여기에서 볼 수있는 것과 같은 SQL 쿼리를 호출 하지 않기 때문입니다.
사용자에게 상태 변경을 표시합니다.
사용자가 로그인했거나 실패했습니다. 이제 뭐? 상기 사용자는 여전히 그것을 알지 못한다. 따라서 실제로 응답을 생성해야하며 이는 뷰의 책임입니다.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
이 경우 뷰는 현재 모델 계층 상태에 따라 두 가지 가능한 응답 중 하나를 생성했습니다. 다른 유스 케이스의 경우 "현재 선택된 기사"와 같은 것을 기반으로 렌더링 할 다른 템플리트를 선택하는보기가 있습니다.
프리젠 테이션 레이어는 실제로 PHP에서 MVC보기 이해에 설명 된대로 상당히 정교해질 수 있습니다 .
그러나 나는 단지 REST API를 만들고 있습니다!
물론 이것이 과잉 상태 인 상황이 있습니다.
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);
}
이것이 지속 가능하지는 않지만 응답 본문을 렌더링하기위한 복잡한 논리가있는 경우이 단순화는보다 사소한 시나리오에 매우 유용합니다. 그러나 경고 복잡한 프리젠 테이션 로직 큰 코드베이스에서 사용하려고 할 때이 방법은, 악몽이 될 것이다.
모델을 구축하는 방법?
위에서 설명한 것처럼 단일 "Model"클래스가 없기 때문에 실제로 "모델을 빌드하지"마십시오. 대신 특정 방법을 수행 할 수있는 서비스 만들기부터 시작하십시오 . 그런 다음 Domain Objects and 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);
}
매퍼를 만드는 방법
지속성의 추상화를 구현하기 위해 가장 유연한 접근 방식은 사용자 정의 데이터 맵퍼 를 작성하는 것 입니다.
부터 : PoEAA book
실제로는 특정 클래스 또는 수퍼 클래스와의 상호 작용을 위해 구현됩니다. 당신이 말할 수 있습니다 Customer
및 Admin
코드에서 (모두에서 상속 User
슈퍼 클래스). 둘 다 서로 다른 필드를 포함하기 때문에 아마도 별도의 일치하는 매퍼를 갖게 될 것입니다. 그러나 공유되고 일반적으로 사용되는 작업으로 끝납니다. 예 : "마지막 온라인" 시간 업데이트 기존 매퍼를보다 복잡하게 만드는 대신보다 실용적인 접근 방식은 일반적인 "사용자 매퍼"를 사용하여 해당 타임 스탬프 만 업데이트하는 것입니다.
추가 의견 :
데이터베이스 테이블 및 모델
데이터베이스 테이블, Domain Object 및 Mapper 간에 직접적인 1 : 1 : 1 관계가있는 경우가 많지만 대규모 프로젝트에서는 예상보다 덜 일반적 일 수 있습니다.
단일 도메인 개체 가 사용하는 정보 는 다른 테이블에서 매핑 될 수 있지만 개체 자체는 데이터베이스에 지속성이 없습니다.
예 : 월간 보고서를 생성하는 경우 이것은 다른 테이블에서 정보를 수집하지만 MonthlyReport
데이터베이스 에는 마법의 테이블 이 없습니다 .
단일 매퍼 는 여러 테이블에 영향을 줄 수 있습니다.
예 :User
객체의 데이터를 저장하는 경우이 도메인 객체 에는 다른 도메인 객체 ( Group
인스턴스) 모음이 포함될 수 있습니다 . 당신이 그들을 변경하고 저장할 경우 User
의 데이터 매퍼 갱신 및 / 또는 여러 테이블에서 항목을 삽입해야합니다.
단일 도메인 개체의 데이터 는 둘 이상의 테이블에 저장됩니다.
예 : 대규모 시스템 (생각 : 중간 규모의 소셜 네트워크)에서는 사용자 인증 데이터와 자주 액세스하는 데이터를 더 큰 콘텐츠 청크와 별도로 저장하는 것이 실용적 일 수 있습니다. 이 경우 여전히 단일 User
클래스 가있을 수 있지만 포함 된 정보는 전체 세부 사항을 가져 왔는지 여부에 따라 다릅니다.
모든 도메인 개체에 대해 둘 이상의 매퍼가있을 수 있습니다
예 : 공용 소프트웨어와 관리 소프트웨어를위한 공유 코드 기반 뉴스 사이트가 있습니다. 그러나 두 인터페이스 모두 동일한 Article
클래스를 사용하지만 관리에는 더 많은 정보가 필요합니다. 이 경우 "내부"와 "외부"의 두 가지 별도 맵퍼가 있습니다. 각각 다른 쿼리를 수행하거나 심지어 다른 데이터베이스를 사용합니다 (마스터 또는 슬레이브에서와 같이).
보기는 템플릿이 아닙니다
MVC의 인스턴스 보기 (패턴의 MVP 변형을 사용하지 않는 경우)는 프리젠 테이션 논리를 담당합니다. 즉, 각 보기 는 일반적으로 최소한 몇 개의 템플릿을 저글링합니다. 모델 레이어 에서 데이터를 획득 한 다음 수신 된 정보를 기반으로 템플릿을 선택하고 값을 설정합니다.
이로부터 얻을 수있는 이점 중 하나는 재사용 성입니다. 당신이 만드는 경우 ListView
잘 작성된 코드를 다음 클래스를, 당신은 동일한 클래스 나눠에게 사용자 목록과 기사 아래 댓글 프리젠 테이션을 할 수 있습니다. 둘 다 동일한 프리젠 테이션 로직을 가지고 있기 때문입니다. 템플릿 만 전환하면됩니다.
네이티브 PHP 템플릿을 사용하거나 타사 템플릿 엔진을 사용할 수 있습니다 . View 인스턴스 를 완전히 대체 할 수있는 타사 라이브러리도있을 수 있습니다 .
이전 버전의 답변은 어떻습니까?
유일한 주요 변경 사항은 이전 버전에서 Model 이라는 모델 이 실제로 서비스라는 것 입니다. "라이브러리 비유"의 나머지 부분은 꽤 잘 유지됩니다.
내가 볼 수있는 유일한 결함은 이것이 도서관에서 당신에게 정보를 반환 할 것이기 때문에 실제로 이상한 도서관 일 것입니다. 그렇지 않으면 추상화 자체가 "누설"시작하기 때문에 책 자체를 만지지 마십시오. 더 적합한 유추를 생각해야 할 수도 있습니다.
View 와 Controller 인스턴스 의 관계는 무엇입니까 ?
MVC 구조는 ui와 model의 두 계층으로 구성됩니다. UI 계층 의 주요 구조 는 뷰와 컨트롤러입니다.
MVC 디자인 패턴을 사용하는 웹 사이트를 처리 할 때 가장 좋은 방법은 뷰와 컨트롤러간에 1 : 1 관계를 유지하는 것입니다. 각보기는 웹 사이트의 전체 페이지를 나타내며 해당 특정보기에 대한 모든 수신 요청을 처리하는 전용 컨트롤러가 있습니다.
예를 들어, 열린 기사를 나타내려면 \Application\Controller\Document
및 이 있어야 \Application\View\Document
합니다. 여기에는 기사를 처리 할 때 UI 계층의 모든 주요 기능이 포함됩니다 (물론 기사와 직접 관련이없는 일부 XHR 구성 요소가 있을 수 있음 ) .