Initial commit
This commit is contained in:
362
lib/Stores/ServiceStore.php
Normal file
362
lib/Stores/ServiceStore.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderMailSystem\Stores;
|
||||
|
||||
use KTXF\Mail\Entity\Address;
|
||||
use KTXF\Mail\Service\IServiceBase;
|
||||
use KTXF\Mail\Service\ServiceIdentityBasic;
|
||||
use KTXF\Mail\Service\ServiceLocation;
|
||||
use KTXF\Mail\Service\ServiceScope;
|
||||
use KTXM\ProviderMailSystem\Providers\Service;
|
||||
use KTXC\Db\DataStore;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Service Store
|
||||
*
|
||||
* @since 2025.05.01
|
||||
*/
|
||||
class ServiceStore {
|
||||
|
||||
public function __construct(
|
||||
private DataStore $store,
|
||||
private LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
private string $serviceCollection = 'mail_provider_smtp_service';
|
||||
|
||||
/**
|
||||
* List all services for a tenant
|
||||
*
|
||||
* @param string $tenantId
|
||||
*
|
||||
* @return array<string|int, Service>
|
||||
*/
|
||||
public function listServices(string $tenantId): array {
|
||||
try {
|
||||
$cursor = $this->store->selectCollection($this->serviceCollection)->find([
|
||||
'tid' => $tenantId,
|
||||
]);
|
||||
|
||||
$services = [];
|
||||
foreach ($cursor as $entry) {
|
||||
$id = (string)($entry['sid'] ?? '');
|
||||
if ($id === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$service = $this->hydrateService($id, is_array($entry) ? $entry : []);
|
||||
if ($service !== null) {
|
||||
$services[$id] = $service;
|
||||
}
|
||||
}
|
||||
|
||||
return $services;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Failed to list services', [
|
||||
'tenantId' => $tenantId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a service by address (checks primary, secondary, and catch-all patterns)
|
||||
*
|
||||
* @param string $tenantId
|
||||
* @param string $address Address to search for
|
||||
*
|
||||
* @return Service|null
|
||||
*/
|
||||
public function findServiceByAddress(string $tenantId, string $address): ?Service {
|
||||
$address = strtolower(trim($address));
|
||||
if ($address === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all services for tenant
|
||||
$services = $this->listServices($tenantId);
|
||||
|
||||
foreach ($services as $service) {
|
||||
if ($service->handlesAddress($address)) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Failed to find service by address', [
|
||||
'tenantId' => $tenantId,
|
||||
'address' => $address,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific service
|
||||
*
|
||||
* @param string $tenantId
|
||||
* @param string|int $serviceId
|
||||
*
|
||||
* @return Service|null
|
||||
*/
|
||||
public function getService(string $tenantId, string|int $serviceId): ?Service {
|
||||
$serviceId = (string)$serviceId;
|
||||
if ($serviceId === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$entry = $this->store->selectCollection($this->serviceCollection)->findOne([
|
||||
'tid' => $tenantId,
|
||||
'sid' => $serviceId,
|
||||
]);
|
||||
|
||||
if ($entry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrateService($serviceId, is_array($entry) ? $entry : []);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Failed to fetch service', [
|
||||
'tenantId' => $tenantId,
|
||||
'serviceId' => $serviceId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new service
|
||||
*
|
||||
* @param string $tenantId
|
||||
* @param IServiceBase $service
|
||||
*
|
||||
* @return string|int Service ID
|
||||
*/
|
||||
public function createService(string $tenantId, IServiceBase $service): string|int {
|
||||
$id = (string)$service->id();
|
||||
if ($id === '') {
|
||||
$id = $this->generateServiceId($tenantId);
|
||||
}
|
||||
|
||||
$now = date('c');
|
||||
$data = $this->dehydrateService($service);
|
||||
|
||||
try {
|
||||
$document = array_merge($data, [
|
||||
'tid' => $tenantId,
|
||||
'sid' => $id,
|
||||
'createdOn' => $now,
|
||||
'modifiedOn' => $now,
|
||||
]);
|
||||
|
||||
$this->store->selectCollection($this->serviceCollection)->insertOne($document);
|
||||
return $id;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Failed to create service', [
|
||||
'tenantId' => $tenantId,
|
||||
'serviceId' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing service
|
||||
*
|
||||
* @param string $tenantId
|
||||
* @param IServiceBase $service
|
||||
*
|
||||
* @return string|int Service ID
|
||||
*/
|
||||
public function updateService(string $tenantId, IServiceBase $service): string|int {
|
||||
$id = (string)$service->id();
|
||||
if ($id === '') {
|
||||
$id = $this->generateServiceId($tenantId);
|
||||
}
|
||||
|
||||
$now = date('c');
|
||||
$data = $this->dehydrateService($service);
|
||||
unset($data['tid'], $data['sid'], $data['createdOn'], $data['modifiedOn']);
|
||||
|
||||
try {
|
||||
$this->store->selectCollection($this->serviceCollection)->updateOne(
|
||||
['tid' => $tenantId, 'sid' => $id],
|
||||
[
|
||||
'$set' => array_merge($data, ['modifiedOn' => $now]),
|
||||
'$setOnInsert' => ['tid' => $tenantId, 'sid' => $id, 'createdOn' => $now],
|
||||
],
|
||||
['upsert' => true]
|
||||
);
|
||||
|
||||
return $id;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Failed to update service', [
|
||||
'tenantId' => $tenantId,
|
||||
'serviceId' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a service
|
||||
*
|
||||
* @param string $tenantId
|
||||
* @param string|int $serviceId
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteService(string $tenantId, string|int $serviceId): bool {
|
||||
$serviceId = (string)$serviceId;
|
||||
if ($serviceId === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->store->selectCollection($this->serviceCollection)->deleteOne([
|
||||
'tid' => $tenantId,
|
||||
'sid' => $serviceId,
|
||||
]);
|
||||
|
||||
return $result->getDeletedCount() === 1;
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Failed to delete service', [
|
||||
'tenantId' => $tenantId,
|
||||
'serviceId' => $serviceId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique service ID
|
||||
*/
|
||||
private function generateServiceId(string $tenantId): string {
|
||||
// Try a few times to avoid collisions if a unique index is ever added.
|
||||
for ($attempt = 0; $attempt < 5; $attempt++) {
|
||||
$id = sprintf('%08x-%04x', time(), mt_rand(0, 0xffff));
|
||||
try {
|
||||
$existing = $this->store->selectCollection($this->serviceCollection)->findOne([
|
||||
'tid' => $tenantId,
|
||||
'sid' => $id,
|
||||
], [
|
||||
'projection' => ['sid' => 1, '_id' => 0],
|
||||
]);
|
||||
|
||||
if ($existing === null) {
|
||||
return $id;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// If the store is unavailable, fall back to generated id.
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('%08x-%04x', time(), mt_rand(0, 0xffff));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate a Service from stored data
|
||||
*/
|
||||
private function hydrateService(string|int $id, array $data): ?Service {
|
||||
try {
|
||||
$service = new Service(
|
||||
providerId: 'smtp',
|
||||
id: $id,
|
||||
label: $data['label'] ?? '',
|
||||
scope: ServiceScope::tryFrom($data['scope'] ?? 'system') ?? ServiceScope::System,
|
||||
owner: $data['owner'] ?? null,
|
||||
enabled: $data['enabled'] ?? true,
|
||||
);
|
||||
|
||||
// Primary address
|
||||
if (isset($data['primaryAddress'])) {
|
||||
$service->setPrimaryAddress(Address::fromArray($data['primaryAddress']));
|
||||
}
|
||||
|
||||
// Secondary addresses
|
||||
if (isset($data['secondaryAddresses']) && is_array($data['secondaryAddresses'])) {
|
||||
foreach ($data['secondaryAddresses'] as $addrData) {
|
||||
$service->addSecondaryAddress(Address::fromArray($addrData));
|
||||
}
|
||||
}
|
||||
|
||||
// Location
|
||||
if (isset($data['location'])) {
|
||||
$service->setLocation(ServiceLocation::fromArray($data['location']));
|
||||
}
|
||||
|
||||
// Identity
|
||||
if (isset($data['identity'])) {
|
||||
$identityType = $data['identity']['type'] ?? 'basic';
|
||||
if ($identityType === 'basic') {
|
||||
$service->setIdentity(ServiceIdentityBasic::fromArray($data['identity']));
|
||||
}
|
||||
}
|
||||
|
||||
return $service;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning('Failed to hydrate service', [
|
||||
'id' => $id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dehydrate a Service to storable data
|
||||
*/
|
||||
private function dehydrateService(IServiceBase $service): array {
|
||||
$data = [
|
||||
'label' => $service->getLabel(),
|
||||
'scope' => $service->getScope()->value,
|
||||
'owner' => $service->getOwner(),
|
||||
'enabled' => $service->getEnabled(),
|
||||
'primaryAddress' => $service->getPrimaryAddress()->jsonSerialize(),
|
||||
'secondaryAddresses' => array_map(
|
||||
fn($a) => $a->jsonSerialize(),
|
||||
$service->getSecondaryAddresses()
|
||||
),
|
||||
];
|
||||
|
||||
// Store location if it's a Service instance
|
||||
if ($service instanceof Service) {
|
||||
$location = $service->getLocation();
|
||||
if ($location !== null) {
|
||||
$data['location'] = $location->jsonSerialize();
|
||||
}
|
||||
|
||||
$identity = $service->getIdentity();
|
||||
if ($identity !== null) {
|
||||
$identityData = $identity->jsonSerialize();
|
||||
// Include password for storage (it's excluded from default serialization)
|
||||
if ($identity instanceof ServiceIdentityBasic) {
|
||||
$identityData['password'] = $identity->getPassword();
|
||||
}
|
||||
$data['identity'] = $identityData;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user