Debugowanie JS w Firefoksie

Nie wiem, jak to się stało, ale dopiero niedawno odkryłem debugowanie skryptów JS w przeglądarce (a konkretniej w Firefoksie). Po tylu latach zwróciłem uwagę na taką możliwość w narzędziach deweloperskich, a gdy zacząłem drążyć temat okazało się, że jest to niesamowicie wygodne i proste w użyciu.

W większości przypadków wystarczy po prostu uruchomić narzędzia deweloperskie w przeglądarce, wskazać interesujący nas plik i założyć w nim breakpointy. Przeglądarka po osiągnięciu wskazanego miejsca w kodzie po prostu wstrzyma jego wykonywanie i pozwoli nam na dalsze przetwarzanie krok po kroku, wraz z podglądem kontekstu, śledzeniem określonych zmiennych, itd.

Przy używaniu debuggera zauważyłem, że nie wszystkie pliki są widoczne na liście źródeł. To jednak też łatwo rozwiązać – wystarczy w dynamicznie ładowanym pliku JS umieścić odpowiednią adnotację.

Również w przypadku często stosowanej minifikacji skryptów nie będzie problemów z debugowaniem – firefoksowy debugger ma wbudowany prettyfier, który sformatuje zminifikowany kod do czytelnej konwencji.

A więc .NET

Dawno mnie tu nie było, ale był też istotny tego powód. Niby człowiek wiedział, ale się łudził i fakt przejęcia Idea Banku przez Pekao spadł na mnie dość nieoczekiwanie, zwłaszcza, że miało to miejsce w południe w sylwestra 2020. Do tamtej pory pracowałem rozwijając aplikacje backoffice’owe w PHP, w Pekao ten język praktycznie nie istnieje poza jakimiś marginalnymi zastosowaniami (zresztą nie wiem, czy ma on rację bytu gdziekolwiek indziej w bankowości poza grupą Leszka Czarneckiego), więc pierwsze tygodnie oznaczały dla mnie miotanie się pomiędzy decyzją o pozostaniu w Pekao i zmianą języka, a zmianą pracy na inną i pozostaniu przy PHP. “Miotanie się” to bardzo adekwatne w tym wypadku określenie – nastrój w tej kwestii zmieniał mi się czasem co tydzień w skrajnych kierunkach. W Pekao dominuje szeroko pojęty .NET, więc dla mnie oznaczałoby to nie tylko zmianę języka, ale zaczynanie praktycznie od zera w większości powiązanych technologii. Tak na szybko listując: język ze słabo typowanego, skryptowego na silnie typowany, kompilowany, DBMS z PostgreSQL na SQL Server, serwery HTTP z Apache/nginx na IIS, środowisko uruchomieniowe z linuksowego na windowsowe, IDE z Eclipse na Visual Studio, do tego dużo drobniejszych rzeczy, jak razor pages, inny ORM, powszechnie używane zasoby sieciowe zamiast NFS/SFTP, itd. Dużo tego. Zupełnie inaczej wyobrażałem sobie też przepięcie na nowe technologie – część kolegów zaczęła swoją technologiczną konwersję praktycznie z dnia na dzień. W moim przypadku ostatecznie było inaczej – system, który tworzyliśmy w IB, podlegał migracji do systemów Pekao, więc było to priorytetem i potrwało do listopada 2021, a ja w tym czasie mogłem się spokojnie oswoić z nowym stackiem technologicznym i zacząć przyswajać wiedzę z tego zakresu, jeszcze wtedy na wolnych obrotach.

Pracę w projektach .NET-owych zacząłem na poważnie w grudniu 2021, więc ostatnio minęły pełne 4 miesiące pracy przy nich, taki większy okres próbny. Postanowiłem więc, że póki mam to jeszcze na świeżo, to opiszę moje wrażenie z takiej przesiadki.

Pierwszy miesiąc to głównie przyzwyczajanie się do silnie typowanego języka. Po ponad 10 lat pracy w PHP trzeba było zmienić przyzwyczajenia nawet w tak podstawowych kwestiach, jak dobór typów danych (i wyleczenie się ze wszechobecnych w PHP, mieszanych tablic asocjacyjnych). Nie było to jednak jedyne przyzwyczajenie do zmiany: samo środowisko uruchomieniowe rządzi się innymi prawami i niektóre rzeczy są rozwiązane w sposób, który nigdy we wcześniejszym stacku nie przyszedłby mi do głowy (koordynator transakcji rozproszonych jako osobna usługa jest moim faworytem do tego rankingu).

