복붙노트

MVC에서 모델을 어떻게 구성해야합니까?

PHP

MVC에서 모델을 어떻게 구성해야합니까?

MVC 프레임 워크에 대해 이해하고 있으며 모델에 얼마나 많은 코드가 있어야하는지 궁금합니다. 나는 다음과 같은 메소드를 가진 데이터 접근 클래스를 갖는 경향이있다.

public function CheckUsername($connection, $username)
{
    try
    {
        $data = array();
        $data['Username'] = $username;

        //// SQL
        $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";

        //// Execute statement
        return $this->ExecuteObject($connection, $sql, $data);
    }
    catch(Exception $e)
    {
        throw $e;
    }
}

내 모델은 데이터베이스 테이블에 매핑되는 엔티티 클래스 인 경향이 있습니다.

모델 객체는 위의 코드뿐만 아니라 모든 데이터베이스 매핑 된 속성을 가지고 있어야합니까? 아니면 실제로 데이터베이스를 작동시키는 코드를 분리해도 괜찮습니까?

내가 4 개의 레이어를 갖게 될까?

해결법

  1. ==============================

    1.

    내가 정리해야 할 첫 번째 사항은 모델이 계층이라는 것입니다.

    둘째 : 고전적인 MVC와 우리가 웹 개발에서 사용하는 것과는 차이가 있습니다. 여기에 내가 쓴 이전 답변의 비트가 있습니다. 간단히 말해서 어떻게 다른지 설명합니다.

    모델은 클래스 또는 단일 객체가 아닙니다. 대부분의 프레임 워크가 이러한 오해를 영속시키기 때문에 (나는 원래의 대답이 내가 다른 방법으로 배우기 시작했을 때 쓰여졌다.) 그렇게하는 것은 매우 흔한 실수이다.

    ORM (Object-Relational Mapping) 기술도 아니고 데이터베이스 테이블의 추상화도 아닙니다. 달리 말하면 ORM 또는 전체 프레임 워크를 '판매'하려고 할 가능성이 큽니다.

    적절한 MVC 적용에서 M은 모든 도메인 비즈니스 로직을 포함하고 Model Layer는 주로 세 가지 유형의 구조로 이루어집니다.

    모델 레이어와 MVC 트라이어드의 다른 부분 간의 통신은 서비스를 통해서만 이루어져야합니다. 명확한 분리에는 몇 가지 추가 이점이 있습니다.

     

    이러한 서비스에 액세스하기 위해 View 및 Controller 인스턴스 ( "UI 레이어"라고 부르는 항목)의 경우 두 가지 일반적인 접근 방식이 있습니다.

    의심 스럽지만 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')
        );
    }
    

    컨트롤러에는 매우 명확한 작업이 있습니다. 사용자 입력을 받아이 입력을 기반으로 비즈니스 논리의 현재 상태를 변경합니다. 이 예제에서 변경되는 상태는 "anonymous user"와 "logged in user"입니다.

    컨트롤러는 비즈니스 규칙의 일부이며 컨트롤러가 여기 또는 여기에서 볼 수있는 것과 같은 SQL 쿼리를 확실히 호출하지 않기 때문에 사용자 입력의 유효성을 검사 할 책임이 없습니다 (증오하지 말고 오해하지 마세요).

    좋아, 사용자가 로그인했거나 실패했습니다. 이제 뭐? 상기 사용자는 여전히 그것을 인식하지 못합니다. 따라서 실제로 응답을 생성해야하며 이는보기의 책임입니다.

    public function postLogin()
    {
        $path = '/login';
        if ($this->identification->isUserLoggedIn()) {
            $path = '/dashboard';
        }
        return new RedirectResponse($path); 
    }
    

    이 경우 뷰는 모델 계층의 현재 상태를 기반으로 두 가지 가능한 응답 중 하나를 생성합니다. 다른 유스 케이스의 경우 "현재 선택한 기사"와 같은 것을 기반으로 렌더링 할 다른 템플릿을 선택하는보기를 갖게됩니다.

    프리젠 테이션 레이어는 실제로 PHP에서 MVC 뷰 이해와 같이 매우 정교해질 수 있습니다.

    물론, 과잉 상황 일 때 상황이 있습니다.

    MVC는 Separation of Concerns 원칙에 대한 구체적인 솔루션 일뿐입니다. MVC는 사용자 인터페이스와 비즈니스 로직을 분리하고 UI에서 사용자 입력 및 표현 처리를 분리했습니다. 이것은 매우 중요합니다. 종종 사람들은 이것을 "트라이어드"로 묘사하지만 사실 3 개의 독립적 인 부분으로 구성되지는 않습니다. 구조는 다음과 같습니다.

    즉, 프리젠 테이션 레이어의 로직이 존재하지 않을 때 실용적인 접근법은 단일 레이어로 유지하는 것입니다. 또한 모델 계층의 일부 측면을 상당히 단순화 할 수 있습니다.

    이 접근 방식을 사용하면 로그인 예 (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);
    }
    

    이것이 지속 가능하지는 않지만 응답 본문을 렌더링하기위한 논리가 복잡 할 때이 단순화는보다 사소한 시나리오에 매우 유용합니다. 그러나 복잡한 프레젠테이션 로직이 포함 된 대형 코드베이스에서이 방법을 사용하면 악몽이 될 수 있습니다.

     

    위에서 설명한 것처럼 하나의 "모델"클래스가 없으므로 실제로 모델을 빌드하지 않습니다. 대신 특정 방법을 수행 할 수있는 서비스를 만들기 시작합니다. 그리고 나서 Domain Objects와 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 function changeIdentityStatus(Entity\Identity $identity, int $status)
    {
        $identity->setStatus($status);
        $identity->setLastUsed(time());
        $mapper = $this->mapperFactory->create(Mapper\Identity::class);
        $mapper->store($identity);
    }
    

    지속성의 추상화를 구현하기 위해 가장 유연한 접근 방식은 맞춤 데이터 매퍼를 만드는 것입니다.

    보낸 사람 : PoEAA 책

    실제로는 특정 클래스 나 수퍼 클래스와의 상호 작용을 위해 구현됩니다. 코드에서 Customer와 Admin (둘 다 User 수퍼 클래스에서 상속받는)이 있다고 가정 해 보겠습니다. 두 필드 모두 서로 다른 필드를 포함하고 있기 때문에 둘 다 결국 매퍼마다 별도의 일치하는 매퍼를 갖게됩니다. 그러나 공유되고 자주 사용되는 작업으로 끝나게됩니다. 예 : "최근 본 온라인"시간 업데이트. 기존 매퍼를 좀 더 복잡하게 만드는 대신 실용적인 접근법은 일반적인 "User Mapper"를 사용하는 것입니다.이 방법은 해당 타임 스탬프 만 업데이트합니다.

  2. ==============================

    2.

    비즈니스 로직 인 모든 것이 데이터베이스 질의, 계산, REST 호출 등 모델에 속합니다.

    모델 자체에서 데이터 액세스를 가질 수 있지만 MVC 패턴은 그렇게하지 못하도록 제한하지 않습니다. 당신은 서비스, 매퍼 (mappers)로 설탕을 칠할 수 있지만 모델의 실제 정의는 비즈니스 로직을 처리하는 계층입니다. 클래스, 함수, 또는 원하는 경우 gazillion 오브젝트가있는 완전한 모듈이 될 수 있습니다.

    모델에서 직접 실행되는 대신 실제로 데이터베이스 쿼리를 실행하는 별도의 개체를 갖는 것이 더 쉽습니다. 모델 쿼리가 모델에 모의 데이터베이스 종속성을 주입하기 쉽기 때문에 특히 유용합니다.

    class Database {
       protected $_conn;
    
       public function __construct($connection) {
           $this->_conn = $connection;
       }
    
       public function ExecuteObject($sql, $data) {
           // stuff
       }
    }
    
    abstract class Model {
       protected $_db;
    
       public function __construct(Database $db) {
           $this->_db = $db;
       }
    }
    
    class User extends Model {
       public function CheckUsername($username) {
           // ...
           $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
           return $this->_db->ExecuteObject($sql, $data);
       }
    }
    
    $db = new Database($conn);
    $model = new User($db);
    $model->CheckUsername('foo');
    

    또한 PHP에서는 역 추적을 유지하기 때문에 예외를 catch / rethrow 할 필요가 거의 없습니다 (특히 예제와 같은 경우). 그냥 예외를 throw하고 대신 컨트롤러에서 잡으십시오.

  3. ==============================

    3.

    웹 - "MVC"에서는 무엇이든 할 수 있습니다.

    원래 개념 (1)은 모델을 비즈니스 로직으로 설명했습니다. 애플리케이션 상태를 나타내야하고 데이터 일관성을 유지해야합니다. 이러한 접근 방식은 흔히 "뚱뚱한 모델"이라고 표현됩니다.

    대부분의 PHP 프레임 워크는 모델이 데이터베이스 인터페이스에 불과한보다 얕은 접근 방식을 따릅니다. 그러나 적어도이 모델은 들어오는 데이터와 관계를 검증해야합니다.

    어느 쪽이든, SQL 항목이나 데이터베이스 호출을 다른 계층으로 분리하면 그리 멀지 않습니다. 이렇게하면 실제 저장소 API가 아닌 실제 데이터 / 동작에만 신경 써야합니다. 그러나이를 과장하는 것은 무리가 있습니다. 예를 들어 데이터베이스 백엔드가 앞으로 설계되지 않은 경우 파일 저장소로 교체 할 수 없습니다.

  4. ==============================

    4.

    보다 자주 응용 프로그램의 대부분은 데이터, 표시 및 처리 부분을 가지며 모든 문자를 M, V 및 C 문자로 나타냅니다.

    Model (M) -> 응용 프로그램의 상태를 유지하는 속성을 가지고 있으며 V 및 C에 대해 어떤 것도 모릅니다.

    View (V) -> 응용 프로그램의 형식을 표시하고 C에서 소화기 모델에 대해서만 알고 C에 대해 신경 쓰지 않습니다.

    컨트롤러 (C) ----> M과 V 사이의 배선 역할을하며 M과 V와는 달리 M, V에 의존합니다.

    전체적으로 각각의 관심사가 분리되어 있습니다. 앞으로는 변경이나 개선 사항을 매우 쉽게 추가 할 수 있습니다.

  5. ==============================

    5.

    제 경우에는 쿼리, 페칭 (fetch) 등과 같은 모든 직접적인 데이터베이스 상호 작용을 처리하는 데이터베이스 클래스가 있습니다. 그래서 MySQL에서 PostgreSQL으로 데이터베이스를 변경해야한다면 아무런 문제가 없을 것입니다. 따라서 추가 레이어를 추가하는 것이 유용 할 수 있습니다.

    각 테이블은 고유 한 클래스를 가질 수 있고 특정 메소드를 가질 수 있지만 실제로 데이터를 얻으려면 데이터베이스 클래스에서이를 처리 할 수 ​​있습니다.

    class Database {
        private static $connection;
        private static $current_query;
        ...
    
        public static function query($sql) {
            if (!self::$connection){
                self::open_connection();
            }
            self::$current_query = $sql;
            $result = mysql_query($sql,self::$connection);
    
            if (!$result){
                self::close_connection();
                // throw custom error
                // The query failed for some reason. here is query :: self::$current_query
                $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
                $error->handleError();
            }
            return $result;
        }
     ....
    
        public static function find_by_sql($sql){
            if (!is_string($sql))
                return false;
    
            $result_set = self::query($sql);
            $obj_arr = array();
            while ($row = self::fetch_array($result_set))
            {
                $obj_arr[] = self::instantiate($row);
            }
            return $obj_arr;
        }
    }
    

    테이블 개체 클래스

    class DomainPeer extends Database {
    
        public static function getDomainInfoList() {
            $sql = 'SELECT ';
            $sql .='d.`id`,';
            $sql .='d.`name`,';
            $sql .='d.`shortName`,';
            $sql .='d.`created_at`,';
            $sql .='d.`updated_at`,';
            $sql .='count(q.id) as queries ';
            $sql .='FROM `domains` d ';
            $sql .='LEFT JOIN queries q on q.domainId = d.id ';
            $sql .='GROUP BY d.id';
            return self::find_by_sql($sql);
        }
    
        ....
    }
    

    이 예제가 훌륭한 구조를 만드는 데 도움이되기를 바랍니다.

  6. from https://stackoverflow.com/questions/5863870/how-should-a-model-be-structured-in-mvc by cc-by-sa and MIT lisence