Extending Entities
You can extend the TransactionEntity and WalletEntity classes to add custom fields to your transactions and wallets. This is useful for storing additional metadata specific to your application.
Extending Transaction Entity
1. Create Database Table
First, create the ledger table of the extended entity: a copy of the base transaction
table with your custom columns added:
CREATE TABLE transaction_extended LIKE transaction;
ALTER TABLE transaction_extended
ADD extra_property VARCHAR(255) DEFAULT NULL,
ADD custom_field INT DEFAULT NULL,
ADD metadata JSON DEFAULT NULL;
- The extended entity's table replaces
transactionas the ledger:TransactionServiceinserts every transaction (all base columns plus your custom ones) into it, so it must have all the columns oftransaction CREATE TABLE ... LIKEcopies the columns and indexes but not the foreign keys or the immutability trigger oftransaction; add them to the new table if you need them
2. Create Extended Entity Class
<?php
namespace App\Entity;
use ByJG\MicroOrm\Attributes\FieldAttribute;
use ByJG\MicroOrm\Attributes\TableAttribute;
use ByJG\Wallets\Entity\TransactionEntity;
#[TableAttribute('transaction_extended')]
class TransactionExtended extends TransactionEntity
{
#[FieldAttribute(fieldName: 'extra_property')]
protected ?string $extraProperty = null;
#[FieldAttribute(fieldName: 'custom_field')]
protected ?int $customField = null;
#[FieldAttribute(fieldName: 'metadata')]
protected ?string $metadata = null;
public function getExtraProperty(): ?string
{
return $this->extraProperty;
}
public function setExtraProperty(?string $extraProperty): void
{
$this->extraProperty = $extraProperty;
}
public function getCustomField(): ?int
{
return $this->customField;
}
public function setCustomField(?int $customField): void
{
$this->customField = $customField;
}
public function getMetadata(): ?string
{
return $this->metadata;
}
public function setMetadata(?string $metadata): void
{
$this->metadata = $metadata;
}
}
3. Create Extended Repository
<?php
namespace App\Repository;
use ByJG\AnyDataset\Db\DatabaseExecutor;
use ByJG\MicroOrm\Query;
use ByJG\Wallets\Repository\TransactionRepository;
class TransactionRepositoryExtended extends TransactionRepository
{
public function __construct(
DatabaseExecutor $dbExecutor,
string $transactionEntity = \App\Entity\TransactionExtended::class,
array $fieldMappingList = []
) {
parent::__construct($dbExecutor, $transactionEntity, $fieldMappingList);
}
/**
* Custom method example: Get transactions by custom field
*/
public function getByCustomField(int $customFieldValue): array
{
return $this->getRepository()->getByQuery(
Query::getInstance()
->table($this->getMapper()->getTable())
->where('custom_field = :value', ['value' => $customFieldValue])
);
}
}
4. Use Extended Repository with Services
use App\Repository\TransactionRepositoryExtended;
use App\Entity\TransactionExtended;
use ByJG\Wallets\Service\TransactionService;
use ByJG\Wallets\Service\WalletService;
use ByJG\Wallets\Entity\WalletEntity;
use ByJG\Wallets\Repository\WalletRepository;
use ByJG\AnyDataset\Db\DatabaseExecutor;
use ByJG\AnyDataset\Db\Factory;
// Create database connection
$dbExecutor = DatabaseExecutor::using(Factory::getDbInstance('mysql://user:pass@localhost/dbname'));
// Initialize with extended repository
$transactionRepo = new TransactionRepositoryExtended($dbExecutor);
$walletRepo = new WalletRepository($dbExecutor, WalletEntity::class);
$transactionService = new TransactionService($transactionRepo, $walletRepo);
// Now transactions will be TransactionExtended instances
$transaction = $transactionService->addFunds($dto);
// Access custom fields
if ($transaction instanceof TransactionExtended) {
$transaction->setExtraProperty('custom value');
$transaction->setCustomField(12345);
$transactionService->getRepository()->save($transaction);
}
5. Set Custom Fields During Transaction
Pass the custom fields in the TransactionDTO with setProperty(), using the property name of
the extended entity. They are written in the same insert as the transaction:
$transaction = $transactionService->addFunds(
TransactionDTO::create($walletId, 1000)
->setProperty('extraProperty', 'custom value')
);
You can also set custom fields after the transaction is created by extending the service:
class TransactionServiceExtended extends TransactionService
{
public function addFundsWithMetadata(
TransactionDTO $dto,
string $metadata
): TransactionExtended {
$transaction = parent::addFunds($dto);
if ($transaction instanceof TransactionExtended) {
$transaction->setMetadata($metadata);
$this->getRepository()->save($transaction);
}
return $transaction;
}
}
Extending Wallet Entity
The process for extending wallets is similar:
1. Create Extended Wallet Table
CREATE TABLE wallet_extended (
walletid INT(11) NOT NULL,
loyalty_points INT DEFAULT 0,
tier VARCHAR(20) DEFAULT 'bronze',
preferences JSON DEFAULT NULL,
PRIMARY KEY (walletid),
CONSTRAINT fk_wallet_extended_wallet
FOREIGN KEY (walletid)
REFERENCES wallet (walletid)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
2. Create Extended Wallet Entity
<?php
namespace App\Entity;
use ByJG\MicroOrm\Attributes\FieldAttribute;
use ByJG\MicroOrm\Attributes\TableAttribute;
use ByJG\Wallets\Entity\WalletEntity;
#[TableAttribute('wallet_extended')]
class WalletExtended extends WalletEntity
{
#[FieldAttribute(fieldName: 'loyalty_points')]
protected int $loyaltyPoints = 0;
#[FieldAttribute(fieldName: 'tier')]
protected string $tier = 'bronze';
#[FieldAttribute(fieldName: 'preferences')]
protected ?string $preferences = null;
public function getLoyaltyPoints(): int
{
return $this->loyaltyPoints;
}
public function setLoyaltyPoints(int $loyaltyPoints): void
{
$this->loyaltyPoints = $loyaltyPoints;
}
public function getTier(): string
{
return $this->tier;
}
public function setTier(string $tier): void
{
$this->tier = $tier;
}
public function getPreferences(): ?string
{
return $this->preferences;
}
public function setPreferences(?string $preferences): void
{
$this->preferences = $preferences;
}
}
3. Create Extended Wallet Repository
<?php
namespace App\Repository;
use ByJG\AnyDataset\Db\DatabaseExecutor;
use ByJG\MicroOrm\Query;
use ByJG\Wallets\Repository\WalletRepository;
class WalletRepositoryExtended extends WalletRepository
{
public function __construct(
DatabaseExecutor $dbExecutor,
string $walletEntity = \App\Entity\WalletExtended::class,
array $fieldMappingList = []
) {
parent::__construct($dbExecutor, $walletEntity, $fieldMappingList);
}
/**
* Get all wallets by tier
*/
public function getByTier(string $tier): array
{
return $this->getRepository()->getByQuery(
Query::getInstance()
->table($this->getMapper()->getTable())
->where('tier = :tier', ['tier' => $tier])
);
}
/**
* Update loyalty points
*/
public function addLoyaltyPoints(int $walletId, int $points): void
{
$wallet = $this->getById($walletId);
if ($wallet instanceof \App\Entity\WalletExtended) {
$wallet->setLoyaltyPoints($wallet->getLoyaltyPoints() + $points);
$this->save($wallet);
}
}
}
4. Use Extended Wallet Repository
use App\Repository\WalletRepositoryExtended;
use ByJG\Wallets\Service\WalletService;
$walletRepo = new WalletRepositoryExtended($dbExecutor);
$walletService = new WalletService($walletRepo, $walletTypeService, $transactionService);
// Create wallet - custom fields can be set after creation
$walletId = $walletService->createWallet('USD', 'user-123', 10000);
$wallet = $walletService->getById($walletId);
if ($wallet instanceof \App\Entity\WalletExtended) {
$wallet->setTier('gold');
$wallet->setLoyaltyPoints(1000);
$wallet->setPreferences('{"notifications": true}');
$walletRepo->save($wallet);
}
Using Observers to React to Changes
You can use MicroORM observers to react to wallet and transaction changes (audit logs, notifications, cache invalidation, etc.):
Create Observer
<?php
namespace App\Observer;
use ByJG\MicroOrm\Enum\ObserverEvent;
use ByJG\MicroOrm\Interface\ObserverProcessorInterface;
use ByJG\MicroOrm\ObserverData;
use Throwable;
class TransactionObserver implements ObserverProcessorInterface
{
public function getObservedTable(): string
{
// Table to watch for changes
return 'transaction';
}
public function process(ObserverData $observerData): void
{
if ($observerData->getEvent() === ObserverEvent::Insert) {
// For transactions created by the TransactionService,
// getData() carries the rehydrated TransactionEntity
$transaction = $observerData->getData();
// e.g. write an audit log or send a notification
}
}
public function onError(Throwable $exception, ObserverData $observerData): void
{
// Called when process() throws; rethrow to propagate
throw $exception;
}
}
Observers are notified after the write is committed. When the transaction is created by
TransactionService (addFunds, withdrawFunds, reserve/accept/reject operations),
getData() contains the entity; the wallet observer also receives the pre-change wallet in
getOldData(). Since MicroORM 7.0 observers are scoped to the database connection of the
repository where they were registered.
Register Observer
class TransactionRepositoryExtended extends TransactionRepository
{
public function __construct(DatabaseExecutor $dbExecutor)
{
parent::__construct($dbExecutor, TransactionExtended::class);
// Register observer
$this->getRepository()->addObserver(new \App\Observer\TransactionObserver());
}
}
Field Mapping
If your database column names don't match your property names, use field mapping:
Select/update functions are MapperFunctionInterface implementations (an instance or a class
name), not closures:
use ByJG\MicroOrm\FieldMapping;
$fieldMappingList = [
FieldMapping::create('extraProperty')
->withFieldName('extra_property'),
FieldMapping::create('metadata')
->withFieldName('metadata')
->withSelectFunction(JsonDecodeMapper::class) // your MapperFunctionInterface classes
->withUpdateFunction(JsonEncodeMapper::class)
];
$repository = new TransactionRepositoryExtended(
$dbExecutor,
TransactionExtended::class,
$fieldMappingList
);
Complete Example
Here's a complete example combining wallets and transactions with custom fields:
<?php
// 1. Setup
$dbExecutor = DatabaseExecutor::using(Factory::getDbInstance('mysql://user:pass@localhost/dbname'));
$walletRepo = new WalletRepositoryExtended($dbExecutor);
$transactionRepo = new TransactionRepositoryExtended($dbExecutor);
$walletTypeService = new WalletTypeService(new WalletTypeRepository($dbExecutor, WalletTypeEntity::class));
$transactionService = new TransactionService($transactionRepo, $walletRepo);
$walletService = new WalletService($walletRepo, $walletTypeService, $transactionService);
// 2. Create wallet with custom fields
$walletId = $walletService->createWallet('USD', 'user-123', 10000);
$wallet = $walletService->getById($walletId);
$wallet->setTier('platinum');
$wallet->setLoyaltyPoints(5000);
$wallet->setPreferences('{"theme": "dark", "alerts": true}');
$walletRepo->save($wallet);
// 3. Create transaction with custom fields
$dto = TransactionDTO::create($walletId, 2500)
->setDescription('VIP purchase')
->setCode('VIP');
$transaction = $transactionService->addFunds($dto);
$transaction->setExtraProperty('VIP member discount applied');
$transaction->setCustomField(42);
$transaction->setMetadata('{"discount": 0.15, "campaign": "summer2024"}');
$transactionRepo->save($transaction);
// 4. Query by custom fields
$goldWallets = $walletRepo->getByTier('gold');
$transactions = $transactionRepo->getByCustomField(42);
// 5. Award loyalty points on transaction
$walletRepo->addLoyaltyPoints($walletId, 25); // Add 25 points
echo "Transaction {$transaction->getTransactionId()} created\n";
echo "Extra property: {$transaction->getExtraProperty()}\n";
echo "Loyalty points: {$wallet->getLoyaltyPoints()}\n";
Best Practices
-
Keep base tables clean
- Don't modify
transactionorwallettables directly - All customizations go in extended tables
- Don't modify
-
Use JSON for flexible metadata
$transaction->setMetadata(json_encode(['ip' => $ipAddress,'session_id' => $sessionId,'device_type' => $deviceType])); -
Type-hint properly
public function processVIPTransaction(TransactionExtended $transaction): void{$metadata = json_decode($transaction->getMetadata(), true);// ...} -
Document custom fields
/*** @property string|null $extraProperty Additional transaction info* @property int|null $customField Application-specific ID* @property string|null $metadata JSON-encoded metadata*/class TransactionExtended extends TransactionEntity -
Test extended functionality
public function testExtendedTransaction(): void{$dto = TransactionDTO::create($this->walletId, 1000);$tx = $this->transactionService->addFunds($dto);$this->assertInstanceOf(TransactionExtended::class, $tx);$tx->setExtraProperty('test');$this->transactionRepo->save($tx);$loaded = $this->transactionService->getById($tx->getTransactionId());$this->assertEquals('test', $loaded->getExtraProperty());}