Skip to main content

Encja

1. Czym jest encja?

Encja to podstawowy element w systemie WiseB2B, który reprezentuje obiekt biznesowy w bazie danych. Można ją porównać do "szablonu" lub "modelu", według którego przechowywane są dane w tabelach bazy danych.

Dzięki temu podejściu system zachowuje spójność i umożliwia łatwiejsze utrzymanie oraz rozwój aplikacji, zgodnie z zasadami Domain-Driven Design (DDD) i Clean Architecture.

2. Struktura encji w systemie

System jest zaprojektowany zgodnie z zasadami architektury modułowej. Każda encja znajduje się w odpowiednim module, w katalogu Domain. Przykładowa struktura modułu Client wygląda następująco:

├── Domain/                          # Warstwa domeny
│ ├── EncjaPrzykładowa/ # Katalog reprezentujący encje
│ │ ├── Event/ # Eventy związane z encją (podstawowe: HasCreated, HasChanged, BeforeRemove, AfterRemove)
│ │ ├── Exception/ # Wyjątki encji
│ │ ├── Interfaces/ # Interfejsy serwisów domenowych, w tym Factory i Repository
│ │ ├── Listener/ # Listenery związane z encją
│ │ └── Service/ # Serwisy domenowe

Przykład dla modułu Client:

Wise/
└── Client/
├── Domain/
│ └── Client/
│ ├── Event/
│ │ ├── ClientAfterRemoveEvent.php # Event wywoływany po usunięciu encji
│ │ ├── ClientBeforeRemoveEvent.php # Event wywoływany przed usunięciem encji
│ │ ├── ClientHasChangedEvent.php # Event wywoływany po zmianie encji
│ │ └── ClientHasCreatedEvent.php # Event wywoływany po utworzeniu encji
│ ├── Exception/
│ ├── Factory/
│ │ └── ClientFactory.php # Fabryka encji
│ ├── Client.php # Klasa encji
│ └── ClientRepositoryInterface.php # Interfejs repozytorium
├── Repository/
│ └── Doctrine/
│ └── ClientRepository.php # Implementacja repozytorium wykorzystująca Doctrine
├── Resources/
│ └── entity/
│ └── Client/
│ └── Client.orm.yml # Konfiguracja mapowania encji na bazę danych
└── Service/
└── Client/
├── AddOrModifyClientService/ # Serwis do dodawania lub modyfikacji encji
├── AddClientService/ # Serwis do dodawania encji
├── ModifyClientService/ # Serwis do modyfikacji encji
├── ListClientService/ # Serwis do pobierania listy encji
├── GetClientDetailsService/ # Serwis do pobierania szczegółowych informacji o encji
└── RemoveClientService/ # Serwis do usuwania encji

Informacje z lekcji 1, dotyczące architektury systemu, pomagają zrozumieć podział na warstwy oraz rolę poszczególnych katalogów i modułów.

3. Tworzenie encji

3.1. Utworzenie klasy encji

Aby utworzyć nową encję, należy:

  1. Stworzyć plik klasy PHP w odpowiednim katalogu Wise/{moduł}/Domain/{encja}/{encja}.php (np. Wise/Client/Domain/Client/Client.php).
  2. Klasa ta MUSI dziedziczyć po Wise\Core\Entity\AbstractEntity, co zapewnia wspólny interfejs i podstawową funkcjonalność dla wszystkich encji.

Przykładowa struktura klasy:

<?php

declare(strict_types=1);

namespace Wise\Client\Domain\Client;

use Wise\Core\Entity\AbstractEntity;

class Client extends AbstractEntity
{
// Deklaracja właściwości encji (np. nazwa, email, status)
protected ?string $name = null;
protected ?string $email = null;
protected ?int $status = null;

// Gettery i settery umożliwiają odczyt i modyfikację właściwości
public function getName(): ?string
{
return $this->name;
}

public function setName(?string $name): self
{
$this->name = $name;
return $this;
}

// Pozostałe gettery i settery
}

