post_ico4

MVC w praktyce z composer – tworzymy system artykułów. cz. 1

Ponad trzy i pół roku temu na moim blogu pojawił się cykl MVC w praktyce. Mimo, że minęło już tyle lat od publikacji nadal cieszy się dużą popularnością. Postanowiłem go odświeżyć i wykorzystać aktualne narzędzia przydatne w programowaniu PHP.

Uwaga: Żeby w pełni zrozumieć ideę tego wzorca projektowego czytelnik musi mieć solidne podstawy znajomości PHP oraz potrafić programować obiektowo.

Co nowego?

Najważniejszymi zmianami w kodzie są:

  • Wykorzystanie przestrzeni nazw
  • Użycie Composer
  • Wykorzystanie standardu PSR-4. Więcej o tym standardzie przeczytasz na blogu Dominika Marczuka
  • Aplikacja zawiera router do tworzenia przyjaznych linków SEO

Trochę teorii…

Model-View-Controller został zaprojektowany w 1979 roku przez norweskiego programistę Trygve Reenskaug pracującego wtedy nad językiem Smalltalk w laboratoriach Xerox i początkowo nosił nazwę Model-View-Editor.

Ideą tego wzorca jest rozdzielenie kodu odpowiedzialnego za przetworzenie danych od kodu odpowiedzialnego za ich wyświetlanie.

Model-View-Controller zakłada podział aplikacji na trzy główne części:

  • Model jest pewną reprezentacją problemu bądź logiki aplikacji.
  • Widok opisuje, jak wyświetlić pewną część modelu w ramach interfejsu użytkownika.
  • Kontroler przyjmuje dane wejściowe od użytkownika i reaguje na jego poczynania, zarządzając aktualizacje modelu oraz odświeżenie widoków.

Brzmi strasznie, ale w praktyce okazuje się, że to wcale nie jest takie trudne …

No to zaczynamy!

Na samym początku stwórzmy szkielet katalogów i plików:

src/ /* Katalog z kodem aplikacji */
src/Controller        /* Miejsce na kontrolery */
src/Model             /* Miejsce na modele */
src/View              /* Miejsce na widoki */
src/template          /* Miejsce na szablony HTML */
src/Engine            /* Silnik aplikacji */
src/Engine/Router     /* Router aplikacji */
vendor/               /* Tu będą pliki Composer */
.htaccess
composer.json         /* Konfiguracja aplikacji dla Composer */
config.php            /* Konfiguracja aplikacji */
config-router.php     /* Tablica dla routera */
index.php

Tworzymy szkielet aplikacji

Mając strukturę katalogów i plików możemy przejść do tworzenia kodu PHP.

Zaczynamy od kilku podstawowych plików

.htacess

Options +FollowSymLinks
RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [NC,L]

Plik config.php zawiera dane dostępu do aplikacji oraz podstawowe ścieżki aplikacji.

<?php
define('DATABASE_NAME', '****');
define('DATABASE_USER', '****');
define('DATABASE_HOST', '****');
define('DATABASE_PASSOWD', '****');
define('DIR_VENDOR', '/sciezka/do/katalogu/vendor/');
define('DIR_TEMPLATE', '/sciezka/do/katalogu/template/');
define('DIR_CONTROLLER', '/sciezka/do/katalogu/Controller/');
define('HTTP_SERVER', 'http://adres-aplikacji.pl/');

Aktualizacja 1.07.2015: Ścieżki w pliku config.php muszą być bezwzględne. Z maili od was wynika, że dla części z Was nie było to jasne.

Plik config-router.php zawiera kolekcję linków dla routera. Więcej o routerze wspomnę później.

<?php
$collection = new \RacyMind\MVCWPraktyce\Engine\Router\RouteCollection();

