System logowania i rejestracji w PHP z wykorzystaniem komponentów Zend Framework

Artykuł dodany: 08 stycznia 2016. Ostatnia modyfikacja: 16 września 2016.

Stopień trudności (1 - dla początkujących, 5 - dla ekspertów): 3

Logowanie oraz rejestracja występują praktycznie na każdej stronie internetowej. Poradników jak je wykonać też jest całe mnóstwo – niestety większość z nich bazuje na przestarzałych metodologiach np. usuniętym w PHP7 rozszerzeniu mysql_*. W artykule mam zamiar podążyć zupełnie inną drogą, pokazać jak można ułatwić sobie pisanie kodu wykorzystując gotowe elementy znalezione w sieci. Rzesza ludzi – naprawdę znających się na swoim zawodzie – pracuje codziennie nad tym, aby udostępniać swój kod choćby poprzez Github.com. Bezsensowne zatem jest pisanie całego kodu od podstaw. Po pierwsze dlatego że będzie on gorszej jakości (zwłaszcza jeżeli dopiero zaczynasz Czytelniku naukę zaawansowanego PHP), nie będzie poddany testom, może nastąpić wiele dodatkowych czynników o których nawet nie pomyślisz na tym etapie.

Co mam zamiar wykorzystać aby stworzyć nasz system logowania i rejestracji? Komponenty frameworka Zenda. Rozmawiając z różnymi ludźmi widzę zawsze ogromne przerażenie w oczach na tym etapie. Ale jak to? Nie znam tego frameworka, słyszałem że jest bardzo trudny. Otóż nic bardziej mylnego. Od czasu wydania wersji 2.5 trwa nieustanna, mozolna praca nad jak największym rozbiciem monolitycznej struktury na dużo mniejsze, niezależne od siebie komponenty. Z drugiej strony, framework ten to sprawdzona marka, można znaleźć bardzo dużo ogłoszeń pracy w których pracodawca wymaga podstawowej znajomości tego narzędzia. Jego choćby częściowe poznanie, ułatwia start w bardzo wielu firmach. Co ważne. Nie będę chciał w poradniku uzależniać się całkowicie od ZF. Wszystkie fragmenty kodu będzie można łatwo wymienić i dostosować tak, abyś miał jak największą swobodę operacji.

Jaka dodatkowa wiedza będzie potrzebna? Na pewno musisz wiedzieć Czytelniku czym są klasy oraz programowanie obiektowe, wymagana będzie podstawowa znajomość narzędzia Composer. Dobrze też, jeżeli miałeś wcześniej kontakt z PDO.

Gotowy projekt można obejrzeć na Github.com.

Przygotowanie projektu

Aby rozpocząć nasz projekt musimy przejść do wiersza poleceń i w katalogu docelowym wykonać:

composer init

Jedyną wymaganą na tym etapie informacją jest `Author`, pozostałe opcje możemy na razie pominąć. Po zatwierdzeniu wszystkich pytań zostanie utworzony plik composer.json zawierający zależności oraz wymagania. Jest to główny plik projektu który możemy edytować w ulubionym edytorze, albo częściowo z linii poleceń.

Wszystkie żądania obsługiwał będzie jeden plik index.php – jego też utwórzmy w katalogu wykonywalnym serwera (będzie to, w zależności od konfiguracji serwera, np. htdocs, public, public_html, www). Nigdy nie należy trzymać kluczowych plików w folderze, do którego ma dostęp użytkownik z zewnątrz.

Kolejną ważną sprawą jest konfiguracja serwera dokładnie pod nasze potrzeby. Jest rzeczą wręcz zadziwiającą, jak wielu programistów zdaje się całkowicie na domyślne ustawienia PHP, dostarczane przez firmę hostingową. Jest to najczęstszy powód dlaczego projekt działa na maszynie A, a nie chce na maszynie B. Jeżeli chcemy aby nasz projekt był przenośny, na wszystkich serwerach działał tak samo, powinniśmy samodzielnie zmienić co najmniej kilka opcji.

phpsettings.php

<?php
$env = APPLICATION_ENV ?: 'development';

if ('production' === $env) {
    error_reporting(0);
    ini_set('display_errors', 'off');
    ini_set('display_startup_errors', false);
} else {
    error_reporting(-1);
    ini_set('display_errors', 'on');
    ini_set('display_startup_errors', true);
}

