Skocz do zawartości
Szukaj na Pecetowiczu
  • Utwórz konto

Kurs PHP by PanKrok - Praktyka piszemy blog #1


Rekomendowane odpowiedzi
(edytowane)

Cześć, dzisiaj postaram się rozpocząć nierówną walkę z pierwszym projektem, będziemy pisali własny skrypt bloga. Od razu chciałbym zaznaczyć że napiszemy wszystko od podstaw stosując wzorzec MVC. Podczas tego kursu stworzymy:

  • prosty router,
  • prosty system szablonów,
  • manager bazy danych

1. Tworzymy szablon projektu i konfigurujemy zależności

Nasz projekt będzie zawierał następującą strukturę:

/
- /app
 |- /Controller
 |- /Model
- /public
 |- /assets
 |- .htaccess
 |- index.php
- /settings
 |- settings.ini
- /templates
- .htaccess
- composer.json
- init.php

zacznijmy od zawartości composer.json

{
    "name": "pankrok/simple-blog",
    "description": "Prosta aplikacja stworzona na potrzebu kursu PHP dla użytkoników pecetowicz.pl",
    "autoload": {
        "psr-4": {
            "App\\": "app"
        }
    }
}

poza ładowaniem przestrzeni nazw z app nic więcej nie będzie nam potrzebne, dodatkowo w załączniku znajdziesz spakowany projekt który posiada już wygenerowany autoload przez composera. Warto zwrócić uwagę że plik index.php nie znajduje się w głównym folderze aplikacji a w folderze "public", jest tam z pewnego powodu. Nie chcemy żeby ktokolwiek miał dostęp do innych folderów niż folder publiczny, poprawi to bezpieczeństwo aplikacji. Ok przejdźmy do plików .htaccess
 

// .htaccess w głównym folderze:
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]

// .htaccess w folderze public:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

taka konfiguracja sprawi że wszystkie requesty będą kierowany na plik index.php w katalogu public. Na tym etapie mamy już gotowy szablon czas zacząć kodowanie.

2. Kontener

Wszystkie nasze zależności będziemy przetrzymywać w tablicy, nie jest to najbardziej optymalne rozwiązane jednak do celów nauki będzie odpowiednie, w przyszłości skorzystamy z biblioteki PHP-DI. Przejdź do katalogu Controller i stwórz katalog Container a w nim plik SimpleContainer.php

<?php declare(strict_types = 1);

namespace App\Controller\Container;

Class SimpleContainer {
    
    private static array $container = [];
    
    public function set(string $name, $object): void
    {
        if (array_key_exists($name, self::$container)) {
            throw new \Exception('Name: ' . $name . ' - already exist in container!');
        }
        
        self::$container[$name] = $object;
    }
    
    public function get(string $name) 
    {
        if (array_key_exists($name, self::$container)) {
            return self::$container[$name];
        }
        
        return null;
    }
    
    public function delete(string $name)
    {
        if (array_key_exists($name, self::$container)) {
            unset(self::$container[$name]);
            return true;
        }
        
        return false;
    }
    
}

W pliku podajemy ścieżkę naszego pliku jako przestrzeń nazw, oraz nazwę klasy zgodną z nazwą pliku. Tworzymy prywatną tablicę, będziemy nią zarządzali przez funkcje.
set() - przyjmuje ciąg znaków jako nazwę klucza w tablicy oraz dowolny obiekt, przed zapisem sprawdzimy czy w naszej tablicy dany klucz już nie występuje (array_key_exists()). Dzięki temu jesteśmy pewni że nie nadpiszemy go nigdzie w kodzie naszego skryptu, dodatkowo wywołamy wyjątek który wyświetli nam co jest nie tak. Ważne aby wyjątki były maksymalnie proste w odczytaniu, dzięki temu będziemy wiedzieli co poszło nie tak.
get() - sprawdza czyd any klucz istnieje, jeśli tak zwróci nam obiekt z tablicy jeśli nie zwróci null,
delete() - usuwamy wybrany klucz.
Jak widzisz zastosowanie prostej funkcji która "obudowuje" zwykłą tablicę będzie dla nas bardzo wygodne, co ważne będziemy mieli dostęp do wszystkich obiektów które znajdą się w owej tablicy z czego skorzystamy później w naszych kontrolerach. Teraz przejdźmy do naszego routera.