3.2. Utworzenie repozytorium

Repozytorium to klasa odpowiedzialna za interakcję z bazą danych (operacje CRUD). Proces jego tworzenia obejmuje:

  1. Interfejs repozytorium – umieszczony w katalogu domeny (np. Wise/Client/Domain/Client/ClientRepositoryInterface.php), definiuje metody dostępu do danych:

    <?php

    declare(strict_types=1);

    namespace Wise\Client\Domain\Client;

    use Wise\Core\Repository\RepositoryInterface;

    interface ClientRepositoryInterface extends RepositoryInterface
    {
    // Dodatkowe metody specyficzne dla encji Client
    }
  2. Implementacja repozytorium – tworzona w katalogu Repository/Doctrine (np. Wise/Client/Repository/Doctrine/ClientRepository.php). Ta klasa dziedziczy po AbstractRepository i implementuje interfejs repozytorium. Ważnym elementem jest stała ENTITY_CLASS, która określa, dla jakiej encji repozytorium działa:

    <?php

    declare(strict_types=1);

    namespace Wise\Client\Repository\Doctrine;

    use Wise\Client\Domain\Client\Client;
    use Wise\Client\Domain\Client\ClientRepositoryInterface;
    use Wise\Core\Repository\AbstractRepository;

    class ClientRepository extends AbstractRepository implements ClientRepositoryInterface
    {
    protected const ENTITY_CLASS = Client::class;
    }

4. Konfiguracja mapowania encji

Aby Doctrine mogło poprawnie odwzorować encję na tabelę bazy danych, należy skonfigurować mapowanie w dwóch etapach:

4.1. Plik .orm.yml

Plik .orm.yml zawiera definicję mapowania encji. W tym pliku deklarujemy:

  • Nazwę klasy encji i typ mapowania (np. entity).
  • Nazwę tabeli w bazie danych.
  • Typ dziedziczenia (np. SINGLE_TABLE).
  • Repozytorium, które obsługuje tę encję.
  • Pola encji wraz z ich typami, długościami, oraz informacjami, czy mogą być puste (nullable).
  • Indeksy i unikalne klucze, które ułatwiają wyszukiwanie oraz zapewniają integralność danych.
  • Relacje z innymi encjami.

Przykład pliku Client.orm.yml:

Wise\Client\Domain\Client\Client:
type: entity
table: client
inheritanceType: SINGLE_TABLE
repositoryClass: Wise\Client\Repository\Doctrine\ClientRepository

# Indeksy - optymalizacja wyszukiwania danych
indexes:
id_external_idx:
columns: [id_external]

# Unikalne klucze - zapewnienie unikalności danych
uniqueConstraints:
client_id_external_unique_idx:
columns: id_external

# Deklaracja pól encji
fields:
name:
type: string
length: 255
nullable: true

email:
type: string
length: 60
nullable: true

status:
type: integer
nullable: true

# Definicja relacji z innymi encjami
manyToOne:
clientGroup:
targetEntity: Wise\Client\Domain\ClientGroup\ClientGroup
joinColumn:
name: client_group_id
referencedColumnName: id

4.2. Konfiguracja w doctrine.yaml

Aby Doctrine znalazło pliki mapowania, należy dodać konfigurację w pliku doctrine.yaml (znajdziesz go w katalogu Resources/config danego modułu):

doctrine:
orm:
mappings:
Client:
is_bundle: false
type: yml
dir: '%wiseb2b_dir%/Wise/Client/Resources/entity/Client'
prefix: 'Wise\Client\Domain\Client'
alias: Client

5. Deklaracja pól w klasie encji

Po zdefiniowaniu pól w pliku .orm.yml, należy zadbać, aby klasa encji zawierała odpowiadające im właściwości oraz metody dostępowe (gettery i settery). Dzięki temu dane z bazy są poprawnie przenoszone do obiektu i odwrotnie.

Przykład:

class Client extends AbstractEntity
{
protected ?string $name = null;
protected ?string $email = null;
protected ?int $status = null;

public function getName(): ?string
{
return $this->name;
}

public function setName(?string $name): self
{
$this->name = $name;
return $this;
}

// Pozostałe gettery i settery
}

6. Tworzenie obiektów encji (Fabryka, Serwis aplikacyjny)

Nie należy tworzyć obiektów encji bezpośrednio przy użyciu operatora new, gdyż takie podejście łamie zasady projektowe i może prowadzić do problemów z zarządzaniem zależnościami. Zamiast tego, należy:

  1. Korzystać z fabryki encji – fabryka to specjalna klasa, która tworzy encję, inicjalizuje pola i zapewnia, że obiekt jest poprawnie skonstruowany.
$entity = $this->entityFactory->create($currentEntityData);

Fabrykę należy umieścić w katalogu Factory w module encji (np Wise/{moduł}/Domain/{Entity}/Factory):

use Wise\{moduł}\Domain\{Entity}\Event\{Entity}HasCreatedEvent;
use Wise\Core\Domain\AbstractEntityFactory;
use Wise\Core\Service\Merge\MergeService;

/**
* Fabryka tworząca obiekt ...
*/
class {Entity}Factory extends AbstractEntityFactory
{
protected const HAS_CREATED_EVENT_NAME = {Entity}HasCreatedEvent::class;

public function __construct(
private readonly string $entity,
private readonly MergeService $mergeService,
){
parent::__construct($entity, $mergeService);
}
}

następnie w pliku services.yaml należy zarejestrować fabrykę:

parameters:
{enity}_entity: {class_with_namespace}

services:
Wise\{moduł}\Domain\{Entity}\Factory\{Entity}Factory:
arguments:
$entity: '%{entity}_entity%'

np:

parameters:
contract_entity: Wise\Agreement\Domain\Contract\Contract

services:
# Factories
Wise\Agreement\Domain\Contract\Factory\ContractFactory:
arguments:
$entity: '%contract_entity%'
  1. Używać serwisu aplikacyjnego – np. AddClientService, który nie tylko tworzy instancję encji, ale także zapisuje ją w bazie danych i wykonuje dodatkową logikę biznesową (m.in obsługje dodatkowe pola "mechanizm AdditionalFields" czy emisji eventu HasCreatedEvent).:

7. Serwisy CRUD

Każda encja MUSI posiadać dedykowane serwisy aplikacyjne do operacji CRUD (Create, Read, Update, Delete). Dzięki temu cała logika związana z operacjami na danych jest skupiona w jednym miejscu, co ułatwia utrzymanie kodu i jego rozwój.

Lista serwisów:

  1. AddEntityService (np. AddClientService) – dodawanie nowego rekordu do bazy danych.
  2. ModifyEntityService (np. ModifyClientService) – modyfikacja istniejącego rekordu.
  3. AddOrModifyEntityService (np. AddOrModifyClientService) – dodanie nowego rekordu lub modyfikacja istniejącego, jeśli już istnieje.
  4. ListEntityService (np. ListClientService) – pobieranie listy rekordów.
  5. GetEntityDetailsService (np. GetClientDetailsService) – pobieranie szczegółowych informacji o pojedynczym rekordzie.
  6. RemoveEntityService (np. RemoveClientService) – usuwanie rekordu.

WAŻNE: Wszystkie operacje na encjach należy wykonywać wyłącznie za pomocą dedykowanych serwisów, a nie bezpośrednio na repozytorium.

8. Eventy encji

Każda encja MUSI emitować cztery podstawowe eventy, które monitorują cykl życia obiektu:

  1. HasCreatedEvent – emitowany po utworzeniu nowego rekordu (np. ClientHasCreatedEvent).
  2. HasChangedEvent – emitowany przy modyfikacji encji (np. ClientHasChangedEvent).
  3. BeforeRemoveEvent – emitowany przed usunięciem rekordu (np. ClientBeforeRemoveEvent).
  4. AfterRemoveEvent – emitowany po usunięciu rekordu (np. ClientAfterRemoveEvent).