ini_set('arg_separator.output', '&amp;');
ini_set('session.use_cookies', true);
ini_set('session.use_only_cookies', true);
ini_set('session.use_trans_sid', false);
ini_set('session.name', 'sessid');
ini_set('url_rewriter.tags', 'a=href,area=href,frame=src,fieldset=fakeentry');

ini_set('date.timezone', 'Europe/Warsaw');
setlocale(LC_ALL, 'pl_PL.UTF8');

ini_set('mbstring.language', 'uni');

ini_set('default_charset', 'UTF-8');
ini_set('input_encoding', 'UTF-8');
ini_set('output_encoding', 'UTF-8');

Aby nie komplikować możemy przyjąć, iż APPLICATION_ENV to stała zdefiniowana przez nas. Możemy ją oczywiście ustawiać też na kilka innych sposobów np. odczytując zmienne środowiskowe lub poprzez biblioteki typu Dotenv.

Routing czyli okno na świat

Wpisując w przeglądarce określony adres chcemy, by po stronie PHP została wykonana czynność lub zbiór czynności. Przechwytujemy wtedy zmienną $_GET i działamy bezpośrednio na niej. W podstawowym zakresie kod mógłby przyjąć postać:

if (isset($_GET['page']) && $_GET['page'] === 'login') {
    // pozostałe akcje np. wyświetlenie formularza logowania
}

Jest to oczywiście dobre rozwiązanie jeżeli posiadamy bardzo prosty kod. Jednak jego modyfikacja na późniejszym etapie może przysporzyć pewnych problemów. W tym miejscu po raz pierwszy przyda nam się biblioteka Zend framework, konkretnie Zend\Http. Powinniśmy dołączyć ją do naszego projektu:

composer require zendframework/zend-http

Komponent Zend\Http opiera się na 3 podstawowych klasach: Request, Response i Headers. Jest to bezpośrednie mapowanie zadań które zachodzą na co dzień w przeglądarkach. Jeżeli otworzysz konsolę (np. Firebug) zobaczysz, że przeglądarka wysyła żądanie do serwera (Request), serwer odpowiada swoją treścią (Response) równocześnie obie strony wysyłają sobie wzajemnie odpowiednie nagłówki (Headers). Tak funkcjonuje sieć i jest to bardzo ważny kawałek wiedzy który należy przyswoić. Wspominam o tym również dlatego, że istnieje od pewnego czasu dokument PSR-7 (stworzony przez Matthew Weier O’Phinney – głównego projektanta Zend Framework) opisujący dokładnie procesy o których wspomniałem i będący podstawą frameworków przyszłości w PHP. W chwili pisania artykułu, dwa najpopularniejsze które zaimplementowały PSR-7 to mikroframeworki Zend Expressive oraz Slim 3.

Pora przetestować komponent Zend\Http. Z chwilą jego pobrania, został utworzony plik Composera – autoload.php który zajmie się wczytywaniem odpowiednich klas.

index.php

<?php

use Zend\Http\PhpEnvironment\Request;

defined('APPLICATION_ENV') || define('APPLICATION_ENV', 'development');

chdir(dirname(__DIR__));

// Wczytujemy wcześniej zdefiniowany plik ustawień
require 'phpsettings.php';
require 'vendor/autoload.php';

// Na wszelki wypadek przechwytujemy wszystkie potencjalne wyjątki
try {
    $request = new Request();
    $paramPage = $request->getQuery('page');

    if ($paramPage === 'login') {
        echo 'Strona logowania';
    }
} catch (\Exception $e) {
    echo $e->getMessage();
}

Odwiedzając teraz przykładowy adres http://naszserwer/projekt/index.php?page=login zostanie wyświetlony komunikat `Strona logowania`.

Możliwe alternatywy: na tym etapie można postawić również na inny router typu Aura.Router, FastRoute. Można wykorzystać tylko warunki PHP jak zostało pokazane na początku sekcji. Jest możliwe także całkowite przejście na jeden z wymienionych wcześniej mikroframeworków.

Przygotowanie bazy danych

