W ostatnim czasie stworzyłem pull request do #PHP-CS-Fixera, którego celem było uniemożliwienie jednoczesnej instalacji stevebauman/unfinalize i friendsofphp/php-cs-fixer, co efektywnie blokowało również instalację Fixera jako deweloperskiej zależności w tymże narzędziu. Na eskalację nie trzeba było długo czekać 😅!

Jakiś czas temu w galaravelaktyce nie tak znowu daleko

W świecie #PHP dyskusja o modyfikatorze final to niekończąca się opowieść. Za każdym razem, gdy temat się pojawia, angażuje mnóstwo ludzi (w tym mnie), ale w zdecydowanej większości jest to zwyczajne marnowanie czasu wszystkich interlokutorów, ponieważ obie strony dyskusji mają swoją, uformowaną opinię i raczej nie są zainteresowane zmianą jej. Jeden z takich wpisów zapoczątkował konflikt interesów:

“Następny tydzień” nastąpił parę miesięcy później, gdy unfinalize zostało wydane. To, co stało się później, to kawał historii PHP!

Pół-żart, pół-intencja

Krótko po tym, jak unfinalize ujrzało światło dzienne, utworzyłem pull request do #PHP-CS-Fixera i wysłałem wiadomość na chacie dla maintainerów:

Easter egg najmniejszym kosztem

Ponieważ unfinalize było swego rodzaju trollingiem (ale z całą powagą w kontekście usuwania final z kodu w imię “wolności” developerów), pomyślałem, że możemy kontynuować w tym nurcie. Oczywiście robienie sobie żartu było tylko częściowo powodem mojego działania, ponieważ byłem śmiertelnie poważny w opisie pull requestu. Być może fakt, że opisałem PR w takim, a nie innym tonie wpłynął na odbiór mojej propozycji przez społeczność? Prawdopodobnie powinienem utrzymać konwencję pastiszu i wysmażyć jakąś płomienną mowę Robina Under-the-Hood’a 😉.

Tak czy siak, cała ta sytuacja prawdopodobnie nie eskalowałaby tak szybko, gdybym nie skomentował tej PRki i nie wdał się w dyskusję, która doprowadziła niejakiego Ghostridera do szału 😅. Był tak bardzo zniesmaczony moim “aktem sabotażu”, że aż przygotował alternatywną implementację unfinalize, która nie korzystała z Fixera (swoją drogą uważam tę zmianę za prawidłową i myślę, że powinna zostać zmerdżowana). Ten pull request podsunął autorowi unfinalize zmiany w Fixerze pod sam nos…

Drama

Tak, ruszyło 😆. Od tego momentu ilość 👎 zaczęła szybko rosnąć, ludzie zaczęli przyklejać mi różne etykietki (przykłady: 1, 2), a dyskusja rozgorzała na dobre 🌶️.

Robiłem co w mojej mocy, by nie dać się sprowokować, by podawać argumenty i powody tej zmiany, ale w większości była to tylko walka z wiatrakami. Niektóre osoby pojawiły się tam tylko po to, by nazwać mnie małostkowym, bez jakiejkolwiek, nawet najmniejszej próby zrozumienia tego, co się stało. Nie to, żeby mnie to w jakiś sposób zaskoczyło 😅.

Ostatecznie mój pull request został wycofany, a Keradus wziął winę na siebie, za co należy mu się szacunek, ale postawmy sprawę jasno: to była moja inicjatywa, ja stworzyłem ten pull request i to ja namawiałem do jego zmerdżowania.

Uzasadnienie pull requestu

Pomińmy chwilowo żartobliwy charakter tej PRki i skupmy się tylko na poważnych powodach, które za nią stały. Jeśli masz wystarczająco dużo czasu i ochoty, możesz poczytać komentarze w originalnym PR i późniejszym revercie, wraz z podlinkowanymi tu i ówdzie tweetami. Nawet jeśli uważam, że decyzja o cofnięciu tej zmiany była najlepszym, co mogliśmy zrobić, wciąż stoję twardo za pobudkami, które mną kierowały.

Modyfikacja cudzego kodu NIE jest standardem kodowania

#PHP-CS-Fixer to narzędzie, którego celem jest podnoszenie standardów w kodzie PHP. Zostało stworzone, aby umożliwić deweloperom nanoszenie poprawek do ich kodu w sposób zautomatyzowany. Jak pokazało narzędzie unfinalize, technicznie jest możliwe, by użyć Fixera również do wprowadzania modyfikacji w cudzym kodzie. Fakt, że jest to możliwe nie jest jednoznaczny z tym, że jest to wskazane. Modyfikacja kodu pochodzącego od zewnętrznych dostawców nie jest częścią żadnego standardu kodowania i właśnie dlatego nie podoba nam się, że Fixer został do tego celu użyty.

