Natknąłem się dzisiaj na Saeghe — nowy menedżer pakietów dla PHP. Oficjalna strona opisuje go jako nowoczesne narzędzie, które sprawia, że tworzeniu kodu (obiektowego lub funkcyjnego) jest wspaniałym doznaniem. Sprawdźmy to!

Przygotowanie środowiska

Saeghe instaluje się globalnie w systemie (wspierane są Linux i MacOS), a co za tym idzie wymaga również instalacji PHP. Dla osób, które z kodem mają do czynienia już dość długo może to być naturalne, ale w czasach wirtualizacji tak naprawdę jest to trochę przestarzała forma dostarczania narzędzia. Ale OK, zróbmy to po staremu 😉.

Dokumentacja mówi o dwóch metodach: ręcznej i z użyciem instalatora. Ja oczywiście skorzystałem z tej drugiej, czyli uruchomiłem:

bash -c "$(curl -fsSL https://raw.github.com/saeghe/installation/master/install.sh)"

Na tym etapie wszystko przebiegło bezproblemowo:

Saeghe: instalacja z użyciem skryptu instalacyjnego

Plik wykonywalny saeghe rzeczywiście automatycznie dostępny był w CLI (po restarcie terminala), więc instalator poprawnie zadbał o rozszerzenie mojego $PATH. Idąc dalej za dokumentacją “Aby rozpocząć” w kolejnym kroku skonfigurowałem token do GitHuba. Kto natknął się na limity API przy instalacji paczek przez Composera ten wie, że jest to konieczne w dużych projektach. Tu również bez niespodzianek: saeghe credential github.com $GITHUB_TOKEN poprawnie zapisało token w pliku konfiguracyjnym Saeghe (~/.saeghe/saeghe/credentials.json).

Saeghe: konfiguracja uwierzytelniania

Saeghe w akcji

Migracja z Composera

Na warsztat wziąłem mój plugin do #Rectora, który jest niewielki i w sam raz nadaje się do takich eksperymentów. Saeghe dostarcza narzedzie do migracji, a zatem uruchomiłem saeghe migrate i oczom moim ukazał się błąd:

saeghe migrate

Warning: opendir(<project_path>/rector-money/vendor/roave/security-advisories): Failed to open directory: No such file or directory in ~/.saeghe/saeghe/Source/Commands/Migrate.php on line 131

Cóż, wygląda na to, że Saeghe nie wspiera meta-paczek, które nie mają w sobie żadnych plików — takich jak roave/security-advisories (której powinieneś używać 😉). No dobra, na potrzeby eksperymentu po prostu pozbądźmy się jej z zależności Composera… Po tym zabiegu komenda migracyjna zadziałała prawidłowo, a w moim projekcie pojawił się katalog Packages oraz 2 pliki:

saeghe.config.json:

{
  "map": {
    "Codito\\Rector\\Money": "src"
  },
  "entry-points": [],
  "excludes": [
    "vendor"
  ],
  "executables": [],
  "packages-directory": "Packages",
  "packages": {
    "https:\/\/github.com\/phpstan\/phpstan.git": "1.9.0",
    "https:\/\/github.com\/rectorphp\/rector.git": "0.14.6",
    "https:\/\/github.com\/moneyphp\/money.git": "v4.0.5",
    "https:\/\/github.com\/phparkitect\/arkitect.git": "0.2.32",
    "https:\/\/github.com\/phpstan\/extension-installer.git": "1.2.0",
    "https:\/\/github.com\/phpstan\/phpstan-strict-rules.git": "1.4.4",
    "https:\/\/github.com\/sebastianbergmann\/phpunit.git": "9.5.26",
    "https:\/\/github.com\/symfony\/dependency-injection.git": "v6.1.5",
    "https:\/\/github.com\/symplify\/easy-coding-standard.git": "11.1.16",
    "https:\/\/github.com\/webmozarts\/assert.git": "1.11.0"
  }
}

saeghe.config-lock.json (fragment):