Wszystkie dane o użytkownikach przechowywane są najczęściej w bazie danych. Oczywiście struktura zależna jest bardzo od samego projektu i mogę tu przedstawić wyłącznie podstawowe pola. Najczęściej wymagany jest login/email, hasło, data rejestracji. Dla bazy PostgreSQL tabela może przyjąć postać:

CREATE TABLE users
(
  id serial NOT NULL,
  email character varying(150) NOT NULL,
  password character varying(60) NOT NULL,
  registration_date timestamp(0) without time zone NOT NULL DEFAULT now(),
  CONSTRAINT users_pkey PRIMARY KEY (id),
  CONSTRAINT email_uq UNIQUE (email)
);

Natomiast dla bazy MySQL:

CREATE TABLE `users` ( 
	`id` Int UNSIGNED AUTO_INCREMENT NOT NULL,
	`email` VarChar( 150 ) NOT NULL,
	`password` VarChar( 60 ) NOT NULL,
	`registration_date` Timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
	PRIMARY KEY ( `id` ),
	CONSTRAINT `unique_email` UNIQUE( `email` ) )
ENGINE = InnoDB
AUTO_INCREMENT = 1;
CREATE INDEX `index_id` USING BTREE ON `users`( `id` );

Dla danych powyżej zakładamy, że użytkownik posługuje się do logowania unikalnym adresem email. Hasło przetworzymy funkcją password_hash która obecnie zwraca 60-cio znakowy ciąg.

Powiązania między klasami

Tworząc nowy projekt w pewnym momencie zawsze pojawia się problem w jaki sposób rozwiązywać zależności pomiędzy klasami. Najgorszym rozwiązaniem, choć jeszcze w dalszym ciągu przez niektórych praktykowanym, jest użycie słowa global. Zend Framework postawił na Service Manager który jest implementacją wzorca Service Locator. Dodatkowo, chciałbym w aplikacji wykorzystać wersję 3.1 która jest znacznie szybsza od v2 oraz posiada kilka ciekawych funkcji niedostępnych wcześniej.

composer require zendframework/zend-servicemanager:^3.1

By korzystać z SM należy go wcześniej nieco przygotować. W głównym folderze aplikacji utwórzmy katalog config z dwoma plikami:

config/container.php

<?php

use Zend\ServiceManager\ServiceManager;
use Zend\ServiceManager\Config;

$config = require __DIR__ . '/config.php';

$sm = (new Config(isset($config['dependencies']) ? $config['dependencies'] : []))->configureServiceManager(new ServiceManager);
$sm->setService('config', $config);
return $sm;

config/config.php

<?php

use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\Glob;

$config = [];
foreach (Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php', Glob::GLOB_BRACE) as $file) {
    $config = ArrayUtils::merge($config, include $file);
}

return $config;

W folderze config/autoload możemy od teraz umieszczać pliki konfiguracyjne – podobnie jak ma to miejsce w ZF2 albo Zend Expressive. Dodajmy jeszcze wczytanie naszego Service Managera do pliku index.php:

index.php