Dodatkowo, unfinalize jest narzędziem CLI zbudowanym z użyciem Laravel Zero, które opakowuje cały kod w jeden plik wykonywalny, ukrywając wszystkie swoje zależności przed użytkownikiem końcowym. Uważam, że nawet jeśli licencja, na bazie której udostępniony jest Fixer, umożliwia swobodne używanie i modyfikowanie, dystrybuowanie go w takiej formie, bez żadnej widocznej wzmianki o autorstwie (nie liczę pliku readme i composer.json na GitHubie) jest nie w porządku.

Umożliwienie niedozwolonego dziedziczenia może być szkodliwe

Projektowanie oprogramowania jest trudne. Utrzymywanie projektów Open Source jest trudne. Gdy połączysz te dwie rzeczy, otrzymasz tykającą bombę.

Szczerze wierzę, że podejście “final domyślnie” jest świetne (więcej tu i tu). Nie oznacza to oczywiście, że deweloperzy powinni umieszczać final wszędzie, w sposób bezmyślny — po prostu oznaczenie klas jako finalne jest dużo bardziej bezpieczne, ponieważ klasę zawsze można otworzyć zachowując kompatybilność, co nie działa w drugą stronę. Deweloperzy powinni projektować swój kod z myślą o użytkownikach, dostarczając punkty rozszerzalności, dzięki którym rozszerzanie (w rozumieniu fizycznego extends) nie będzie konieczne — zasada Open-Closed nie jest bezpośrednio związana z dziedziczeniem.

Mając to na uwadze, myślę, że unfinalize może być szkodliwe dla swoich użytkowników. Usuwanie final z zewnętrznego kodu łamie kontrakt między autorem tego kodu a użytkownikami, którzy tego dokonują. Pamiętajcie, że z perspektywy autorów takie klasy wciąż są finalne, a oni utrzymują je i rozwijają z całkowicie innym nastawieniem niż klasy, które zostały jawnie otwarte na potrzeby dziedziczenia. Gdy usuniesz final z cudzego kodu i rozszerzysz klasy, które w swym zamierzeniu miały być nierozszerzalne, możesz napotkać różnego rodzaju niekompatybilności podczas dowolnej aktualizacji paczek Composera. Co więcej: nie możesz oczekiwać wsparcia od autorów, ponieważ używasz ich kodu niezgodnie z tym, jak go zaprojektowali.

Przykładowo: chcielibyście mieć w pewnej klasie jakąś metodę, ale jej tam nie ma, więc usuwacie final z cudzej klasy, rozszerzacie ją i dodajecie metodę, po czym za jakiś czas ta metoda pojawia się w nowej wersji tej klasy, tylko że ma inną sygnaturę i PHP rzuca fatal error… Jest to oczywiście coś, co można w miarę szybko poprawić, ale czy naprawdę warto tracić czas na dostosowanie się do zmian wprowadzonych po stronie dostawcy tego kodu, zamiast od początku zrobić to jak należy? Zawsze należy się zapoznać z tym, co oferuje dany kod, próbować dostosować go do swoich potrzeb korzystając z istniejących, publicznych kontraktów, a gdy nie jest możliwe uzyskanie oczekiwanego rezultatu pierwszą rzeczą jaką należy zrobić, jest utworzenie zgłoszenia w repozytorium danego projektu. Modyfikowanie cudzego kodu powinno być ostatnią opcją, po jaką się sięga, a nawet jeśli naprawdę jest to konieczne, to można to zrobić na wiele różnych sposobów i były one dostępne jeszcze zanim pojawiło się unfinalize.

Nawet sobie nie wyobrażam skali szkodliwości tego narzędzia, jeśli ten pomysł zostanie zaimplementowany… 🙄

Unfinalize to poważny żart

Ostatni i najmniej istotny powód: naprawdę nie podoba mi się jak unfinalize jest reklamowane. Release notes są “zabawne” i sugerują, że całość to żart, ale jak już mówiłem wcześniej, jest też tam duża szczypta powagi. Steve naprawdę uważa, że używanie final w OSS jest złe, a maintainerzy są sługami. Cały ten bełkot o “przywróceniu wolności” zwyczajnie mnie irytuje, ponieważ final nie odbiera nikomu wolności, a jedynie określa pewnego rodzaju granicę i pozostawia kontrolę po stronie autorów (co jest całkowicie prawidłowe, ponieważ to właśnie oni są odpowiedzialni za ten kod). Rzeczywistą “wolność” można zaoferować użytkownikom na wiele sposobów, nie tylko poprzez dziedziczenie.

Wiele osób twierdziło, że mój pull request odzwierciedlał moje osobiste preferencje. Steve nawet sugerował, że to vendetta 😂. Wszystkie te osoby koncentrowały się na niewłaściwej rzeczy. Opis pull requestu był klarowny: propozycja dotyczyła tego, że Fixer był używany jako silnik napędzający unfinalize. Mówienie o osobistych preferencjach jest zatem śmiechu warte.

Często podnoszone kontrargumenty

Łatki z użyciem Composera istniały od lat

Oczywiście, i uważam że to świetne narzędzie. Fakt, że takowe narzędzie już istniało, sprawia, że unfinalize jest narzędziem wtórnym i całej tej dramy można było uniknąć zwyczajnie… nie implementując go 🤷‍♂️.