{
  "packages": {
    "https:\/\/github.com\/phpstan\/phpstan.git": {
      "version": "1.9.0",
      "hash": "e08de53a5eec983de78a787a88e72518cf8fe43a",
      "owner": "phpstan",
      "repo": "phpstan"
    },
    ...
  }
}

Moim zdaniem nazewnictwo tych plików mogłoby być lepsze, osobiście poszedłbym po prostu w saeghe.json oraz saeghe.lock.json (lub po prostu saeghe.lock). W prostocie siła, a dodatkowo byłoby to bardziej spójne z mocno już ustandaryzowaną konwencją Composera. To akurat detal, gdyż inne rzeczy przykuły tutaj moją uwagę, ale o tym później…

Instalacja zależności

Po migracji chciałem zobaczyć jak zachowa się komenda saeghe install, ku mojemu zaskoczeniu zostałem zasypany błędami w stylu:

Warning: rename(<project_path>/rector-money/Packages/phpstan/phpstan-phpstan-ed473a6,<project_path>/rector-money/Packages/phpstan/phpstan): Directory not empty in ~/.saeghe/saeghe/Source/Git/GitHub.php on line 149)

Usunąłem zatem cały folder Packages i ponownie uruchomiłem instalację — tym razem przeszła prawidłowo, ale trwała aż 52 sekundy.

Aktualizacja zależności

Komenda update odbiega od tego co znamy z Composera, ponieważ operujemy w niej na repozytoriach Gita, a nie nazwach paczek. Nie da się również wykonać aktualizacji wszystkich zależności. Używamy jej następująco:

saeghe update https://github.com/{owner}/{repo}.git --version={version-tag}

Niestety, nie byłem w stanie sprawdzić jej działania, ponieważ cały czas otrzymywałem błąd:

saeghe update https://github.com/phpstan/phpstan.git
  
  Warning: file_get_contents(<project_dir>/rector-money/Packages/phpstan/phpstan/saeghe.config.json):
  Failed to open stream: No such file or directory in ~/.saeghe/saeghe/Source/FileManager/FileType/Json.php on line 7

Wygląda na to, że Saeghe poprawnie wspiera jedynie repozytoria, które… już korzystają z Saeghe 🤷‍♂️.

Budowanie aplikacji

Przy pierwszych próbach saeghe build również otrzymywałem podobne błędy, ale w końcu zadziałało. Niestety, budowanie trwało 43 sekundy, a przy drugim wywołaniu nawet 59 sekund… To dużo, jak na tak mały projekt i na tak mocny sprzęt, na jakim przeprowadzam ten eksperyment (MacBook Pro M1).

W każdym razie w builds/development pojawiły się pliki, które w zasadzie odzwierciedlają mój projekt. Różnica między katalogiem vendor, a builds/development/Packages to 0.04MB. Co zatem zyskuję? 🤔

Budowanie w czasie rzeczywistym (watcher)

W założeniu komenda ta ma w czasie rzeczywistym reagować na zmiany w plikach źródłowych i generować pliki wynikowe — mechanizm znany z wielu narzędzi, jak chociażby Hugo wykorzystywany na moim blogu. Teoria fajna, ale praktyka już niekoniecznie.

Uruchomienie saeghe watch skutkowało u mnie ścianą tych samych ostrzeżeń, co przy update, zatem nie byłem w stanie zapoznać się z tą funkcjonalnością.

Developer Experience

Krótka przygoda z Saeghe nie pozwala oczywiście na wyrobienie sobie ostatecznej oceny o tym menedżerze, jednak nawet po tak krótkim czasie byłem w stanie zauważyć wiele niedociągnięć i/lub braków:

Brak wsparcia dla GITHUB_TOKEN

Saeghe potrzebuje tokenu do GitHuba, jednak uważam, że zamiast zaszywać go w kolejnym pliku konfiguracyjnym, mógłby po prostu wspierać ustawianie go za pomocą zmiennej środowiskowej GITHUB_TOKEN (która to już jest stosowana np. do konfiguracji GitHub CLI).