try {
    /* @var $container Zend\ServiceManager\ServiceManager */
    $container = require 'config/container.php';
    ...

Możemy teraz przygotować adapter bazy danych. Dzięki komponentowi Zend\Db wystarczy że wymienimy tylko konfigurację i możemy pracować na dowolnym z obsługiwanych SBD.

composer require zendframework/zend-db:^3.0-dev

Adapter do działania wymaga podania prawidłowych danych jak baza, nazwa użytkownika, hasło. Możemy w tym momencie wykorzystać utworzoną wcześniej strukturę konfiguracyjną. Od tej pory będę działał na bazie danych PostgreSQL.

W katalogu config/autoload potrzebne nam będą dwa nowe pliki. Te z rozszerzeniem local.php traktowane są przez system jako dostępne tylko na naszym lokalnym komputerze – nie powinny być nigdzie wysyłane, mogą zawierać wrażliwe dane.

config/autoload/db.local.php

<?php
return ['db' => 
    [
        'username'  => 'użytkownik',
        'password'  => 'hasło'
    ]
];

config/autoload/db.global.php

<?php
return ['db' => 
    [
        'driver'    => 'Pdo_Pgsql',
        'database'  => 'testdb',
        'schema'    => 'public'
    ]
];

Pora poinformować SM o naszym adapterze. Utwórzmy kolejny plik:

config/autoload/dependencies.global.php

<?php

use Zend\Db\Adapter;

return [

    'dependencies' => [
        'factories' => [
            Adapter\AdapterInterface::class => Adapter\AdapterServiceFactory::class,
        ],
    ],
];

Specjalna konstrukcja ::class zwraca pełną nazwę klasy. Jeżeli zajrzymy do pliku AdapterServiceFactory widać, że poszukuje on automatycznie klucza o nazwie `db` z naszego Service Managera. Dlatego wcześniej w konfiguracji zwracaliśmy właśnie taką tablicę. Dla ułatwienia moglibyśmy ustalić alias dla naszego adaptera np. `dbadapter` jednak trzeba pamiętać, że każde takie odwołanie to dodatkowa praca po stronie PHP – powoduje spadek wydajności SM. W jaki sposób wyciąga się dane z naszego kontenera? Dodałem wcześniej w pliku index.php zmienną $container. Wywołując:

var_dump($container->get(Zend\Db\Adapter\AdapterInterface::class));

Utworzyliśmy nową instancję klasy Zend\Db\Adapter\Adapter (zgodnie z Zend\Db\Adapter\AdapterServiceFactory). Domyślnie każde takie odwołanie poprzez `get()` zwraca ciągle jedną, tą samą cache’owaną instancję (shared). Service Manager Zenda w wersji 3 zyskał nową metodę – build() – jej wywołanie powoduje utworzenie zawsze nowej instancji. Dodatkowo możemy w metodzie tej przekazać tablicę opcji która zostanie użyta przy wywołaniu klasy.

Możliwe alternatywy: Service Manager to tylko jedna z opcji. Możemy go zastąpić np. Pimple lub wykorzystać zwykły DI (Dependency Injection) wstrzykując bezpośrednio wymagane klasy.

Formularze i walidacja danych

Pora przejść do tworzenia formularzy. Ponieważ klasa Zend\Form jest odrobinę zbyt skomplikowana i posiada bardzo dużo zależności, wykorzystamy znacznie prostszy system. Z pomocą przyjdą nam Aura\Input – zbiór klas odpowiedzialnych za generowanie formularza, Aura\Html – dodatkowe “helpery” ułatwiające renderowanie utworzonych pól.

composer require aura/input aura/html

Przed pierwszym użyciem zalecam oczywiście zapoznanie się z dokumentacją aby lepiej zrozumieć działanie komponentów. Gotowa klasa rejestracji będzie rozszerzać Aura\Input\Form, metoda init() wywoływana jest automatycznie w konstruktorze klasy. Od tego momentu będę wykorzystywał Framework Bootstrap do ustalenia wyglądu, stąd zaczną pojawiać się klasy CSS w nim wykorzystywane. Są to także pierwsze klasy naszego systemu, stąd powstaje konieczność utworzenia odpowiednich katalogów. Przyjęło się że pliki trzymamy w folderze src katalogu głównego. Struktura która będzie nam potrzebna w całości prezentuje się tak:

|-src
|-src-App
|-src-App-Db
|-src-App-Entity
|-src-App-Form

Aby Composer był w stanie prawidłowo odszukać nasze nowe ścieżki, musimy do pliku composer.json dodać nową regułę:

"autoload": {
    "psr-4": {
        "App\\": "src/App/"
    }
}

Możemy teraz przejść do tworzenia klas formularzy:

src/App/Form/RegisterForm.php

<?php
namespace App\Form;

use Aura\Input\Form;

class RegisterForm extends Form
{
    public function init()
    {
        $this->setField('email')->setAttribs([
            'maxlength' => 150, 'required' => 'required', 'class' => 'form-control'
        ]);
        $this->setField('password', 'password')->setAttribs([
            'maxlength' => 20, 'required' => 'required', 'class' => 'form-control'
        ]);
        $this->setField('password_confirm', 'password')->setAttribs([
            'maxlength' => 20, 'required' => 'required', 'class' => 'form-control'
        ]);

        $this->setField('submit', 'submit')->setAttribs([
            'value' => 'Zarejestruj się', 'class' => 'btn btn-primary'
        ]);

        $filter = $this->getFilter();

        $filter->setRule(
            'email', 
            'Wprowadź poprawny adres email.', 
            function ($value) {
                return filter_var($value, FILTER_VALIDATE_EMAIL);
            }
        );

        $filter->setRule(
            'password', 
            'Hasło musi posiadać minimum 6 znaków.', 
            function ($value) {
                if (strlen($value) >= 6) {
                    return true;
                }
                return false;
            }
        );

        $filter->setRule(
            'password_confirm',
            'Hasła muszą być zgodne.',
            function ($value, $fields) {
                return $value == $fields->password;
            }
        );
    }
}

Dodajemy 3 nowe pola – wszystkie wymagane – email, password, password_confirm. Do sprawdzenia poprawności adresu e-mail wykorzystujemy wbudowaną w PHP funkcję filter_var(), hasło musi mieć minimum 6 znaków. Od razu możemy też utworzyć dość podobny formularz logowania:

src/App/Form/LoginForm.php

<?php
namespace App\Form;

use Aura\Input\Form;

class LoginForm extends Form
{
    public function init()
    {
        $this->setField('email')->setAttribs([
            'maxlength' => 150, 'required' => 'required', 'class' => 'form-control'
        ]);
        $this->setField('password', 'password')->setAttribs([
            'maxlength' => 20, 'required' => 'required', 'class' => 'form-control'
        ]);

        $this->setField('submit', 'submit')->setAttribs([
            'value' => 'Zaloguj się', 'class' => 'btn btn-primary'
        ]);

        $filter = $this->getFilter();

        $filter->setRule(
            'email', 
            'Wprowadź poprawny adres email.', 
            function ($value) {
                return filter_var($value, FILTER_VALIDATE_EMAIL);
            }
        );

        $filter->setRule(
            'password', 
            'Hasło musi posiadać minimum 6 znaków.', 
            function ($value) {
                if (strlen($value) >= 6) {
                    return true;
                }
                return false;
            }
        );
    }
}

Teraz ciekawa część. Będziemy chcieli nasze formularze wyciągać bezpośrednio z Service Managera. Potrzebne nam będą zatem pliki fabryk.

src/App/Form/RegisterFormFactory.php

<?php
namespace App\Form;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Aura\Input\Builder;
use Aura\Input\Filter;

class RegisterFormFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $form = new RegisterForm(new Builder, new Filter);
        return $form;
    }
}