3. Router

Wszystkie nowoczesne strony korzystają z przyjaznych adresów URL, my również będziemy chcieli mieć taką możliwość, z tego powodu stworzymy router który wywoła odpowiedni kontroler oraz jego funkcję. Zacznijmy od stworzenia modelu, w katalogu Model stwórz katalog Router i utwórz w nim plik RouterModel.php

<?php declare(strict_types = 1);

namespace App\Model\Router;

class RouteModel {
    
    protected array $method;
    protected string $route;
    protected string $controller;
    protected string $function;
    protected array $params = [];
    
    public function __construct(array $method, string $route, string $controller, string $function) 
    {
        $this->method = $method;
        $this->route = $route;
        $this->controller = $controller;
        $this->function = $function;
    }
    
    public function getMethod(): array
    {
        return $this->method;
    }
    
    public function getRoute(): string
    {
        return $this->method;
    }
    
    public function getController(): string
    {
        return $this->controller;
    }
    
    public function getFunction(): string
    {
        return $this->function;
    }
    
    public function getParams(): array
    {
        return $this->params;
    }
    
    public function setParams(array $params): void
    {
        $this->params = $params;
    }
    
    public function countParams(): int
    {
        return count($this->params);
    }
}

Jak widzisz klasa modelu nie jest skomplikowana, posiada konstruktor który pozwala na przesłanie odpowiednich zmiennych takich jak metoda (GET/POST), ścieżkę, kontroler oraz funkcję i parametry jakie mają być pobrane ze ścieżki. Utworzymy również gettery które pozwolą pobrać nam każdy z parametrów i funkcję pomocniczą liczącą ile jest parametrów. Teraz czas utworzyć kontroler, w katalogu Controller utwórz katalog Router a w nim plik RouterController.php

<?php declare(strict_types = 1);

namespace App\Controller\Router;
use App\Model\Router\RouteModel;
use App\Controller\Container\SimpleContainer;

class RouterController {
    
    public const GET = 'GET';
    public const POST = 'POST';
    
    protected array $routes;
    protected array $config;
    protected SimpleContainer $container;
    
    public function __construct(SimpleContainer $container = null) 
    {
        $this->config = parse_ini_file(MAIN_DIR . '/settings/settings.ini', true);
        $this->container = $container;
    }
    
    public function addRoute(array $method, string $route, string $call): bool
    {       
        list($controller, $function) = explode(':', $call);
        if ($method === '' || $route === '') {
            return false;
        }
        
        $model = new RouteModel($method, $route, $controller, $function);
        preg_match_all('/:\S+/', $route, $matches);
        if (empty($matches[0]) === false) {
            $route = explode(':', $route)[0];
            $route = substr($route, 0, -1);
            $matches = str_replace(':', '', $matches[0][0]);
            $params = explode('/', $matches);
            $model->setParams($params);
        }
        
        $this->routes[$route] = $model;
        return true;
    }
    
    public function run(): void
    {
        $params = null;
        $isAllowed = false;
        $uri = substr($_SERVER['REQUEST_URI'], strlen($this->config['uri']));
        $handler = explode('/', $uri);
        foreach ($handler as $v) {
            
            if (!is_array($handler)) {
                break;
            }
            
            $findRoute = implode('/', $handler);
            if (isset($this->routes[$findRoute])) {
                $model = $this->routes[$findRoute];
                break;
            }
            
            array_pop($handler);
        }
    
        if (!isset($model)) {
            echo '404';
            http_response_code(405);
            return;
        }
        
        foreach ($model->getMethod() as $method ) {
            if($_SERVER['REQUEST_METHOD'] === $method) {
                $isAllowed = true;
            }
        }
        
        if ($isAllowed === false) {
            echo '405';
            http_response_code(404);
            return;
        }
        
        if (strlen($findRoute) !== strlen($uri)) {
            $handler = substr($uri, strlen($findRoute)+1);
            $handler = explode('/', $handler);
            $modelParams = $model->getParams();
            if (count($handler) > $model->countParams()) {
                echo '404';
                http_response_code(404);
                return;
            }
            
            foreach ($modelParams as $k => $v) {
                $params[$v] = $handler[$k] ?? null;
            }            
        }

        $controller = $this->container->get($model->getController());
        $function = $model->getFunction();
        $controller->$function($params, $_POST);

    }  
     
}