Z silnym typowaniem z perspektywy czasu jest jednak jak z przejściem ze skrzyni manualnej na automatyczną – na początku wkurza zahaczanie nogą o szeroki hamulec i odruchowe wpadanie lewej nogi w dziurę, gdzie powinno być sprzęgło, jednak po czasie docenia się to, że pewne rzeczy wyłapywane są już na etapie kompilacji. Początkowo było to dla mnie nieco uporczywe i zdawało mi się zbędnym narzutem, jednak nie zauważałem pewnego istotnego faktu: wcześniejsze projekty znałem dość dobrze i w przypadku zmian na poziomie modelu wiedziałem, gdzie szukać obszarów do refaktoryzacji. Gdy jednak nie ma się jeszcze takiej wiedzy, to w przypadku PHP często błędy wychodziły dopiero podczas uruchomienia pominiętego kodu. Tutaj projekty są dla mnie nowe, więc takiej wiedzy nie mam, a przeszukiwanie kodu źródłowego nie zawsze jest na tyle efektywne, aby zidentyfikować wszystkie konieczne miejsca.

Silne typowanie ma jeszcze jeden plus – dzięki niemu autouzupełnianie w IDE i ogólnie asystent kodu stoją na zupełnie innym poziomie, niż w PHP. Tutaj na każdym etapie zmienne mają znany edytorowi typ, więc praktycznie wszędzie mamy dostęp do podpowiedzi, co w przypadku PHP – nie było tak oczywiste.

Pisanie aplikacji webowej w C# z użyciem ASP.NET MVC to też pole do zmiany przyzwyczajeń. Dużo się tu dzieje w middleware’ach. W PHP korzystałem z Zenda i operowanie na surowej tablicy wartości z POST, lekko tylko przerobionej (np. przefiltrowanej) przez framework było dla mnie naturalnym podejściem. Tutaj królują modele, które są automatycznie mapowane z danych POST/GET. Za tym idzie mnóstwo innych rzeczy, jak np. sprzęgnięta walidacja, itd. Po krótkim przyzwyczajeniu jest to dużo wygodniejsze, niż praca na surowych danych z POST.

Konstrukcja języka C# mi się podoba. Są rzeczy, których brakowało mi w PHP (część z nich jednak w międzyczasie została wprowadzona w wersji 8 – gdzie one były przez tyle lat mojej pracy? :P), na przykład named parameters. Dłużej musiałem przysiąść nad powszechnie używanymi wyrażeniami lambda, których czasem w PHP się używało, ale w zupełnie inny sposób. Podobnie z typami generycznymi, z których prototypem (szablonami) miałem do czynienia dawno temu, gdy pisałem w C++. Do tego mnóstwo różnych, drobnych rzeczy, których nie znałem, a których używa się na co dzień (jak chociażby postępowanie z Nullable, bardziej zaawansowane używanie list, itd.).

A propos list, nie można zapomnieć o LINQ. Bardzo spodobało mi się to rozwiązanie, po opisanym wcześniej ogarnięciu wyrażeń lambda można wiele zrobić z kolekcjami w łatwy i krótki sposób. Dodatkowo, w zastosowaniach bazodanowych, w połączeniu z IQueryable czułem jak się w domu ze względu na przyzwyczajenie do zendowych selectów.

Polubiłem też wykorzystywany tu ORM i sposób tworzenia modelu. I choć widzę, że mam w tym temacie jeszcze mnóstwo do nauczenia, to jest przede wszystkim dużo szybciej niż w PHP, zarówno z mapowaniem struktury bazy danych na model, jak i chociażby z migracjami bazodanowymi. Pamiętam, że podobnie miałem, gdy w 2012 przesiadłem się z CodeIgnitera na Zenda i jego active record i Zend_Db_Select. Zresztą ostatnie miesiące bardzo przypominają mi 2012 – przyswajam dużo nowej wiedzy w krótkim czasie, wtedy zdaje się, że porównywałem pierwsze pół roku pierwszej pracy na etacie do całego drugiego stopnia studiów pod kątem przyswojonej wiedzy.