src/App/Form/LoginFormFactory.php

<?php
namespace App\Form;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Aura\Input\Builder;
use Aura\Input\Filter;

class LoginFormFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $form = new LoginForm(new Builder, new Filter);
        return $form;
    }
}

Możemy je teraz dodać do konfiguracji SM:

<?php

use Zend\Db\Adapter;

return [

    'dependencies' => [
        'factories' => [
            Adapter\AdapterInterface::class => Adapter\AdapterServiceFactory::class,
            App\Form\RegisterForm::class => App\Form\RegisterFormFactory::class,
            App\Form\LoginForm::class => App\Form\LoginFormFactory::class,
        ],
    ],
];

Formularze są gotowe ale w jaki sposób je wyświetlić? Najbardziej oczywiste będzie utworzenie pseudo-plików widoku w czystym PHP. Bardziej zaawansowani użytkownicy z całą pewnością zastanowią się nad wykorzystaniem projektów takich jak PHPTAL, Twig, Smarty. Kodu z całą pewnością będzie wtedy mniej, z drugiej strony będzie on bardziej czytelny i bezpieczny. Aby nie komplikować postawiłem na zwykłe pliki PHP. Ponieważ są dość duże, można je obejrzeć bezpośrednio na Github.com.

<?php
use Aura\Html\HelperLocatorFactory;
$factory = new HelperLocatorFactory;
$helper = $factory->newInstance();

Kod powyżej użyty jest w obu plikach widoku. Tworzy nową instancję “helperów” do których dostęp uzyskujemy poprzez:

<?= $helper->input($form->get('email')); ?>

Linia powyżej powoduje utworzenie pola input na bazie danych, pochodzących z formularza.

<input type="text" name="email" maxlength="150" required="required" class="form-control" />

Po wysłaniu formularza pola zostaną zweryfikowane pod kątem poprawności danych. W przypadku wystąpienia błędów zostaną wyświetlone za pomocą kodu:

<?php if (count($form->getMessages()) > 0): ?>
    <div class="row" style="margin-top: 2em;">
        <p class="text-danger">Wystąpiły błędy w formularzu:</p>
        <?php foreach ($form->getMessages() as $message) {
            echo '<li>', $message[0], '</li>';
        } ?>
    </div>
<?php endif; ?>

Encje oraz baza danych

Do tej pory skonfigurowaliśmy większość projektu. Nie ma jednak jeszcze obsługi samej bazy danych – dodawania użytkowników. Aby uporządkować dane powinniśmy zmapować je w relacji 1:1 na klasy w PHP. Dokonamy tego za pomocą tak zwanych encji. Jest to bazowa klasa odpowiadająca danym z zewnątrz – dane mogą pochodzić z bazy danych, plików JSON, XML itd. Klasa powinna zawierać settery/gettery dla poszczególnych pól.

src/App/Entity/UserEntity.php

<?php

namespace App\Entity;

use Zend\Stdlib\ArraySerializableInterface;

class UserEntity
{
    protected $id;
    protected $email;
    protected $password;
    protected $registrationDate;

    public function getId()
    {
        return $this->id;
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getRegistrationDate()
    {
        return $this->registrationDate;
    }

    public function setId($id)
    {
        $this->id = (int) $id;
        return $this;
    }

    public function setEmail($email)
    {
        $this->email = $email;
        return $this;
    }

    public function setPassword($password)
    {
        $this->password = $password;
        return $this;
    }

    public function setRegistrationDate($registrationDate)
    {
        $this->registrationDate = $registrationDate;
        return $this;
    }

    /**
     * @param array $array
     */
    public function exchangeArray(array $array)
    {
        foreach ($array as $key => $value) {
            if (property_exists($this, $key)) {
                if ($key === 'password') {
                    $value = password_hash($value, PASSWORD_DEFAULT);
                }
                $this->$key = $value;
            }
        }
    }
    /**
     * @return array
     */
    public function getArrayCopy()
    {
        $data = [];
        foreach (get_object_vars($this) as $key => $value) {
            $data[$key] = $value;
        }
        return $data;
    }
}

Dodatkowe metody exchangeArray() oraz getArrayCopy() ułatwiają transformację tablica<->właściwości. W przypadku pola hasła, postanowiłem również dokonywać od razu konwersji z gołego tekstu na ciąg zahaszowany funkcją password_hash(). Jest to tylko podstawowa koncepcja którą można znacznie ulepszyć stosując np. hydrator. Proces hydracji umożliwia przekształcanie obiektów w tablicę i vice-versa, z wykorzystaniem dodatkowych filtrów i strategii. Moglibyśmy np. ustalić, że pole timestamp z bazy, było by przekształcane od razu na obiekt DateTime. Polecam zapoznanie się dokumentacją Zend\Hydrator gdyż jest to ciekawe i potężne narzędzie.

Potrzebna nam będzie jeszcze klasa wykonująca akcje bezpośrednio na wybranej bazie danych. Zend\Db wykorzystuje wzorzec “Table Data Gateway”, dlatego też jego implementację wdrożymy.

src/App/Db/UserTableGateway.php

<?php

namespace App\Db;

use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Sql\TableIdentifier;
use App\Entity\UserEntity;

class UserTableGateway extends TableGateway
{

    public function __construct(AdapterInterface $adapter)
    {
        $table = new TableIdentifier('users');
        parent::__construct($table, $adapter);
    }

    /**
     * Fetch user by id
     * 
     * @param int $id
     * @return array|\ArrayObject|null
     */
    public function fetchById($id)
    {
        return $this->select(['id' => $id])->current();
    }

    /**
     * Fetch user by email
     * 
     * @param string $email
     * @return array|\ArrayObject|null
     */
    public function fetchByEmail($email)
    {
        return $this->select(['email' => $email])->current();
    }

    /**
     * Check if email address is registered
     * 
     * @param string $email
     * @return boolean
     */
    public function checkEmailIsRegistered($email)
    {
        $result = $this->select(['email' => $email]);
        return ($result->count() > 0) ? true : false;
    }