Nasza klasa będzie zawierała 2 stałe, które pomogą nam w tworzeniu ścieżek i będą odpowiadały za metodę jaka została użyta przez przeglądarkę, ponadto tworzymy dwie prywatne tablice gdzie przechowamy informacje o ścieżkach oraz o konfiguracji. Na koniec dodajemy również zmienną kontenera która będzie dostępna w tej instancji. Konstruktor pobierze dane z pliku konfiguracyjnego (w teorii nie powinno się kodować ścieżki do pliku, bo wywoła to pewien problem przy jakieś zmianie w przyszłości, ale dla ułatwienia zostawimy tak jak jest) oraz zapiszemy do lokalnej zmiennej kontener z danymi.
Nasz router musi mieć możliwość dodania nowej ścieżki, w tym celu tworzymy funkcję addRoute() która przyjmie argumenty:

  • tablicę metod dostępnych dla ścieżki,
  • ciąg znaków jako adres url/ścieżka,
  • ciąg znaków odpowiadający za wywołanie odpowiedniej klasy i funkcji przechowywanej w kontenerze.

funkcja php list pozwala na przypisanie poszczególnych wartości z tablicy, w tym wypadku naszą tablicę tworzymy za pomocą funkcji explode, która "eksploduje" stringa za pomocą wybranego znaku, dzięki temu zapis HomeController:index zostanie przypisany do zmiennych $controller = 'HomeContoller', $function = 'index'.
Teraz sprawdzimy czy nasze metody i ścieżka nie są puste, w taki wypadku chcemy od razu zwrócić informację ze dodanie ścieżki nie powiodło się, zwracamy fałsz. Jeśli nasze dane są poprawne możemy zbudować nasz model ścieżki, tworzymy nową instancję modelu i przesyłamy do jego konstruktora metody, adres url, nazwę kontrolera oraz funkcję.
Musimy jeszcze sprawdzić czy nasza ścieżka posiada jakieś parametry, skorzystamy z funkcji przeszukiwania ciągu znaków  preg_match_all() - szukamy elementów ścieżki zawierających dwukropek. Jeśli takie znajdziemy, obcinamy odpowiednio ścieżkę, usuwamy dwukropek i dopasowania zamieniamy na tablicę którą dodajemy do parametrów modelu. Pozostaje już tylko zapisać model w naszej tablicy, dla odpowiedniej ścieżki i zwrócić wartość true.

Funkcja run() - odpowiada ze pobranie danych na temat adresu url pod którym jest klient przeglądarki i zaserwowania odpowiedniej funkcji i odpowiedniego kontrolera, tutaj dzieje się cała magia 🙂 Na początku stworzymy sobie pomocnicze zmienne, parametry oraz czy dany request jest dozwolony, następnie pobierzemy dokładny adres ze zmiennej globalnej $_SERVER, ucinając ewentualny nadmiar znaków na początku. OK mamy już ścieżką pod którą jest użytkownik, eksplodujmy ją za pomocą znaku / - bo taki to znak jest widoczny w pasku adresu i przeliterujmy wszystkie elementy. Na starcie sprawdzamy czy nasz uchwyt na dane jest tablicą, jeśli nie opuszczamy pętlę, w innym wypadku sklejamy to co jest w tablicy za pomocą znaku / i sprawdzamy czy dana ścieżka jest w tablicy wszystkich ścieżek naszego skryptu, jeśli ją znajdziemy pobieramy model danej ścieżki i kończymy pętlę, jeśli nie usuwamy ostatni element z naszego uchwytu.
Teraz musimy sprawdzić czy znaleźliśmy nasz model, jeśli nie zwrócimy informację o błędzie 404 - takiej strony nie ma w naszym skrypcie. W dalszym etapie wiemy że nasz model istnieje, musimy więc pobrać jego metody i sprawdzić czy są zgodne z metodą jaką otrzymał serwer od przeglądarki, jeśli metoda znajdzie się w tablicy zmienimy wartość $isAllowed na true.

