Przejdź do treści

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:

reservations
├── reservationable_type  # Item lub ActivityItem
└── reservationable_id    # UUID obiektu

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

php artisan support:make-domain --domainName=Product

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ę:

$this->middleware('role:admin|supervisor|employee', ['only' => ['store', 'update']]);

Warstwa 2: Laravel Policies

Granularna autoryzacja per zasób — sprawdza czy użytkownik ma dostęp do konkretnego obiektu:

$this->authorize('update', Item::find($itemId));

BasePolicy

Wszystkie policies dziedziczą z Support\Policy\BasePolicy:

  • Admin bypass — admin ma pełny dostęp (metoda before())
  • Organization membershipbelongsToOrganization() sprawdza przynależność do organizacji
  • Item managementisItemManager() 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:

{
    "id": "uuid",
    "name": "Sala konferencyjna",
    "can": {
        "update": true,
        "changeState": false
    }
}

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_idreservation.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 w booted()
  • 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

  1. Czytaj przed edycją — zawsze przeczytaj plik przed modyfikacją
  2. Single-Action — nowa akcja = nowa klasa
  3. Traity — logika biznesowa w traitach, nie kontrolerach
  4. Obserwatory — efekty uboczne w obserwatorach
  5. Transakcje — middleware obsługuje automatycznie
  6. Kolejki — ciężkie operacje do jobów
  7. Cache — używaj dla rzadko zmieniających się danych