Czy dobrze rozumiem wzorzec MVC?

1

Tak jak w temacie. Chciałbym się tego dowiedzieć. Przygotowałem wstępny kod aby zobrazować tok rozumowania. Proszę o ocenę i jeżeli pojawią się jakieś uwagi to było by fajnie gdyby pojawiło się również uzasadnienie. Zakładamy, że jest włączone przepisywanie adresów url w pliku .htaccess i wszystko trafia do router'a. Zakładamy, że jest to sklep odzieżowy i np pojawia się taki adres url w pasku /szukaj/kalesony/.


 <?php

function autoloadClass($ClassName)  //wiadomo, automatycznie ładujemy klasy
{
  if(file_exists("php/model/$ClassName.php"))
  {
    require "php/model/$ClassName.php";
  }
  else if(file_exists("php/view/$ClassName.php"))
  {
    require "php/view/$ClassName.php";  
  }
  else if(file_exists("php/controler/$ClassName.php"))
  {
      require "php/controler/$ClassName.php";
  }
}

 spl_autoload_register('autoloadClass');

 class Router
 {
     public $ModelName;
     public $ViewName;
     public $ControlerName;
     public $Parameter;

    function __construct()  //tutaj parsujemy adres url i wyodrębniamy akcję
    {
      //jakieś działania na ciągach znaków
      $this->ModelName = $JakasZmiennaPrzechowujacaNazwe;
      $this->ViewName = $JakasZmiennaPrzechowujacaNazwe;
      $this->ControlerName = $JakasZmiennaPrzechowujacaNazwe;
      $this->Parameter = $TuPewnieJakasTablica
    }   
 }

 class SearchByItemModel //klasa modelu
 {
    private $Item;

    public function SetItem($Item)
    {
      $this->Item = $Item;
    }

    public function FindItemInDataBase()
    {
      //łączenie z bazą danych, pobieranie danych itp cała logika tu pracuje
      return $JakasZmiennaZapewneTablica;
    }

 }

 class SearchByItemView
 {
   private $ItemFinder;

   function __construct($ItemFinder)
   {
     $this->ItemFinder = $ItemFinder;
   }

   function Render()
   {
     //tutaj wyświetlane jest wszystko co potrzebne, tutaj jest bezpośredni dostęp do modelu
   }   
 }

 class SearchByItemControler
 {
    private $Model;

    function __construct($Model)
    {
      $this->Model = $Model;
    }

    function Search($Item)
    {
     $this->Model->SetItem($Item);
    }   
 }

 //tworzymy instancje naszych obiektów;

 $router = new Router();
 $model = new $router->ModelName();
 $controler = new $router->ControlerName($model);
 $parameter = $router->Parameter;

 $controler->Search($Parameter);
 $view = new SearchByItemView($model);
 $view->render();

?>
1

Zanim zabierzesz się za pisanie własnego MVC od zera, rzuć okiem np. na Laravela i to, jak jego twórcy podeszli do tematu - da Ci to dobre rozeznanie w kwestii podziału oraz nazewnictwa w PHP (którego w obecnej formie nie przestrzegasz).

0

To co sobie wymyslił twórca Laravela nie jest żadnych wyznacznikiem czy standardem.

Bardziej niż Laravelem zajmij się przestrzeganiem wzorców projektowych takich jak SOLID czy DDD. To co masz wyżej to pomieszanie MVC z DDD. Poczytaj o tym.

Jeżeli już obrałeś jakieś nazewnictwo czy strukturę która Ci pasuje, najważniejsze to ściśle się jej trzymać - to powoduje szereg ułatwień od automatyzacji refaktoringu po generalny porządek w kodzie.

3

@Mostek87:

  1. Nie pisz własnego autoloadera - wykorzystaj Composera (Composer ma dwie funkcjonalności: zarządzanie paczkami oraz budowanie autoloadera - Tobie potrzebna jest głównie ta druga).

  2. protected $foo; (a nie protected $Foo;; to samo odnośnie private czy public - widoczność określiłem tylko na rzecz przykładu; taka jest konwencja języka).

  3. function doSomething(), a nie function DoSomething() (taka jest konwencja języka).

  4. Nie modelName, tylko modelClassName itd. (niby niewielka różnica, ale widząc modelName możesz zacząć się zastanawiać, czy znajduje się tam ścieżka do pliku, czy może coś innego - a modelClassName jest dosyć jednoznaczne).

Ogólnie nie ma źle - rzucam się głównie o stylistykę i nazewnictwo, ale są to kwestie dosyć istotne ;-)

Sam koncept wydaje mi się, że rozumiesz, chociaż mógłbyś podać jakiś bardziej wymagający przykład (np. ogólny zarys kodu zarządzającego postami w bazie danych bloga).