Ufff pozostało już tylko sprawdzić czy w danym adresie URL są jakieś parametry, porównajmy długość łańcucha znalezionej ścieżki i adresu url i jeśli są różne przypiszmy odpowiedeni parametry to nowej tablicy z naszymi argumentami. Tworzymy w tym celu nowy uchwyt do którego przypiszemy ciąg znaków z usuniętym "sztywnym" adresem URL, następnie wysadzimy go i przeliterujemy sprawdzając czy ilość zmiennych nie jest zbyt duża. W przypadku kiedy ilość zmiennych będzie większa niż ta zapisana w modelu zwróćmy 404 w innym wypadku przypiszmy odpowiednie parametry adresu url do parametrów modułu.
Mamy finisz, pozostało pobrać odpowiedni kontroler z naszego kontenera i wykonać funkcję przekazując ewentualne parametry urla lub requestu POST. 

Musimy jescze utworzyć plik settings.ini w katalogu settings, będzie on zawierał tylko jedną linijkę, zakładając że skrypt jest w katalogu głównym naszego serwera (zazwyczaj public_html):
uri = ""

Router gotowy, czas go przetestować.


4. Testujemy działanie routera

W katalogu Controller utwórz katalog Page i utwórz w nim plik HomeController.php
 

<?php declare(strict_types = 1);

namespace App\Controller\Page;

Class HomeController {
    
    public function index(): void
    {
       echo 'HomeController index() action';
    }
    
    public function foo($arg): void
    {
       echo 'HomeController foo() action, and my arg is:' . PHP_EOL;
       var_dump($arg);
    }

}

dodamy tam 2 proste funkcje które jedyne co zrobię to wyświetlą informację jaki kontroler i jaka akcja została wywołana, w drugim przypadku pobierzemy też argumenty z adresu url.
Teraz edytujmy plik init.php:

<?php declare(strict_types = 1);

use App\Controller\Container\SimpleContainer;
use App\Controller\Router\RouterController;
use App\Controller\Page as Page;

DEFINE('MAIN_DIR', __DIR__);

include 'vendor/autoload.php';


$c = new SimpleContainer();
$router = new RouterController($c);
$c->set('HomeController', new Page\HomeController($c));
$router->addRoute([RouterController::GET], '/', 'HomeController:index');
$router->addRoute([RouterController::GET], '/foo/:bar', 'HomeController:foo');
$router->run();

teraz przejdź pod adres swojego skryptu i sprawdź co wyświetli strona główna, jeśli zobaczysz to co wpisałeś w kontrolerze gratuluję 🙂 udało Ci się przejść bez błędów, jeśli nie spróbuj znaleźć błędy lub podziel się nimi w komentarzu, postaram się pomóc. Możemy również sprawdzić przekazywanie argumentów z adresu url, przejdź pod adres swojego skryptu i dopisz:
/foo/1
powinieneś zobaczyć:

HomeController foo() action, and my arg is: array(1) { ["bar"]=> string(1) "1" }

Na dziś to wszystko w załączniku znajdziecie gotowy template oraz pełny kod skryptu na jego obecnym etapie.

pecetowicz_template.zip pecetowicz-kurs-praktyka-1.zip

Edytowane przez Pankrok
Odnośnik do komentarza
Udostępnij na innych stronach

Super. Świetnie opisany kurs!

Odnośnik do komentarza
Udostępnij na innych stronach

Kontynuuj dyskusję

Dołącz do Pecetowicza, aby kontynuować dyskusję w tym wątku.

  • Dodaj nową pozycję...
  • Dodaj nową pozycję...