Laminas Mezzio jest swego rodzaju klejem dla innych komponentów. Dzięki temu, że jest on zgodny ze standardami, dużo łatwiej można te klocki wymieniać. Standardy PSR to zbiór dobrych praktyk kodowania oraz interfejsów ułatwiających komunikację między zewnętrznymi bibliotekami. Żeby zacząć przygodę z Mezzio wystarczy zaznajomić się z jego 4 głównymi konceptami.
Pierwszym i najważniejszym, który determinuje sposób tworzenia aplikacji jest podejście do obsługi żądań i odpowiedzi serwera. Głównymi bohaterami są tutaj: pośrednik (middleware) i obsługa żądania (request handler). Cała komunikacja jest zgodna z PSR-7 i PSR-15. Pozostałe idee są już analogiczne jak w większości frameworków. Kontener Dependency Injection zgodny z PSR-11 i Router to dwa wymagane komponenty. Ostatnim opcjonalnym jest silnik szablonów, który przyda się w aplikacjach z widokami.
Instalacja Laminas Mezzio
Oczywiście, przygodę z tym microframeworkiem można rozpocząć od pliku composer.json
i samodzielnie skonfigurować cały projekt. W większości nowoczesnych rozwiązań istnieje jednak możliwość skorzystania ze szkieletu aplikacji. Tak właśnie uczynię. Jest to szybsze i prostsze. Jeśli w aplikacji planowane jest coś niestandardowego to łatwiej jest dostosować gotowy szablon, niż startować od zera. Ewentualnie można samodzielnie przygotować repozytorium z własnym uniwersalnym szkieletem, ale mój nie odbiega aż tak bardzo od oryginalnego, więc szkoda zachodu.
Instalacja zależności Laminas Mezzio to odpalenie jednej komendy za pomocą composera. Ja jeszcze wcześniej przygotuję sobie obraz dockera, w którym to mam właśnie serwer Apache2, PHP8 i Composer. Dla zainteresowanych poniżej zamieszczam przykładową konfigurację. Oprócz tego warto dodać sobie w lokalnych hostach wpis customer-api.local 10.25.0.10
tak by móc się łączyć z API po domenie, a nie przez adres IP.
#Dockerfile
FROM php:8.0-apache
RUN apt-get update \
&& apt-get install -y libzip-dev zip \
&& docker-php-ext-install zip \
&& a2enmod rewrite ssl \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN mkdir /etc/apache2/ssl \
&& openssl req \
-new \
-newkey rsa:4096 \
-days 365 \
-nodes \
-x509 \
-subj "/C=/ST=/L=/O=/CN=customer-api.local" \
-keyout /etc/apache2/ssl/apache.key \
-out /etc/apache2/ssl/apache.crt
RUN echo "ServerName customer-api" >> /etc/apache2/apache2.conf
RUN service apache2 restart
WORKDIR /var/www
#docker-compose.yml
version: '3.7'
services:
server:
build:
context: .
dockerfile: Dockerfile
container_name: 'customer_api'
restart: 'on-failure'
tty: true
volumes:
- .:/var/www
- ./000-default.conf:/etc/apache2/sites-enabled/000-default.conf
networks:
customer-api:
ipv4_address: 10.25.0.10
networks:
customer-api:
name: customer-api
driver: bridge
ipam:
config:
- subnet: 10.25.0.0/16
#000-default.conf
<VirtualHost *:443>
ServerName customer-api.local
DocumentRoot /var/www/public
<Directory /var/www/public>
DirectoryIndex index.php
AllowOverride All
Order allow,deny
Allow from all
<IfModule mod_authz_core.c>
Require all granted
</IfModule>
</Directory>
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/apache.crt
SSLCertificateKeyFile /etc/apache2/ssl/apache.key
</VirtualHost>
Jeśli preferujecie inne środowisko to nie ma problemu. W każdym razie mając już przygotowane technologie wystarczy uruchomić w terminalu polecenie.
composer create-project mezzio/mezzio-skeleton customer-api
W tym momencie uruchomi się interaktywna instalacja. Dzięki temu można w łatwy sposób rozpocząć projekt. Nie trzeba przywiązywać się do dokonanych wyborów, łatwo można je zmienić zaraz po instalacji. Biblioteki, na które się zdecydujecie zostaną dociągnięte za pomocą composera i wprowadzą odpowiedni wpis w pliku konfiguracyjnym. Cały proces dostępny na poniższym zdjęciu. Jak widzicie, instalacja to zaledwie 5 kroków, które w znacznej mierze odnoszą się do podstawowych konceptów, o których wspomniałem wcześniej.


