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:
- Stworzyć plik klasy PHP w odpowiednim katalogu
Wise/{moduł}/Domain/{encja}/{encja}.php
(np.Wise/Client/Domain/Client/Client.php
). - 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:
-
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
} -
Implementacja repozytorium – tworzona w katalogu
Repository/Doctrine
(np.Wise/Client/Repository/Doctrine/ClientRepository.php
). Ta klasa dziedziczy poAbstractRepository
i implementuje interfejs repozytorium. Ważnym elementem jest stałaENTITY_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:
- 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%'
- 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 eventuHasCreatedEvent
).:
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:
- AddEntityService (np.
AddClientService
) – dodawanie nowego rekordu do bazy danych. - ModifyEntityService (np.
ModifyClientService
) – modyfikacja istniejącego rekordu. - AddOrModifyEntityService (np.
AddOrModifyClientService
) – dodanie nowego rekordu lub modyfikacja istniejącego, jeśli już istnieje. - ListEntityService (np.
ListClientService
) – pobieranie listy rekordów. - GetEntityDetailsService (np.
GetClientDetailsService
) – pobieranie szczegółowych informacji o pojedynczym rekordzie. - 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:
- HasCreatedEvent – emitowany po utworzeniu nowego rekordu (np.
ClientHasCreatedEvent
). - HasChangedEvent – emitowany przy modyfikacji encji (np.
ClientHasChangedEvent
). - BeforeRemoveEvent – emitowany przed usunięciem rekordu (np.
ClientBeforeRemoveEvent
). - 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) orazdoctrine.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.