Nie mogę w tym zestawieniu pominąć debugowania aplikacji w Visual Studio. Coś, co działa z automatu i czego używa się codziennie to dla mnie zdecydowanie nowość. Wcześniej, w PHP, używałem xdebug do debugowania kodu krok po kroku lub do profilowania tylko w wyjątkowych sytuacjach, gdy logika była na tyle zaawansowana, że ciężko było ją debugować przez wypluwanie wartości lub wtedy, gdy trzeba było przyjrzeć się z wysokiego poziomu wykonywaniu kodu i systemowego i systemowo go zoptymalizować. Zresztą sama konfiguracja xdebug w PHP była dość problematyczna, szczególnie na obwarowanych zabezpieczeniami komputerach bankowych i takowej infrastrukturze sieciowej. Nie mówiąc już o szybkości działania tak debugowanej aplikacji.

Co do samego Visual Studio, to spodobał mi się gitowy merge tool, który jest znacznie wygodniejszy, niż wcześniej preferowane przeze mnie ręczne rozwiązywanie konfliktów – tutaj mam kod z dwóch stron konfliktu na podzielonym ekranie, mogę pojedyncze linie włączać do rozwiązania konfliktu checkboksami, a dodatkowo edytować go w połączonej wersji. Coś wspaniałego. Rozczarował mnie natomiast brak narzędzia do częściowego stage’owania zmian w gicie. Zostało podobno wprowadzone w VS 2022, więc chętnie wypróbuję, jednak póki co korzystam z VS 2019, więc pozostaje mi nadal dobrze znana obsługa gita z konsoli.

Podsumowując, z przesiadką na nowy stack technologiczny jest lepiej, niż przewidywałem. Nadal jestem na samym początku drogi, więc najbliższe miesiące to na pewno czas dalszej nauki w tym kierunku, jednak sam jestem zdziwiony, że wcześniejsze przyzwyczajenia i uprzedzenia w sumie dość szybko odpuściły. Zawsze byłem też z tych, którzy mocno się do takich rzeczy przyzwyczajają i ciężko było mi zwizualizować sobie trwałą, nieincydentalną przesiadkę np. na zupełnie inny język programowania. Nie doceniałem w tej kwestii jednak doświadczenia, które na pewno ma tu spore znaczenie – znając dobrze zasadę działania aplikacji internetowych, język to tylko narzędzie, sam sposób myślenia, programowanie obiektowe i sposób rozwiązywania problemów są w zasadzie takie same i cieszę się, że przekonałem się o tym na własnej skórze.

Czy scrum rujnuje świetnych inżynierów, czy robisz to źle?

Na blogu stackoverflow trafiłem ostatnio na tytułowy wpis: Does scrum ruin great engineers or are you doing it wrong? Traktuje głównie o scrumie, ale jego większość dotyczy ogólniejszych problemów zarządzania projektami i zespołami. Dawno nie czytałem czegoś tak celnego i przystępnego, toteż polecam i wrzucam na blog, żebym sam o nim nie zapomniał.

Trigger ON UPDATE i ON DELETE na spartycjonowanej tabeli w PostgreSQL

Kolejna ciekawostka, na którą się naciąłem ostatnio w pracy. Otóż w przypadku spartycjonowanej tabeli, triggery ON UPDATE i ON DELETE są wykonywane jedynie na tabeli potomnej zawierającej dane wiersza określonego przez warunki w WHERE (o ile oczywiście istnieją).

Oznacza to, że w przypadku partycjonowania tabel na podstawie np. wartości klucza głównego poprzez ograniczenie CHECK, planner najpierw spróbuje wyznaczyć partycje zawierające wiersz(e) spełniające warunki zapytania, a następnie wywoła triggery ON UPDATE/ON DELETE jedynie na tabelach zawierających dane do usunięcia/aktualizacji.

Jest to zachowanie inne niż w przypadku ON INSERT, gdzie trigger wywoływany jest bezpośrednio na tabeli, na której wywołano instrukcje INSERT mimo, że sam trigger ON INSERT na niej może delegować operacje wstawienia do innych tabel potomnych.

