Architektura i technikalia projektu
View on GitHubWarstwowa architektura backendu
Controller → Service → Repository, bez wyjątków i bez skrótów — kontroler nigdy nie buduje zapytania Eloquent, a repozytorium nigdy nie zawiera logiki biznesowej ani nie wystrzeliwuje eventu. Prześledzone poniżej przez jedno prawdziwe, kompletne żądanie.
Zasada dla każdej warstwy
- Controller (
app/Http/Controllers/) — waliduje żądanie (inline$request->validate([...])albo klasa Form Request), wywołuje dokładnie jedną metodę Service, żeby wykonać faktyczną pracę, i zwracaredirect()(trasy mutujące) albo odpowiedźInertia::render(...)(trasy stron). Nigdy nie dotyka Eloquent bezpośrednio, nigdy nie zawiera warunku reprezentującego regułę biznesową. - Service (
app/Services/) — posiada logikę biznesową i każdy efekt uboczny: zapis wpisu doActivityLog, wystrzelenie domenowegoEvent, wymuszenie niezmiennika przezValidationException. Wywołuje jedno lub więcej Repozytoriów po faktyczny dostęp do danych, i może wywoływać inne Serwisy (np.ProjectMemberServicewywołujeRoleService). Nigdy nie zwraca odpowiedzi HTTP, nigdy nie czyta$requestbezpośrednio. - Repository (
app/Repositories/) — każde zapytanie Eloquent w aplikacji żyje tutaj:where(), eager-loading, sortowanie, paginacja, agregacja. Metoda repozytorium robi dokładnie to, co mówi jej nazwa, i nic więcej — żadnych reguł biznesowych, żadnych eventów, żadnej walidacji.
Prześledzone przez prawdziwe żądanie
PATCH /projects/{project}/members/{user} — awansowanie członka — od początku do końca:
ProjectMemberController::updateRole()
- $this->authorize('updateMemberRole', $project) [Policy check]
- $request->validate([...]) [shape check]
- $this->projectMemberService->updateRole($project, $user, $role)
│
▼
ProjectMemberService::updateRole()
- assertIsMember() / assertNotOwner() [business rules]
- $this->roleService->syncSystemRoleForMember(...) [another Service]
- $this->activityLogService->log(...) [side effect]
│
▼
RoleService::syncSystemRoleForMember()
- ensureSystemRoles($project) [find/create tier roles]
- $this->projectMemberRepository->syncSystemRole(...) [data access]
│
▼
ProjectMemberRepository::syncSystemRole()
- swaps the member's system role in the project_user_role pivot
│
▼ (back up through Service, back up through Controller)
redirect()->back()->with('success', "...")
Każda warstwa zna tylko warstwę bezpośrednio poniżej siebie — Controller nigdy nie wywołuje Repository, a Repository nigdy nie wie, że istnieje Policy.
Autoryzacja żyje poza wszystkimi trzema warstwami
app/Policies/ to czwarty, przekrojowy temat, nie warstwa w powyższym łańcuchu — Policy jest wywoływana tylko z Controllera ($this->authorize(...)) albo, do obliczenia zwykłego propa boolean na potrzeby UI zamiast zabezpieczania mutującego żądania, bezpośrednio z metody Modelu (Project::hasPermission() — zobacz sekcję architektury w ../permissions/README.mdRole i uprawnienia (RBAC)Każdy projekt ma własną kontrolę dostępu opartą na rolach: cztery wbudowane poziomy roli systemowej (Owner, Admin, Member, Viewer) oraz dowolną liczbę proj… po dokładnie to, kiedy używać której).
Dodawanie zupełnie nowej domeny w tym samym wzorcu
Jeśli dodajesz zupełnie nowy zasób (nie rozszerzasz istniejącego — zobacz każdą inną kategorię w documentation/ po to), checklista, na przykładzie Comment jako wzorca (mniejszy, kompletny przykład niż Issue):
- Migracja + Model (
app/Models/) — pola fillable, casty, relacje (belongsTo,hasMany). - Repository (
app/Repositories/) — jedna metoda na kształt zapytania, jakiego funkcja faktycznie potrzebuje (getForIssue(), nie generycznyfindBy(array $criteria)— zobacz każde istniejące repozytorium po tę konwencję: wąskie, nazwane po celu metody zamiast generycznego wrappera na query builder). - Service (
app/Services/) — jedna publiczna metoda na przypadek użycia (addComment(),updateComment(),deleteComment()), każda wywołująca Repository i wystrzeliwująca jakikolwiekEvent/wpisActivityLog, jaki implikuje ten przypadek użycia. - Policy (
app/Policies/) — jedna metoda na zdolność, na wzórhasPermission(), jeśli zasób jest scope'owany do projektu (zobacz../permissions/01-add-a-new-permission.mdRole i uprawnienia (RBAC)Dodaj nowe uprawnienieOgólny, krok-po-kroku wzorzec dodawania dowolnego nowego uprawnienia projects./issues./comments. i sprawienia, żeby poprawnie pojawiło się w Settings → Rol…). - Controller (
app/Http/Controllers/) — jedna akcja na trasę, każda zabezpieczona$this->authorize(...), delegująca do dokładnie jednego wywołania Service. - Trasy (
routes/web.php) — nazwane, zgrupowane z powiązanymi trasami zasobu. - Typy i komponenty frontendu — typ w
resources/js/types/odzwierciedlający kształt modelu, potem jakiekolwiek Atoms/Molecules/Organisms, jakich potrzebuje UI funkcji (zobacz03-frontend-architecture-and-atomic-design.mdArchitektura i technikalia projektuArchitektura frontendu i atomic designBrowser requests /projects/5 │ ▼ routes/web.php → ProjectController::show() - Inertia::render('Projects/Show', [ ...props ]) │ ▼ Inertia (server) serialize…).
Testy
Każda warstwa dostaje własny plik testów, na wzór prawdziwych: tests/Feature/<Domain>ServiceTest.php, tests/Feature/<Domain>RepositoryTest.php, tests/Feature/<Domain>ControllerTest.php, a jeśli domena ma ciekawe zachowanie na poziomie modelu (cast, obliczony atrybut) — tests/Feature/Models/<Domain>Test.php. Zobacz tests/Feature/CommentServiceTest.php/CommentRepositoryTest.php/CommentControllerTest.php po najmniejszy kompletny prawdziwy przykład tego czteroplikowego kształtu.