Brak wsparcia dla krótkich komend

W Composerze istnieje mechanizm, który sprawia, że komendy dostępne są pod najkrótszymi możliwymi, unikalnymi aliasami. Ponieważ update jest jedyną komendą na literę u, możliwe jest jej użycie jako composer u (a wręcz c u, gdy używa się aliasu dla composer). Jest to bardzo wygodna funkcjonalność, która znacznie minimalizuje konieczność stukania w klawisze.

Saeghe nie posiada tej opcji, a zatem za każdym razem trzeba wpisywać pełne komendy, np. saeghe update (lub s update jeśli stosuje się alias wspomniany na początku).

Niewygodna aktualizacja

Jak wspomniałem wcześniej, do aktualizacji paczek potrzebujemy URL do repozytorium, z którego dana paczka pochodzi. Nie ma co oczekiwać, że będziemy znać te adresy na pamięć, zatem istnieje konieczność robienia kopiuj/wklej. Niestety, Saeghe przechowuje te URL w sposób, który to uniemożliwia, przykładowo: https:\/\/github.com\/rectorphp\/rector.git. Jasne, istnieje szansa, że będziemy mieć taką komendę w historii swojej powłoki, ale osobiście uważam, że ten interfejs jest po prostu niewygodny.

W Composerze, format vendor/package i przechodzenie przez jedną warstwę abstrakcji (Packagist) to nie jest żadne widzimisię, tylko przemyślany mechanizm, dzięki któremu:

  • paczki są uniezależnione od ich fizycznej lokalizacji: autor może migrować kod i dla użytkowników końcowych jest to niezauważalne
  • praktycznie zlikwidowane jest ryzyko kolizji nazw: każdy dostawca może nazywać swoje paczki dowolnie w obrębie swojej przestrzeni (a zatem może być foo/collections oraz bar/collections)
  • operacje na zależnościach (dodawanie, aktualizowanie, usuwanie itd) opierają się o nazwę paczki, co jest dużo prostsze do zapamiętania i użycia w komendach

W tym kontekście Saeghe wydaje się płynąć pod prąd, ale może w związku z tym utonąć 😉.

Zarządzanie wersjami paczek

W saeghe.config.json definiujemy packages, czyli paczki, które mają być instalowane. Niestety, w odróżnieniu od Composera nie da się tu używać zakresów, każda paczka wymaga podania dokładnej wersji (tagi z prefiksem v lub bez niego, zależnie od konwencji projektu). Uważam, że to wielki krok wstecz — ideą constraintów w Composerze jest to, by jednorazowo określić minimalną wymaganą wersję i później robić jedynie update. Nie wyobrażam sobie za każdym razem ręcznie zmieniać wersji paczki, wykonywania komendy opartej o URL itd. Słabo.

Brak cache dla paczek

Po opróżnieniu Packages instalacja moich zależności trwała 52 sekundy. Po każdym usunięciu Packages wszystkie paczki znowu są pobierane i trwa to mniej więcej tyle samo. Dla porównania, Composer pobrane wersje paczek przechowuje w cache, dzięki czemu nawet po usunięciu katalogu vendor i uruchomieniu composer install instalacja jest błyskawiczna, ponieważ paczki brane są z pamięci podręcznej. Działa to globalnie — tę samą paczkę, w danej wersji, pobieramy tylko raz, a w każdym projekcie instalowana jest z cache.

Migracja pół-automatyczna

Wspomniałem, że migracja przeszła bez problemu, jednak są drobne detale, które sprawiają, że wymagane były również ręczne poprawki. Komenda saeghe migrate nie dodaje do .gitignore zarówno Packages, jak i builds, a zatem po takiej migracji Git informuje nas o setkach, czy nawet tysiącach nowych, nieśledzonych wcześniej plików.

Uproszczona konwencja owner-repo

