Poniższy poradnik pomoże Wam skonfigurować i wdrożyć na serwer w pełni działającą aplikację opartą o PHP Symfony oraz zautomatyzować cały proces testowania i deploymentu przy pomocy Buddy CI/CD.
Symfony i Continuous Delivery
Poradnik opisuje następujące elementy:
- Instalację Symfony
- Tworzenie nowego projektu Symfony
- Dodawanie nowej funkcjonalności
- Tworzenie formularza, kontrolera oraz widoku
- Konfigurację pipeline'a pod Continuous Integration
- Konfigurację pipeline'a pod Continuous Delivery
- Wdrażanie aplikacji Symfony na serwer (tu: AWS Elastic Beanstalk)
Kompletny i działający projekt, o którym mowa w tym tekście, znajduje się na GitHubie pod adresem https://github.com/buddy-works/symfony-first-steps.
Tworzenie projektu Symfony
Zaczynamy od instalacji Symfony:
$ wget https://get.symfony.com/cli/installer -O - | bash
Następnie tworzymy nowy projekt o nazwie symfony-first-steps:
$ symfony new symfony-first-steps
Otwieramy katalog i stawiamy serwer deweloperski, aby sprawdzić czy wszystko poszło zgodnie z planem (i czy można już otworzyć pierwsze piwko):
$ cd symfony-first-steps
$ symfony serve
Pod adresem 127.0.1:8000
powinieneś zobaczyć coś takiego:
Działająca aplikacja Symfony
Wszystko bangla, można więc rzucić okiem na specyfikację aplikacji do napisania.
Businnes logic
W opisie czytamy: “Aplikacja ma przyjmować dowolny tekst i zwracać ilość wystąpień każdego słowa w zadanym tekście.” Wszystko jasne – do roboty.
Instalacja PHPUnit
Zaczynamy od instalacji PHPUnit, w końcu TDD to nasz chleb powszedni:
$ composer require --dev phpunit/phpunit
Jak na prawilnego deva przystało, pamiętaj aby sprawdzić czy wszystko działa:
$ bin/phpunit
Pisanie testów
Biorąc pod uwagę, że to pierwsza instalacja, musimy chwilę poczekać, aż wszystko przejdzie. Finalnie wyświetli się nam komunikat, że nie ma żadnych testów. W takim razie trzeba go napisać:
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Service;
use App\Service\WordCounter;
use PHPUnit\Framework\TestCase;
final class WordCounterTest extends TestCase
{
/**
* @dataProvider textDataProvider
*/
public function testWordCounter(string $text, array $result): void
{
self::assertEquals($result, (new WordCounter())->count($text));
}
public function textDataProvider(): \Generator
{
yield 'basic text' => ['Lorem ipsum dolor sit amet, ipsum dolor sit.', [
'Lorem' => 1,
'ipsum' => 2,
'dolor' => 2,
'sit' => 2,
'amet' => 1,
]];
yield 'leetspeak text' => ['l0r3m 1p5um d0l0r 517 4m37, 1p5um d0l0r 517.', [
'l0r3m' => 1,
'1p5um' => 2,
'd0l0r' => 2,
'517' => 2,
'4m37' => 1,
]];
yield 'ignore spaces' => ['Some text with tabs and spaces', [
'Some' => 1,
'text' => 1,
'with' => 1,
'tabs' => 1,
'and' => 1,
'spaces' => 1,
]];
}
}
Na tą chwilę testy będą padać (czerwono po odpaleniu bin/phpunit
), dodajmy więc implementację wspomnianego serwisu do zliczania słów:
<?php
declare(strict_types=1);
namespace App\Service;
final class WordCounter
{
/**
* @return array<string,int>
*/
public function count(string $text): array
{
$words = explode(' ', preg_replace('/[^A-Za-z0-9?![:space:]]/', '', $text));
return array_reduce($words, function (array $counts, string $word): array {
if (trim($word) !== '') {
$counts[$word] = isset($counts[$word]) ? ++$counts[$word] : 1;
}
return $counts;
}, []);
}
}
Na tę chwilę uruchomienie bin/phpunit
zwróci nam coś takiego:
Elegancko. Teraz wchodzimy na drogę tworzenia formularza, który przyjmie tekst, a następnie przekaże go (po wstępnej walidacji) do naszego serwisu. Na końcu całość zwrócimy na nasz template, żeby wyświetlić wszystko userowi. To wszystko będzie się działo w kontrolerze, który będzie działał na danym URL (w tym przypadku będzie to po prostu /
). Kontroler będzie tzw. punktem wejścia do naszej aplikacji. Zaczynamy więc od utworzenia kontrolera.
Praca z kontrolerem
Domyślnie każdy URL jest konfigurowany w pliku config/routes.yaml.
Na potrzeby tego guide'a skorzystamy jednak z anotacji, które są trochę wygodniejsze. Do tego będziemy potrzebowali dodatkowej paczki. Wpisujemy więc w konsoli:
$ composer require annotations
Następnie tworzymy nowy plik: src/Controller/HomeController.php
z następującą zawartością:
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class HomeController
{
/**
* @Route("/")
*/
public function index(): Response
{
return new Response('Word Counter');
}
}
Odświeżamy przeglądarkę (http://127.0.0.1:8000/) i widzimy napis:
Działający kontroler
Więcej o tworzeniu kontrolerów możesz przeczytać w oficjalnej dokumentacji Symfony
Ogień, pierwsza wartość biznesowa jest już dowieziona. Jedziemy dalej!
Praca z widokami
Jak zauważyłeś, z kontrolera zwróciliśmy prosty tekst, ale zazwyczaj będziemy chcieli wyrenderować stronę w HTML. Żeby uniknąć łączenia kodu HTML z PHP użyjemy do tego silnika szablonów (template engine). Domyślnym takim silnikiem dla Symfony jest Twig
W konsoli wpisujemy:
$ composer require twig
Następnie tworzymy nowy widok w katalogu templates
(został automatycznie utworzony po instalacji Twiga). Dodajemy nowy plik: templates/home.html.twig
o następującej treści:
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col">
<h1>Word Counter</h1>
<p>Count unique words in given text.</p>
</div>
</div>
</div>
{% endblock %}
Wykorzystujemy tutaj od razu gotowe komponenty Bootstrapa, którego dodamy sobie później w trakcie tworzenia formularza.
Teraz musimy trochę przerobić nasz kontroler. Aby ułatwić cały proces, dziedziczymy klasę AbstractController
i wykorzystujemy metodę render
, aby wygenerować widok. Całość po zmianach wygląda następująco:
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
final class HomeController extends AbstractController
{
/**
* @Route("/")
*/
public function index(): Response
{
return $this->render('home.html.twig');
}
}
W efekcie strona wygląda teraz tak:
Licznik słów z podpisem
Praca z formularzem
Dodamy teraz formularz, który pozwoli nam przyjąć dane od użytkownika, zwalidować je, a następnie przekazać do naszego serwisu liczącego ilość słówek. W przypadku Symfony, rekomendowany flow pracy wygląda tak:
- Utworzenie formularza w kontrolerze Symfony albo wykorzystanie dedykowanej klasy formularza
- Wyrenderowanie formularza w templejcie, tak aby użytkownik był w stanie go edytować i submitować
- Procesowanie formularza: walidacja przesłanych danych, przeformatowanie na PHP i dalsze działana (np. zapisanie w bazie danych)
Zaczynamy od zaciągnięcia paczki z formularzami:
composer require symfony/form
Symfony zaleca, by ograniczać ilość logiki w kontrolerach do minimum. Zamiast definiować wszystko w akcjach kontrolera, najlepiej parować złożone formularze i elementy do dedykowanych klas, aby móc je wykorzystywać w wielu akcjach i usługach równocześnie.
Tak więc w pliku: src/Form/Type/CountWordType.php
tworzymy następujący formularz:
<?php
declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
final class CountWordType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('text', TextareaType::class, ['label' => 'Text to count'])
->add('submit', SubmitType::class, ['label' => 'Count words'])
;
}
}
Dodatkowo dodamy sobie jeszcze Bootstrapa, żeby nasz formularz ładnie się renderował – a Ty byś mógł składać apkę niczym klocki Lego (jak za starych dobrych czasów). Dobra, można otworzyć drugie piwo, zrobiło się sentymentalnie.
W pliku templates/home.html.twig
dodajemy style i JavaScript wymagane przez Bootstrapa:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
{% endblock %}
</body>
</html>
Teraz czas wstawić stworzony formularz CountWordType
do naszego szablonu ze stroną główną. Najpierw stworzymy go w kontrolerze – akcja index
będzie wyglądać następująco:
public function index(): Response
{
$form = $this->createForm(CountWordType::class);
return $this->render('home.html.twig', [
'form' => $form->createView(),
]);
}
Następnie wstawiamy formularz w szablon. W tym celu wystarczy dodać {{ form(form) }}
zaraz pod pierwszym paragrafem:
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col">
<h1>Word Counter</h1>
<p>Count unique words in given text.</p>
{{ form(form) }}
</div>
</div>
</div>
{% endblock %}
Tak powinna wyglądać nasza strona:
Licznik słów z działającym formularzem
Następnym krokiem jest dodanie obsługi formularza w kontrolerze. Wypełniając formularz przykładowym tekstem i klikając w button Count words, wysyłamy do naszej aplikacji nowy request, który trzeba obsłużyć. Używając formularza z Symfony robimy to jedną prostą metodą handleRequest
:
public function index(Request $request): Response
{
$form = $this->createForm(CountWordType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $this->render('home.html.twig', [
'results' => $this->wordCounter->count($form->get('text')->getData()),
'form' => $form->createView(),
]);
}
return $this->render('home.html.twig', [
'form' => $form->createView(),
]);
}
Jak widać, jeżeli formularz był submitowany i jest poprawny ($form->isSubmitted() && $form->isValid()
), wtedy zwracamy na widok wynik działania naszego serwisu:
'results' => $this->wordCounter->count($form->get('text')->getData())
Pozostaje nam tylko wyświetlenie wyników przy użyciu stworzonego wcześniej szablonu:
{% block body %}
<div class="container">
<div class="row">
<div class="col">
<h1>Word Counter</h1>
<p>Count unique words in given text.</p>
{% if results is defined %}
<h2>Results:</h2>
<ul style="column-count: 4;">
{% for word, count in results|sort|reverse %}
<li>{{ word }}: {{ count }}</li>
{% endfor %}
</ul>
{% endif %}
{{ form(form) }}
</div>
</div>
</div>
{% endblock %}
Continuous Integration
Przygotujemy teraz pipeline, który umożliwi Ci wdrożenie procesu Ciągłej Integracji.
Warto tutaj przypomnieć, że pipeline sam w sobie nie równa się ciągłej integracji. Ciągłą integracją nazywamy technikę, w którym każda zmiana w kodzie jest natychmiastowo testowana i scalana z główną linią kodu (branchem). Pipeline to narzędzie, które pomaga taką technikę wdrożyć.
Scalanie wyników pracy oznacza że:
- Kod znajduje się w tym samym branchu
- Aplikacja buduje się poprawnie
- Aplikacja działa poprawnie (testy)
- Aplikacja wdraża się poprawnie (deployment)
Przygotowanie skryptów Composera
Zaczniemy od przygotowania paru skryptów Composera. W pliku composer.json
w sekcji “scripts”
dodajemy:
"phpunit": [
"bin/phpunit --colors=always"
],
"tests": [
"@phpunit"
]
W ten sposób pozostawiamy sobie miejsce na dodanie dodatkowych narzędzi (np. PHPStan lub PHP CS Fixer) bez zmieniana samego skryptu do testowania:
composer tests
Tworzenie nowego pipeline'a
Na tym etapie Twój kod powinien być skomitowany i spushowany do repozytorium. Zacznijmy od stworzenia nowego projektu w Buddym i dodaniu pierwszego pipeline'a:
Dodawanie projektu Symfony do Buddy'ego
Klikamy Add a new pipeline i konfigurujemy pipeline:
- podajemy nazwę (np. 'test')
- ustawiamy Trigger mode na On push
- jako branch triggerujący wybieramy 'Branch by wildcard' i ustawiamy go na
refs/*
Konfiguracja pipeline'a testującego
W ten sposób nasz pipeline będzie uruchamiał się dla wszystkich pushowanych zmian. Dzięki temu możesz mieć pewność, że spuszowany kod działa prawidłowo.
Pipeline postawiony, dodajemy akcję PHP:
Dodawanie akcji PHP
Akcja PHP w Buddy to nic innego jak wyizolowany kontener z zainstalowanym PHP w wybranej wersji i dociągniętymi dependencjami, w którym możesz wykonywać komendy – tak jak w terminalu bashowym.
Kolejny krok to konfiguracja komend do wywołania:
composer validate
composer install
composer tests
Konfiguracja akcji PHP
Tak przygotowany pipeline przetestuje nasz kodzik po każdym puszu do repozytorium. Czas na release naszej aplikacji na produkcję! 👹
Continuous Delivery
Do hostingu aplikacji użyjemy AWS Elastic Beanstalk. Możesz też użyć innego serwisu tego typu, np. Google App Engine, Heroku, albo Azure App Service.
Konfiguracja pipeline'a
Dodajemy nowy pipeline o nazwie 'deploy' ustawiony na branch master. W związku z tym, że to release na produkcję, tryb uruchamiania ustawiamy na manualny (chyba, że ktoś potrzebuje adrenaliny i lubi sobie puścić releasa po przypadkowym pushu, to może wybrać inny :)
Konfiguracja pipeline'a wdrożeniowego
Teraz dodajemy akcję Elastic Beanstalk z sekcji Amazona:
Sekcja akcji Amazon w Buddym
Pojawi się okno dodawania integracji AWS. Autoryzacja możę się odbywać po kluczu, przez dodanie Buddy'ego do listy zaufanych aplikacji, albo poprzez asumowanie roli:
Okno dodawania integracji z AWS
Dodanie integracji jest jednorazowe i pozwala na korzystanie z pozostałych akcji Amazonowych w Twoich pipeline'ach.
Po dodaniu integracji wyświetli się okno konfiguracji akcji EBS. Zacznijmy od utworzenia nowej aplikacji w samym Beanstalku:
Tworzenie nowej aplikacji z akcji Elastic Beanstalk
W konsoli AWS tworzymy nową aplikację EBS o nazwie 'word-counter`, do której będziemy deployować nasze dzieło:
Dodawanie nowej aplikacji Elastic Beanstalk w konsoli AWS
Po utworzeniu appki możemy wrócić do Buddy'ego, aby dokończyć konfigurację naszej akcji. Ustawiamy region i wybieramy 'word-counter' z listy dostępnych aplikacji (może byc potrzebne przeładowanie strony, żeby odświeżyć listę):
Wybór aplikacji docelowej w akcji Elastic Beanstalk
Buddy ma fajny ficzer który, podpowiada jakie akcje możemy dodać w następnej kolejności do pipeline'u. W tym przypadku jest to monitoring środowiska Elastic Beanstalk, który pozwoli nam sprawdzić czy aplikacja wstała po deployu:
Podgląd podpowiadania akcji w pipelinie
Ponownie wybieramy odpowiedni region, aplikację i environment.
W ustawieniach akcji warto zaznaczyć opcję Failed if yellow i ustawić Wait until ready na 5 minut. Dzięki temu akcja poczeka przed przeprowadzeniem testów zanim wdrożenie w pełni się zakończy a aplikacja się odpali.
Podsumowując, nasz pipeline wdrażający składa się teraz z dwóch akcji:
Pipeline wdrażający
Czas go odpalić i zobaczyć czy wszystko działa. Buddy zaciągnie ostatnią wersję z repozytorium, wyśle pliki do wskazanej aplikacji na EB, a na sam koniec odpali testy:
Widok podsumowujący po wdrożeniu
Po wdrożeniu aplikacja będzie dostępna z adresu w panelu AWS:
Panel AWS
Konfiguracja środowiska Elastic Beanstalk
Po pierwszym wdrożeniu aplikacja nie będzie jeszcze działać poprawnie: trzeba jeszcze skonfigurować środowisko EBS, wskazując główną lokalizację dla serwera webowego. Robimy to w konsoli AWS wchodząc w ustawienia środowiska, a następnie w sekcji Software klikamy Modify. W polu Document root wpsiujemy wartość /public
:
Konfiguracja środowiska EBS
Teraz trzeba chwilkę poczekać aż AWS uploaduje config na serwer i po chwili możemy cieszyć się w pełni działającą aplikacją! 🙌
Wdrożona aplikacja na serwerze Elastic Beanstalk
Polecam wkleić sobie ten adres w ustawieniach pipeline'a, żeby mieć możliwość otwarcia witryny bezpośrednio z Buddy'ego:
Skrót do URL'a na pipelinie
Kolejny krok: Continuous Deployment
Na sam koniec spróbujemy scalić oba pipeline'y w jedno środowisko CI/CD. Aby to zrobić, użyjemy akcji, która uruchomi pipeline wdrażający od razu po przeprowadzeniu testów. Akcja nazywa się Run next pipeline i należy ją dodać na koniec pipeline'u testującego, wybierając pipeline 'deploy' jako element docelowy:
Akcja Run Another Pipeline
Pipeline testujący działa na wildcardzie, co znaczy, że testy są uruchamiane na pushu do każdego brancha w repozytorium. W związku z tym, że końcowe wdrożenie odbywa się na produkcję, nas interesuje tylko branch master. Postawimy więc jeden warunek: deployment odpali się jedynie gdy przejdą testy na branchu master. Do tego celu wykorzystamy zmienne środowiskowe:
- Na ekranie konfiguracji akcji Run next pipeline przejdź do zakładki Condition
- Wybierz opcję ENV VAR z menu wyboru
- Ustaw nazwę zmiennej na
$BUDDY_EXECUTION_BRANCH
, a wartość namaster
Konfiguracja warunków triggerujących
W ten sposób udało nam się w pełni zautomatyzować proces wdrażania: za każdym razem gdy zostanie spuszowany kod, Buddy uruchomi testy. Jeżeli testy przejdą pomyślnie, a branchem docelowym był master, uruchomi się kolejny pipeline, który wdroży naszą aplikację na produkcję. Taki typ workflowu nazywa się Continuous Deployment.
W przeciwieństwie do Continous Delivery, w Continuous Deployment cały proces wdrażania jest całkowicie automatyczny, wliczając w to release na live. Z tego powodu nie polecamy tego typu rozwiązania przy pracy z dużym projektami – chyba że każdy etap developmentu, od środowiska lokalnego, przez stage, po deploy na live jest dokładnie pokryty testami (jednostkowymi, end-to-end, przeglądarkowymi), a narzędzie do wdrażania jest w 100% stabilne i przetestowane w warunkach bojowych.
Opcjonalnie: Notyfikacje
Ostatnią z funkcjonalności Buddy'ego, o której chciałbym wspomnieć, są notyfikacje warunkowe. Tego typu notyfikacje są wysyłane tylko wtedy, gdy spełnione są konkretne warunki – na przykład akcja się wysypie. Dzięki nim możesz poinformować zespół DevOps, że coś poszło nie tak w czasie deploymentu, albo dać znać oddziałowi QA, żeby zerknął na wyniki testów, bo coś nie działa. Buddy obsługuje wszystkie najpopularniejsze komunikatory, wliczając w to Slack, Telegram, Discord, SMS i klasyczny e-mail (obecnie trwają prace nad wsparciem Microsoft Teams).
Aby wysłać taką wiadomość, dodaj akcję notyfikacyjną do sekcji Actions run on failure na dole pipeline'a:
Pipeline z notyfikacją warunkową
Podsumowanie
Dzięki za czas poświęcony na przeczytanie i dobrnięcie do konca tego poradnika. Mam nadzieję, że zawarte w nim wskazówki i informacje przydają Wam się w praktyce w pracy z PHP i Symfony, a być może również ułatwią wdrożenie CI/CD do Waszego workflowu. Ze swojej strony dodam, że Buddy to produkt całkowicie polski, developowany przez zespół 30 osób z Bielska-Białej. Naprawdę warto dać mu szansę, tym bardziej, że nie znajdziecie łatwiejszej w obsłudze i szybszej platformy DevOps – mogę się założyć o każde wspomniane w tekście piwko 😎.
Powodzenia! Arek Kondas
Powiązane artykuły
- 5 ways to deploy PHP applications – od git pulla do Dockera, czyli 5 sposobów na wdrażanie aplikacji PHP
- 11 examples of CI/CD pipelines – 11 przykładów pipeline'ów do najpopularniejszych use case'ów w pracy developera
- How to use MySQL in PHP builds – poradnik w jaki sposób podpiąć serwis MySQL do kontenera PHP w Buddym (przydatne do testów)
- Buddy Actions explained – wszystkie akcje z Buddy'ego ułożone kategoriami i dokładnie opisane