1

To co wyżej napisał Patryk to znowu nie jest żaden standard ani wyznacznik.

Możesz robić tak jak robisz "kamelizująć" już od pierwszej litery - tyle, że bądź w tym konsekwentny.

Jeżeli chcesz to równie dobrze Twoje zmienne i fukcje mogą wyglądać tak:

public function do_something, protected $model_name etc - to jest trochę starszy ale nadal bardzo dobry sposób nazewnictwa, który część ludzi preferuje ponad kamelizację.

0

Super, dzięki bardzo za uwagi. Faktycznie co do stylistyki powinienem co najmniej być konsekwentny - czego u mnie zabrakło. Sam się na tym przyłapałem, ale szczerze mówiąc zwyczajnie z lenistwa nie chciało mi się poprawiać ;) Teraz i tak muszę poprawić cały kod - popełniłem błąd i kontroler najpierw pobierał dane z modelu a potem przekazywał je widokowi. Co ciekawe w sieci jest pełno tutoriali, które błędnie stwierdzają, że tak właśnie powinno być. Jak się potem okazało w MVC widok ma bezpośredni dostęp do modelu. Polecam dwa artykuły, które mi pomogły

https://r.je/views-are-not-templates.html
https://r.je/mvc-tutorial-real-application-example.html

Ja czmycham poprawiać kod, jak skończę to wrzucę już bardziej praktyczny przykład do oceny.

0

Poprawiłem kod swej aplikacji. Jeśli komuś będzie się chciało analizować moje wypociny to właśnie one:
Router

function autoloadClass($className) //no dobra wiem, że composer byłby lepszy ale póki co nie chce mi się tego ogarniać :p
{
  if(file_exists("php/model/$className.php"))
  {
    require "php/model/$className.php";
  }
  else if(file_exists("php/view/$className.php"))
  {
    require "php/view/$className.php";  
  }
  else if(file_exists("php/controler/$className.php"))
  {
     require "php/controler/$className.php"; 
  }
  else if(file_exists("php/extra/$className.php"))
  {
     require "php/extra/$className.php";   
  }
}

spl_autoload_register('autoloadClass');

class Router
{
  public $parameters = null;
  public $modelName;
  public $viewName;
  public $controlerName;

  private function getClassName(&$action)
  {
    $classes = array(
    "kategorie" => "Categories" );

    return $classes[$action];
  }

  function __construct()
  {
        $arr = explode("/",$_SERVER['REQUEST_URI']);      //tu wyodrębniam sekcję jaką chce odwiedzić użytkownik
        $userAction = $arr[1];
        $userAction = ($userAction === "" || $userAction === "strona") ? "MainPage" : $this->getClassName($userAction); 

        $arrLength = count($arr);

        $this->modelName = $userAction."Model";
        $this->viewName = $userAction."View";
        $this->controlerName = $userAction."Controler";

        if($arrLength > 3)
        {
            $this->parameters = array();  //tu będę pakował parametry - 

            for($i = 2; $i < $arrLength - 1; ++$i)
            {
              array_push($this->parameters, $arr[$i]);  //ładuję parametry do tablicy aby potem przekazać je do kontrolera
            }       

        } 
  }

}

$router = new Router;
$model = new $router->modelName();
$controler = new $router->controlerName($model);
$controler->initiate($router->parameters);
$view = new $router->viewName($model);
$view->render();

klasa abstrakcyjna kontrolera

abstract class Controler
{
  private $model;
  abstract function initiate($parameters);
}

kontroler dla strony głównej


class MainPageControler extends Controler 
{
    function __construct($model)
    {
      $this->model = $model;    
    }

    function initiate($parameters)
    {
      $pageNumber = (empty($parameters)) ? 1 : $parameters[0];
      $this->model->currentPage = Intval($pageNumber);    
    }
}

model dla strony głównej

class MainPageModel
{
    public $currentPage;

    private function createQuery()  //w zależności od numeru strony tworzę odpowiednie zapytanie do bazy danych
   {
      $pageNumber = $this->currentPage - 1;

      if($pageNumber == 0)
      {
        return "SELECT * FROM Pornusy ORDER BY klucz DESC LIMIT 50";   
      }
      else
      {
        $offset = $pageNumber * 50;
        return "SELECT * FROM Pornusy ORDER BY klucz DESC LIMIT 50 OFFSET $offset";     
      }

    }

    private function getRandomImages()
    {
        $id1 = rand(1,20);   //wybieram dwa losowo wybrane zdjęcia
        $id2 = rand(1,20);

        while($id1 === $id2)
       {
         $id1 = rand(1,20);
            $id2 = rand(1,20);  
        }

       $this->randomImage1 = "decoration-image-".$id1;
       $this->randomImage2 = "decoration-image-".$id2;  
    }