$collection->add('category/delete', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'kategorie/usun/<id>?',
    array(
        'file' => DIR_CONTROLLER.'Category.php',
        'method' => 'delete',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Category'
    ),
    array(
        'id' => '\d+'
    ),
    array(
        'id' => 0
    )
));
$collection->add('category/add', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'kategorie/dodaj',
    array(
        'file' => DIR_CONTROLLER.'Category.php',
        'method' => 'add',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Category'
    )
));
$collection->add('category/index', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'kategorie',
    array(
        'file' => DIR_CONTROLLER.'Category.php',
        'method' => 'index',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Category'
    )
));
$collection->add('article/delete', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly/usun/<id>?',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'delete',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    ),
    array(
        'id' => '\d+'
    ),
    array(
        'id' => 0
    )
));
$collection->add('article/one', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly/wyswietl/<id>?',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'one',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    ),
    array(
        'id' => '\d+'
    ),
    array(
        'id' => 0
    )
));
$collection->add('article/add', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly/dodaj',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'add',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    )
));
$collection->add('article/index', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'artykuly',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'index',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    )
));
$collection->add('homepage', new \RacyMind\MVCWPraktyce\Engine\Router\Route(
    HTTP_SERVER.'',
    array(
        'file' => DIR_CONTROLLER.'Article.php',
        'method' => 'index',
        'class' => '\RacyMind\MVCWPraktyce\Controller\Article'
    )
));


$router = new \RacyMind\MVCWPraktyce\Engine\Router\Router($_SERVER['REQUEST_URI'], $collection);

index.php