Mechanizm eventów umożliwia wykonanie dodatkowych operacji (np. logowanie, wysyłanie powiadomień) w odpowiedzi na zmiany w encji.

Przykład użycia eventu w metodzie:

protected function entityHasChanged(string $newHash): void
{
DomainEventManager::dispatch(new ClientHasChangedEvent($this));
}

8.1 Jak wygląda i jak utworzyć HasCreatedEvent

Emitowany po utworzeniu encji.

Implementacja:

namespace Wise\{moduł}\Domain\{Entity}\Event;

use Wise\Core\Domain\Event\ExternalDomainEvent;

class {Entity}HasCreatedEvent implements ExternalDomainEvent
{
public const NAME = '{entity}.created';

public function __construct(
private readonly int $id,
) {}

public function getId(): int
{
return $this->id;
}

public static function getName(): ?string
{
return self::NAME;
}
}

8.2 Jak wygląda i jak utworzyć HasChangedEvent

Emitowany po modyfikacji encji.

Implementacja:

namespace Wise\{moduł}\Domain\{Entity}\Event;

use Wise\Core\Domain\Event\ExternalDomainEvent;

class {Entity}HasChangedEvent implements ExternalDomainEvent
{
public const NAME = '{entity}.changed';

public function __construct(
protected int $id,
) {}

public function getId(): int
{
return $this->id;
}

public static function getName(): ?string
{
return self::NAME;
}
}

8.3 Jak wygląda i jak utworzyć BeforeRemoveEvent

Emitowany przed usunięciem encji.

Implementacja:

namespace Wise\{moduł}\Domain\{Entity}\Event;

use Wise\Core\Domain\Event\InternalDomainEvent;

class {Entity}BeforeRemoveEvent implements InternalDomainEvent
{
public const NAME = '{entity}.before.remove';

public function __construct(
protected int $id
) {}

public function getId(): int
{
return $this->id;
}

public static function getName(): ?string
{
return self::NAME;
}
}

8.4 Jak wygląda i jak utworzyć AfterRemoveEvent

Emitowany po usunięciu encji.

Implementacja:

namespace Wise\{moduł}\Domain\{Entity}\Event;

use Wise\Core\Domain\Event\EntityAfterRemoveEvent;
use Wise\Core\Domain\Event\ExternalDomainEvent;

class {Entity}AfterRemoveEvent extends EntityAfterRemoveEvent implements ExternalDomainEvent
{
public const NAME = '{entity}.after.remove';

public static function getName(): ?string
{
return self::NAME;
}
}

Podsumowanie

  • Encja – to klasa PHP reprezentująca obiekt biznesowy, odwzorowująca dane z tabeli bazy i zawierająca logikę biznesową.
  • Struktura – encje są umieszczone w odpowiednich modułach (katalog Domain), a dodatkowe elementy takie jak repozytoria, fabryki, eventy i konfiguracje mapowania wspierają ich działanie.
  • Tworzenie – encję tworzymy poprzez dziedziczenie po AbstractEntity, deklarujemy właściwości, gettery i settery oraz dokumentujemy logikę walidacyjną.
  • Repozytorium – definiujemy interfejs w domenie i implementację w katalogu Repository/Doctrine, która umożliwia operacje na bazie danych.
  • Mapowanie – konfigurujemy encję w plikach .orm.yml (pola, indeksy, relacje) oraz doctrine.yaml.
  • Tworzenie obiektów – stosujemy fabryki lub serwisy aplikacyjne (np. AddClientService) zamiast bezpośredniego wywoływania operatora new.
  • Serwisy CRUD – każda encja powinna być obsługiwana przez dedykowane serwisy do dodawania, modyfikacji, pobierania i usuwania danych.
  • Eventy – encja emituje cztery kluczowe eventy (HasCreated, HasChanged, BeforeRemove, AfterRemove), co umożliwia reagowanie na zmiany stanu obiektu.