Jak widać powyżej, w saeghe.config-lock.json zapisana jest lista aktualnie zainstalowanych paczek i ich wersji. Zastanawiające jest jednak mocno uproszczone podejście do pochodzenia paczki: każda posiada pola owner oraz repo. To opiera się o mocno uproszczone założenie, że repozytorium pochodzi z lokalizacji, która ma dwa stopnie zagnieżdżenia. Widać, że autor Saeghe nie miał do czynienia z Gitlabem, w którym grupy projektowe mogą być zagnieżdżane wielokrotnie, co skutkowałoby adresami w stylu https://gitlab.com/foo/bar/baz/package - czym zatem tutaj jest owner, a czym repo 🤔?

Patrząc na funkcyjną implementację komunikacji z Githubem i sztywną implementację repozytorium zastanawiam się czy autor w ogóle przewidział inne źródła, niż GitHub, ale to już zupełnie inna historia…

Wymagana wersja PHP

Nie zauważyłem nigdzie w Saeghe możliwości zdefiniowania wymaganej wersji PHP, co powoduje że nie jesteśmy w stanie wymusić wymaganej wersji środowiska uruchomieniowego.

Rozwiązywanie konfliktów w zależnościach

Nie istnieje. Instalowane są dokładnie te wersje paczek, które są zdefiniowane w saeghe.config.json, a czy współgrają ze sobą, to już zupełnie inna historia…

Mizerny interfejs CLI

Saeghe: interfejs CLI

Saeghe posiada interfejs CLI, ale jest on mocno toporny. Jak wspominałem powyżej, obsługa błędów w komendach pozostawia wiele do życzenia, a same komendy nie są wygodne w użyciu ze względu na brak trybu verbose czy nawet brak pomocy (wywołania z --help nie oferują dodatkowych informacji o działaniu komendy).

W porównaniu do komend w aplikacjach CLI opartych o symfony/console, ten interfejs jest po prostu ubogi i nieprzyjazny.

Kompozycja a saeghe build

Czytając zasadę działania Saeghe zastanawiam się jak wygląda wsparcie dla kompozycji w kodzie… Skoro build generuje nam kod składający się tylko i wyłącznie z plików, które są używane, to czy nie istnieje ryzyko, że implementacje interfejsów zostaną w trakcie budowania wycięte?

Wyobraźmy sobie sytuację, że istnieją interface Foo {}, class Bar { public method __construct(private Foo $foo); } oraz class Baz implements Foo {}. Instancję klasy Baz do konstruktora klasy Bar wstrzykuje nam kontener Dependency Injection, a my w całej klasie Bar operujemy jedynie na interfejsie Foo, nie znając nawet jaka jest jej implementacja — co z tym zrobi Saeghe? Zwłaszcza, gdy konfiguracja kontenera byłaby w YAMLu, a nie w pliku PHP? 🤔

Może odpowiedź na to pytanie nadejdzie już po publikacji 😉

Podsumowanie

Saeghe póki co wygląda mi na niestabilny i nie do końca przemyślany eksperyment. Nie byłem w stanie przetestować go w pełni i być może nie do końca go rozumiem, jednak ilość problemów i błędów z jakim się zetknąłem przez ten krótki czas, każe poddawać w wątpliwość sens wykorzystania Saeghe w realnych projektach. Co ciekawe najnowszą wersją jest 1.6, ale osobiście uważam, że powinno to być raczej 0.1.6… Dla porównania #PHPStan 0.12 (niewspierane, obecna wersja to 1.x), czy #Rector 0.14 są potężnymi narzędziami nieporównywalnie bardziej rozwiniętymi od obecnego Saeghe.

Osobiście uważam, że Saeghe wciąż powinno być w wersji 0.x, powoli się rozwijać, kształtować publiczne API i nadawać sobie kierunek rozwoju na podstawie odzewu ze strony społeczności PHP. Na pewno nie jest to narzędzie stabilne i gotowe do komercyjnego użytku.

Życzę jednak autorowi wytrwałości i powodzenia 🙂