    /**
     * Insert new user
     * 
     * @param UserEntity $user
     * @return int
     */
    public function insertUser(UserEntity $user)
    {
        $data = $user->getArrayCopy();
        unset($data['id'], $data['registrationDate']);
        return $this->insert($data);
    }

}

Metody są dość proste, wymagają tylko zapoznania się z dokumentacją. Trzecia wersja Zend\Db na której pracujemy, ma bardzo uproszczony kod jeżeli chodzi o operacje na bazie. Stąd wystarczają proste polecenia $this->select, $this->insert aby w szybki sposób pobierać lub wstawiać dane. W drugiej wersji tego pakietu kodu było znacznie więcej.

Podobnie jak dla formularzy potrzebny nam będzie jeszcze plik fabryki dla Service Managera:

<?php

namespace App\Db;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\Db\Adapter\AdapterInterface;

class UserTableGatewayFactory implements FactoryInterface
{

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $adapter = $container->get(AdapterInterface::class);
        return new UserTableGateway($adapter);
    }

}

Za pomocą kontenera pobieramy z rejestru adapter bazy danych i wstrzykujemy go do klasy UserTableGateway. Wystarczy teraz dodać powiązanie do pliku konfiguracji SM:

config/autoload/dependencies.global.php

use App\Db;

'factories' => [ //...
    Db\UserTableGateway::class => Db\UserTableGatewayFactory::class
]

Logika aplikacji

Mamy już przygotowane wszystko, oprócz samej logiki. Pora wrócić do pliku index.php:

try {
    /* @var $container Zend\ServiceManager\ServiceManager */
    $container = require 'config/container.php';
    $request = new Request();
    $paramPage = $request->getQuery('page');

    switch ($paramPage) {
        case 'login':
            /* @var $form \Aura\Input\Form */
            $form = $container->get(App\Form\LoginForm::class);
            if ($request->isGet()) {
                require 'views/login.php';
            } elseif ($request->isPost()) {
                $data = $request->getPost()->toArray();
                $form->fill($data);
                if ($form->filter()) {
                    $userGateway = $container->get(App\Db\UserTableGateway::class);
                    $result = $userGateway->fetchByEmail($request->getPost('email'));
                    if (password_verify($request->getPost('password'), $result['password'])) {
                        echo 'Użytkownik zalogowany prawidłowo.';
                    } else{
                        echo 'Nie udało się zalogować użytkownika.';
                    }
                } else {
                    require 'views/login.php';
                }
            }
            break;
        case 'register':
            /* @var $form \Aura\Input\Form */
            $form = $container->get(App\Form\RegisterForm::class);
            if ($request->isGet()) {
                require 'views/register.php';
            } elseif ($request->isPost()) {
                $data = $request->getPost()->toArray();
                $form->fill($data);
                if ($form->filter()) {
                    $u = new \App\Entity\UserEntity();
                    $u->exchangeArray($data);
                    $userGateway = $container->get(App\Db\UserTableGateway::class);
                    try {
                        $userGateway->insertUser($u);
                        echo 'Użytkownik zarejestrowany prawidłowo.';
                    } catch (Zend\Db\Exception\ExceptionInterface $e) {
                        echo 'Nie udało się zarejestrować użytkownika. Błąd: ', $e->getMessage();
                    }
                } else {
                    require 'views/register.php';
                }
            }
            break;
        default:
            break;
    }
} catch (\Exception $e) {
    echo $e->getMessage();
}

Wspomagając się Routerem utworzyliśmy bardzo prosty “switch”, który na podstawie parametrów page=login oraz page=register przełącza widoki. Metody isGet, isPost sprawdzają odpowiednio czy żądanie przyszło z GET czy POST. Router umożliwia też od razu pobranie zawartości całej tablicy parametrów $_POST za pomocą $request->getPost()->toArray(). Z pomocą przychodzą nam utworzone wcześniej fabryki. Z Service Managera wyciągamy odpowiednie klasy formularzy oraz UserTableGateway. Obsługę błędów oraz dalszych akcji, pozostawiłem już do samodzielnego uzupełnienia w zależności od potrzeb. Czym warto się jeszcze zainteresować, to zabezpieczenie formularza przed atakiem CSRF – jest opisane w dokumentacji Aura\Input. Wprowadzenie tego zabezpieczenia wymaga dodania obsługi sesji, dlatego krok pominąłem. Uniemożliwi też to od razu odświeżanie formularza w nieskończoność. Formularz będzie prawidłowo wypełniony tylko, gdy token sesyjny będzie prawidłowy. Wyjątki przechwytywane przez naszą aplikację możemy zapisywać do pliku lub bazy danych celem późniejszej analizy. Wprowadzenie systemu szablonów poprawiłoby czytelność dlatego polecam przejrzeć dostępne na rynku i wybrać jeden ze spełniających oczekiwania. Osobiście, gdybym chciał kod mocniej rozwijać, poszedłbym jeszcze w kierunku utworzenia klasy będącej korzeniem całej aplikacji.