Profilowanie aplikacji w PHP

Krótko i na temat, bo pierwszy raz zacząłem korzystać z profilowania aplikacji w PHP dawno temu, a często przydaje mi się odświeżenie informacji na ten temat.

Po instalacji xdebug wystarczy dodać do .htaccess:

php_value xdebug.profiler_enable 1
php_value xdebug.profiler_output_dir /output/path/
php_value xdebug.profiler_output_name cachegrind.out.%t-%s
php_value xdebug.profiler_enable_trigger 1

Następnie wczytujemy w przeglądarce żądaną stronę (z włączonym profilerem może trwać to nawet kilkanaście razy dłużej), aby we wskazanym wyżej katalogu uzyskać logi profilera. Logi te można wczytać za pomocą np. KCacheGrind (dostępny zarówno pod Linuksem, jak i Windowsem).

W KCacheGrind po podpięciu kodu źródłowego otrzymujemy listę wywołań z kosztem czasowym każdego z nich. Dzięki temu łatwo wyłapać najbardziej czasochłonne części kodu.

NOW() przy zapytaniach na spartycjonowanych tabelach

Ostatnio dowiedziałem się, że funkcje NOW() oraz CURRENT_TIMESTAMP w PostgreSQL nie są immutable, co powoduje, że wyrażenia z tymi funkcjami nie pomogą w optymalizacji zapytań na spartycjonowanych tabelach, których klauzule CHECK dotyczą przedziałów czasowych. Wartości tych funkcji nie są znane planerowi w momencie wykonywania zapytania:

Constraint exclusion only works when the query’s WHERE clause contains constants (or externally supplied parameters). For example, a comparison against a non-immutable function such as CURRENT_TIMESTAMP cannot be optimized, since the planner cannot know which partition the function value might fall into at run time.

https://www.postgresql.org/docs/10/ddl-partitioning.html

Aby zapytanie korzystało jedynie z właściwych partycji, należy więc przekazywać daty/przedziały czasowe jako stałe (np. pobierając je z poziomu języka programowania aplikacji korzystającej z bazy danych) lub z innych tabel. W niektórych rozwiązaniach można po prostu stworzyć tabelę z jednym wpisem i kolumną typu date, której wartość będzie codziennie aktualizowana (nadal jednak utracimy możliwość wyszukiwania uwzględniającego godzinę).

Kolejność w zapytaniu z DISTINCT ON

Na problem z nieustawioną kolejnością w zapytaniu zawierającym DISTINCT ON natknąłem się już jakiś czas temu, jednak dopiero teraz zebrałem się, aby go opisać. A warto, bo niby błahy, a jednak ciekawy i sam trochę ignorowałem jego znaczenie.

Otóż korzystając w zapytaniu z DISTINCT ON na zestawie kolumn powinniśmy ustawić kolejność prezentacji wierszy poprzez standardową klauzulę ORDER BY. DISTINCT ON pomija bowiem kolejne wiersze o wartościach, które wystąpiły już wcześniej:

DISTINCT ON ( expression [, …] ) keeps only the first row of each set of rows where the given expressions evaluate to equal. […] Note that the “first row” of each set is unpredictable unless ORDER BY is used to ensure that the desired row appears first. […] The DISTINCT ON expression(s) must match the leftmost ORDER BY expression(s).

http://www.postgresql.org/docs/9.0/static/sql-select.html#SQL-DISTINCT

Bez ustawionej kolejności może być przypadkowa (chodź technicznie to nie do końca prawda) i różna w zależności od sposobu użycia.

Przykład? Opiszę mój przypadek w nieco zmodyfikowanej formie. Miałem zapytanie korzystające z dwóch tabel: pierwsza to tabela z numerami telefonów klientów wraz z etykietami. Jeden klient może posiadać kilka telefonów, nawet z tymi samymi etykietami (ze względu na przypisywanie tych numerów do jego umów, których może być wiele):

id_customer
id_contractnumberlabel
512
600123123
Company Ltd.
513600123123Company Ltd.
514600123123John Poe

Z tabeli pobierane były unikalne numery telefonów dla klienta wraz z pierwszą etykietą – ta bowiem nie była do końca ważna. Korzystałem więc z zapytania:

SELECT DISTINCT ON (id_customer, number)
    id_customer,
    number,
    label
FROM customers

Nakładając na zapytanie ograniczenie na ID żądanego klienta otrzymywałem wiersze z jego telefonami oraz etykietą. Jeżeli do jednego numeru klienta było kilka etykiet, mogła zostać zaprezentowana dowolna z nich, każda bowiem była prawidłowa i w tym zastosowaniu nie miało to znaczenia.

Trzeba było jednak rozszerzyć tabelę o dodatkowe dane (tagi), które nie występowały – jak etykiety – przy każdym wierszu klienta. Jeżeli już jednak istniały, należało je zaprezentować, a jeżeli było ich kilka – najważniejszą z nich według zadanego priorytetu. Pominę przy analizie priorytet, ponieważ nie jest on aż tak istotny. Tabela po zmianach wyglądała więc tak:

id_customer
id_contractnumberlabeltag
512
600123123
Company Ltd.
urgent
513600123123Company Ltd.
514600123123John Poe

Do zapytania dodałem tylko kolumnę z interesującą mnie daną:

SELECT DISTINCT ON (id_customer, number)
    id_customer,
    number,
    label,
    tag
FROM customers

Przy testach wszystko było OK, zawsze zwracane były wiersze z wypełnioną kolumną tag. To nieco uśpiło moją czujność i nie przetestowałem takiej liczb przypadków, abym trafił na pustą wartość mimo istnienia tagu dla numeru.

Sytuacja zmieniła się, gdy zapytanie trafiło do SQL-owego widoku. To zmieniło sposób sortowania wyników, przez co pobierając dane z widoku miałem przypadki zwracania numerów bez tagów mimo, że te ewidentnie istniały. Żeby było zabawniej, wykonanie zapytania bezpośrednio (z pominięciem widoku) działało tak, jak bym chciał.

Wróciłem więc do dokumentacji i stackoverflow. To naprowadziło mnie na podany na początku wpisu fragment dokumentacji. Aby poprawić zapytanie, należało więc użyć ORDER BY:

SELECT DISTINCT ON (id_customer, number)
    id_customer,
    number,
    label,
    tag
FROM customers
ORDER BY tag NULLS LAST

Dzięki temu, wiersze z tagami były zwracane jako pierwsze i ewentualne późniejsze wiersze z pustymi wartościami w tej kolumnie dla tego samego klienta i numeru były pomijane, a nie odwrotnie.

Nie można więc zapominać o klauzuli ORDER BY po kolumnach niewystępujących w DISTINCT ON, bo prędzej czy później się to zemści.

Chrome dla Androida – zdalne debugowanie aplikacji webowej

Dostosowujemy ostatnio aplikację webową do urządzeń mobilnych. Pierwszym pytaniem, które mi się nasunęło, było: “czy przeglądarki mobilne mają w ogóle wbudowane narzędzia deweloperskie?”. Odpowiedź w uproszczeniu brzmi: nie. W tym sensie, że nie uruchomimy tych narzędzi bezpośrednio na urządzeniu, co zresztą byłoby dość kłopotliwe ze względu na ich złożoność.

Krótkie przeszukiwanie internetu naprowadziło mnie na informacje o zdalnym debugowaniu. Brzmiało zbyt pięknie, aby było możliwe bez większego wysiłku. Pracuję na Linuksie, więc już sobie wyobrażałem rozwiązywanie problemów z połączeniem. Postanowiłem jednak przebić się przez ten krótki opis. Efekty były zaskakujące.

Okazało się, że w zasadzie wszystko działa out of the box. Po stronie komputera nie wymagało w zasadzie żadnej konfiguracji – urządzenie było widoczne od razu na liście. Zmian wymagały jedynie ustawienia na telefonie/tablecie.

Ale po kolei: na początku musimy włączyć debugowanie USB w opcjach programisty. W moim telefonie ze starszym Androidem 4.1.2 opcje programisty dostępne są bezpośrednio z ustawień:

Na nowszych należy je uaktywnić poprzez siedmiokrotne stuknięcie w “Build number” (zgodnie z opisem).

