Skocz do zawartości

Kurs PHP by PanKrok - Praktyka piszemy blog #1


Pankrok
 Udostępnij

Rekomendowane odpowiedzi

  • Ekspert
Opublikowano (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

Problem wciąż nierozwiązany? Dodaj swoją odpowiedź

Jeśli chcesz dodać odpowiedź, zaloguj się lub zarejestruj nowe konto. Jedynie zarejestrowani użytkownicy mogą komentować zawartość tej strony.

Zarejestruj nowe konto

Załóż nowe konto. To bardzo proste!

Zarejestruj się

Zaloguj się

Posiadasz już konto? Zaloguj się poniżej.

Zaloguj się
 Udostępnij

×