<?php
require_once 'config.php';
$loader = include DIR_VENDOR.'autoload.php';
require_once 'config-router.php';
$router = new \RacyMind\MVCWPraktyce\Engine\Router\Router('http://'.$_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"]);
$router->run();
$file=$router->getFile();
$classController=$router->getClass();
$method=$router->getMethod();
require_once($file);
$obj = new $classController();
$obj->$method();
Abstrakcyjne klasy dla kontrolera, widoku i modelu

Kolejną rzeczą jaką stworzymy są abstrakcyjne klasy, które będą dziedziczone przez konkretne kontrolery, widoki i modele.

src/Engine/Controller.php
<?php

namespace RacyMind\MVCWPraktyce\Engine;
/**
 * This class includes methods for controllers.
 * @package RacyMind\MVCWPraktyce\Engine
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version: 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 *
 * @abstract
 */
abstract class Controller
{
    /**
     * Przekierowuje na wskazany adres
     *
     * @param string $url URL do przekierowania
     *
     * @return void
     */
    public function redirect($url)
    {
        header("location: " . $url);
    }

    /**
     * Generuje link.
     * @param $name
     * @param null $data
     * @return bool|string
     */

    public function generateUrl($name, $data = null)
    {
        $router = new \RacyMind\MVCWPraktyce\Engine\Router\Router('http://' . $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"]);
        $collection = $router->getCollection();
        $route = $collection->get($name);
        if (isset($route)) {
            return $route->geneRateUrl($data);
        }
        return false;
    }
}

Klasa Controller zawiera 2 metody. Metoda redirect() przekierowuje użytkownika na wskazany adres URL. Metoda generateUrl() tworzy adres URL na postawie kolekcji zawartej w pliku config.router.php.

src/Engine/Model.php
<?php

namespace RacyMind\MVCWPraktyce\Engine;
use \PDO;

/**
 * @package RacyMind\MVCWPraktyce\Engine
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version: 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 */

/**
 * This class includes methods for models.
 *
 * @abstract
 */
abstract class Model {

    /**
     * object of the class PDO
     *
     * @var object
     */
    protected $pdo;

    /**
     * It sets connect with the database.
     *
     * @return void
     */
    public function __construct() {
        try {
            $this->pdo = new PDO('mysql:host=' . DATABASE_HOST . ';dbname=' . DATABASE_NAME, DATABASE_USER, DATABASE_PASSOWD, array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (DBException $e) {
            echo 'The connect can not create: ' . $e->getMessage();
        }
    }

}

Klasa Model tworzy połączenie z bazą danych. Do manipulacji bazą danych wykorzystuję PDO.

src/Engine/View.php
<?php

namespace RacyMind\MVCWPraktyce\Engine;
 
/**
 * This class includes methods for views.
 * @package RacyMind\MVCWPraktyce\Engine
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version: 1.0
 * @license http://www.gnu.org/copyleft/lesser.html
 *
 * @abstract
 */
abstract class View{

    /**
     * Generuje link.
     * @param $name
     * @param null $data
     * @return bool|string
     */

    public function generateUrl($name, $data=null) {
        $router = new \RacyMind\MVCWPraktyce\Engine\Router\Router('http://' . $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"]);
        $collection = $router->getCollection();
        $route=$collection->get($name);
        if (isset($route)) {
            return $route->geneRateUrl($data);
        }
        return false;
    }

    /**
     * Wyświetla kod HTML szablonu
     *
     * @param string $name Nazwa pliku
     * @param string $path Ścieżka do szablonu
     *
     * @return void
     */
    public function renderHTML($name, $path='') {
        $path=DIR_TEMPLATE.$path.$name.'.html.php';
        try {
            if(is_file($path)) {
                require $path;
            } else {
                throw new \Exception('Can not open template '.$name.' in: '.$path);
            }
        }
        catch(\Exception $e) {
            echo $e->getMessage().'<br />
                File: '.$e->getFile().'<br />
                Code line: '.$e->getLine().'<br />
                Trace: '.$e->getTraceAsString();
            exit;
        }
    }

    /**
     * Wyświetla dane JSON.
     * @param array $data Dane do wyświetlenia
     */
    public function renderJSON($data) {
        header('Content-Type: application/json');
        echo json_encode($data);
        exit;
    }

    /**
     * Wyświetla dane JSONP.
     * @param array $data Dane do wyświetlenia
     */
    public function renderJSONP($data) {
        header('Content-Type: application/json');
        echo $_GET['callback'] . '(' . json_encode($data) . ')';
        exit();
    }

    /**
     * Ładuje nagłówek strony
     */
    public function getHeader() {
        return $this->renderHTML('header', 'front/');
    }

    /**
     * Ładuje stopkę strony
     */
    public function getFooter() {
        return $this->renderHTML('footer', 'front/');
    }
    /**
     * It sets data.
     *
     * @param string $name
     * @param mixed $value
     *
     * @return void
     */
    public function set($name, $value) {
        $this->$name=$value;
    }
    /**
     * It sets data.
     *
     * @param string $name
     * @param mixed $value
     *
     * @return void
     */
    public function __set($name, $value) {
        $this->$name=$value;
    }
    /**
     * It gets data.
     *
     * @param string $name
     *
     * @return mixed
     */
    public function get($name) {
        return $this->$name;
    }
    /**
     * It gets data.
     *
     * @param string $name
     *
     * @return mixed
     */
    public function __get($name) {
        if( isset($this->$name) )
            return $this->$name;
        return null;
    }
}

Opis najważniejszych metod:

  • generateUrl() – podobnie jak w kontrolerze, metody tworzy odpowiedni adres URL na podstawie konfiguracji routera.
  • renderHTML() – wyświetla wskazany plik z szablonem HTML.
  • renderJSON() – zwraca dane w formacie JSON.
  • renderJSONP() – zwraca dane w formacie JSONP.
  • getHeader() – wyświetla nagłówek strony.
  • getFooter() – wyświetla stopkę strony.
Nagłówek i stopka strony

Nagłówek i stopka będą wyświetlane na każdej podstronie aplikacji.

src/template/front/header.html.php

<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="pl-PL">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta charset="utf-8"/>
    <title>MVC</title>

</head>
<body>
<ul>
    <li><a href="<?php echo $this->generateUrl('category/add'); ?>">Dodaj kategorię</a></li>
    <li><a href="<?php echo $this->generateUrl('category/index'); ?>">Lista kategorii</a></li>
    <li><a href="<?php echo $this->generateUrl('article/add'); ?>">Dodaj artykuł</a>
    <li><a href="<?php echo $this->generateUrl('article/index'); ?>">Lista artykułów</a>
</ul>

src/template/front/footer.html.php

</body>
</html>
Routing linków

W aplikacji MVC wykorzystuję opisany już jakiś czas temu router linków. Dzięki temu w łatwy sposób mogę tworzyć linki przyjazne dla SEO. Jeżeli jesteś ciekaw na jakiej zasadzie działa router zapraszam do lektury :).

src/Engine/Router/Route.php
<?php

namespace RacyMind\MVCWPraktyce\Engine\Router;

/**
 * Klasa zawiera pojedyńczy element do routingu.
 * @package RacyMind\MVCWPraktyce\Engine\Router
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version 1.0
 */
class Route
{
    /**
     * @var string Ścieżka URL
     */
    protected $path;
    /**
     * @var string Ścieżka do kontrolera
     */
    protected $file;
    /**
     * @var string Nazwa klasy
     */
    protected $class;
    /**
     * @var string Nazwa metody
     */
    protected $method;
    /**
     * @var array Zawiera wartości domyślne dla parametrów
     */
    protected $defaults;
    /**
     * @var array Zawiera reguły przetważania dla parametrów
     */
    protected $params;

    /**
     * @param string $path Ścieżka URL
     * @param array $config Tablica ze ścieżką do kontrolera oraz nazwą metody
     * @param array $params Tablica reguł przetważania dla parametrów
     * @param array $defaults Tablica wartości domyślne parametrów
     */
    public function __construct($path, $config, $params = array(), $defaults = array())
    {
        $this->path = $path;
        $this->file = $config['file'];
        $this->method = $config['method'];
        $this->class = $config['class'];
        $this->setParams($params);
        $this->setDefaults($defaults);
    }

    /**
     * @param string $file
     */
    public function setFile($file)
    {
        $this->file = $file;
    }

    /**
     * @return string
     */
    public function getFile()
    {
        return $this->file;
    }

    /**
     * @param string $class
     */
    public function setClass($class)
    {
        $this->class = $class;
    }

    /**
     * @return string
     */
    public function getClass()
    {
        return $this->class;
    }

    /**
     * @param array $defaults
     */
    public function setDefaults($defaults)
    {
        $this->defaults = $defaults;
    }

    /**
     * @return array
     */
    public function getDefaults()
    {
        return $this->defaults;
    }

    /**
     * @param string $method
     */
    public function setMethod($method)
    {
        $this->method = $method;
    }

    /**
     * @return string
     */
    public function getMethod()
    {
        return $this->method;
    }

    /**
     * @param array $params
     */
    public function setParams($params)
    {
        $this->params = $params;
    }

    /**
     * @return array
     */
    public function getParams()
    {
        return $this->params;
    }

    /**
     * @param string $path
     */
    public function setPath($path)
    {
        $this->path =$path;
    }

    /**
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Generuje przyjazny link.
     * @param array $data
     * @return string
     */
    public function generateUrl($data)
    {
        if (is_array($data) && sizeof($data)>0) {
            $key_data = array_keys($data);
            foreach ($key_data as $key) {
                $data2['<' . $key . '>'] = $data[$key];
            }
            $url = str_replace(array('?', '(', ')'), array('', '', ''), $this->path);
            return str_replace(array_keys($data2), $data2, $url);
        } else {
            $url=preg_replace("#<[a-zA-Z0-9]*>#", '', $this->path, 1);
            return str_replace(array('?', '(', ')'), array('', '', ''), $url);
        }
    }
} 
src/Engine/Router/RouteCollection.php
<?php

namespace RacyMind\MVCWPraktyce\Engine\Router;

/**
 * Klasa zawiera kolekcję elementów klasy Route.
 * @package RacyMind\MVCWPraktyce\Engine\Router
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version 1.0
 */
class RouteCollection
{
    /**
     * @var array Tablica obiektów klasy Route
     */
    protected $items;

    /**
     * Dodaje obiekt Route do kolekcji
     * @param string $name Nazwa elementu
     * @param Route $item Obiekt Route
     */
    public function add($name, $item)
    {
        $this->items[$name] = $item;
    }

    public function get($name)
    {
        if (array_key_exists($name, $this->items)) {
            return $this->items[$name];
        } else {
            return null;
        }
    }

    /**
     * Zwraca wszystkie obiekty kolekcji
     * @return array array
     */
    public function getAll()
    {
        return $this->items;
    }
} 
src/Engine/Router/Router.php
<?php

namespace RacyMind\MVCWPraktyce\Engine\Router;

/**
 * Klasa Routera.
 * @package RacyMind\MVCWPraktyce\Engine\Router
 * @author Łukasz Socha <kontakt@lukasz-socha.pl>
 * @version 1.0
 */
class Router
{
    /**
     * @var String URL do przetworzenia
     */
    protected $url;
    /**
     * @var array Zawiera objekt RouteCollecion.
     */
    protected static $collection;
    /**
     * @var string Ścieżka do kontrolera
     */
    protected $file;
    /**
     * @var string Nazwa klasy
     */
    protected $class;
    /**
     * @var string Nazwa metody
     */
    protected $method;

    public function __construct($url, $collection = null)
    {
        if ($collection != null) {
            Router::$collection = $collection;
        }
        $url=explode('?', $url);
        $this->url = $url[0];
    }


    /**
     * @param array $collection
     */
    public function setCollection($collection)
    {
        Router::$collection = $collection;
    }

    /**
     * @return array
     */
    public function getCollection()
    {
        return Router::$collection;
    }

    /**
     * @param string $class
     */
    public function setClass($class)
    {
        $this->class = $class;
    }

    /**
     * @return string
     */
    public function getClass()
    {
        return $this->class;
    }

    /**
     * @param string $file
     */
    public function setFile($file)
    {
        $this->file = $file;
    }

    /**
     * @return string
     */
    public function getFile()
    {
        return $this->file;
    }

    /**
     * @param string $method
     */
    public function setMethod($method)
    {
        $this->method = $method;
    }

    /**
     * @return string
     */
    public function getMethod()
    {
        return $this->method;
    }


    /**
     * @param String $url
     */
    public function setUrl($url)
    {
        $this->url = $url;
    }

    /**
     * @return String
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * Sprawdza czy URL pasuje do przekazanej reguły.
     * @param Route $route Obiekt reguły
     * @return bool
     */
    protected function  matchRoute($route)
    {
        $params = array();
        $key_params = array_keys($route->getParams());
        $value_params = $route->getParams();
        foreach ($key_params as $key) {
            $params['<' . $key . '>'] = $value_params[$key];
        }
        $url = $route->getPath();
        // Zamienia znaczniki na odpowiednie wyrażenia regularne
        $url = str_replace(array_keys($params), $params, $url);
        // Jeżeli brak znacznika w tablicy $params zezwala na dowolny znak
        $url = preg_replace('/<\w+>/', '.*', $url);
        // sprawdza dopasowanie do wzorca
        preg_match("#^$url$#", $this->url, $results);
        if ($results) {
            $this->url=str_replace(array($this->strlcs($url, $this->url)), array(''), $this->url);
            $this->file = $route->getFile();
            $this->class = $route->getClass();
            $this->method = $route->getMethod();
            return true;
        }
        return false;
    }

    /**
     * Szuka odpowiedniej reguły pasującej do URL. Jeżeli znajdzie zwraca true.
     * @return bool
     */
    public function run()
    {
        foreach (Router::$collection->getAll() as $route) {
            if ($this->matchRoute($route)) {
                $this->setGetData($route);
                return true;
            }
        }
        return false;
    }

    /**
     * @param Route $route Obiekt Route pasujący do reguły
     */
    protected function setGetData($route)
    {
        $routePath=str_replace(array('(', ')'), array('', ''), $route->getPath());
        $trim=explode('<', $routePath);
        $parsed_url=str_replace(array(HTTP_SERVER), array(''), $this->url);
        $parsed_url=preg_replace("#$trim[0]#", '', $parsed_url, 1);
        // ustawia parametry przekazane w URL
        foreach ($route->getParams() as $key => $param) {
            if($parsed_url[0]=='/') {
                $parsed_url = substr($parsed_url, 1);
            }
            preg_match("#$param#", $parsed_url, $results);
            if (!empty($results[0])) {
                $_GET[$key] = $results[0];
                $temp_url=explode($results[0], $parsed_url, 2);
               // $parsed_url=str_replace($results[0], '', $temp_url[1]);
                //$parsed_url=preg_replace($patern, '', $temp_url[1], 1);
                $parsed_url=$temp_url[1];
            }
        }
        // jezeli brak parametru w URL ustawia go z tablicy wartości domyślnych
        foreach ($route->getDefaults() as $key => $default) {
            if (!isset($_GET[$key])) {
                $_GET[$key] = $default;
            }
        }
    }

    /**
     * Zwraca część wspólną ciągów
     * @param string $str1 Ciąg 1
     * @param string $str2 Ciąg 2
     * @return string część wspólna
     */
    protected function strlcs($str1, $str2){
        $str1Len = strlen($str1);
        $str2Len = strlen($str2);
        $ret = array();

        if($str1Len == 0 || $str2Len == 0)
            return $ret; //no similarities

        $CSL = array(); //Common Sequence Length array
        $intLargestSize = 0;

//initialize the CSL array to assume there are no similarities
        for($i=0; $i<$str1Len; $i++){
            $CSL[$i] = array();
            for($j=0; $j<$str2Len; $j++){
                $CSL[$i][$j] = 0;
            }
        }

        for($i=0; $i<$str1Len; $i++){
            for($j=0; $j<$str2Len; $j++){
//check every combination of characters
                if( $str1[$i] == $str2[$j] ){
//these are the same in both strings
                    if($i == 0 || $j == 0)
//it's the first character, so it's clearly only 1 character long
                        $CSL[$i][$j] = 1;
                    else
//it's one character longer than the string from the previous character
                        $CSL[$i][$j] = $CSL[$i-1][$j-1] + 1;

                    if( $CSL[$i][$j] > $intLargestSize ){
//remember this as the largest
                        $intLargestSize = $CSL[$i][$j];
//wipe any previous results
                        $ret = array();
//and then fall through to remember this new value
                    }
                    if( $CSL[$i][$j] == $intLargestSize )
//remember the largest string(s)
                        $ret[] = substr($str1, $i-$intLargestSize+1, $intLargestSize);
                }
//else, $CSL should be set to 0, which it was already initialized to
            }
        }
//return the list of matches
        if(isset($ret[0])) {
            return $ret[0];
        } else {
            return '';
        }
    }


} 

Instalacja Composer

Composer jest bardzo przydatnym narzędziem z dwóch powodów. Zapewnia prosty w użyciu autoloader oraz ułatwia dodawanie do projektu zewnętrznych bibliotek.

Opis jak zainstalować composer w systemie znajdziesz tutaj.

W naszej aplikacji plik composer.json wygląda następująco.

{
    "name": "RacyMind/MVCWPraktyce",
    "description": "MVCWPraktyce od Racy Mind",
    "keywords": ["MVCWPraktyce", "Racy Mind"],
    "homepage": "http://racymind.pl",
    "type": "education, mvc",
    "license": "GNU GPL",
    "require": {
        "php": ">=5.3.0"
    },
    "autoload": {
        "psr-4": {
            "RacyMind\\MVCWPraktyce\\": "src/"
        }
    }
}

Tworzymy tabele bazy danych

Na zakończenie tej części stwórzmy tabele kategorii i artykułów w bazy danych:

CREATE TABLE IF NOT EXISTS `categories` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8_polish_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci;

CREATE TABLE IF NOT EXISTS `articles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) COLLATE utf8_polish_ci DEFAULT NULL,
  `content` text COLLATE utf8_polish_ci,
  `date_add` datetime DEFAULT NULL,
  `author` varchar(100) COLLATE utf8_polish_ci DEFAULT NULL,
  `id_categories` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id_categories` (`id_categories`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci;

W kolejnej części cyklu zaczniemy już dokładniej poznawać idee MVC. Stworzymy fragment aplikacji odpowiedzialny za dodawanie kategorii.

Przejdź do drugiej części cyklu MVC w praktyce z composer

Co sądzisz o wpisie?
BeżnadziejnySłabyŚredniDobryBardzo dobry (3 głosów, średnia ocen: 5,00 z 5)
Loading...
  • Zacznijmy od tego, że tak naprawdę MVC nie da się odwzorować w PHP, to wszystko tylko udaje ten wzorzec projektowy, ale skoro mamy 2015 rok to czemu nie użyłeś Depedency Injection ?

    • Jasne, „wzorcowego” MVC w PHP nie wprowadzisz ze względu na ograniczenia protokołu HTTP. Nie mniej powstały wariacje wzorca MVC dostosowane pod ograniczenia HTTP. Nie ma sensu się spierać, czy to powinno się nazywać MVC, czy nie. Na tego typu dywagacje szkoda czasu.

      Co do DI. Nie widzę konieczności implementacji tego wzorca w zaproponowanej przeze mnie wariacji MVC.

      • Ja bym widział chociażby do Config itp :)

        • W sumie.. można by. Z drugiej strony do stałych z configu mam globalny dostęp, a więc w pewnych sytuacjach może być to wygodniejsze :)

          • tak, ale to nie jest dobra praktyka, bo raczej teraz dąży się do IoC.

  • erochan

    Dobry artykuł z użyciem composera, ale póki co na razie zostaję przy starej prostej metodzie MVC do póki projektu nie skończę(nie chcę już mieszać w kodzie jak mam z 3/4 części zrobione). Później ewentualnie przećwiczę sobie tą metodę albo zabiorę się chociaż raz za jakiś framework(choć nie wiem wybrać). Trochę mało czasu a tyle nauki :D.

    • Ano, lepsze jest wrogiem dobrego :). Kilka razy sam nie skończyłem jakiegoś projektu, bo ciągle go ulepszałem… Teraz już się ograniczam – dany kod ma w miarę dobrze wyglądać i działać,a ze coś można lepiej napisać… zawsze tak będzie :)

      Framework do pracy zawodowej prędzej, czy później będzie potrzebny, ale najpierw warto poznać dobrze „czysty” PHP.

      • erochan

        No cóż php na tyle znam że wystarczy mi dokumentacja by wiedzieć co zrobić z daną funkcją. OOP to chyba tylko tyle że wiem jak stworzyć klasy, odwoływać się do nich i co ma dana klasa robić więc chyba nic trudnego. Szkoda że nie kontynuowałeś studiów od razu po obronie, jaja były z Assemblera :D

        • W OOP trzeba pojąć idee programowania obiektowego. Mi to też trochę czasu zajęło, ale potem już łatwiej jest, np. dość szybko potem uczysz się „dużego” frameworka itp.

          Nie mów, że jeszcze bardziej męczyli z tego niż na licencjacie…? :)

          • erochan

            W sumie nie wiem jak było na licencjacie bo ja z pwsz przyszedłem tam, więc ja tam miałem tylko kod przepisywać z książki. No w sumie zależało na jaką grupę się trafiło było raz dobrze raz źle, jak to Kotliński :D każdego inaczej traktuje. Dobra była moja grupa jak to na zajęciach można było wciskać kitu ile się da, tylko udawać ze się coś robiło :D

          • Guest

            aa, miałem z Kotlińskim jedne zajęcia, więc już sobie wyobrażam jak Asembler wyglądał… :)

          • aa, miałem z p. Kotlińskim jedne zajęcia, więc już wyobrażam sobie jak Asembler wyglądał.. :)

  • witam. mam pytanie – dlaczego nigdzie nie masz domkniętych nawiasów kończących blok kodu php?

    • Witam,
      możesz podać przykład? Nie do końca rozumiem.

  • Rysiek

    Witam.
    Czytałem zarówno Twój stary tutorial – MVC w praktyce – jak i ten i zauważyłem pewną zasadniczą zmianę w modelu.
    W starej wersji używałeś funkcji select w bazowej klasie modelu i przekazywałeś do niej parametry.
    W tej wersji do selektów używasz dedykowanych metod np metoda getAll – robi selekta wszystkiego metoda getOne wybiera tylko jeden konkretny rekord itp.

    Właśnie wziąłem się za przerabianie mojego zakurzonego skryptu i zastanawiam się jakie rozwiązanie zastosować.

    1. Porobić sobie metody Insert, Select, Update, Delete i przekazywać im odpowiednie parametry – stosuje do wszystkiego
    2. Czy w każdej klasie dla osobnego „zagadnienia” (np użytkownicy, artykuły, galeria, newsy) trzymać własne metody typu getAll, getOne, Insert itp dla każdego z nich (działu)

    Czy mógłbyś coś poradzić?

    • Witam,
      ja na teraz preferuję opcję nr 2. Przy moich aktualnych projektach staram się pisać jak najbardziej optymalne zapytania. Pierwsza opcja nie gwarantuje tego.

      Ewentualnie możesz wykorzystać jakiś ORM, np. Doctrine.

      • Rysiek

        Ja właśnie zawsze używałem nr 2 do momentu aż ktoś mi gdzieś tłumaczył jak to fajnie jest mieć osobną klasę typu DBQuery gdzie tworze sobie zapytania. Generalnie napisałem sobie coś co używałem np tak:

        $query = new DBQuery;

        $query->collection(‚user_id’);
        $query->table(‚users’);
        $query->where(
        array(
        ‚login’ => $login,
        ‚conn’ => ‚AND’,
        ‚password’ => $paswd
        )
        );
        $query->limit(1);

        $result = $this->conn->query($query->fetchQuery(‚SELECT’));

        Używałem z pół roku po czy zadałem sobie pytanie. Po co mi to k… jest
        skoro można od razu:

        $result = $this->conn->query(‚Moje zapytanie’);

        Chyba wrócę do tej 2 i porobię sobie osobne (w sumie do podobnych celów) metody w osobnych klasach ale bez zbędnego moim zdaniem kombinowania

        • Moim zdaniem trzeba potraktować osobno 2 przypadki:
          1) Są projekty, gdzie optymalizacja zapytań ma znaczenie. Wtedy piszę zapytania SQL.
          2) W mniejszych projektach nie ma konieczności optymalizacji i wtedy można sobie usprawnić pisanie :) W takiej sytuacji można wykorzystać jakąś gotową bibliotekę.

  • Art

    Dzięki, porządna dawka wiedzy, mógłbyś jeszcze opisać jak zrobić paginacje dla listy niusów dla tego mvc, nie potrafię tego ogarnąć jeszcze.

    • Dedalem sobie do listy wpis o paginacji. Na ten moment nie jestem w stanie powiedzieć kiedy konkretnie pojawi się wpis.

  • Cześć, kiedy jakiś samouczek do Zend Framework? ;) Najlepiej z podziałem na zenda1 i zenda2. Pozdrawiam

    • Nie korzystam z Zenda, więc na moim blogu raczej nie znajdziesz wpisów o tym frameworku :)

  • kerim_91

    Witam. Mam problem z uruchomieniem tego przykładu na xamppie. Błędy jaki mi się pojawiają to
    Warning: require_once(C:XAMPPhtdocsmvcwpraktyce): failed to open stream: Permission denied in C:XAMPPhtdocsmvcwpraktyceindex.php on line 11

    Fatal error: require_once(): Failed opening required ” (include_path=’.;C:XAMPPphpPEAR’) in C:XAMPPhtdocsmvcwpraktyceindex.php on line 11

    Bardzo proszę o pomoc. Chciałbym to uruchomić bo na tych przykładach zrozumiałem MVC.

    • failed to open stream: Permission denied – Masz problem z prawami dostępu do pliku. Poczytaj o tym

      • kerim_91

        no tak ale o jakie pliki chodzi. wrzuciłem skrypty na serwer az.pl i jest taki sam problem jak na lokalu. pliki mają chmod 644 a katalogi 755?

        • Wyślij pliki na maila. Sprawdzę ścieżki

          • nuky

            Witaj, jak rozwiązać problem o którym pisze kerim_91 ? Jak zmienić prawa dostępu do plików ? Mógłbyś opisać rozwiązanie tego problemu, prawdopodobnie nie jesteśmy jedni którzy spotykają się z tym problem

          • U kerim_91 okazało się, że brakowało „/” na końcu w stałej HTTP_SERVER

  • DIV

    Dostaję poniższy komunikat przy próbie uruchomienia kodu. Prawa dostępu ? Jeśli chodzi o plik rozruchowy index.php – nie rozumiem od linijki 10 do 12 co tak naprawdę się dzieje. Chyba kod jest za ciężkostrawny narazie dla mnie ;)
    Warning: require_once(C:xampphtdocsprojekt): failed to open stream: Permission denied in C:xampphtdocsprojektindex.php on line 11

    Fatal error: require_once(): Failed opening required ” (include_path=’.;C:xamppphpPEAR’) in C:xampphtdocsprojektindex.php on line 11

    • DIV

      Ok, naprawiłem ten błąd. Dla tych co szukają rozwiązania sprawdźcie czy macie „/” na końcu ścieżek w pliku config.php. Jedna uwaga – widzę że komentarze są w języku PL jak i EN – fajnie byłoby zachować jedną konwencję językową.

      • Tak, trzeba pilnować znaków /. Nie pierwszy masz z tym problem :)

        Co do komentarzy to masz rację. Nie powinno się mieszać języków w komentarzach. Kod z angielskimi komentarzami powstał wcześniej. Od pewnego czasu komentuję kod po polsku i stąd ta niespójność. Nie chciało mi się już tego poprawiać.

        • Patryk Ratajczak

          Witam. Mam poprawne ścieżki, lecz błąd nadaj się pokazuje

  • Łukasz Rzanny

    Bosz… Ile się namęczyłem, żeby to uruchomić.

    Podpowiedź na przyszłość: Jeżeli uruchamiasz skrypt na serwerze z innym portem niż 80 (w moim przypadku 8080), to pamiętaj żeby dodać w pliku index.php w linii 5 swój niestandardowy port :)

    Pozdrawiam

    • Postanowiłem wyróżnić twój komentarz dla potomnych :) Pod takim kątem nie testowałem skryptu.

      • Łukasz Rzanny

        Jeszcze mała uwaga nie sprawdzałem dokładnie ale porównując pliki z zipka na końcu i kod który masz zawarty na stronie w którymś pliku z folderu engine/router jest różnica o parę linii w kodzie – Nie sprawdzałem czego brakuje ale różnica jest :)

        Pozdrawiam!