Po włączeniu debugowania USB, wystarczy podłączyć urządzenie kablem USB do komputera oraz włączyć Chrome na obu urządzeniach. Na komputerze wystarczy teraz włączyć narzędzia dla programistów (CTRL+SHIFT+I) i z menu wybrać: More tools -> Remote devices:

Na liście powinniśmy zobaczyć podłączone urządzenie. Po kliknięciu możemy wpisać adres do otwarcia na nim w nowej karcie lub debugować którąś z już otwartych kart na urządzeniu mobilnym poprzez kliknięcie na: “Inspect” przy jej nazwie. Pojawi się wtedy nowe okno z podglądem widoku strony na urządzeniu mobilnym oraz panel z narzędziami developerskimi. Zmiany wykonane na urządzeniu widoczne są w podglądzie na komputerze i vice versa:

Z takimi możliwościami rozwijanie aplikacji webowych na urządzenia mobilne to czysta przyjemność. 🙂

Ratchet – WebSockety w PHP w praktyce

UWAGA! Ten wpis ma już 6 lat. Pewne stwierdzenia i poglądy w nim zawarte mogą być nieaktualne.

WebSockety to jeden z najciekawszych od kilku lat tematów, z którymi przyszło mi zmierzyć się w pracy. Pierwszy raz słyszałem o nich gdzieś w 2013 roku i od tego czasu zdążyły zdobyć ogromną popularność w internecie. YouTube, Stack Overflow, Facebook – tam wszędzie WebSockety wykorzystywane są już od dawna do różnych rzeczy (przesyłanie i odświeżanie komentarzy oraz głosów, komunikacja w czasie rzeczywistym, itp.). To tyle słowem wstępu – jeżeli tu trafiłeś, prawdopodobnie wszystko to wiesz i szukasz informacji, jak wykorzystać WebSockety w praktyce.

Do zeszłego roku “WebSockety” i “node.js” stanowiły w zasadzie synonimy, jeżeli chodzi o ich implementację. O uruchomieniu ich w PHP czytałem same złe rzeczy i nawet najbardziej zagorzali programiści używający tego języka odradzali. Tematu jednak nie zgłębiałem i przyglądałem się nieco z boku. Jednak gdy w pracy pojawił się temat obsługi zdarzeń z centrali telefonicznej w naszym systemie, nie było wyjścia – trzeba zacząć się nimi szybko interesować.

Kolega z sąsiedniego zespołu już jakiś czas wcześniej podejmował ten temat w oparciu właśnie od node.js. Jego opinie nie były życzliwe, zwłaszcza, że nie miał wiele czasu na stworzenie tego rozwiązania. Podobnie z czasem było u mnie. Dodatkowo zniechęcał fakt, że w zasadzie nie mamy żadnej infrastruktury pod node.js, a ostatnią rzeczą, której bym chciał, to zarządzanie tym wszystkim od strony administracyjnej. Zacząłem więc szukać czegoś o WebSocketach w PHP.

Natychmiast trafiłem na Ratchet – bibliotekę do prostej implementacji WebSocketów w aplikacjach PHP. Brzmiało świetnie zwłaszcza, że mogłem korzystać z “dobrodziejstw” całego naszego systemu. W praktyce okazało się, że jest jeszcze lepiej i łatwiej, niż mi się wydawało.

Stwórzmy więc wspólnie skręcaną z drutów aplikację z pomocą Ratchet, która będzie niczym innym, jak prostym echotestem. Dodatkowo, oprócz odpowiadania na otrzymane wiadomości, serwer będzie sam wysyłał do wszystkich wiadomość, np. co 10 sekund. Dzięki temu otrzymasz bazę do większości zastosowań – nasza aplikacja będzie zarówno odpowiadała na komunikaty z inicjatywy klientów, jak i sama w pętli wysyłała coś od siebie.

Najpierw potrzebujemy samego Ratchet. Najłatwiej zlecić pobranie go composerowi. Jeżeli nie korzystałeś jeszcze z composera, spróbuj po prostu odwiedzić stronę pobierania i przejdź do sekcji “Manual download”, skąd możesz ściągnąć najnowszą wersję composer.phar. Umieść ten plik w katalogu głównym projektu, po czym możesz przejść do pobierania Ratcheta:

php composer.phar require cboden/ratchet

Kolejnym krokiem będzie stworzenie klasy obsługującej wszystkie potrzebne zdarzenia (połączenie, rozłączenie, wystąpienie błędu) oraz zawierającej metodę, którą serwer będzie cyklicznie wywoływać. Oto jej treść:

<?php
namespace WebSocket;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Handler implements MessageComponentInterface
{
    protected $clients;

    public function __construct()
    {
        $this->clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);

        echo 'Nowe połączenie: ' . $conn->resourceId . PHP_EOL;
    }

    public function onMessage(ConnectionInterface $from, $msg)
    {
        // iteracja po dostępnych połączeniach
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                // bieżące połączenie to nadawca wiadomości
                
                // odsyłamy wiadomość
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn)
    {
        $this->clients->detach($conn);

        echo 'Zamknięto połączenie: ' . $conn->resourceId . PHP_EOL;
    }

    public function onError(ConnectionInterface $conn, \Exception $e)
    {
        echo 'Wystąpił błąd: ' . $e->getMessage() . PHP_EOL;

        $conn->close();
    }
    
    public function onLoop($options = array())
    {
        foreach($this->clients as $client) {
            $client->send('Cykliczna wiadomość');
    
            echo 'Wysłano komunikat cykliczny' . PHP_EOL;
        }
    }
}

Klasę umieściłem w pliku handler.php.

Metody onOpen, onClose i onError są raczej oczywiste. W onMessage występuje obsługa zdarzenia pojawiającego się w momencie otrzymania wiadomości od klienta. Iterujemy tam po prostu po tablicy dostępnych połączeń i sprawdzamy, czy trafiliśmy na połączenie zgodne z identyfikatorem zasobu nadawcy. Jeżeli tak, to korzystamy z tego zasobu do odesłania odpowiedzi tej samej treści, którą otrzymaliśmy.

Metodę onLoop wykorzystamy natomiast do wywoływania cyklicznego. Wysyła ona do wszystkich połączeń komunikat o tej samej treści.

Mając taką klasę, możemy stworzyć plik, w którym uruchomimy sam serwer. W moim wypadku jest to server.php o następującej treści:

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use WebSocket\Handler;

require 'vendor/autoload.php';
require 'handler.php';

// utworzenie obiektu klasy obsługującej
$handler = new Handler();

// stworzenie instancji serwera websocketów
$server = IoServer::factory(
    new HttpServer(
        new WsServer($handler)
    ),
    8080    // port
);

// ustawienie pętli
$server->loop->addPeriodicTimer(10, function() use ($server, $handler) {
    // uruchomienie metody obrotu pętli
    $handler->onLoop(array(
        'server' => $server,
    ));
});

echo 'Serwer uruchomiony' . PHP_EOL;

// uruchomienie serwera
$server->run();

Jak widać, tworzymy najpierw instancję serwera nasłuchującego na porcie 8080, podając mu klasę z obsłużonymi zdarzeniami, a następnie dodajemy pętlę, która będzie wywoływana co 10 sekund.

Teraz wystarczy uruchomić serwer poprzez wywołanie:

php server.php

W konsoli powinniśmy dostawać informacje o przebiegu działania serwera.

Jak się połączyć z naszym serwerem? Najprościej uruchamiając konsolę w przeglądarce (w Firefoksie możemy wywołać ją kombinacją CTRL-SHIFT-K) i wklejając:

var conn = new WebSocket('ws://localhost:8080');
conn.onopen = function(e) {
    console.log("Połączono pomyślnie!");
};

conn.onmessage = function(e) {
    console.log(e.data);
};

Po chwili powinniśmy otrzymywać z serwera co 10 sekund wiadomość.

Aby wysłać wiadomość do serwera, wystarczy, że w tej samej konsoli wkleimy:

conn.send('Test');

To wszystko! Mamy już działający serwer.

Oczywiście, docelowa aplikacja musi zawierać obsługę uwierzytelniania, jednak celem tego wpisu było pokazanie prostoty stworzenia serwera WebSocketów.

Wpis był mocno inspirowany oficjalnym tutorialem Ratcheta, który rozbudowałem o cykliczne wywoływanie zdarzeń przez serwer.