Podsumowanie

Mam nadzieję że kod który przedstawiłem w artykule nie okazał się za trudny (koncepcja fabryk, kontenerów). Tym samym nie zniechęcił Was do dalszych eksperymentów. Moim celem było pokazanie, że możliwe jest wyjście z programowania strukturalnego na rzecz dużo bardziej rozwiniętego kodu obiektowego. Wykorzystując przedstawione tutaj klasy z łatwością można przejść do pisania aplikacji w mikro-frameworkach (zbędny byłby wtedy Router, część przedstawionego kodu jest bazą aplikacji pisanej w Zend Expressive). Ich użycie sprawia, iż struktura aplikacji staje się dużo bardziej uporządkowana, zyskujemy doświadczenie, a w przyszłości dużo łatwiejsza będzie zmiana na pełnowymiarowy framework. Najważniejszy dla programisty jest rozwój i polepszanie swoich umiejętności. Czego Wam wszystkim życzę.

Komentarze

  • Avatar użytkownika viking

    Masz tutaj: ini.default-charset

    Dostępne od PHP 5.6. Od tej wersji używanie `iconv.output_encoding` itd. zwraca komunikat DEPRECATED.

  • Mam pytanko odnośnie :

    ini_set(‘input_encoding’, ‘UTF-8’);
    ini_set(‘output_encoding’, ‘UTF-8’);

    wujcio google znajduje tylko – iconv. output_encoding,

    manual – http://php.net/manual-lookup.php?pattern=input_encoding&scope=quickref

    utworzyłem plik index.php

    <?php
    include (‘phpsettings.php’);

    var_dump(
     ini_get(‘default_charset’),
     ini_get(‘input_encoding’),
     ini_get(‘output_encoding’)
    );

    wynik :

    string ‘UTF-8’ (length=5)
    boolean false
    boolean false

    Powinno być UTF-8 a dostaję false ?

  • Avatar użytkownika viking

    Fakt, dzięki za zwrócenie uwagi. Reszta opcji jest poprawna – do ustawienia przez ini_set().

    Najłatwiej będzie też skorzystać z plików .user_ini.

  • Cóż, manual prawdę Ci powie :)
    Po wrzuceniu w google “ini_set” pierwsze dwa wyniki…
    Dyrektywa “short_open_tag” jest “PHP_INI_PERDIR” co oznacza, że można ją zmienić TYLKO przy pomocy php.ini lub .htaccess i nie zależy to od tego jaki to system operacyjny.
    W .htaccess wystarczy wrzucić “php_flag short_open_tag off” , oczywiście bez cudzysłowów i … i działa :)
    Resztę też dobrze by było sprawdzić w manualu.

  • Witam
    Specjalistą od php ani apache nie jestem ale wrzuciłem do pliku “phpsettings.php” to co zechciałeś napisać m.in. “ini_set(‘short_open_tag’, false);” , w jednym z plików wykasowałem “php” z “<?php” i cisza… żadnego błędu… “off” zamiast “false” też nie pomogło. Jak zmieniłem w php.ini short_open_tag = On na Off wtedy dostałem “Fatal error: Class ‘FW_Autoloader … … ‘”. Pojęcia nie mam czy to tylko na Debianie którego używam czy też pod innymi systemami też tak ale u mnie to nie działa…

Dodaj komentarz

*
Nazwa zostanie wyświetlona wraz z komentarzem. Możesz też utworzyć nowe konto w serwisie, dzięki czemu uzyskasz dodatkową funkcjonalność.
*
Akceptowana jest ograniczona składnia Textile. Wszystkie tagi HTML zostaną usunięte.