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:
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 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
orazbar/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 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 🙂