Architektura systemu¶
Domain-Driven Design — trójwarstwowa architektura z 31 domenami biznesowymi.
Przegląd architektury¶
System Zajmij.to został zbudowany zgodnie z zasadami Domain-Driven Design (DDD). Kod jest podzielony na trzy główne warstwy:
flowchart TB
subgraph app[Aplikacja]
D[Domain<br/>Logika biznesowa]
I[Infrastructure<br/>Frameworki i zewnętrzne]
S[Support<br/>Współdzielone narzędzia]
end
D --> S
I --> S
D -.-> I Trzy warstwy¶
1. Domain (Domena)¶
Lokalizacja: app/Domain/
Rdzeń aplikacji — logika biznesowa zorganizowana w 31 kontekstów domenowych.
| Element | Opis |
|---|---|
Model/ | Modele Eloquent |
Controller/ | Kontrolery HTTP |
Action/ | Klasy akcji (Single-Action) |
Service/ | Traity z logiką biznesową |
Request/ | Walidacja żądań |
Resource/ | Transformacja odpowiedzi API |
Observer/ | Obserwatorzy modeli |
Event/ | Zdarzenia domenowe |
Job/ | Zadania asynchroniczne |
Notification/ | Powiadomienia |
Command/ | Komendy Artisan |
2. Infrastructure (Infrastruktura)¶
Lokalizacja: app/Infrastructure/
Warstwa techniczna — framework, middleware, zewnętrzne integracje.
| Element | Opis |
|---|---|
Middleware/ | Middleware HTTP |
Providers/ | Service Providers |
Exceptions/ | Obsługa wyjątków |
Commands/ | Komendy infrastrukturalne |
Vendor/ | Integracje zewnętrzne |
3. Support (Wsparcie)¶
Lokalizacja: app/Support/
Współdzielone narzędzia używane przez wszystkie domeny.
| Element | Opis |
|---|---|
Service/TraitSupport.php | 50+ metod pomocniczych |
Action/ | Akcje współdzielone |
Model/ | Bazowe klasy modeli |
Job/ | Współdzielone joby |
Stub/ | Szablony generatorów |
Wzorce architektoniczne¶
Single-Action Controllers¶
Każda akcja kontrolera to osobna klasa:
Domain/{Domain}/Action/Controller/
├── IndexOrganizationController.php
├── ShowOrganizationController.php
├── StoreOrganizationController.php
├── UpdateOrganizationController.php
└── DeleteOrganizationController.php
Zalety: - Łatwiejsze testowanie - Jasna odpowiedzialność - Mniejsze pliki
Trait-Based Services¶
Logika biznesowa w traitach:
class OrganizationController extends Controller
{
use TraitSupport;
use TraitControllerOrganization;
public function index(Request $request)
{
return $this->indexOrganizationController($validated);
}
}
Traity: - TraitSupport — bazowe metody dla wszystkich - Trait{Domain} — logika domenowa - TraitController{Domain} — orkiestracja kontrolera
Observer Pattern¶
Obserwatorzy modeli w Action/Other/:
Domain/Reservation/Action/Other/
├── CreatingReservationObserver.php
├── UpdatingReservationObserver.php
└── DeletingReservationObserver.php
Middleware Pipeline¶
Każde żądanie API przechodzi przez pipeline:
flowchart LR
A[Request] --> B[check_versions]
B --> C[global_database_transaction]
C --> D[auth:api]
D --> E[app_mode]
E --> F[to_user_access]
F --> G[user_accepted_agreements]
G --> H[Controller] | Middleware | Funkcja |
|---|---|
check_versions | Weryfikacja wersji API/klienta |
global_database_transaction | Automatyczna transakcja DB |
auth:api | Uwierzytelnianie JWT |
app_mode | Kontrola trybu aplikacji |
to_user_access | Kontrola dostępu użytkownika |
user_accepted_agreements | Weryfikacja zgód |
Global Database Transaction¶
Wszystkie żądania API w transakcji:
| Status odpowiedzi | Akcja |
|---|---|
| 200, 201, 422 | Auto-commit |
| Inne | Auto-rollback |
Routing¶
Struktura plików¶
routes/
├── web.php # Fallback — redirect na frontend
├── api.php # Główny plik z middleware
├── auth.php # Include authenticated routes
├── noAuth.php # Include public routes
└── api/
├── auth/ # Chronione endpointy
│ ├── reservation.php
│ ├── organization.php
│ └── ...
└── noAuth/ # Publiczne endpointy
├── reservation.php
└── ...
Web routes
Aplikacja jest API-only. Wszystkie web routes (w tym /) przekierowują na FRONTEND_URL. Endpoint /up został usunięty — Docker health check korzysta z /api/no-auth/current-time.
Konwencje¶
| Wzorzec | Przykład |
|---|---|
| Lista | GET /reservations |
| Szczegóły | GET /reservations/{id} |
| Tworzenie | POST /reservations |
| Aktualizacja | PATCH /reservations/{id} |
| Usunięcie | DELETE /reservations/{id} |
| Akcja | PATCH /reservations/{id}/change-state |
Kolejki i asynchroniczność¶
8 dedykowanych kolejek¶
| Kolejka | Przeznaczenie |
|---|---|
default | Ogólne zadania |
events | Zdarzenia systemowe |
errors | Logowanie błędów |
notifications | Powiadomienia email |
domain | Operacje domenowe |
media-library | Przetwarzanie mediów |
legal-documents | Dokumenty prawne |
reports | Generowanie raportów |
Job Callback¶
Wszystkie joby w JobCallback:
flowchart TD
A[Job::handle] --> B[JobCallback]
B --> C[Begin Transaction]
C --> D{Sukces?}
D -->|Tak| E[Commit]
D -->|Nie| F[Rollback]
F --> G[Log Error]
G --> H[Send Alert] Baza danych¶
Konwencje¶
| Element | Konwencja |
|---|---|
| Klucze główne | UUID (string) |
| Nazwy tabel | snake_case, liczba mnoga |
| Klucze obce | {model}_id |
| Timestamps | created_at, updated_at |
| Soft delete | deleted_at |
Relacje polimorficzne¶
Wiele modeli używa polimorfizmu:
Strategia indeksowania¶
System stosuje kompleksową strategię indeksowania bazy danych dla optymalnej wydajności przy dużym ruchu.
Automatyczne indeksy:
- Klucze główne (UUID) — zawsze indeksowane
- Klucze obce (
->foreign()) — MySQL/InnoDB automatycznie tworzy indeks - Kolumny polimorficzne (
uuidMorphs) — Laravel tworzy indeks kompozytowy[type, id] - Kolumny
->unique()— unique constraint = indeks
Ręczne indeksy na kolumnach filtrowanych:
| Typ kolumny | Przykłady | Powód |
|---|---|---|
| Flagi boolean | is_active, is_public, is_canceled | Częste filtrowanie w WHERE |
| Statusy | status, payment_status, priority | Filtrowanie i grupowanie |
| Daty | created_at, start_at, end_at | ORDER BY i zapytania zakresowe |
| Soft delete | deleted_at | Global scope dodaje WHERE do każdego zapytania |
| Wyszukiwanie | email, phone, name | Wyszukiwanie użytkowników/klientów |
Indeksy kompozytowe (wielokolumnowe):
| Tabela | Kolumny | Zastosowanie |
|---|---|---|
reservations | [organization_id, status, created_at] | Listy rezerwacji z filtrowaniem |
schedules | [item_id, start_at, end_at] | Sprawdzanie kolizji terminów |
activity_items | [activity_id, date] | Wyświetlanie harmonogramu zajęć |
complaints | [organization_id, status] | Panel reklamacji admina |
Zasada indeksowania
Indeksy dodajemy na kolumnach używanych w WHERE, ORDER BY i JOIN. Nie indeksujemy kolumn typu text, json ani kolumn rzadko filtrowanych.
Generowanie kodu¶
Tworzenie nowej domeny¶
Generuje pełną strukturę: - Model, Controller, Request, Resource - Akcje (Controller/, Attr/, Other/) - Traity serwisowe - Plik routingu
Diagramy domen¶
Domeny podstawowe¶
flowchart LR
R[Reservation] --> I[Item]
R --> A[ActivityItem]
A --> AC[Activity]
I --> O[Organization]
AC --> O
R --> C[Client]
R --> P[Pricing] Domeny użytkowników¶
flowchart LR
U[User] --> RO[Role]
U --> O[Organization]
O --> E[Employee]
I[Item] --> M[Manager]
T[Trainer] --> AI[ActivityItem] Domeny komunikacji¶
flowchart LR
R[Reservation] --> N[Notification]
R --> RE[Review]
R --> CO[Complaint]
CO --> TH[Thread/Messages] System autoryzacji (Policies)¶
System implementuje dwuwarstwową autoryzację:
Warstwa 1: Role Middleware¶
Gruboziarnista kontrola — sprawdza czy użytkownik ma odpowiednią rolę:
Warstwa 2: Laravel Policies¶
Granularna autoryzacja per zasób — sprawdza czy użytkownik ma dostęp do konkretnego obiektu:
BasePolicy¶
Wszystkie policies dziedziczą z Support\Policy\BasePolicy:
- Admin bypass — admin ma pełny dostęp (metoda
before()) - Organization membership —
belongsToOrganization()sprawdza przynależność do organizacji - Item management —
isItemManager()sprawdza zarządzanie zasobem
Kategorie policies¶
| Kategoria | Logika | Przykłady |
|---|---|---|
| A — z organization_id | Bezpośredni dostęp | Organization, Item, Activity, Reservation, Complaint, Review, Schedule |
| B — pośredni dostęp | Przez relację do org | ItemBlock, ItemManager, ActivityItem, OrganizationEmployee, ComplaintMessage |
| C — polimorficzne | Przez morphTo → org | Pricing, Media, Note, StatusLog, Document |
| D — gmina-wide | Bez org_id, wszystkie role | Client, Trainer, Category |
Pole can w API¶
Każda odpowiedź API zawiera pole can z uprawnieniami frontendu:
Endpoint /api/me/abilities¶
Zwraca globalne uprawnienia użytkownika:
{
"create_organization": false,
"create_item": true,
"create_activity": true,
"manage_users": false,
"view_reports": true
}
flowchart TD
A[Request] --> B{Role Middleware}
B -->|Brak roli| C[403 Forbidden]
B -->|Ma rolę| D{Policy authorize}
D -->|Brak dostępu do zasobu| C
D -->|Ma dostęp| E[Kontroler]
E --> F[Response + pole can] Global Scopes — filtrowanie danych (READ)¶
System stosuje dwa globalne scope'y rejestrowane w Infrastructure\Vendor\CustomModel:
UserAccessesGlobalScope¶
Plik: Support\Action\Scope\UserAccessesGlobalScope
Automatycznie filtruje dane READ (zapytania SELECT) na poziomie Eloquent. Każdy model dziedziczący z CustomModel przechodzi przez ten scope.
flowchart TD
A[Query] --> B{Auth?}
B -->|Nie| C[Brak filtrowania]
B -->|Tak| D{Admin?}
D -->|Tak| C
D -->|Nie| E{Typ modelu}
E -->|User-owned| F[where user_id]
E -->|Organization| G[whereIn organization_id]
E -->|Przez relację| H[whereHas parent → org]
E -->|Polimorficzny| I[whereHasMorph → org]
E -->|Kolumna organization_id| G
E -->|Kolumna reservation_id| J[whereHas reservation → org]
E -->|Default| C Strategie filtrowania:
| Strategia | Modele | Mechanizm |
|---|---|---|
| User-owned | UserRefreshToken, DatabaseNotification | where('user_id/notifiable_id', $userId) |
| Organization | Organization | whereIn('id', $orgIds) |
| Przez relację | ActivityItem, ItemBlock, ItemManager, ComplaintMessage | whereHas('parent', → org) |
| Polimorficzny | Pricing, Media, StatusLog, Note, Document | whereHasMorph / whereHas('priceable/model') |
| 2-level join | ReservationPaymentReceipt, ReservationPaymentRefund | whereHas('payment.reservation', → org) |
| Auto-detect organization_id | Item, Activity, Reservation, Complaint, Review, Schedule, OrganizationEmployee, WaitingReservation, ReservationPaymentLog | Kolumna organization_id w tabeli |
| Auto-detect reservation_id | ReservationPayment, ReservationSlot, ReservationPaidLog, ReservationPaymentReminderLog | Kolumna reservation_id → reservation.organization_id |
| Brak filtrowania | Client, Trainer, Category, User, Configuration, AppMode, Command, Error, Job, Report, Role, TERYT, pivoty | Default — dane systemowe lub gmina-wide |
Wyjątki od CustomModel:
Media— rozszerza Spatie Media, rejestruje scope bezpośrednio wbooted()ActivityLog— rozszerza Spatie Activity, NIE rejestruje scope (admin-only via middleware)User— rozszerza Laravel Authenticatable, NIE rejestruje scope (filtrowany przez ToUserAccessMiddleware)
IsActiveGlobalScope¶
Plik: Support\Action\Scope\IsActiveGlobalScope
Filtruje niezalogowanych użytkowników — ukrywa nieaktywne/niepubliczne/anulowane rekordy.
Best practices¶
Wskazówki dla programistów
- Czytaj przed edycją — zawsze przeczytaj plik przed modyfikacją
- Single-Action — nowa akcja = nowa klasa
- Traity — logika biznesowa w traitach, nie kontrolerach
- Obserwatory — efekty uboczne w obserwatorach
- Transakcje — middleware obsługuje automatycznie
- Kolejki — ciężkie operacje do jobów
- Cache — używaj dla rzadko zmieniających się danych