Ale, ale… dg/bypass-finals robi to samo

I tak, i nie.

dg/bypass-finals ma na celu ułatwienie testowania. Jak wszyscy wiemy, gdy klasa jest finalna, to mechanizm mockowania w PHPUnit nie działa, zatem jeśli chcesz oznaczyć klasy jako finalne, ale jednocześnie potrzebujesz je mockować, to proste DG\BypassFinals::enable(); w bootstrapie testów sprawi, że finalne klasy ponownie zyskają możliwość mockowania.

Oczywiście, nikt nie zabrania nam dodania tego w bootstrapie faktycznej, produkcyjnej aplikacji. Tak samo jak nikt nie zabrania nam wkładania gwoździ do gniazdka elektrycznego albo picia benzyny

Faktyczny wpływ mojego pull requestu

Wróćmy do tej dowcipnej sfery całego zamieszania. To było całkiem interesujące doświadczenie: móc obserwować reakcje ludzi, widząc jak bardzo pominęli faktyczny wpływ wdrożenia tego wpisu w sekcji conflict. Dużo było głosów sugerujących, że “zabiłem projekt”, więc przyjrzyjmy się faktom:

  • conflict wprowadzony w moim PR efektywnie zaczął działać w Fixerze od wersji v3.32.0, ale unfinalize wcale nie potrzebowało z tej wersji korzystać. Projekt wciąż mógł używać starszej wersji Fixera żeby zbudować swój własny PHAR i wszystko działałoby dokładnie tak samo, bez jakiejkolwiek zmiany po ich stronie (ponieważ Composer rozwiązałby zależności automatycznie, instalując najstarszą zgodną wersję).
  • Ponieważ unfinalize jest dostarczany jako wstępnie zbudowane narzędzie CLI (plik wykonywalny) i może zostać pobrane bezpośrednio z release’ów, nie ma konieczności by instalować go z użyciem Composera, a zatem conflict nie ma żadnego znaczenia z perspektywy użytkowników narzędzia.
  • Duża część potencjalnych użytkowników unfinalize jest związana ze społecznością Laravela, która nie używa Fixera, tylko Laravel Pint (tak naprawdę to efektywnie używają Fixera, ale często nawet o tym nie wiedząc, w końcu czekali na Pinta całe swoje życie 😂). A ponieważ Pint również jest dostarczany jako jeden plik wykonywalny (z Fixerem w środku), nie ma mowy o żadnym konflikcie z unfinalize.
  • Sam wpis w sekcji conflict był bardzo naiwnym podejściem do zablokowania koegzystencji Fixera i unfinalize, które mogło być bardzo łatwo przełamane na wiele różnych sposobów.

Powiem zatem wprost: wpływ był praktycznie zerowy 😆. Jedyny scenariusz, w którym ten cały conflict wpłynąłby na użytkowników: Fixer zainstalowany jako bezpośrednia zależność deweloperska w projekcie w przypadku chęci skorzystania z unfinalize. Wydaje mi się, że jest to dość marginalna część projektów, co oznacza, że cała ta drama i polowanie na czarownice, które urządziła część społeczności PHP, były zupełnie niepotrzebne.

Wyciągnięte wnioski

Najważniejszą nauczką z całej tej historii jest to, że open source nie jest piaskownicą, ani areną cyrkową. Są tam realni użytkownicy, z realnymi projektami, którzy oczekują, by zewnętrzne paczki były stabilne i godne zaufania. Nawet jeśli efektywny wpływ mojego pull requestu na użytkowników był pomijalny, wciąż mógł dotknąć jakąś część deweloperów i sprawić, by marnowali swój cenny czas na rozwiązanie kłopotu, który tak naprawdę ich nie interesuje. Po wielu dyskusjach i bazując na swoich własnych przemyśleniach, chciałbym przeprosić wszystkich, którzy potencjalnie mogli napotkać ten problem. To wcale nie oznacza, że zmieniłem zdanie co do unfinalize — nie, wciąż stoję twardo za wszystkim, co napisałem i powiedziałem na temat tego narzędzia, po prostu uważam, że używanie tak dojrzałego, szeroko stosowanego narzędzia, jakim jest PHP-CS-Fixer jako platformy do zwalczania żartu żartem, nie miało większego sensu.

Kolejna lekcja jest bardziej przyziemna: Laraludkom brakuje luzu 😂. To nie jest pierwsza sytuacja, w której wykazują się podwójnymi standardami i szczerze mówiąc na tym etapie już mnie to nawet nie zaskakuje. #GoodVibesOnly wygląda świetnie, ale w praktyce część tych ludzi jest zwyczajnie toksyczna.

Ostatnia nauczka jest bardzo prosta: nie marnuj czasu na przekonywanie kogoś, kto nie ma ochoty słuchać. Zawsze jestem otwarty na dyskusję, ale kłótnie z osobami o zamkniętych poglądach są zwyczajnie jak rzucanie grochem o ścianę.