Modsx — moduły dla Laravela oparte na konwencji
Podziel aplikację Laravela na moduły przy pomocy samej konwencji nazewnictwa katalogów — a potem rób ich backupy, wersjonuj je i przywracaj z linii poleceń.
📖 Pełna dokumentacja — ten sam README z bocznym menu nawigacyjnym.
Ten dokument w innym języku: English
Idea
Większość pakietów modułowych do Laravela wymaga przebudowy aplikacji: osobne drzewo źródeł, service provider na moduł, własny autoloading, własne przestrzenie nazw dla widoków i tras. To sporo maszynerii do wdrożenia — i sporo do rozplątania, jeśli zmienisz zdanie.
Modsx robi odwrotnie. Moduł to po prostu zbiór katalogów o wspólnej nazwie. Tworzysz je sam, w miejscach, w których Laravel i tak trzyma swoje rzeczy:
resources/views/modsx-blog/
app/Http/Controllers/ModsxBlog/
To już jest moduł. Działa od razu — Laravel znajduje te widoki i kontrolery dokładnie tak jak zawsze, bo we frameworku nic się nie zmieniło. Bez providera, bez rejestrowania przestrzeni nazw, bez reguł autoloadingu.
Ten pakiet nie tworzy tej struktury i nie bierze udziału w jej działaniu. On ją tylko znajduje i nią zarządza: backup, wersjonowanie, przywracanie, usuwanie.
Wynikają z tego trzy rzeczy:
- Możesz stosować konwencję bez instalowania czegokolwiek. Zacznij prefiksować katalogi już dziś, a pakiet doinstaluj w dniu, w którym faktycznie będziesz chciał backupy.
- Możesz go odinstalować i nic nie tracisz. Usuwasz pakiet, moduły działają dalej — to przecież zwykłe katalogi Laravela i nigdy nie były niczym innym.
- Nie gryzie się z resztą ekosystemu. Livewire, Filament, Inertia, Folio — wszystko, co czyta z
app/,resources/czyroutes/, widzi zwykłe katalogi, bo to właśnie są zwykłe katalogi.
Kompromis jest uczciwy: to nie jest menedżer pakietów. Nie rozwiązuje zależności między modułami, nie zarządza wymaganiami Composera i nie dotyka bazy danych. Patrz Ograniczenia.
Wymagania
| Wymaganie | Wersja |
|---|---|
| PHP | 8.3+ |
| Laravel | 12.x, 13.x |
Instalacja
composer require --dev synerdy/modsx
--dev, bo Modsx to narzędzie: poza komendą artisana nie robi zupełnie nic — service provider wychodzi natychmiast, jeśli aplikacja nie działa w konsoli, a żaden fragment Twojej aplikacji nigdy nie sięga do tego pakietu. Konwencja działa również przy odinstalowanym Modsx; o to w niej właśnie chodzi.
Instaluj do require, jeśli uruchamiasz modsx:* tam, gdzie zależności deweloperskich nie ma — skrypt wdrożeniowy backupujący moduł przed zmianą albo CI odpalające modsx:doctor po composer install --no-dev.
Service provider jest wykrywany automatycznie. Żeby cokolwiek zmienić, opublikuj konfigurację:
php artisan vendor:publish --tag=modsx-config
Backupy trafiają do modsx-backups/ w katalogu głównym projektu. Prawie na pewno chcesz je trzymać poza kontrolą wersji:
# .gitignore
/modsx-backups
Zostawienie ich w Gicie też jest sensownym wyborem, jeśli chcesz, żeby wersje modułów podróżowały razem z repozytorium — pamiętaj tylko, że backup to pełna kopia katalogów, więc repo urośnie przy każdym.
Windows i watchery plików
Na Windows rename() jest odrzucane, dopóki inny proces trzyma plik otwarty — a każdy zapis tego pakietu kończy się właśnie rename(), bo wynik powstaje w katalogu tymczasowym i trafia na miejsce dopiero jako całość. Wystarczy działający npm run dev: watcher Vite zauważa nowe pliki, otwiera katalog, żeby go obserwować, i przeniesienie kończące backup się nie udaje.
Modsx radzi sobie z tym sam. Gdy rename() zostanie odrzucone, kopiuje zamiast przenosić — a kopiowanie nie wymaga wyłącznego uchwytu, więc backup, eksport i import i tak dochodzą do skutku. Nic poniżej nie jest więc konieczne, ale dwie rzeczy warto zrobić mimo to:
Trzymaj drzewo backupów poza watcherem. Każdy backup zapisuje pełną kopię modułu, a watcher, który to zobaczy, przeładuje stronę z powodu plików niebędących częścią aplikacji:
// vite.config.js
export default defineConfig({
server: {
watch: {
ignored: ['**/modsx-backups/**'],
},
},
});
Po zmianie trzeba zrestartować dev server. Ta sama myśl dotyczy dowolnego watchera — wpisu files.watcherExclude w VS Code, wykluczenia w antywirusie czy reguły ignorowania w synchronizacji folderów.
Przywracanie na plik, który ktoś trzyma otwarty, nie zadziała — na Windows, nikomu. Otwartego pliku nie da się zastąpić ani usunąć, a kopiowanie dookoła niego zostawiłoby moduł opróżniony ze wszystkiego poza tym jednym plikiem, którego nikt nie mógł ruszyć. Modsx sprawdza to, zanim cokolwiek przeniesie, i zatrzymuje się z podaniem powodu:
ERROR Could not move [resources/views/modsx-blog] out of the way: another process is
holding it open. Nothing was changed.
Zamknij plik albo zatrzymaj proces i uruchom ponownie. modsx:backup to nie dotyczy — on tylko czyta.
Jeśli jakiś przebieg został przerwany, zanim zdążył posprzątać, modsx:doctor wypisze pozostawione katalogi tymczasowe, a modsx:doctor --fix je usunie. Nazywają się .modsx-tmp-* i nie ma w nich niczego, co byłoby Ci potrzebne.
Aktualizacja
Dopóki Modsx jest w 0.x, nowy minor nie przyjdzie zwykłym composer update. Trzeba go wskazać wprost:
composer require --dev synerdy/modsx:~0.7.0
Composer traktuje wszystko poniżej 1.0.0 z ostrożnością należną wersjom przedpremierowym: ^0.6.1 znaczy >=0.6.1 <0.7.0, czyli minor stoi w miejscu, w którym normalnie stoi major. ~0.7.0 mówi to samo bez znaku, który zjada PowerShell — patrz uwaga niżej. Dlatego composer update zostaje przy zainstalowanym minorze — celowo, a nie przez przypadek — a composer why-not synerdy/modsx 0.7.0 to potwierdzi. Wymuszenie nowego minora przepisuje ograniczenie i aktualizuje za jednym razem.
Warto najpierw zajrzeć do changelogu: w 0.x minor może nieść zmianę łamiącą i wtedy jest to tam wyraźnie zaznaczone.
Wypróbowanie bety 1.0
Komend oznaczonych 1.0 w tabeli poniżej nie ma w 0.7.0. Są w wydaniu przedpremierowym, którego Composer nie zainstaluje, dopóki mu tego nie powiesz:
composer require --dev "synerdy/modsx:~1.0@beta" # najnowsza przedpremiera 1.0
@beta zdejmuje filtr stabilności wyłącznie dla tego pakietu — minimum-stability nie trzeba ruszać — a ~1.0 znaczy >=1.0 <2.0, więc trzyma Cię na najnowszej przedpremierze w miarę, jak się pojawiają, i kolejne composer update synerdy/modsx pobierze następną.
Tylda jest celowa, tu i wszędzie indziej na tej stronie. ^1.0@beta znaczy dla Composera dokładnie to samo, ale PowerShell usuwa ^ z argumentu niezależnie od cytowania, a Composer widzi wtedy 1.0@beta i odmawia:
Root composer.json requires synerdy/modsx 1.0@beta (exact version match) ...
but it does not match the constraint.
Tylda nie ma tego problemu w żadnej powłoce.
Gorsze jest to, co daszek robi, gdy wynik mimo wszystko się rozwiąże: composer require --dev synerdy/modsx:^0.7 wpisuje do composer.json 0.7 — przypięcie zamiast zakresu — i nic o tym nie mówi. ~0.7.0 znaczy to, co znaczyło ^0.7, i przeżywa powłokę. Jeśli chcesz daszka na Windowsie, wpisz ograniczenie wprost do composer.json, gdzie żadna powłoka go nie tknie.
Przypnij konkretne wydanie, jeśli wolisz zostać przy tym, na którym testowałeś:
composer require --dev synerdy/modsx:1.0.0-beta.5 # dokładnie ta beta
Najnowsza przedpremiera to 1.0.0-beta.5. Wszystkie są wypisane na stronie wydań, razem z tym, co się w każdej zmieniło.
Zwykłe composer require synerdy/modsx nadal wybierze najnowsze wydanie stabilne, więc nikt nie dostanie bety przypadkiem. Powrót to ta sama komenda ze stabilnym ograniczeniem:
composer require --dev synerdy/modsx:~0.7.0
To beta dlatego, że dwie rzeczy, które ten pakiet zapisuje — modsx-state.json i _snapshots/*.json — nie były jeszcze używane poza jego własnymi testami. Jeśli któraś okaże się potrzebować innego kształtu, zmiana teraz nic nie kosztuje; po 1.0.0 kosztuje ścieżkę migracji albo wersję major. Traktuj zapisywane drzewo backupów jak coś, o czyje skasowanie i odtworzenie możesz zostać poproszony.
Konwencja nazewnictwa
To jedyna sekcja, którą naprawdę warto przeczytać uważnie. Cała reszta z niej wynika.
Moduł ma jedną kanoniczną nazwę zapisaną w StudlyCase:
Blog UserProfile AdminPanel
Z tej nazwy wyprowadzane są wszystkie pozostałe formy — Modsx dopasowuje każdą:
| Gdzie | Forma | Blog |
UserProfile |
|---|---|---|---|
Katalogi w resources/, public/, lang/ |
modsx- + kebab-case |
modsx-blog |
modsx-user-profile |
Katalogi przestrzeni nazw w app/, database/, tests/ |
Modsx + StudlyCase |
ModsxBlog |
ModsxUserProfile |
Pojedyncze pliki — routes/, config/, lang/ |
modsx- + kebab-case |
modsx-blog.php |
modsx-user-profile.php |
| Nazwy migracji, po timestampie | modsx_ + snake_case |
modsx_blog_… |
modsx_user_profile_… |
Dwie pierwsze to konwencja samego Laravela, a nie wymysł tego pakietu — framework mapuje App\View\Components\UserProfile na <x-user-profile> dokładnie tą samą konwersją StudlyCase ↔ kebab-case. Z prefiksem jest identycznie: App\View\Components\ModsxUserProfile\PostCard to <x-modsx-user-profile.post-card>. Jeśli Twoje katalogi już trzymają się nazewnictwa Laravela, to tym samym trzymają się i tego.
Podział na dwie formy nie jest kosmetyczny. Katalogi w app/, database/ i tests/ to segmenty przestrzeni nazw PSR-4, a identyfikator PHP nie może zawierać myślnika — App\Support\modsx-blog to nazwa, której PHP nigdy nie załaduje. Wszędzie indziej nazwa jest po prostu ścieżką, więc obowiązuje kebab-case.
Każda forma musi pochodzić od tej samej nazwy.
modsx-userprofileiModsxUserProfileto dwa różne moduły: pierwszy toUserprofile, drugiUserProfile. Zrobisz backupUserProfilei widoki zmodsx-userprofilezostaną po cichu pominięte.Najpierw zapisz nazwę w StudlyCase, potem konwertuj:
UserProfile→user-profile, nigdyuserprofile. Albo pozwól, żebyphp artisan modsx:scaffold UserProfileiphp artisan modsx:makezapisały je za Ciebie — to jedyny sposób, żeby mieć pewność, że się zgadzają. Jeśli podejrzewasz, że gdzieś już Ci się to przydarzyło,php artisan modsx:doctorto znajdzie.
Sam prefiks modsx jest konfigurowalny, więc jeśli wolisz mod- albo inicjały firmy, zmieniasz go raz i obowiązują te same reguły.
Które nazwy należą do modułu
Jedna zasada: nazwa identyfikuje moduł. Tak samo w katalogu, w pliku i w migracji. modsx-blog należy do Bloga; modsx-blog-post to inna nazwa, więc inny moduł.
Katalogi — obie formy nazwy:
| Ścieżka | Moduł | Dlaczego |
|---|---|---|
app/Models/ModsxBlog/ |
Blog | forma StudlyCase |
resources/views/modsx-blog/ |
Blog | forma kebab-case |
resources/views/modsx-blog-post/ |
BlogPost | inna nazwa to inny moduł |
resources/views/modsx-blogging/ |
Blogging | tak samo |
Pojedyncze pliki — nazwa do pierwszej kropki, dokładnie:
| Ścieżka | Moduł | Dlaczego |
|---|---|---|
config/modsx-blog.php |
Blog | dokładnie ta nazwa |
routes/modsx-blog.php |
Blog | dowolna skanowana ścieżka |
lang/en/modsx-blog.php, lang/pl/modsx-blog.php |
Blog | każdy język |
public/modsx-blog.css, public/modsx-blog.min.js |
Blog | dowolne rozszerzenie, cięcie na pierwszej kropce |
config/modsx-blog-post.php |
BlogPost | nie Blog — tak samo jak katalog |
config/modsx-blog-admin.php |
BlogAdmin | nazywa moduł; niczyj, jeśli takiego nie ma |
config/blog-modsx.php |
żaden | prefiks nie z przodu |
app/Support/ModsxBlog.php |
żaden | klasy mieszkają w katalogu modułu — app/Support/ModsxBlog/ChangeFormat.php |
Nazwy modułów są unikalne, więc do jednego pliku pasuje najwyżej jeden moduł — dwa nie mogą go sobie przypisać naraz.
Forma pojedynczego pliku jest dla miejsc, w których Laravel oczekuje jednego pliku na dany temat — routes/, config/, lang/. Wszędzie indziej moduł jest katalogiem: widoki w resources/views/modsx-blog/, źródła assetów w resources/css/modsx-blog/ i resources/js/modsx-blog/, klasy w app/Support/ModsxBlog/. Tak właśnie tworzy je modsx:scaffold i to jest forma, po którą sięgać w razie wątpliwości — katalog dopasowuje się po dokładnej nazwie i pomieści dowolnie dużo. Zadziała nawet config/modsx-blog/settings.php, które Laravel czyta jako config('modsx-blog.settings').
Migracje — moduł z przodu, potem zwykła nazwa Laravela:
| Nazwa po timestampie | Moduł | Dlaczego |
|---|---|---|
modsx_blog_create_posts_table |
Blog | |
modsx_blog_add_slug_to_posts_table |
Blog | |
modsx_blog_post_create_comments_table |
BlogPost, a gdy go nie ma — Blog | wygrywa dłuższa nazwa |
modsx_blogging_create_x_table |
Blogging | |
create_modsx_blog_posts_table |
żaden | modsx:doctor zgłosi to i poda nazwę do zmiany |
Migracja jest jedyną rzeczą, której nie da się nazwać samą nazwą modułu i niczym więcej — każda potrzebuje własnej nazwy — więc to jedyne miejsce, gdzie dłuższa nazwa modułu ma pierwszeństwo.
Blog obok BlogPost to układ wspierany:
app/Models/ModsxBlog/ Blog
app/Models/ModsxBlogPost/ BlogPost
config/modsx-blog.php Blog
config/modsx-blog-post.php BlogPost
..._modsx_blog_create_posts_table.php Blog
..._modsx_blog_post_create_comments_table.php BlogPost
modsx:delete Blog rusza wyłącznie pierwszą kolumnę.
Co należy do modułu
| Backupowane | Przywracane | Usuwane przez modsx:delete |
|
|---|---|---|---|
Katalogi (ModsxBlog/, modsx-blog/) |
tak | tak | tak |
Pliki (routes/modsx-blog.php, …) |
tak | tak | tak |
| Migracje | tylko archiwum | nie | nie |
Migracje to celowy wyjątek. Modsx nigdy nie dotyka bazy danych, więc przywrócenie starego pliku migracji przy schemacie, który poszedł do przodu, zostawiłoby repozytorium i bazę w niezgodzie — bez żadnego sygnału. Usunięcie takiego pliku, gdy jego tabele wciąż istnieją, byłoby jeszcze gorsze. Dlatego są kopiowane do każdego backupu dla wglądu — zawsze możesz odczytać, jak schemat wyglądał wcześniej — a poza tym zostawiane dokładnie tam, gdzie są.
Przykładowy układ
app/
├── Http/Controllers/ModsxBlog/
│ ├── PostController.php
│ └── CategoryController.php
├── Livewire/ModsxBlog/
│ └── PostList.php
├── Models/ModsxBlog/
│ └── Post.php
└── Services/ModsxBlog/
└── PostPublisher.php
resources/
├── views/modsx-blog/
│ ├── index.blade.php
│ └── show.blade.php
├── views/components/modsx-blog/
│ └── post-card.blade.php
├── css/modsx-blog/
│ └── blog.css
└── js/modsx-blog/
└── editor.js
routes/modsx-blog.php
config/modsx-blog.php
database/migrations/2026_01_01_000000_modsx_blog_create_posts_table.php
Nic z tego nie jest obowiązkowe. Modułem może być pojedynczy katalog widoków.
Livewire
Livewire 3 i 4 działają bez dodatkowej obsługi, bo Livewire wykrywa komponenty po katalogach:
app/Livewire/ModsxBlog/PostList.php → <livewire:modsx-blog.post-list />
resources/views/livewire/modsx-blog/post-list.blade.php
Komponenty jednoplikowe z Livewire 4 leżą w resources/views/components/, więc obowiązuje ten sam prefiks:
resources/views/components/modsx-blog/post-list.blade.php
W kodzie pakietu nie ma niczego specyficznego dla Livewire — widzi zwykłe katalogi i właśnie dlatego działa niezależnie od jego wersji.
Komendy
Uruchom dowolną komendę bez argumentów, a zapyta Cię o resztę — z listą do wyboru zamiast przepisywania nazw.
| Komenda | Do czego |
|---|---|
modsx:make {generator} {Moduł/Nazwa} |
Uruchamia generator Laravela z wpisanym modułem |
modsx:scaffold {name} {path?*} |
Tworzy szkielet modułu — katalogi, a gdzie wskazane, pliki |
modsx:list |
Moduły obecne w aplikacji |
modsx:status {name?} |
1.0 Każdy moduł: co jest, z czego się wzięło, co się ruszyło |
modsx:deps {name?} |
1.0 Których modułów potrzebuje moduł — wyliczone z kodu |
modsx:path {name?} |
Wszystko, co należy do modułu |
modsx:backup {name?} |
Kopiuje moduł do nowej numerowanej wersji, chyba że nic się nie zmieniło |
modsx:backuplist {name?} |
Dostępne wersje w backupie |
modsx:export {name?} {version?} |
Pakuje wersję backupu do przenośnego .zip |
modsx:import {path} |
Rozpakowuje .zip stworzony przez modsx:export |
modsx:delete {name?} |
Robi backup, po czym usuwa moduł |
modsx:restore {name?} {version?} |
Backupuje stan bieżący, po czym przywraca wersję |
modsx:diff {name?} {version?} {against?} |
Porównanie z wersją w backupie albo dwóch wersji ze sobą |
modsx:info {name?} |
Rozmiar, liczba plików i historia backupów |
modsx:prune {name?} |
Usuwa stare wersje według liczby albo duplikaty przez --duplicates |
modsx:snapshot {name?} |
1.0 Zapis wersji każdego modułu jako jeden snapshot |
modsx:snapshotlist |
1.0 Wykonane snapshoty |
modsx:rollback {snapshot?} |
1.0 Cofnięcie całego projektu do snapshotu |
modsx:snapshotprune |
1.0 Usunięcie starych snapshotów, zachowując najnowsze |
modsx:doctor |
Szuka problemów z nazwami i osieroconych backupów |
Komendy oznaczone 1.0 są w wydaniu przedpremierowym 1.0.0-beta.5 i nie ma ich w bieżącym wydaniu stabilnym — patrz Wypróbowanie bety 1.0.
Opcje wspólne dla komend
Trzy opcje znaczą to samo wszędzie tam, gdzie występują.
| Opcja | Co robi |
|---|---|
--dry-run |
Ustala, co by się stało, wypisuje to i niczego nie zmienia |
--force |
Nie pyta przed zmianą — i nic ponadto |
--json |
Wyjście odczytywalne maszynowo i nic więcej na standardowym wyjściu |
--dry-run
Przeprowadza całą decyzję i zatrzymuje się przed jej wykonaniem, żeby dało się przeczytać plan, zanim się na niego zgodzisz. Przyjmują ją trzy komendy — modsx:prune, modsx:snapshotprune oraz modsx:make, która wypisuje wywołanie generatora, jakie by uruchomiła:
php artisan modsx:prune Blog --keep=3 --dry-run
php artisan modsx:prune Blog --duplicates --dry-run
php artisan modsx:snapshotprune --keep=5 --dry-run
php artisan modsx:make migration Blog/create_posts_table --dry-run
Dostajesz to samo zestawienie, które pokazałby prawdziwy przebieg, zakończone zdaniem mówiącym, że nic się nie wydarzyło:
INFO Blog
0001 ......................................... identical to 0003
0002 before the refactor ....... identical to 0003, will ask
INFO 1 version(s) would be removed and 1 asked about. Nothing was changed.
Żeby wykonać, uruchom tę samą komendę bez --dry-run. Nic nie jest zapamiętywane pomiędzy jednym a drugim — plan liczy się od nowa, więc zmiana zrobiona w międzyczasie zostanie uwzględniona.
--dry-run i --json łączą się ze sobą, i tak właśnie pipeline ogląda plan, nie wykonując go:
php artisan modsx:prune --duplicates --dry-run --json
modsx:restore, modsx:rollback i modsx:delete nie mają --dry-run i nie potrzebują go: każda pokazuje, co zamierza ruszyć, i dopiero pyta — czyli to samo w jednym kroku. Żeby zobaczyć to bez uruchamiania komendy w ogóle, modsx:diff odpowiada dokładnie na pytanie, na które odpowiadałby dry run przywracania:
php artisan modsx:diff Blog 0003 # co zmieniłoby przywrócenie 0003
php artisan modsx:diff Blog 0003 0007 # co dzieli dwie wersje
--force
Znaczy nie pytaj mnie i tylko tyle. Pomija pytanie potwierdzające w komendach, które coś zmieniają albo usuwają, a przy braku terminala jest wymagana, nie opcjonalna — komenda, która nie może zapytać i nie dostała odpowiedzi, zatrzymuje się i mówi o tym, zamiast zgadywać.
php artisan modsx:prune Blog --keep=3 --force
php artisan modsx:rollback 0004 --force
Nigdy nie przełamuje zabezpieczenia. Wersji trzymanej przez snapshot nie usunie modsx:prune --force, a pliku otwartego przez inny proces nie zastąpi modsx:restore --force. Te odmowy są odpowiedziami, nie pytaniami, a --force odpowiada wyłącznie na pytania.
--json
Wypisuje jeden dokument JSON i nic poza tym: bez banera, bez tabeli, bez postępu. To właśnie czyni go parsowalnym — i dlatego ostrzeżenie, które inaczej poszłoby obok, staje się polem w tym dokumencie.
php artisan modsx:status --json
php artisan modsx:doctor --json # kod wyjścia 1, gdy znaleziono problemy
php artisan modsx:deps --json
Poproszenie o JSON to nie zgoda. Komenda, która by zapytała, nadal potrzebuje --force; format odczytywalny maszynowo mówi, jak chcesz przeczytać odpowiedź, a nie że już się zgodziłeś.
Gdzie moduł trzyma swoją dokumentację
W docs/, nazwaną po module jak wszystko inne:
docs/modsx-blog.md jedna strona
docs/modsx-blog/ katalog, gdy jedna nie wystarcza
installation.md
api.md
Nie ma tu nowej reguły. docs/modsx-blog.md należy do Bloga dokładnie z tego samego powodu co config/modsx-blog.php — nazwa identyfikuje moduł, w pliku dokumentacji tak samo jak wszędzie indziej. A to znaczy, że reszta przychodzi za darmo:
modsx:backup Blog |
kopiuje dokumentację razem z kodem |
modsx:restore Blog 0004 |
przywraca dokumentację, którą tamta wersja miała |
modsx:diff Blog 0004 |
liczy przepisaną stronę jako zmianę |
modsx:delete Blog |
usuwa ją razem z całą resztą Bloga |
modsx:path Blog |
wypisuje ją |
modsx:info Blog |
wskazuje na nią |
Pierwsza para to powód, żeby trzymać dokumentację właśnie tutaj, a nie obok dokumentacji samego projektu. Przywrócenie modułu sprzed trzech tygodni daje dokumentację sprzed trzech tygodni, a nie dzisiejszy opis kodu, którego już nie ma.
INFO Module [Blog]
Status ........................... present in the application
Directories ...................... 4
Files ............................ 2
Size ............................. 18 KB
Documentation .................... docs/modsx-blog.md
Obowiązuje zwykła granica nazw, więc strony nie mogą przypaść dwóm modułom naraz:
| Strona | Należy do |
|---|---|
docs/modsx-blog.md |
Blog |
docs/modsx-blog-post.md |
BlogPost — nie Blog |
docs/architecture.md |
nikogo; dotyczy aplikacji, nie modułu |
Jedna różnica między tymi formami jest warta zapamiętania. Katalog tworzy moduł, plik nie — ta sama reguła co wszędzie. Więc docs/modsx-shop/ powołuje Shop do istnienia, zanim powstanie linijka jego kodu, co bywa wygodne, gdy najpierw pisze się plan; samo docs/modsx-shop.md nie powołuje, a modsx:doctor zgłosi je jako stronę nazywającą moduł, którego nie ma.
docs jest skanowany domyślnie. Usuń go ze scan_paths, a strony przestaną być zawartością modułu — będą wtedy zwykłymi plikami, których nic nie backupuje i nic nie usuwa.
Jak to utworzyć
modsx:scaffold tworzy obie formy i sam wylicza nazwę z prefiksem — a to jest cały powód, żeby użyć komendy zamiast mkdir albo „nowego pliku" w edytorze:
php artisan modsx:scaffold Blog docs # docs/modsx-blog/
php artisan modsx:scaffold Blog "docs/{kebab}.md" # docs/modsx-blog.md
Wpis, którego ostatni segment ma kropkę, tworzy plik. Każdy inny tworzy katalog. To jedyna reguła i wynika z samej nazwy, a nie z listy uznanych rozszerzeń — bo to, co moduł trzyma, jest Twoją decyzją.
Plik powstaje pusty, a istniejący nigdy nie jest nadpisywany. Nic nie jest do niego wpisywane, i to celowo: ten sam kształt wpisu tworzy config/modsx-blog.php i routes/modsx-blog.php, a nagłówek pomocny w Markdownie byłby w obu tych plikach błędem składni.
Żeby każdy nowy moduł dostawał ten sam punkt wyjścia, wpisz to do config/modsx.php — wtedy samo modsx:scaffold Blog utworzy to razem z resztą:
'scaffold' => [
'app/Http/Controllers/{Studly}',
'resources/views/{kebab}',
'docs/{kebab}.md',
],
app/Http/Controllers/ModsxBlog ................................. created
resources/views/modsx-blog ..................................... created
docs/modsx-blog.md ............................................. created
INFO Created 3 path(s) for [Blog].
Tak samo działa to dla wszystkiego innego, co nazywa konwencja — config/{kebab}.php, routes/{kebab}.php — czego do tej pory nie dało się utworzyć inaczej niż wpisując prefiks ręcznie.
Wewnątrz katalogu z dokumentacją nazwy plików są już Twoje: panel.md, api.md, cokolwiek moduł potrzebuje. Plik należy tam do modułu przez położenie, a nie przez to, jak się nazywa.
modsx:path Blog potwierdzi, że modsx to widzi:
php artisan modsx:path Blog
INFO Blog
resources/views/modsx-blog ..................................... directory
docs/modsx-blog.md .................................................. file
Jeśli się tam nie pojawia, gdzieś jest zła nazwa — sprawdź ją w tabeli powyżej, zamiast zgadywać.
modsx:make
Uruchamia jeden z generatorów Laravela, wpisując moduł za Ciebie.
php artisan modsx:make controller Blog/PostController
php artisan modsx:make view blog.index
php artisan modsx:make migration Blog/create_posts_table
To jest php artisan make:* z wyciętym prefiksem. Generator jest Laravela, opcje są Laravela, wyjście jest Laravela — Modsx ustala wyłącznie nazwę:
| Wpisujesz | Uruchamia się |
|---|---|
modsx:make controller Blog/PostController |
make:controller ModsxBlog/PostController |
modsx:make view blog.index |
make:view modsx-blog/index |
modsx:make config Blog/settings |
make:config modsx-blog-settings |
modsx:make migration Blog/create_posts_table |
make:migration modsx_blog_create_posts_table --create=posts |
Trzy formy jednej nazwy, inna przy każdym generatorze, to ta część konwencji, w której najłatwiej o subtelną pomyłkę — a pomyłka daje dwa moduły, które czyta się jak jeden. Która forma gdzie trafia, mówi tabela w config/modsx.php:
'generators' => [
'*' => '{Studly}/', // ModsxBlog/PostController
'view' => '{kebab}/', // modsx-blog/index
'config' => '{kebab}-', // modsx-blog-settings
'migration' => '{snake}_', // modsx_blog_create_posts_table
],
Wszystko, czego nie ma na liście, dostaje * — co jest poprawne dla każdej klasy PHP.
Wszystko, czego nie ma na liście, dostaje *, a dostępne generatory to te zarejestrowane w Twojej aplikacji, a nie sztywny zbiór — generator z pakietu jest obsłużony tak samo dobrze jak ten od Laravela.
Wszystkie generatory Laravela i nazwa, jaką dostają
Laravel używa w swoich generatorach trzech stylów nazewnictwa i powyższa tabela pokrywa wszystkie trzy. Rozpisane w całości, dla Laravela 12/13:
PascalCase — do katalogu przestrzeni nazw modułu. Wszystkie podpadają pod *:
| Wpisujesz | Uruchamia się |
|---|---|
modsx:make cast Blog/MoneyCast |
make:cast ModsxBlog/MoneyCast |
modsx:make channel Blog/OrderChannel |
make:channel ModsxBlog/OrderChannel |
modsx:make class Blog/PaymentService |
make:class ModsxBlog/PaymentService |
modsx:make command Blog/SendEmails |
make:command ModsxBlog/SendEmails |
modsx:make component Blog/Alert |
make:component ModsxBlog/Alert |
modsx:make controller Blog/UserController |
make:controller ModsxBlog/UserController |
modsx:make enum Blog/OrderStatus |
make:enum ModsxBlog/OrderStatus |
modsx:make event Blog/OrderCreated |
make:event ModsxBlog/OrderCreated |
modsx:make exception Blog/PaymentException |
make:exception ModsxBlog/PaymentException |
modsx:make factory Blog/UserFactory |
make:factory ModsxBlog/UserFactory |
modsx:make interface Blog/PaymentGateway |
make:interface ModsxBlog/PaymentGateway |
modsx:make job Blog/ProcessOrder |
make:job ModsxBlog/ProcessOrder |
modsx:make job-middleware Blog/RateLimited |
make:job-middleware ModsxBlog/RateLimited |
modsx:make listener Blog/SendWelcomeEmail |
make:listener ModsxBlog/SendWelcomeEmail |
modsx:make mail Blog/OrderShipped |
make:mail ModsxBlog/OrderShipped |
modsx:make middleware Blog/Authenticate |
make:middleware ModsxBlog/Authenticate |
modsx:make model Blog/User |
make:model ModsxBlog/User |
modsx:make notification Blog/InvoicePaid |
make:notification ModsxBlog/InvoicePaid |
modsx:make observer Blog/UserObserver |
make:observer ModsxBlog/UserObserver |
modsx:make policy Blog/UserPolicy |
make:policy ModsxBlog/UserPolicy |
modsx:make provider Blog/AppServiceProvider |
make:provider ModsxBlog/AppServiceProvider |
modsx:make request Blog/StoreUserRequest |
make:request ModsxBlog/StoreUserRequest |
modsx:make resource Blog/UserResource |
make:resource ModsxBlog/UserResource |
modsx:make rule Blog/ValidPhoneNumber |
make:rule ModsxBlog/ValidPhoneNumber |
modsx:make scope Blog/PopularScope |
make:scope ModsxBlog/PopularScope |
modsx:make seeder Blog/UserSeeder |
make:seeder ModsxBlog/UserSeeder |
modsx:make test Blog/UserTest |
make:test ModsxBlog/UserTest |
modsx:make trait Blog/HasRoles |
make:trait ModsxBlog/HasRoles |
Trzy, które się różnią:
| Wpisujesz | Uruchamia się | Forma |
|---|---|---|
modsx:make view blog.users.index |
make:view modsx-blog/users.index |
ścieżka widoku |
modsx:make config blog.services |
make:config modsx-blog-services |
kebab-case |
modsx:make migration blog.create_users_table |
make:migration modsx_blog_create_users_table --create=users |
snake_case |
Dowolny separator, przy każdym generatorze. Tabele powyżej wybierają ten, który czyta się naturalniej, ale moduł kończy się na pierwszym /, \ albo . — niezależnie od tego, co generujesz. To są te same wywołania:
| Z ukośnikiem | Z kropką |
|---|---|
modsx:make config Blog/services |
modsx:make config blog.services |
modsx:make migration Blog/create_users_table |
modsx:make migration blog.create_users_table |
modsx:make controller Blog/UserController |
modsx:make controller Blog.UserController |
make:config to jedyne miejsce, w którym Modsx odchodzi od gołego Laravela, gdzie nazwa konfiguracji jest w snake_case. I musi: config/modsx_blog_services.php nie zostałby rozpoznany jako plik modułu — reguła szuka kebabowego prefiksu modsx- — więc konfiguracja byłaby osierocona, backupowana z niczym i usuwana z niczym. Kebab-case jest tu wymuszony przez konwencję, a nie wybrany.
Zupełnie poza zakresem modułu: make:cache-table, make:session-table, make:notifications-table, make:queue-table, make:queue-batches-table i make:queue-failed-table nie przyjmują nazwy — generują stałą migrację frameworka. Podanie jej przez modsx:make kończy się odpowiedzią Laravela „No arguments expected", i słusznie: te tabele należą do aplikacji, a nie do modułu.
Generatory z innych pakietów są traktowane tak samo — lista to te zarejestrowane w Twojej aplikacji, a * jest już właściwą formą dla klasy:
| Wpisujesz | Uruchamia się | Co dostajesz |
|---|---|---|
modsx:make livewire Blog/Alert |
make:livewire ModsxBlog/Alert |
app/Livewire/ModsxBlog/Alert.php oraz views/livewire/modsx-blog/alert.blade.php, czyli <livewire:modsx-blog.alert /> |
modsx:make filament-resource Blog/PostResource |
make:filament-resource ModsxBlog/PostResource |
app/Filament/Resources/ModsxBlog/PostResource.php |
Livewire wyprowadza ścieżkę widoku z tego, gdzie trafiła klasa — dokładnie tak jak komponenty samego Laravela — więc StudlyCase to wszystko, czego od nas potrzebuje. Wpis w modsx.generators dopisujesz tylko wtedy, gdy * jest dla danego generatora niewłaściwe — gdy jego nazwa jest ścieżką albo nazwą pliku, a nie klasy.
layout, page, partial
Własne widoki modułu mieszkają w resources/views/modsx-blog/, ale jego layout jest wycinkiem wspólnego layouts/ aplikacji — najpierw katalog frameworka, potem moduł, dokładnie jak w resources/css/modsx-blog/. Sięgają tam trzy własne nazwy Modsx:
php artisan modsx:make layout blog.app # -> resources/views/layouts/modsx-blog/app.blade.php
php artisan modsx:make page blog.index # -> resources/views/pages/modsx-blog/index.blade.php
php artisan modsx:make partial blog.head # -> resources/views/partials/modsx-blog/head.blade.php
W Laravelu nie ma make:layout. To wpisy w tej samej tabeli konfiguracji, a różni je to, że wpis podaje nie tylko formę, ale i generator do uruchomienia:
'generators' => [
'view' => '{kebab}/', // uruchamia make:view
'layout' => ['view', 'layouts/{kebab}/'], // też make:view, tylko gdzie indziej
'page' => ['view', 'pages/{kebab}/'],
'partial' => ['view', 'partials/{kebab}/'],
],
Nazwy są więc Twoje do wyboru. 'service' => ['class', 'Services/{Studly}/'] daje modsx:make service Blog/PostPublisher zapisujące app/Services/ModsxBlog/PostPublisher.php — i pojawia się w liście wyboru obok generatorów Laravela.
Świadomie nie ma tu component: make:component to generator samego Laravela i już teraz ląduje poprawnie — zapisuje klasę, a Laravel wyprowadza views/components/modsx-blog/ z tego, gdzie ta klasa trafiła.
Forma dotyczy całej nazwy, nie tylko modułu. Wpisujesz, jak Ci wygodnie, a generator dostaje nazwę w formie ze swojego wpisu, przerobioną człon po członie:
php artisan modsx:make view blog.PostList # -> modsx-blog/post-list
php artisan modsx:make view blog.admin.PostList # -> modsx-blog/admin.post-list
php artisan modsx:make config Blog/MailSettings # -> modsx-blog-mail-settings
php artisan modsx:make migration Blog/CreatePostsTable
# -> modsx_blog_create_posts_table
php artisan modsx:make controller Blog/PostController
# -> ModsxBlog/PostController, bez zmian
Wpis {Studly} zostawia resztę nazwy w spokoju, bo nazwa klasy jest już zapisana tak, jak generator jej oczekuje.
Opcje generatora piszesz tam, gdzie i tak byś je napisał — są przekazywane bez zmian:
php artisan modsx:make controller Blog/PostController --resource --model=Post
php artisan modsx:make model Blog/Post -fs
php artisan modsx:make component blog.alert --view
modsx:make ma dokładnie jedną własną opcję, --dry-run. Wszystko, czego nie rozpoznaje, należy do generatora i jest przekazywane dalej — dlatego dla make:livewire ani żadnego innego pakietu nie trzeba niczego deklarować.
-- nadal działa i jest sposobem na opcję generatora, która koliduje z naszą:
php artisan modsx:make controller Blog/PostController -- --dry-run
Moduł oddzielasz / albo . — tym, co lepiej czyta się z danym generatorem. Nazwę widoku w dokumentacji Laravela pisze się kropkami, więc tutaj pisz ją tak samo: małymi literami, dokładnie jak wpisałbyś ją do make:view:
php artisan modsx:make view blog.create # -> make:view modsx-blog/create
php artisan modsx:make view blog.admin.index # -> make:view modsx-blog/admin.index
php artisan modsx:make controller Blog/PostController
Dzieli wyłącznie pierwszy separator — nazwa modułu nie może zawierać żadnego — więc reszta nazwy zachowuje własne kropki. Obie formy są wymienne, podobnie jak wynik: make:view sam zamienia kropki na ukośniki, dlatego modsx-blog/create to ten sam widok co modsx-blog.create.
Backslash też zadziała, ale unikaj go w powłoce POSIX: bez cudzysłowu zostaje usunięty, zanim Modsx go zobaczy, więc Blog\PostController dociera jako BlogPostController. PowerShell używa jako escape'a backticka, więc tam przeżywa.
--dry-run wypisuje komendę, którą by uruchomił, i kończy:
$ php artisan modsx:make migration Blog/create_posts_table --dry-run
Would run:
php artisan make:migration modsx_blog_create_posts_table --create=posts
To --create=posts nie jest ozdobnikiem. Laravel zgaduje tabelę z nazwy migracji wzorcem /^create_(\w+)_table$/, którego modsx_blog_create_posts_table nie może spełnić z modułem z przodu — więc bez tego każda migracja tworząca tabelę wychodziłaby jako pusty stub. Modsx uruchamia zgadywanie na tej części, którą faktycznie napisałeś, i przekazuje wynik dalej.
Jeśli moduł jeszcze nie istnieje, dowiesz się o tym i dostaniesz jedno pytanie — domyślnie „tak", z najbliższą istniejącą nazwą na wypadek literówki. Uruchomienie nieinteraktywne ostrzega i leci dalej: utworzenie pliku nie jest destrukcyjne, a to jedyna komenda w pakiecie, która nigdy nie jest.
Jednej rzeczy nie naprawi: przy make:model -m migrację nazywa Laravel, więc wychodzi jako create_posts_table, bez prefiksu modułu, i do modułu nie należy — nie zostanie z nim zbackupowana. Modsx o tym ostrzega. Wygeneruj migrację osobno.
modsx:scaffold
Tworzy katalogi nowego modułu. Konwencja działa świetnie i bez tej komendy — możesz zrobić katalogi ręcznie i niczego nie instalować — ale wpisywanie obu form samodzielnie to jedyny sposób, żeby się pomylić. Tutaj obie pochodzą z jednej nazwy, więc nie mogą się rozjechać.
php artisan modsx:scaffold Blog
php artisan modsx:scaffold user-profile # dowolna wielkość liter, jest normalizowana
Lista z konfiguracji
Które katalogi powstaną, zależy od Ciebie — config/modsx.php:
'scaffold' => [
'app/Http/Controllers/{Studly}',
'app/Models/{Studly}',
'resources/views/{kebab}',
],
{Studly} staje się ModsxBlog, {kebab} staje się modsx-blog. Oba z tej jednej nazwy, którą wpisałeś.
Opublikowany config niesie dłuższą listę, zakomentowaną — Livewire, serwisy, form requesty, fabryki, seedery, testy, resources/css/, resources/js/, komponenty — więc odkomentowujesz to, co Twoje moduły faktycznie mają. Domyślna lista jest krótka celowo: katalog, którego nikt nie wypełni, jest niewidoczny dla gita i zgłasza go modsx:doctor, więc hojna wartość domyślna robiłaby tylko robotę dla --fix.
Widoki mają ten sam kształt co wszystko inne — najpierw katalog frameworka, moduł w środku, dokładnie jak resources/css/modsx-blog/:
resources/views/
├── components/
│ ├── layouts/app.blade.php aplikacji
│ └── modsx-blog/card.blade.php Bloga -> <x-modsx-blog.card>
├── layouts/
│ ├── app.blade.php aplikacji
│ └── modsx-blog/ Bloga
├── partials/modsx-blog/ Bloga
└── modsx-blog/ własne strony Bloga
Jedno i drugie żyje obok siebie: layouts/app.blade.php ze starter kitu nie nosi prefiksu, więc żaden moduł go nie zagarnie, a layouts/modsx-blog/ jest Bloga i wędruje razem z nim. Moduły są znajdowane na dowolnej głębokości, więc zagnieżdżenie o poziom nic nie kosztuje.
Zwróć uwagę na kolejność. layouts/modsx-blog/ — nie modsx-blog/layouts/. W całej tej konwencji moduł idzie do wnętrza katalogu frameworka i widoki nie są wyjątkiem; odwrócenie tego akurat tutaj sprawiłoby, że <x-modsx-blog.card> przestaje się rozwiązywać.
Wskazanie katalogów wprost
Podaj katalogi po nazwie modułu, a powstaną one zamiast listy z konfiguracji — ten jeden, którego potrzebujesz teraz, bez zmieniania tego, co dostaje każdy przyszły moduł:
php artisan modsx:scaffold Blog resources/css
# resources/css/modsx-blog/
php artisan modsx:scaffold Blog resources/css resources/js app/Services
# resources/css/modsx-blog/
# resources/js/modsx-blog/
# app/Services/ModsxBlog/
Zwróć uwagę na trzeci: ModsxBlog, nie modsx-blog. Piszesz ścieżkę tak, jak wygląda w projekcie, a forma katalogu modułu jest odczytywana z tego, dokąd ta ścieżka prowadzi:
| Wpisujesz | Powstaje | Dlaczego |
|---|---|---|
resources/css |
resources/css/modsx-blog |
ścieżka, więc kebab-case |
resources/js |
resources/js/modsx-blog |
|
resources/views/layouts |
resources/views/layouts/modsx-blog |
|
public/vendor |
public/vendor/modsx-blog |
|
lang/en |
lang/en/modsx-blog |
|
app/Services |
app/Services/ModsxBlog |
app/ to PSR-4 |
app/Livewire |
app/Livewire/ModsxBlog |
|
database/factories |
database/factories/ModsxBlog |
database/ to PSR-4 |
tests/Feature |
tests/Feature/ModsxBlog |
tests/ to PSR-4 |
app/, database/ i tests/ to korzenie PSR-4 standardowej aplikacji Laravela — App\, Database\, Tests\ — a myślnik nie jest poprawnym identyfikatorem PHP, więc katalogi pod nimi biorą formę StudlyCase. Wszędzie indziej nazwa jest wyłącznie ścieżką. To ta sama reguła, którą opisuje tabela konwencji, tylko zastosowana za Ciebie.
Gdy to odgadnięcie jest nietrafione — na przykład przy własnym korzeniu PSR-4 — wpisujesz placeholder i to on rozstrzyga:
php artisan modsx:scaffold Blog "modules/Shared/{Studly}"
# modules/Shared/ModsxBlog/
php artisan modsx:scaffold Blog "storage/exports/{kebab}"
# storage/exports/modsx-blog/
Istniejący katalog jest raportowany i zostawiany w spokoju, tak samo jak przy liście z konfiguracji, więc ponowne uruchomienie jest bezpieczne:
$ php artisan modsx:scaffold Blog resources/css
resources/css/modsx-blog ...................................... created
$ php artisan modsx:scaffold Blog resources/css
resources/css/modsx-blog ................................ already existed
A ścieżka nie może opuścić projektu:
$ php artisan modsx:scaffold Blog ../../etc
ERROR Invalid path [../../etc]. Give a directory inside the project, such as
"resources/css" or "app/Services"; it may not contain "..".
Tworzy katalogi i nic poza tym — żadnych stubów kontrolerów, żadnego boilerplate'u. Generowanie kodu uczyniłoby z tego generator, czyli dokładnie to, czym Modsx nie jest. Niczego też nie nadpisuje: istniejące katalogi są raportowane i zostawiane w spokoju, więc komendę można bezpiecznie uruchomić ponownie.
Pamiętaj, że git nie śledzi pustych katalogów, więc szkielet, którego nie wypełnisz, po cichu zniknie przy następnym commicie. Tak ma być: katalogi, których faktycznie używasz, będą miały w sobie pliki. Do tego czasu wciąż leży na dysku — php artisan modsx:doctor --fix znajduje i usuwa puste katalogi modułu, więc nie trzeba ich szukać ręcznie.
modsx:list
php artisan modsx:list
php artisan modsx:list --json
Module Directories Files Backups Latest
Blog 4 2 3 0003
UserProfile 2 - - -
Moduł pojawia się na liście, jeśli istnieje którykolwiek z jego katalogów — moduł to zbiór katalogów i to one go tworzą. Pliki i migracje nazwane od modułu do niego należą i są liczone w kolumnach powyżej, ale go nie powołują do istnienia: config/modsx-blog.php bez żadnego katalogu modsx-blog jest zgłaszany przez modsx:doctor jako nazywający nieistniejący moduł, a modsx:backup Blog powie to samo.
modsx:status
php artisan modsx:status
php artisan modsx:status Blog
php artisan modsx:status --json
Module State Current Latest backup Changes
Blog modified 0001 0002 1
Shop clean 0001 0001 0
Billing untracked - - -
Admin missing - 0001 -
Układ jest ten sam, którego nauczyła Cię kontrola wersji. Current to wersja, z której wzięło się drzewo robocze, więc Changes liczy to, co się od tamtej pory wydarzyło — a nie odległość od najnowszego backupu, bo to inne pytanie i ma własną kolumnę.
| Stan | Znaczenie |
|---|---|
clean |
Jest w aplikacji, zgodny z wersją, z której się wziął |
modified |
Jest w aplikacji, różni się od niej — Changes mówi o ile plików |
untracked |
Jest w aplikacji, nigdy nie backupowany |
missing |
Backupy istnieją, ale modułu nie ma w aplikacji |
Moduł może być clean i mimo to być w tyle — i właśnie o tym połączeniu warto wiedzieć:
WARN [Blog] is working from 0001, but 0002 exists. Backing up now would build the next version on the older one.
Skąd bierze się Current
modsx:backup i modsx:restore zapisują wersję w modsx-backups/Blog/modsx-state.json. modsx:import celowo nie zapisuje nic: dokłada wersję do drzewa backupów, nie dotykając aplikacji, więc drzewo robocze się z niej nie wzięło.
Ten plik nigdy nie decyduje o tym, czy moduł istnieje. Wykrywanie zostaje takie, jakie było zawsze — katalog nazwany zgodnie z konwencją. Moduł zrobiony przez mkdir nie ma tu żadnego wpisu i jest pokazany jako untracked, co jest prawdą, a nie brakiem. Skasuj wszystkie te pliki, a pakiet zachowa się dokładnie tak jak przed ich powstaniem; pusta zrobi się jedynie kolumna Current.
Leży obok wersji, na które wskazuje, bo bez nich nic nie znaczy. Przytnij wskazywaną wersję, a modsx:doctor to zgłosi, natomiast modsx:status będzie działać dalej, mierząc względem najnowszej.
Jeśli trzymasz drzewo backupów w repozytorium, zostaw ten plik poza nim — to, z której wersji wzięła się Twoja kopia robocza, jest faktem lokalnym, a nie wspólnym:
modsx-backups/*/modsx-state.json
modsx:deps
php artisan modsx:deps # wszystkie moduły
php artisan modsx:deps Blog # jeden moduł i to, co objąłby jego snapshot
php artisan modsx:deps --json
INFO Blog
Media ......................... found in the code
User .......................... found in the code
INFO Media
needs nothing else
Graf jest wyprowadzany, a nie deklarowany. Moduł o nazwie Media występuje w kodzie innych modułów jako ModsxMedia, modsx-media albo modsx_media i w żadnej innej postaci, więc odwołanie do niego jest czymś, co można znaleźć, a nie czymś, o czym trzeba pamiętać.
To cały argument za czytaniem zamiast prowadzenia listy. Ręcznie utrzymywana lista zależności to lista, której nic nie sprawdza: dopiszesz odwołanie do innego modułu, zapomnisz zaktualizować listę i snapshot z niej zbudowany jest po cichu niekompletny — a to gorsze niż brak snapshotu, bo się mu ufa.
| Gdzie szuka | Co liczy się jako odwołanie |
|---|---|
use App\Models\ModsxMedia\Asset; |
forma Studly, w dowolnym PHP |
@include('modsx-media.player') |
forma kebab, w dowolnym pliku |
config('modsx-media.disk') |
ta sama forma, w dowolnym wywołaniu |
$table = 'modsx_media_assets'; |
forma snake, razem z nazwami tabel |
Wzmianka w komentarzu albo w stringu też się liczy. To celowe: błąd, który stąd wynika, to snapshot obejmujący o jeden moduł za dużo, czyli koszt jednego katalogu — a błąd przeciwny psuje rollback.
Granice nazw są respektowane, więc ModsxBlogPost jest odwołaniem do BlogPost, a nie przy okazji do Blog — ta sama reguła, która rozstrzyga, czyj jest plik. Jedyny wyjątek to forma snake, gdzie sufiks jest czymś zwyczajnym: modsx_media_assets to tabela Media, więc podkreślenie może po niej wystąpić.
Przy modułach Blog, BlogPost, Media i MediaAssets linia znaleziona w Blog daje:
| Linia w module Blog | Krawędź do |
|---|---|
use App\Models\ModsxMedia\Asset; |
Media |
use App\Models\ModsxMediaAssets\Row; |
MediaAssets — nie Media |
@include('modsx-media.player') |
Media |
@include('modsx-media-assets.row') |
MediaAssets — nie Media |
view('modsx-blog-post.comment') |
BlogPost — nie Blog |
config('modsx-media.disk') |
Media |
$table = 'modsx_media_assets'; |
Media i MediaAssets — forma snake dopuszcza sufiks |
// zobacz też ModsxMedia |
Media — komentarz też się liczy |
use App\Models\ModsxBlog\Post; w samym Blogu |
nic; moduł nigdy nie potrzebuje sam siebie |
use App\Models\ModsxGhost\Thing; bez modułu Ghost |
nic; krawędzią może być tylko istniejący moduł |
Czego czytanie nie zobaczy
Nazwy klasy sklejanej ze stringa, listenera podpiętego gdzie indziej. To trafia do konfiguracji, która krawędzie dodaje i nigdy nie zastępuje tych znalezionych w kodzie:
// config/modsx.php
'dependencies' => [
'Blog' => ['Search'],
],
Krawędź znaleziona w obu miejscach jest raportowana jako found in the code — z dwóch twierdzeń to jest to, które da się wskazać palcem.
Moduły zależne od siebie nawzajem są wypisywane, a nie traktowane jak usterka. Pierścień bywa świadomym projektem; jedyną konsekwencją jest tu to, że snapshot któregokolwiek z nich obejmuje je wszystkie.
modsx:path
Pokazuje dokładnie, co Modsx uznaje za część modułu — czyli dokładnie to, co skopiuje backup. Warto uruchomić przed pierwszym modsx:delete.
php artisan modsx:path Blog
php artisan modsx:path # wszystkie moduły
php artisan modsx:path --json
Katalogi, pliki i migracje są wypisywane osobno, przy czym migracje są oznaczone jako archiwalne — żeby było jasne, że przywracanie ich nie odtworzy.
modsx:backup
Kopiuje każdy katalog należący do modułu do nowej, kolejnej wersji.
php artisan modsx:backup Blog
php artisan modsx:backup Blog -m "przed przejściem na repository pattern"
php artisan modsx:backup --all # wszystkie moduły naraz
php artisan modsx:backup Blog --even-if-unchanged # zapisz wersję mimo to
php artisan modsx:backup Blog --json
modsx-backups/
└── Blog/
├── 0001/
│ ├── modsx.json
│ ├── app/Http/Controllers/ModsxBlog/ ← przywracane
│ ├── routes/modsx-blog.php ← przywracane
│ └── _archive/
│ └── database/migrations/... ← tylko do wglądu
└── 0002/
└── ...
Numer wersji bierze się z najwyższego istniejącego numeru, a nie z tego, co system plików wypisze jako ostatnie, a komenda odmawia zapisu do ścieżki, która już istnieje. Wersje nigdy nie są nadpisywane ani używane ponownie.
Zarchiwizowane migracje leżą w _archive/, z dala od reszty. To nie jest etykieta — przywracanie czyta listę ścieżek i plików z manifestu i nigdzie indziej nie zagląda, więc nie ma tu flagi, którą dałoby się źle ustawić.
-m/--comment dopina do wersji opcjonalną, dowolną notatkę tekstową — całkowicie opt-in, nie ma o to promptu. Widać ją w modsx:backuplist i modsx:info.
Moduł, który się nie zmienił, nie jest kopiowany drugi raz. Każdy backup najpierw porównuje moduł z jego najnowszą wersją plik po pliku, a gdy są zgodne — mówi o tym i nic nie zapisuje:
INFO Nothing to back up: [Blog] is identical to version 0004.
Druga kopia modułu, który się nie ruszył, nie zapisuje żadnego faktu, a kosztuje pełny katalog — dlatego nie jest zachowaniem domyślnym. Zmieniona migracja nie liczy się tu jako zmiana, bo nie jest częścią tego, co przywracanie odtwarza.
--even-if-unchanged zapisze wersję mimo to — na wypadek, gdy druga kopia tego samego jest właśnie sensem: oznaczenie momentu, który ma znaczenie, choć kod się nie zmienił:
php artisan modsx:backup Blog --even-if-unchanged -m "wdrożone na produkcję"
Sam komentarz tego nie wymusza. Jeśli nic się nie zmieniło, wersja, do której miał trafić, nie powstaje — a komenda mówi wprost, że komentarz przepadł, zamiast gubić go po cichu:
INFO Nothing to back up: [Blog] is identical to version 0004.
⇂ The comment was not recorded. Pass --even-if-unchanged to write a version for it
Ta sama reguła dotyczy kopii zabezpieczających, które modsx:restore i modsx:delete robią, zanim cokolwiek zmienią: gdy jakaś wersja już trzyma bieżący stan, to jest dokładnie to, po co te kopie istnieją, i druga identyczna nie powstaje.
Każda wersja ma manifest modsx.json z nazwą modułu, czasem utworzenia, dokładną listą ścieżek i plików źródłowych, zarchiwizowanymi migracjami, opcjonalnym komentarzem oraz wersjami PHP, Laravela i pakietu. Przywracanie go czyta, dzięki czemu odkłada rzeczy tam, skąd zostały wzięte, zamiast zgadywać ich położenie.
Całość kopiowana jest najpierw do katalogu tymczasowego i dopiero na końcu przenoszona na miejsce, więc przerwany backup nie zostawia po sobie wersji zapisanej w połowie.
Backup dwóch modułów, których nazwy różnią się wyłącznie wielkością liter, jest odrzucany. Na Windows i macOS UserProfile i Userprofile to ten sam katalog, więc dzieliłyby jedną sekwencję wersji, a przywracanie mogłoby zwrócić nie ten moduł. Odmowa obowiązuje na każdej platformie: zachowanie zależne od systemu plików jest gorsze niż konsekwentne „nie".
modsx:backuplist
php artisan modsx:backuplist # wszystkie moduły
php artisan modsx:backuplist Blog
php artisan modsx:backuplist Blog --limit=5 # 5 najnowszych
php artisan modsx:backuplist --json
Blog
Version Created Directories Files Archived Comment
0001 2026-08-20T09:14:02+02:00 2 -
0002 2026-08-21T17:40:55+02:00 2 przed przejściem na repository pattern
modsx:export
Pakuje jedną wersję backupu do przenośnego .zip, zapisanego obok katalogu wersji, z którego powstał.
php artisan modsx:export Blog # najnowsza wersja
php artisan modsx:export Blog 0003 # konkretna wersja
php artisan modsx:export # interaktywnie
modsx-backups/
└── Blog/
├── 0001/
├── 0002/
└── 0002.zip ← utworzony przez modsx:export
Zip to pochodny, tworzony na żądanie artefakt, a nie nowa wersja. Same wersje pozostają rozpakowanymi katalogami, celowo: otwórz jedną w eksploratorze plików albo wejdź do niej przez cd, a zobaczysz dokładnie, co należy do modułu, natychmiast — bez rozpakowywania, bez narzędzi. modsx:export tego domyślnego zachowania nie zmienia; dokłada jednoplikową formę do tej jednej rzeczy, w której rozpakowane katalogi wypadają gorzej — przeniesienia wersji gdzie indziej. Ponowne uruchomienie modsx:export na tej samej wersji nadpisuje jej zip — nie ma tu zabezpieczenia "już istnieje" takiego jak przy samej wersji. Usunięcie wersji przez modsx:prune usuwa też jej zip.
Miejsce, gdzie ląduje zip, nie jest konfigurowalne — przeniesienie go gdziekolwiek indziej to zwykłe cp/mv, nie coś, co modsx musi wiedzieć.
modsx:import
Rozpakowuje .zip stworzony przez modsx:export z powrotem do drzewa backupów, pod moduł i wersję, które nazywa jego własny modsx.json — tak właśnie moduł podróżuje między projektami jako jeden plik zamiast drzewa katalogów.
php artisan modsx:import sciezka/do/Blog-0002.zip
Odmawia importu na wersję, która już istnieje — z tego samego powodu, dla którego modsx:backup odmawia nadpisania istniejącej: raz zapisana wersja nigdy nie jest po cichu zastępowana. Po imporcie przywracasz ją normalnie: php artisan modsx:restore Blog 0002.
modsx:delete
Najpierw robi backup i nie usuwa niczego, jeśli backup się nie powiódł.
php artisan modsx:delete Blog
php artisan modsx:delete Blog --force # bez pytania, do CI
php artisan modsx:delete Blog --skip-backup # jeśli naprawdę tego chcesz
php artisan modsx:delete Blog --force --json
Wszystko, co zostanie usunięte, jest wypisywane przed pytaniem o potwierdzenie, a numer utworzonej wersji jest drukowany — więc zawsze wiesz, co podać do modsx:restore.
Migracje też są wypisywane — jako zostawione. Zostają w aplikacji, bo ich tabele wciąż są w bazie, a usunięcie pliku, który je dokumentuje, zostawiłoby schemat bez żadnego wyjaśnienia. modsx:doctor przypomni Ci później, że należą do modułu, którego już nie ma.
modsx:restore
php artisan modsx:restore Blog # najnowsza wersja
php artisan modsx:restore Blog 0003 # konkretna wersja
php artisan modsx:restore # interaktywnie
php artisan modsx:restore Blog --json
php artisan modsx:restore Blog 0003 --force # bez pytania, do skryptów
Kolejność działań:
- Backup bieżącego stanu modułu, żeby samo przywracanie też dało się cofnąć.
- Skopiowanie wybranej wersji z backupu do katalogu tymczasowego.
- Odsunięcie całego bieżącego stanu na bok, jednym przebiegiem.
- Przeniesienie przywracanego stanu na miejsce.
Wszystko jest wyciągane z backupu zanim cokolwiek zostanie ruszone w aplikacji, więc uszkodzony lub niekompletny backup ujawnia się, gdy bieżący stan jest jeszcze nienaruszony. A ponieważ krok 3 odsuwa stary stan w całości, zamiast kasować ścieżkę po ścieżce, awaria w kroku 4 jest cofana: dostajesz z powrotem dokładnie to, co miałeś, a nie mieszankę starego z nowym.
Wszystko, czego dana wersja nie zawierała, po przywróceniu znika — zostało odsunięte i nie wróciło. Tak właśnie musi działać „przywróć dokładnie ten stan", a modsx:diff powie Ci z wyprzedzeniem, czego to dotyczy.
Dotyczy to wszystkiego, co moduł ma w tej chwili, a nie tylko tego, co wymienia manifest wersji — inaczej wynik nie odpowiadałby żadnej wersji. Załóżmy, że powstał backup Blog 0001, a potem dołożyłeś:
| Dodane po backupie | Po modsx:restore Blog 0001 |
|---|---|
resources/views/modsx-blog/extra.blade.php |
znika — w katalogu należącym do modułu |
routes/modsx-blog.php |
znika — osobny plik należący do modułu |
app/Models/ModsxBlog/Draft.php |
znika — to samo, w formie Studly |
config/unrelated.php |
nietknięty — nie należy do żadnego modułu |
config/modsx-shop.php |
nietknięty — należy do Shop, nie do Blog |
To usuwanie jest bezpieczne wyłącznie dlatego, że nigdy nie dotyczy ostatniej kopii. Krok 1 powyżej najpierw backupuje stan bieżący, więc plik zmieciony przez przywracanie jest o jedną wersję wstecz, a nie stracony:
php artisan modsx:restore Blog 0001 --force # extra.blade.php znika...
php artisan modsx:backuplist Blog # ...do wersji 0002, wykonanej przed chwilą
php artisan modsx:restore Blog 0002 # i oto jest z powrotem
Tak samo działa modsx:rollback, który przed ruszeniem czegokolwiek wykonuje cały snapshot bezpieczeństwa.
Wyjątkiem, który bywa mylący, jest modsx:import: on nigdy nie dotyka aplikacji. Rozpakowuje zip wyłącznie do drzewa backupów, więc w Twojej kopii roboczej nic się nie zmienia, dopóki czegoś nie przywrócisz.
Zarchiwizowane migracje nigdy nie są przywracane. Na tym etapie w ogóle nie są czytane.
Jeśli modułu nie ma aktualnie w aplikacji, kroki 1 i 3 są pomijane i staje się to instalacją z backupu — i tak właśnie przenosi się moduł między projektami: kopiujesz modsx-backups/Blog/ i przywracasz.
modsx:prune
php artisan modsx:prune # wszystkie moduły, domyślna wartość z configu
php artisan modsx:prune Blog --keep=5
php artisan modsx:prune --keep=3 --dry-run # pokazuje plan, nic nie zmienia
php artisan modsx:prune --dry-run --json # plan w formacie JSON, do CI
php artisan modsx:prune Blog --keep=3 --force # bez pytania, do skryptów
Wypisuje dokładnie, które wersje znikną, i dopiero pyta. Najnowsza wersja nie jest usuwana nigdy, niezależnie od --keep.
modsx:snapshot
php artisan modsx:snapshot # cały projekt
php artisan modsx:snapshot --comment="przed przepisaniem"
php artisan modsx:snapshot Blog # Blog i wszystko, czego potrzebuje
php artisan modsx:snapshot --json
Module Version
Blog 0002 backed up
Media 0001 unchanged
User 0001 unchanged
INFO Snapshot 0002 taken, holding 3 module(s).
Snapshot zapisuje, która wersja każdego modułu była bieżąca w jednej chwili. Odpowiada na pytanie, na które backup pojedynczego modułu odpowiedzieć nie potrafi:
Nie mogę przywrócić Bloga sprzed trzech tygodni, bo zależał wtedy od innej wersji User.
Nic nie kopiuje. Wersje, które wymienia, już są w drzewie backupów, więc zapisywane jest kilkaset bajtów numerów w modsx-backups/_snapshots/0002.json. Kopiowanie ich podwoiłoby koszt dysku i dałoby jednej wersji dwa miejsca zamieszkania.
Moduł, który nie zmienił się od ostatniego backupu, nie dostaje nowej wersji — tylko kolejne odwołanie do tej, którą już miał. Snapshot nietkniętego projektu nie zapisuje więc nic poza samym snapshotem. I o to chodzi: snapshot, którego nikomu nie żal zrobić, to ten, który będzie na miejscu, gdy okaże się potrzebny.
Podanie nazwy modułu obejmuje ten moduł i domknięcie jego zależności, wyliczone dokładnie tak, jak pokazuje modsx:deps.
modsx:snapshotlist
php artisan modsx:snapshotlist
php artisan modsx:snapshotlist --limit=5
php artisan modsx:snapshotlist --json
Snapshot Created Scope Modules Comment
0001 2026-09-03T20:42:51+00:00 whole project 3 przed przepisaniem
0002 2026-09-03T20:42:52+00:00 whole project 3 -
0003 2026-09-03T20:43:01+00:00 Blog 2 -
Snapshot pokazany na czerwono wskazuje wersję, której nie ma już w drzewie backupów, i nie da się do niego cofnąć. modsx:prune do tego nie doprowadzi — wstrzymuje takie wersje — więc oznacza to, że ktoś zmieniał drzewo backupów ręcznie.
modsx:rollback
php artisan modsx:rollback # najnowszy snapshot
php artisan modsx:rollback 0001
php artisan modsx:rollback 0001 --force
Module Current Snapshot
Blog 0002 0001 will move
Media 0001 0001 already there
User 0001 0001 already there
Osobny czasownik niż modsx:restore, i to celowo: przywracanie dotyczy jednego modułu i jednej wersji, a rollback rusza wszystkim naraz — tych dwóch rzeczy nikt nie powinien móc pomylić o drugiej w nocy. Plan pokazywany jest przed pytaniem, bo liczbą, która ma znaczenie, nie jest to, ile modułów obejmuje snapshot, tylko ile z nich jest teraz gdzie indziej.
Co dokładnie gwarantuje:
| Etap | Co robi |
|---|---|
| Sprawdzane najpierw | Istnienie każdej wymienionej wersji jest potwierdzane, zanim cokolwiek zostanie tknięte. To jest awaria, która faktycznie się zdarza, i wychodzi na jaw, gdy aplikacja jest jeszcze cała. |
| Droga powrotna | Snapshot stanu bieżącego powstaje przed wszystkim innym, a jego numer jest wypisywany na końcu. |
| W obrębie modułu | Każdy moduł jest stage'owany i podmieniany osobno, więc awaria wewnątrz jednego zostawia go nietkniętym. |
| Pomiędzy modułami | Awaria w połowie cofa moduły już przywrócone do stanu, w jakim zastał je snapshot bezpieczeństwa, i mówi o tym wprost. |
Czym to nie jest, to pojedyncza transakcja systemu plików — coś takiego nie istnieje w poprzek kilkunastu drzew katalogów. Ostatni wiersz to kompensacja, nie atomowość, i właśnie dlatego snapshot bezpieczeństwa jest nazwany, a nie pozostawiony Tobie do odszukania.
Rusza wyłącznie modułami wymienionymi w snapshocie. Moduł utworzony po jego wykonaniu nigdy nie należał do tamtej chwili i zostaje dokładnie tam, gdzie jest; moduł skasowany od tamtej pory wraca. Ostrzeżenie powyżej dotyczy więc plików wewnątrz przywracanego modułu, a nie projektu jako całości.
--force jest wymagany w trybie nieinteraktywnym, również przy --json. Format odczytywalny maszynowo to nie jest zgoda.
Nie dotyka bazy danych. Cofnięcie kodu nie cofa schematu, a modsx nie uruchamia żadnych migracji, w żadną stronę. Zarchiwizowane migracje podróżują z backupem swojego modułu, więc odzyskujesz pliki; czy schemat nadal do nich pasuje, oceniasz sam.
modsx:snapshotprune
php artisan modsx:snapshotprune --keep=5
php artisan modsx:snapshotprune --keep=5 --dry-run
php artisan modsx:snapshotprune --keep=5 --force
Snapshoty wstrzymują wersje przed modsx:prune, więc ta komenda istnieje po to, żeby móc któryś puścić. Usunięcie snapshotu nie usuwa żadnych wersji — przestaje je tylko wstrzymywać, więc następny modsx:prune znowu weźmie je pod uwagę.
modsx:prune --duplicates
Usuwa wersje trzymające dokładnie to, co wersja następująca po nich:
php artisan modsx:prune Blog --duplicates --dry-run
php artisan modsx:prune Blog --duplicates
php artisan modsx:prune --duplicates --force # wszystkie moduły, bez pytania
INFO Blog
0001 ................................................. identical to 0003
0002 before the refactor ...... identical to 0003, will ask
INFO 1 version(s) would be removed and 1 asked about. Nothing was changed.
Ignoruje --keep. Te dwa tryby zadają wersji różne pytania: --keep dotyczy tego, ile historii zachować, a to — historii, która niczego nie zapisuje.
Liczą się wyłącznie wersje sąsiadujące. Dwie identyczne wersje z inną pomiędzy nimi to nie powtórka, tylko powrót: moduł został zmieniony i zmieniony z powrotem. Usunięcie tej późniejszej sprawiłoby, że modsx:backuplist i modsx:status wskazywałyby stan, w którym Twoja aplikacja nie jest — więc zostaje nietknięta:
| Wersje | Co się dzieje |
|---|---|
0001 A, 0002 A, 0003 A |
znikają 0001 i 0002, zostaje 0003 |
0001 A, 0002 B, 0003 A |
nic nie znika — 0003 to powrót, nie powtórka |
0001 A, 0002 A, 0003 B |
znika 0001, zostają 0002 i 0003 |
Z każdego ciągu zostaje najnowsza wersja, z tego samego powodu: to ona musi dalej znaczyć „najnowsza wersja".
O komentarz zawsze się pyta, nigdy nie zakłada. Treść przetrwa w wersji, z którą dana jest identyczna, ale notatka o momencie to nie treść — i tylko ten, kto ją napisał, wie, czy nadal ma znaczenie. Tekst jest pokazany wprost:
[Blog]: which of these commented duplicates should go?
◻ 0002 before the refactor
◻ 0006 shipped to production
Na początku nic nie jest zaznaczone. Przy --force, --json albo bez terminala, w którym dałoby się zapytać, wersje z komentarzem zostają, a odpowiedź to odnotowuje — --with-comments to sposób, w jaki skrypt mówi, że już zdecydował.
Zarchiwizowane migracje liczą się tu do bycia identycznym, inaczej niż w sprawdzeniu pomijającym niezmieniony backup. Tamto pyta, czy przywracanie cokolwiek zrobi; to dotyczy kasowania, a wersja trzymająca jedyną kopię migracji nie może wyglądać na zbędną.
Wersje wskazywane przez snapshot albo przez wskaźnik stanu nie są usuwane nigdy, tak samo jak przy --keep.
Snapshoty a przycinanie
modsx:prune nie skasuje wersji wymienionej w snapshocie — tak jak tag nie pozwala zebrać commita. Bez tego rollback dowiadywałby się o stracie dopiero w chwili, w której ta wersja była mu potrzebna, czyli o wiele za późno. Mówi przy tym wprost, co zostawił i dlaczego:
Shop 0001 ....... kept, held by a snapshot
Nie ma flagi, która by to obeszła. --force w całym pakiecie znaczy nie pytaj mnie, a pozwolenie, by znaczyło również zignoruj zabezpieczenie, sprawiłoby, że skryptowy prune po cichu unieważniałby każdy snapshot wymieniający te wersje. Żeby je zwolnić, trzeba puścić snapshot przez modsx:snapshotprune — wtedy następny prune znowu je uwzględni.
Snapshot nadal może wskazywać wersję, której już nie ma, jeśli ktoś grzebał w drzewie backupów ręcznie. Zgłasza to modsx:doctor, a sprząta modsx:snapshotprune.
Przykład od początku do końca
Trzy moduły, gdzie Blog korzysta z obu pozostałych:
php artisan modsx:deps
INFO Blog
Media ......................... found in the code
User .......................... found in the code
INFO Media
needs nothing else
INFO User
needs nothing else
Snapshot przed zabraniem się za coś ryzykownego:
php artisan modsx:snapshot --comment="przed przepisaniem płatności"
Module Version
Blog 0001 backed up
Media 0001 backed up
User 0001 backed up
INFO Snapshot 0001 taken, holding 3 module(s).
Trzy tygodnie później Blog i User poszły dalej. Kolejny snapshot:
php artisan modsx:snapshot
Module Version
Blog 0004 backed up
Media 0001 unchanged
User 0002 backed up
INFO Snapshot 0002 taken, holding 3 module(s).
Media przez te trzy tygodnie nie była ruszana, więc nie powstała dla niej nowa wersja — tylko kolejne odwołanie do 0001. To właśnie sprawia, że snapshot jest na tyle tani, by robić go przed każdą ryzykowną rzeczą.
Teraz przepisywanie trzeba porzucić. Przywrócenie samego Bloga nie załatwi sprawy:
php artisan modsx:restore Blog 0001 # Blog 0001 obok User 0002, którego nigdy nie znał
php artisan modsx:rollback 0001 # cała chwila, taka jaka była
Module Current Snapshot
Blog 0004 0001 will move
Media 0001 0001 already there
User 0002 0001 will move
WARN Anything not in the snapshot is replaced by what was. Files added since are moved aside and not put back.
INFO Rolled back to snapshot 0001. 3 module(s) restored.
The state before this is snapshot ......................................... 0003
Snapshot 0003 to droga powrotna, wykonana automatycznie, zanim cokolwiek się ruszyło. Zmiana zdania po raz kolejny:
php artisan modsx:rollback 0003
Jak sprawdzić, gdzie się jest w dowolnym momencie:
php artisan modsx:status
php artisan modsx:snapshotlist
Snapshot Created Scope Modules Comment
0001 2026-09-03T09:14:02+00:00 whole project 3 przed przepisaniem płatności
0002 2026-09-24T16:30:55+00:00 whole project 3 -
0003 2026-09-24T16:41:18+00:00 whole project 3 before rolling back to snapshot 0001
A gdy drzewo backupów urośnie — najpierw puść stare chwile, dopiero potem przycinaj wersje:
php artisan modsx:snapshotprune --keep=5 --dry-run # zobacz, które odejdą
php artisan modsx:snapshotprune --keep=5 # puść je
php artisan modsx:prune --keep=3 # teraz te wersje są wolne
| Czego chcesz | Komenda |
|---|---|
| Tylko ten moduł z powrotem | modsx:restore Blog 0004 |
| Ten moduł i to, czego wtedy potrzebował | najpierw modsx:snapshot Blog, potem modsx:rollback |
| Cały projekt taki, jaki był | modsx:rollback 0001 |
| Najpierw zobaczyć, co się ruszy | modsx:rollback 0001 i przeczytać tabelę przed odpowiedzią |
| Wiedzieć, co obejmie snapshot | modsx:deps Blog |
modsx:diff
php artisan modsx:diff Blog # względem najnowszej wersji
php artisan modsx:diff Blog 0003 # względem konkretnej wersji
php artisan modsx:diff # interaktywnie
php artisan modsx:diff Blog --json
Porównuje moduł w aplikacji z wersją w backupie plik po pliku, po skrócie zawartości:
- Dodane — są teraz w aplikacji, nie było ich w tamtej wersji. Przywrócenie je usunie.
- Zmienione — są po obu stronach, ale zawartość się różni. Przywrócenie je nadpisze.
- Usunięte — były w tamtej wersji, nie ma ich w aplikacji. Przywrócenie je przywróci.
- Bez zmian — identyczne po obu stronach.
Porównywana jest zawartość plików, a nie nazwy katalogów, więc moduł, w którym przepisano wszystkie pliki w miejscu, zostanie pokazany jako zmieniony, a nie jako niezmieniony.
php artisan modsx:diff Blog --summary # same liczby, bez listy plików
Dwie wersje względem siebie
Podaj drugą wersję, a aplikacja całkowicie wypada z porównania — porównywane są obie wersje ze sobą:
php artisan modsx:diff Blog 0002 0004
Pierwsza wersja jest punktem odniesienia, druga tym, z czym ją porównujemy — dokładnie tak, jak się to czyta na głos: co stało się z Blogiem między 0002 a 0004. Trzy grupy zachowują nazwy i zmieniają znaczenie:
- Dodane — tylko w
0004; pojawiły się po0002. - Zmienione — są w obu wersjach, ale zawartość się różni.
- Usunięte — tylko w
0002; do0004już ich nie ma.
Zamiana argumentów miejscami daje to samo porównanie widziane z drugiej strony: co było dodane, staje się tym, czego brakuje. W żadną stronę nie ma tu przywracania, więc nic nie jest opisane w jego kategoriach.
W tym trybie katalog roboczy nie jest w ogóle czytany, więc odpowiedź jest ta sama niezależnie od tego, w jakim stanie jest w tej chwili aplikacja.
--summary i --json działają również tutaj. W JSON-ie zamiast version pojawiają się from i to, więc skrypt rozpozna tryb po samym kształcie odpowiedzi.
Warto uruchomić przed modsx:restore — pokazuje dokładnie, co się za chwilę straci.
modsx:info
php artisan modsx:info Blog
php artisan modsx:info --json
Pokazuje:
- Stan bieżący: czy moduł istnieje w aplikacji, jego katalogi i pliki, jaki zajmuje rozmiar na dysku
- Historia backupów: liczbę wersji, łączny rozmiar backupów, tabelę każdej wersji z datą utworzenia, rozmiarem, liczbą zarchiwizowanych migracji i komentarzem (jeśli został podany przy backupie)
Przydatne do zrozumienia zużycia miejsca na dysku i podjęcia decyzji, czy czyścić stare wersje.
modsx:doctor
php artisan modsx:doctor
php artisan modsx:doctor --json # kod wyjścia 1, gdy znaleziono problemy — do CI
php artisan modsx:doctor --fix # usuwa puste katalogi modułu
Problemy (kod wyjścia 1):
- Nazwy modułów różniące się wyłącznie granicami słów, np.
UserprofileobokUserProfile. Obie nazwy są poprawne, więc nic innego tego nie wyłapie — a to prawie zawsze jeden moduł, który miał być jednym. - Drzewa backupów różniące się wyłącznie wielkością liter. Na Windows i macOS to jeden katalog, więc dwa moduły dzielą sekwencję wersji, a przywracanie może zwrócić nie ten.
- Wersje backupu bez czytelnego
modsx.json.
Informacyjnie (kod wyjścia 0):
- Migracje, które nazywają moduł, ale nie są z nim archiwizowane — klasyczne
create_modsx_blog_posts_table— wraz z nazwą, której potrzebują zamiast tego. Bez tego konwencja po prostu po cichu nic by nie robiła, a Ty nigdy byś się nie dowiedział dlaczego. - Backupy zrobione, gdy skonfigurowany był inny prefiks.
- Katalogi w drzewie backupów, które nie są wersjami i przez to są pomijane przy listowaniu.
- Moduły istniejące tylko w jednej z dwóch form katalogów.
- Backupy bez odpowiadającego im modułu w aplikacji.
- Puste katalogi modułu — pozostawione przez
modsx:scaffoldalbo przez ręczne usunięcie ostatniego pliku.--fixje usuwa. Sprawdzanie uwzględnia też pliki ukryte, więc katalog trzymany celowo dzięki.gitkeepnigdy nie jest ruszany — zgłaszany jest tylko katalog, w którym naprawdę nic nie ma, na żadnej głębokości. - Pliki nazywające nieistniejący moduł, np.
config/modsx-blog-admin.php, gdy nie ma modułuBlogAdmin. Plik nadal działa w aplikacji — po prostu do niczego nie należy i z niczym nie jest backupowany, co warto wiedzieć. - Nazwa jednego modułu będąca przedłużeniem drugiej, np.
BlogPostobokBlog. Układ wspierany, wypisywany po to, żeby reguła dla ich migracji była gdzieś powiedziana wprost: wygrywa dłuższa nazwa. - Moduł zapisany jako pochodzący z wersji, której już nie ma, po jej przycięciu. To nie usterka: zapis nadal zgodnie z prawdą mówi, skąd wzięło się drzewo robocze, a
modsx:statusdziała dalej, mierząc względem najnowszej wersji. Skasowanie pliku w pełni to naprawia. - Snapshot wskazujący wersję, której już nie ma — do czego doprowadzić może wyłącznie ręczna zmiana w drzewie backupów, bo
modsx:prunetakie wersje wstrzymuje. Snapshot nadal jest na liście i nadal wygląda na sprawny, podczas gdy jedyna rzecz, do której istnieje — cofnięcie się do niego — jest już niemożliwa. Sprząta tomodsx:snapshotprune.
Konfiguracja
config/modsx.php:
return [
// Prefiks katalogów. 'modsx' dopasowuje modsx-blog i ModsxBlog.
'prefix' => env('MODSX_PREFIX', 'modsx'),
// Gdzie zapisywane są wersjonowane backupy.
'backup_path' => env('MODSX_BACKUP_PATH', base_path('modsx-backups')),
// Przeszukiwane są tylko te ścieżki. Krótka lista to właśnie to,
// co utrzymuje szybkość wykrywania: pełny skan katalogu głównego
// przechodziłby przez storage/, .git/ i public/build/.
'scan_paths' => [
'app', 'config', 'database', 'lang', 'public', 'resources', 'routes', 'tests',
],
// Nazwy katalogów, do których nigdy nie wchodzimy.
'exclude' => [
'vendor', 'node_modules', 'storage', 'bootstrap/cache', '.git', '.idea', '.vscode',
],
// Co tworzy modsx:scaffold, gdy sam nie wskażesz katalogów. Oba
// placeholdery pochodzą z jednej nazwy, którą wpisujesz — i to właśnie
// zapobiega rozjechaniu się obu form. Opublikowany plik niesie pod spodem
// dłuższą listę, zakomentowaną — Livewire, serwisy, form requesty,
// fabryki, seedery, testy, resources/css, resources/js, komponenty —
// więc odkomentowujesz to, co Twoje moduły faktycznie mają.
'scaffold' => [
'app/Http/Controllers/{Studly}',
'app/Models/{Studly}',
'resources/views/{kebab}',
],
// Co modsx:make przekazuje każdemu generatorowi. Wartość sama w sobie
// uruchamia generator o tej nazwie; para uruchamia generator, który
// wskażesz, z modułem umieszczonym w jednym z katalogów aplikacji.
'generators' => [
'*' => '{Studly}/',
'view' => '{kebab}/',
'config' => '{kebab}-',
'migration' => '{snake}_',
'layout' => ['view', 'layouts/{kebab}/'],
'page' => ['view', 'pages/{kebab}/'],
'partial' => ['view', 'partials/{kebab}/'],
],
// 4 daje 0001, 0002, ...
'version_padding' => 4,
// Domyślna wartość dla modsx:prune.
'prune' => ['keep' => 5],
// Dodatkowe zależności, dla tego, czego czytanie plików modułu nie zobaczy.
// Dokłada krawędzie do grafu wyprowadzanego przez modsx:deps, nigdy ich nie zastępuje.
'dependencies' => [
// 'Blog' => ['Search'],
],
];
Dwie uwagi:
- Jeśli zmienisz
prefixpo utworzeniu modułów, zmień nazwy istniejących katalogów. Pod starym prefiksem nic nie zostanie znalezione. - Katalog backupu nigdy nie jest przeszukiwany w poszukiwaniu modułów, gdziekolwiek go ustawisz — także wtedy, gdy leży wewnątrz przeszukiwanej ścieżki.
Ograniczenia
Świadome, ale warto je znać, zanim na tym polegniesz:
- Bez bazy danych, a więc i bez przywracania migracji. Przywrócenie starszej wersji nie cofa migracji ani nie rusza danych. Pliki migracji są archiwizowane w każdym backupie, żeby dało się odczytać, jak schemat wyglądał wcześniej, ale nigdy nie są przywracane ani usuwane — odłożenie starego pliku przy schemacie, który poszedł do przodu, zostawiłoby repozytorium i bazę w niezgodzie, bez żadnego sygnału. To decyzja, a nie luka do wypełnienia w przyszłości.
- Migracja pasująca do dwóch modułów trafia do dłuższej nazwy.
BlogiBlogPostwspółistnieją bez problemu — pliki nazywają po jednym module — alemodsx_blog_post_create_comments_tablepasuje do obu i wygrywa dłuższa nazwa. Dla migracji BlogPosta to poprawne; jeśli Blog kiedyś potrzebuje migracji, której nazwa zaczyna się jak nazwa BlogPosta, musi ją nazwać inaczej. To jedyna reguła, której nie odczytasz z samej nazwy pliku. - Bez rozwiązywania zależności. Modsx nie wie, że
BlogpotrzebujeUsers. Przywrócenie jednego nie przywróci drugiego. - Bez integracji z Composerem. Zewnętrzne pakiety, od których zależy moduł, pozostają problemem Twojego
composer.json. - Backupy to zwykłe kopie katalogów. Bez kompresji, bez deduplikacji. Duży moduł zbackupowany pięćdziesiąt razy zajmuje pięćdziesiąt kopii — stąd
modsx:prunei pomijanie niezmienionych modułów. - Przywracanie jest odwracalne, ale nie atomowe. Bieżący stan jest odsuwany w całości, zanim wejdzie przywracany, więc awaria w połowie jest automatycznie cofana. Maszyna, która padnie dokładnie w złym momencie, wciąż może zostawić moduł w kawałkach — ale wszystko, co miał, leży w jednym miejscu, a backup sprzed przywracania nadal tam jest.
FAQ
Czy pakiet jest potrzebny, żeby używać konwencji?
Nie. O to właśnie chodzi. Prefiksujesz katalogi i wszystko działa. Pakiet instalujesz wtedy, gdy chcesz backupy. modsx:scaffold i modsx:make to udogodnienia dla tych, którzy już go mają, a nie wymóg — tworzą katalogi i nazwy, które równie dobrze możesz wpisać ręcznie.
Dlaczego moje migracje nie są archiwizowane?
Prawie na pewno przez nazwę. Konwencja mówi, że nazwa po timestampie zaczyna się od prefiksu modułu: 2026_01_01_000000_modsx_blog_create_posts_table.php, a nie typowe „czasownik z przodu" ..._create_modsx_blog_posts_table.php. Uruchom php artisan modsx:doctor — znajduje migracje, które wspominają moduł, ale nie są od niego nazwane, i podpowiada, na co je przemianować. php artisan modsx:make migration Blog/create_posts_table zapisuje nazwę poprawnie od razu.
Co się stanie z modułami, jeśli go odinstaluję?
Nic. To zwykłe katalogi Laravela i nigdy nie były niczym innym. Bez opieki zostaje tylko modsx-backups/ — a to zwykłe pliki, które możesz zachować albo usunąć.
Czy gryzie się z nwidart/laravel-modules?
Oba rozwiązują ten sam problem w niekompatybilny sposób, więc używanie obu naraz to zły pomysł. Na dysku się jednak nie pobiją: domyślny katalog backupu to modsx-backups/ właśnie po to, żeby ominąć drzewo źródeł Modules/ tamtego pakietu.
Czy mogę przenieść moduł do innego projektu?
Tak, na dwa sposoby. Skopiuj modsx-backups/Blog/ do katalogu backupów w projekcie docelowym i uruchom php artisan modsx:restore Blog. Albo, żeby przenieść jeden plik zamiast drzewa katalogów: zrób modsx:export, skopiuj .zip, zrób tam modsx:import, potem przywróć. Przestrzenie nazw przeżywają, bo przeżywa układ katalogów.
Dlaczego numerowane wersje, a nie znaczniki czasu? Są krótkie, poprawnie się sortują i łatwo je wybrać w promptcie. Czas utworzenia jest w manifeście.
Czy dwa moduły mogą dzielić katalog? Nie. Katalog należy do dokładnie jednego modułu — tego, którego nazwę koduje.
Czy można to uruchamiać na produkcji?
To narzędzia deweloperskie. Pytają przed zniszczeniem czegokolwiek i odmawiają działania nieinteraktywnie bez --force, ale pipeline wdrożeniowy to nie jest miejsce, w którym katalogi modułów powinny się przemieszczać.
Plany
Nic w kolejce. Przywracanie migracji jest świadomie nieobecne, a nie odłożone na później — patrz Ograniczenia.
Współpraca
Zgłoszenia i pull requesty mile widziane — patrz CONTRIBUTING.md.
composer install
composer test
composer lint
composer analyse
# Uruchomienie komendy ręcznie w aplikacji Testbench
composer smoke -- modsx:doctor
composer smoke przed uruchomieniem przebudowuje wykrywanie pakietów. Zestaw testów rejestruje service providera samodzielnie, więc po jego uruchomieniu zostaje manifest pakietów, w którym Modsx nie występuje — a wtedy vendor/bin/testbench w ogóle nie widzi komend.
Licencja
MIT. Patrz LICENSE.