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->projectMemberRepository->updateRole(...) [data access]
- $this->roleService->syncSystemRoleForMember(...) [another Service]
- $this->activityLogService->log(...) [side effect]
│
▼
ProjectMemberRepository::updateRole()
- $project->users()->updateExistingPivot($userId, ['role' => $role->value])
│
▼ (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()/hasPermissionOrTier() — zobacz sekcję architektury w ../permissions/README.md 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órhasPermissionOrTier()/hasPermission(), jeśli zasób jest scope'owany do projektu (zobacz../permissions/01-add-a-new-permission.md). - 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.md).
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.