    private function calculatePageControls(&$total)   //obliczam wartości dla kontrolek sterujących podstronami
    {
      $maxPageIndex = ceil($total / 50);
      $pageNumber = &$this->currentPage;
      $pageControls = new PageControls();

      if($maxPageIndex - 5 >= $pageNumber)
      {
        $pageControls->pageControlIndex1 = $pageNumber;
        $pageControls->activePagePosition = 1;
      }
      else
      {
        $pageControls->pageControlIndex1 = $maxPageIndex - 4;
        $pageControls->activePagePosition = 5 + $pageNumber - $maxPageIndex;
      }

      $pageControls->topIndex = ++$total - (--$pageNumber * 50); 
      $pageControls->maxPageIndex = $maxPageIndex;

      $id1 = rand(1,20);   //wybieram dwa losowo wybrane zdjęcia
      $id2 = rand(1,20);

        while($id1 === $id2)
       {
         $id1 = rand(1,20);
         $id2 = rand(1,20); 
        }

       $pageControls->randomImage1 = "decoration-image-".$id1;
       $pageControls->randomImage2 = "decoration-image-".$id2;

      return $pageControls;

    }

    function getMoviesData()
    {
        $dataBase = mysqli_connect();

        if(!$dataBase)
        {
          return null;
        }
        else
        {
          $result = mysqli_query($dataBase,'SELECT COUNT(*) as total FROM Pornusy');
          $row = mysqli_fetch_assoc($result);
          $totalMovies = $row['total'];
          mysqli_query($dataBase,"SET CHARSET utf8");
          $query = $this->createQuery();
          $result = mysqli_query($dataBase, $query);
          $movies = array();

          while($row = mysqli_fetch_assoc($result))
          {
            array_push($movies, new Movie($row['opis'], $row['gwiazdy'],$row['wyswietlenia'], $row['sekundy'],$row['minuty'],$row['godziny'], $row['lektorpl']));
          }

          mysqli_close($dataBase);
          $pageControls = $this->calculatePageControls($totalMovies);
          return new PagePackage($movies, $pageControls);

        }
    }
}

no i widok


class MainPageView extends View
 {
     private $dataPackage;

    function __construct($model)
    {
        $this->model = $model;
    }

    function render()
    {
      if(file_exists("php/template/MainPage.php"))
      {
        $this->dataPackage = $this->model->getMoviesData();
        require "php/template/MainPage.php"; 
      }
    }

    private function createBackForwardControls()              //wyświetlanie kontrolek sterujących podstronami
    {
      $pageControls = &$this->dataPackage->pageControls;
      $LeftControlNumber = &$pageControls->PageControlIndex1;
      $MaxPageIndex = &$pageControls->MaxPageIndex;
      $ActivePagePosition = &$pageControls->CurrentPage;

         if($LeftControlNumber > 1)
        {
          $PreviousPageNumber = $ActivePagePosition - 1;
          $LeftAnchor = 'href="/strona/'.$PreviousPageNumber.'/"';
        }
        else
        {
          $LeftAnchor = "";
        }

        if($ActivePagePosition >= $MaxPageIndex)
        {
          $RightAnchor = "";
        }
        else
        {
          $NextPageNumber = $ActivePagePosition + 1;
          $RightAnchor = 'href="/strona/'.$NextPageNumber.'/"';
        }

         echo '<li class="controls-element controls-element-bottom">
          <a '.$LeftAnchor.' class="page-selection page-selection-second-layer">wstecz</a>
          </li>';

        echo '<li class="controls-element controls-element-bottom">
          <a '.$RightAnchor.' class="page-selection page-selection-second-layer">dalej</a>
        </li>';

    }

     function createPageNumbers()  //wyświetlanie kontrolek sterujących podstronami
    {
     $pageControls = &$this->dataPackage->pageControls;
     $LeftNumber = &$pageControls->pageControlIndex1;
     $ActiveControl = &$pageControls->activePagePosition;

        for($I = 0; $I < 5; ++$I)
        {
            $Class = ($I + 1 == $ActiveControl) ? "active" : "";
            $PageNumber = $I + $LeftNumber;

            echo '<li class="controls-element">
                   <a href="/strona/'.$PageNumber.'/" id="PageControl-'.$I.'" class="page-selection page-selection-first-layer '.$Class.'">'.$PageNumber.'</a>
                 </li>';
        }
    }

    private function createTimeLabel($movie)   //tworzenie etykiety z czasem trwania filmu np. 00:34
    {

      $seconds = &$movie->seconds;
      $hours = &$movie->hours;
      $minutes = &$movie->minutes;

      $seconds = $seconds < 10 ? "0$seconds" : $seconds;
      $hours = $hours != null ? "0$hours:" : "";

      if($minutes == null)
      {
         $minutes = "00:";  
      }
      else
      {
        $minutes = ($minutes < 10) ? "0$minutes:" : "$minutes:";
      }  

      return $hours.$minutes.$seconds;
    }

    function showMovies()  //wyświetlanie filmów
    {
        $currentID = &$this->dataPackage->pageControls->topIndex;

        foreach($this->dataPackage->moviesData as $movie)
        {
            $time = $this->createTimeLabel($movie);
            $description = &$movie->tittle;
            $views = &$movie->views;
            $stars = &$movie->stars;

            if($stars != null)
            {
              $stars = 'data-stars="'.$stars.'"';   
            }
            else
            {
              $stars = "";  
            }

           echo '<div class="movie-complete">
               <div class="image-and-duration"><img class="movie-small-image" src="/images/movie/main/'.$currentID.'.jpg" /><time class="duration">'.$time.'</time></div>
               <div class="movie-description">'.$description.'</div>
                  <div class="additional-options">
                     <span class="views">'.$views.' odsłon</span>
                     <img alt="podgląd filmu" title="kiliknij aby podejrzeć film" class="magnifier-icon" '.$stars.' data-id="'.$currentID.'" src="/images/controls/magnifier.png"/>
                  </div>
          </div>';  

          --$currentID;
        }
    }
 }