W pierwszym kroku należy wybrać typ instalacji. Ja zawsze decyduję się na opcje numer 3, czyli podział modularny. W skrócie każdy moduł jest osobnym bytem i jest zgodny z PSR-4. Z łatwością można go użyć w innym projekcie, wymienić na inny lub pozbyć się go z aplikacji. Pozostałe dwie opcje to minimalna instalacja bez narzuconej struktury katalogów i plików oraz domyślny wybór, czyli cały kod w jednym katalogu src
. Główna różnica między opcjami 2 i 3 jest taka, że w opcji flat tak naprawdę wszystko jest jednym modułem (src/App
). Za to w modular można ich tworzyć więcej (src/App/{src|test|templates}
, src/Log/{src|test|templates}
).
Drugi krok to wybór kontenera DI. Jeśli znacie Symfony to może dobrym wyborem będzie właśnie ten komponent. Ewentualnie każdy inny, który Wam odpowiada. Ja znam jedynie te od Symfony i Laminasa. Naturalnie jest opcja skorzystania z bibliotek niewystępujących na tej liście. Wówczas trzeba samodzielnie zainstalować go za pomocą Composera. Jeden warunek – zgodność z wcześniej wymienionymi standardami. Ja skorzystam z laminas-servicemanager
, dlatego że dobrze go znam, ale co ważniejsze bardzo lubię bazować na fabrykach.
Trzecim krokiem jest wybór Routera. Tutaj jest już znacznie mniej opcji. Dobrze znam ten od Laminasa, ale sięgam po znacznie szybszy i łatwy w użyciu, czyli FastRoute
. Jak widzicie jest to opcja domyślna, co sugeruje dojrzałość rozwiązania. Twórcy nie wpychają na siłę własnych bibliotek.
Do stworzenia API nie będę potrzebował silnika szablonów. Rezygnuję więc z jego instalacji. Normalnie sięgnąłbym raczej po Twiga
. Z Plates
nie miałem do czynienia, za to z laminas-view
działam na co dzień. Jest w porządku, bo wystarczy znajomość PHP, ale jednak Twig
wygrywa.
Obsługa błędów w API wygląda nieco inaczej, dlatego nie instaluję Whoops
. Dodam tylko, że jest to rozwiązanie mocno ułatwiające development. W przyjaznej formie prezentuje występujący błąd oraz podgląd kodu, w którym wystąpił. Warto rozważyć jego użycie – oczywiście tylko na środowiskach deweloperskich.
I to wszystko, instalacja zakończona. Teraz przejdę przez najważniejsze pliki i krótko o nich opowiem oraz dokonam kilku zmian względem domyślnej struktury plików i katalogów.
Główne pliki/katalogi oraz ich odpowiedzialność
Po instalacji, wewnątrz projektu można znaleźć sporo plików i folderów. Zacznę od tych drugich:
bin
– w tym miejscu znajdą się skrypty konsolowe. Na ten moment jest tam wyłącznieclear-config-cache.php
, który jak sama nazwa wskazuje, odpowiedzialny jest za wyczyszczenie cache plików konfiguracji. Pliki te nie zmieniają się aż tak często więc dobrze, że są cachowane. Przy wdrożeniu można więc usunąć je ręcznie albo właśnie wywołać owy skrypt wpisując w terminalu:php bin/clear-config-cache.php
. W procesie developmentu dla ułatwienia pliki konfiguracyjne nie są cachowane.config
– ten folder to wszystkie pliki konfiguracyjne aplikacji. Te zawierające w sobielocal.php
powinny być wykluczone z repozytorium i często zawierają dane wrażliwe. Te zglobal.php
to ogólna konfiguracja dla całego projektu, która trafia do repozytorium. Niektóre z nich zostaną jeszcze dokładniej opisane poniżej.data
– to kontener na pliki wynikające z projektu. Wspomniany powyżej cache, różnego rodzaju raporty, pliki tekstowe i graficzne, logi i tak dalej. Większość z nich najczęściej nie trafia do repozytorium.public
– tutaj umiejscowione są pliki z dostępem publicznym. Większość dotyczy front-endu. Oprócz tego .htaccess odpowiedzialny za konfigurację serwera i przede wszystkim Front Controller, czyli plik od którego zaczyna się każdy request:index.php
.src
– i wreszcie miejsce na faktyczny kod aplikacji, czyli moduły stanowiące projekt.vendor
– wszystkie zewnętrzne zależności zarządzane za pomocą composera. Kod umiejscowiony wewnątrz jest tylko do podglądu i nie należy wykonywać w nim żadnych zmian.
Zastosuję kilka zmian w stosunku do pierwotnej wersji. Po pierwsze czyszczę plik composer.json
ze zbędnych skryptów, których nie używam oraz definiuję w nim właściwe dla aplikacji własności. Kolejna rzecz to zmiana nazwy domyślnego modułu. App
zdecydowanie mi nie odpowiada, w przypadku tej aplikacji zmieniam na Api
. Oprócz zmiany nazwy folderu należy podmienić namespace dla istniejących wewnątrz plików oraz ścieżkę autoloadingu. Zostawię tylko jeden przykładowy HomePageHandler
i jego fabrykę. Jeszcze czyszczenie komentarzy (mogą być pomocne przy poznawaniu narzędzia) oraz konfiguracja phpunit
i phpcs
. Z grubsza to tyle zmian.
Teraz zostają do omówienia główne pliki. Jeśli chodzi o Handler (przykładowy już istnieje) oraz Middleware (stworzę w przyszłości) to ich szczegółowe omówienie nastąpi w kolejnych wpisach. Na ten moment warto zahaczyć o index.php
, ConfigProvider.php
oraz pozostałe pliki konfiguracyjne: config.php
, container.php
, pipeline.php
i routes.php
.
Najważniejsze co dzieje się w głównym pliku index.php
to uruchomienie aplikacji: $app->run()
. Zanim jednak do tego dojdzie najpierw dołączony jest autoloader oraz wszystkie wyżej wymienione pliki konfiguracyjne (niewidoczny config.php
dołączany jest w kontenerze).
<?php
declare(strict_types=1);
use Mezzio\Application;
use Mezzio\MiddlewareFactory;
use Psr\Container\ContainerInterface;
if (PHP_SAPI === 'cli-server' && $_SERVER['SCRIPT_FILENAME'] !== __FILE__) {
return false;
}
chdir(dirname(__DIR__));
require 'vendor/autoload.php';
(function () {
/** @var ContainerInterface $container */
$container = require 'config/container.php';
/** @var Application $app */
$app = $container->get(Application::class);
$factory = $container->get(MiddlewareFactory::class);
(require 'config/pipeline.php')($app, $factory, $container);
(require 'config/routes.php')($app, $factory, $container);
$app->run();
})();
ConfigProvider.php
to klasa odpowiedzialna za konfigurację zależności. Oczywiście można tam dorzucić również inne szczegóły specyficzne dla tego modułu. Każdy moduł ma swoją klasę, której główne założenia mówią, że nie przyjmuje żadnych argumentów w kontruktorze i można się do niej odwołać poprzez __invoke()
. Metoda ta zwraca całą konfigurację w postaci tablicy.
<?php
declare(strict_types=1);
namespace Api;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => $this->getDependencies()
];
}
public function getDependencies(): array
{
return [
'factories' => [
Handler\HomePageHandler::class => Factory\HomePageHandlerFactory::class
]
];
}
}
Żeby włączyć moduł w aplikacji należy właśnie powyższą klasę zarejestrować poprzez jej dodanie w pliku config.php
. Klasa ConfigAgregator.php
odpowiedzialna jest za zebranie konfiguracji wszystkich modułów i ich odpowiednie połączenie. Odpowiednie pliki są łączone w takiej samej kolejności jak ich przekazywanie. Oczywiście każdy kolejny ma wyższy priorytet, więc mógłby nadpisać wcześniejsze wystąpienie tego samego klucza.
<?php
declare(strict_types=1);
use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ConfigAggregator\PhpFileProvider;
$cacheConfig = [
'config_cache_path' => 'data/cache/config-cache.php',
];
$aggregator = new ConfigAggregator([
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
Laminas\HttpHandlerRunner\ConfigProvider::class,
new ArrayProvider($cacheConfig),
Mezzio\Helper\ConfigProvider::class,
Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class,
Laminas\Diactoros\ConfigProvider::class,
Api\ConfigProvider::class,
new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'),
new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
], $cacheConfig['config_cache_path']);
return $aggregator->getMergedConfig();
W konfiguracji konternera prawdpodobnie nie będzie potrzeby grzebać. Ja korzystam z service-managera
, który jest tworzony w pliku container.php
.
<?php
declare(strict_types=1);
use Laminas\ServiceManager\ServiceManager;
$config = require __DIR__ . '/config.php';
$dependencies = $config['dependencies'];
$dependencies['services']['config'] = $config;
return new ServiceManager($dependencies);
Z dwoma pozostałymi plikami będę pracował trochę więcej. Na razie wrzucam je bez opisu. Zostaną dokładniej wytłumaczone w kolejnych artykułach. Dodam tylko, że są one odpowiedzialne za konfiguracje middleware i routingu.
<?php
declare(strict_types=1);
use Laminas\Stratigility\Middleware\ErrorHandler;
use Mezzio\Application;
use Mezzio\Handler\NotFoundHandler;
use Mezzio\Helper\ServerUrlMiddleware;
use Mezzio\Helper\UrlHelperMiddleware;
use Mezzio\MiddlewareFactory;
use Mezzio\Router\Middleware\DispatchMiddleware;
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
use Mezzio\Router\Middleware\ImplicitOptionsMiddleware;
use Mezzio\Router\Middleware\MethodNotAllowedMiddleware;
use Mezzio\Router\Middleware\RouteMiddleware;
use Psr\Container\ContainerInterface;
return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void {
$app->pipe(ErrorHandler::class);
$app->pipe(ServerUrlMiddleware::class);
$app->pipe(RouteMiddleware::class);
$app->pipe(ImplicitHeadMiddleware::class);
$app->pipe(ImplicitOptionsMiddleware::class);
$app->pipe(MethodNotAllowedMiddleware::class);
$app->pipe(UrlHelperMiddleware::class);
$app->pipe(DispatchMiddleware::class);
$app->pipe(NotFoundHandler::class);
};
<?php
declare(strict_types=1);
use Api\Handler\HomePageHandler;
use Mezzio\Application;
use Mezzio\MiddlewareFactory;
use Psr\Container\ContainerInterface;
return static function (
Application $app,
MiddlewareFactory $factory,
ContainerInterface $container
): void {
$app->get('/', HomePageHandler::class, 'home');
};
Podsumowanie instalacji Laminas Mezzio
Myślę, że z taką wiedzą można już zaczynać budować aplikację w oparciu o Laminas Mezzio. Resztę istotnych według mnie kwestii będę oczywiście tłumaczył na bieżąco. W kolejnym wpisie zajmę się implementacją osobnego modułu odpowiedzialnego za uwierzytelnianie.
Kod dostępny na w repozytorium jako gałąź step01. Zachęcam do sprawdzenia procesu instalacji w oficjalnej dokumentacji Mezzio.
Cześć Krystian, jak dostać się do tego projektu z host’a, jeśli został wystartowany za pomocą `docker compose up`? Czy powinienem dodać routing do `10.25.0.10`?
Dorzuciłem też do wpisu adnotację z tym żeby dorzucić sobie do lokalnych hostów wpis z domeną – dzięki za komentarz.
Ahh, zabrakło mapowania portów, sekcja ports winna zawierać np. – 9876:443. Wtedy mamy dostęp do serwisu z hosta, „https://localhost:9876”
Ja nie mapuje portów tylko działam na domyślnym czyli 443. Dodaję sobie jednak wpis z domeną (w linuxie plik /etc/hosts). Na zasadzie: customer-api.local 10.25.0.10 – wtedy jest dostęp przez „https://customer-api.local”. Normalnie przez IP też mogłeś wbić, ale że jest to port 443 to musisz dorzucić https://
Jeśli wew. docker’a jest PHP i Composer to chciałbym z nich skorzystać podczas instalacji zależności, np. tak `docker exec -it laminas-api_server_run_cf7c034096cf composer create-project mezzio/mezzio-skeleton customer-api`.
Niestety odbijam się od https://github.com/mezzio/mezzio-skeleton/blob/3.8.x/composer.json#L39.
Pomaga dodanie flagi ignorowania wymagań dot. platformy, tj. `docker exec -it laminas-api_server_run_cf7c034096cf composer create-project mezzio/mezzio-skeleton customer-api –ignore-platform-reqs`.
Mezzio w wersji 3.8 nie jest jeszcze gotowy na PHP 8 – czy się mylę?
Jeden komponent chyba nie był gotowy w momencie jak stawiałem projekt i był to CLI Tooling.