0

Przepraszam nie analizowałem wszystkiego z braku czasu, chciałbym jedynie zwrócić uwagę na umiejscowienie klas.

Popełniasz moim zdaniem tutaj błąd, który niestety masowo jest powielany, można go też zobaczyć w większości frameworków.

Błąd polega na przydzielaniau osobnych katalogów na klasy widoków, klasy kontrolerów, i inne.

Może się wydawać, że zrobienie czegoś takiego ułatwia programowanie, ale to jest złudne, na dłuższą metę utrudnia.

  • stosująć taki autoloading musisz sprawdzać kilka katalogów

  • kiedy chciałbyś tworzyć aplikację z części lub modułów, te modułu muszą być dostosowane do takiej struktury katalogów

  • różne frameworki mają różne struktury katalogów, dlatego najlepsze co można zrobić w celu poprawienia przenosności jest maksymalne uproszczenie tej kwestii bez straty np. na bezpieczeństwie

Takim wyjściem jest określenie jednego jedynego katalogu który zawiera klasy, to może być np. katalog class, i ładowania klas wg. namespaces i nazwy klasy.

Np. klasa \myshop\controller\CartController, czyli kasa CartController znajdująca się w przestrzeni nazw myshop\controller, będzie mieć ścieżkę: class\myshop\controller\CartController.php, a klasa \myothermodule\dupamaryna\CheckPrivileges będzie mieć ścieżkę class\myothermodule\dupamaryna\CheckPrivileges

To jest bardzo proste i elastyczne.

Nie trzeba mieć specjalnego osobnego katalogu na kotroler, w routingu po prostu określa się pełną ścieżkę do klasy, podobnie z innymi sprawami. To bardzo elastyczne i dające dużo wolności rozwiązanie w duchu PHP.

0
TomRZ napisał(a):

Przepraszam nie analizowałem wszystkiego z braku czasu, chciałbym jedynie zwrócić uwagę na umiejscowienie klas.

Popełniasz moim zdaniem tutaj błąd, który niestety masowo jest powielany, można go też zobaczyć w większości frameworków.

Błąd polega na przydzielaniau osobnych katalogów na klasy widoków, klasy kontrolerów, i inne.

Może się wydawać, że zrobienie czegoś takiego ułatwia programowanie, ale to jest złudne, na dłuższą metę utrudnia.

  • stosująć taki autoloading musisz sprawdzać kilka katalogów

Co Ty znowu bredziesz człowieku ? Co musi sprawdzać? każesz mu robić syf w katalogach pchając wszystko w jeden folder a potem szukanie jednego pliku wśród widoków/modelów i innych?
Wystarczy użyć psr-4 przez composera i może mieć nawet 200 folderów z podfolderami a autoloading i tak będzie działał.

0

Wypraszam sobie do mnie teksty w stylu "co Ty bredzisz". Jeżeli nie umiesz się zachować, to wyjdź.

I jeszcze jedno: w PSR4 nie ma nic na temat struktury katalogów która rozdziela osobno kotrolery i inne rzeczy, może najpierw poczytaj zanim czymś się podeprzesz.

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0