Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\AI;
@@ -6,10 +7,10 @@ namespace App\Infrastructure\AI;
use App\Domain\AI\AiModel;
use App\Domain\AI\AiProvider;
use App\Domain\AI\AiQueryHandlerInterface;
use App\Framework\HttpClient\HttpClient;
use App\Infrastructure\AI\GPT4All\Gpt4AllQueryHandler;
use App\Infrastructure\AI\Ollama\OllamaQueryHandler;
use App\Infrastructure\AI\OpenAI\OpenAiQueryHandler;
use App\Framework\HttpClient\HttpClient;
use InvalidArgumentException;
final readonly class AiHandlerFactory
@@ -19,7 +20,8 @@ final readonly class AiHandlerFactory
private string $openAiApiKey = '',
private string $gpt4AllApiUrl = 'http://host.docker.internal:4891',
private string $ollamaApiUrl = 'http://host.docker.internal:11434'
) {}
) {
}
public function createForModel(AiModel $model): AiQueryHandlerInterface
{
@@ -59,12 +61,15 @@ final readonly class AiHandlerFactory
};
}
/**
* @return array<AiProvider>
*/
public function getAvailableProviders(): array
{
$providers = [];
// OpenAI ist verfügbar wenn API Key gesetzt ist
if (!empty($this->openAiApiKey)) {
if (! empty($this->openAiApiKey)) {
$providers[] = AiProvider::OPENAI;
}
@@ -86,7 +91,8 @@ final readonly class AiHandlerFactory
public function getAvailableModels(): array
{
$availableProviders = $this->getAvailableProviders();
$providerSet = array_flip($availableProviders);
$providerValues = array_map(fn (AiProvider $provider) => $provider->value, $availableProviders);
$providerSet = array_flip($providerValues);
$availableModels = [];
foreach (AiModel::cases() as $model) {
@@ -114,11 +120,12 @@ final readonly class AiHandlerFactory
public function getOllamaAvailableModels(): array
{
if (!$this->isProviderAvailable(AiProvider::OLLAMA)) {
if (! $this->isProviderAvailable(AiProvider::OLLAMA)) {
return [];
}
$ollamaHandler = new OllamaQueryHandler($this->httpClient, $this->ollamaApiUrl);
return $ollamaHandler->getAvailableModels();
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\AI;
@@ -13,7 +14,8 @@ final readonly class AiService
{
public function __construct(
private AiHandlerFactory $handlerFactory
) {}
) {
}
public function query(
string $message,
@@ -51,6 +53,7 @@ final readonly class AiService
// Erstes verfügbares Modell verwenden
$fallbackModel = $availableModels[0];
return $this->query($message, $fallbackModel, $messages, $temperature, $maxTokens);
}
}

View File

@@ -1,25 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\AI\GPT4All;
use App\Domain\AI\AiProvider;
use App\Domain\AI\AiQuery;
use App\Domain\AI\AiQueryHandlerInterface;
use App\Domain\AI\AiResponse;
use App\Domain\AI\Exception\AiProviderUnavailableException;
use App\Domain\AI\AiProvider;
use App\Framework\Http\Headers;
use App\Framework\Http\Method;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\HttpClient;
use App\Framework\HttpClient\Exception\CurlExecutionFailed;
use App\Framework\HttpClient\HttpClient;
final readonly class Gpt4AllQueryHandler implements AiQueryHandlerInterface
{
public function __construct(
private HttpClient $httpClient,
private string $gpt4AllApiUrl = 'http://host.docker.internal:4891'
) {}
) {
}
public function isAvailable(): bool
{
@@ -28,6 +30,7 @@ final readonly class Gpt4AllQueryHandler implements AiQueryHandlerInterface
$request = ClientRequest::json(Method::GET, $healthUrl, []);
$this->httpClient->send($request);
return true;
} catch (CurlExecutionFailed $e) {
return false;
@@ -38,7 +41,7 @@ final readonly class Gpt4AllQueryHandler implements AiQueryHandlerInterface
public function __invoke(AiQuery $query): AiResponse
{
if (!$this->isAvailable()) {
if (! $this->isAvailable()) {
throw new AiProviderUnavailableException(
AiProvider::GPT4ALL,
"GPT4All Server ist nicht erreichbar unter {$this->gpt4AllApiUrl}. " .
@@ -67,7 +70,7 @@ final readonly class Gpt4AllQueryHandler implements AiQueryHandlerInterface
$data
)->with([
'headers' => new Headers()
->with('Content-Type', 'application/json')
->with('Content-Type', 'application/json'),
]);
$response = $this->httpClient->send($request);

View File

@@ -1,26 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\AI\Ollama;
use App\Domain\AI\AiProvider;
use App\Domain\AI\AiQuery;
use App\Domain\AI\AiQueryHandlerInterface;
use App\Domain\AI\AiResponse;
use App\Domain\AI\Exception\AiProviderUnavailableException;
use App\Domain\AI\AiProvider;
use App\Framework\Http\Headers;
use App\Framework\Http\Method;
use App\Framework\HttpClient\ClientOptions;
use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\HttpClient;
use App\Framework\HttpClient\Exception\CurlExecutionFailed;
use App\Framework\HttpClient\HttpClient;
final readonly class OllamaQueryHandler implements AiQueryHandlerInterface
{
public function __construct(
private HttpClient $httpClient,
private string $ollamaApiUrl = 'http://host.docker.internal:11434'
) {}
) {
}
public function isAvailable(): bool
{
@@ -59,7 +61,7 @@ final readonly class OllamaQueryHandler implements AiQueryHandlerInterface
$response = $this->httpClient->send($request);
$body = json_decode($response->body, true);
return array_map(fn($model) => $model['name'], $body['models'] ?? []);
return array_map(fn ($model) => $model['name'], $body['models'] ?? []);
} catch (\Exception $e) {
return [];
}
@@ -67,7 +69,7 @@ final readonly class OllamaQueryHandler implements AiQueryHandlerInterface
public function __invoke(AiQuery $query): AiResponse
{
if (!$this->isAvailable()) {
if (! $this->isAvailable()) {
throw new AiProviderUnavailableException(
AiProvider::OLLAMA,
"Ollama Server ist nicht erreichbar unter {$this->ollamaApiUrl}. " .
@@ -79,7 +81,7 @@ final readonly class OllamaQueryHandler implements AiQueryHandlerInterface
$availableModels = $this->getAvailableModels();
$modelExists = in_array($query->model->value, $availableModels, true);
if (!$modelExists) {
if (! $modelExists) {
throw new AiProviderUnavailableException(
AiProvider::OLLAMA,
"Modell '{$query->model->value}' ist nicht verfügbar. " .
@@ -101,7 +103,7 @@ final readonly class OllamaQueryHandler implements AiQueryHandlerInterface
'stream' => false, // Keine Streaming-Antwort
'options' => [
'temperature' => $query->temperature,
]
],
];
if ($query->maxTokens !== null) {
@@ -115,7 +117,7 @@ final readonly class OllamaQueryHandler implements AiQueryHandlerInterface
$data
)->with([
'headers' => new Headers()
->with('Content-Type', 'application/json')
->with('Content-Type', 'application/json'),
]);
$response = $this->httpClient->send($request);

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\AI\OpenAI;
@@ -16,7 +17,8 @@ final readonly class OpenAiQueryHandler implements AiQueryHandlerInterface
public function __construct(
private HttpClient $httpClient,
private string $openAiApiKey = ""
) {}
) {
}
public function __invoke(AiQuery $query): AiResponse
{
@@ -40,7 +42,7 @@ final readonly class OpenAiQueryHandler implements AiQueryHandlerInterface
$data
)->with([
'headers' => new Headers()
->with('Authorization', 'Bearer ' . $this->openAiApiKey)
->with('Authorization', 'Bearer ' . $this->openAiApiKey),
]);
$response = $this->httpClient->send($request);

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api;
@@ -55,7 +56,7 @@ final class GitHubClient
data: [
'name' => $name,
'description' => $description,
'private' => $isPrivate
'private' => $isPrivate,
]
);

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\Http\Method;
@@ -8,7 +10,8 @@ final readonly class BlacklistService
{
public function __construct(
private RapidMailApiClient $apiClient
) {}
) {
}
/**
* Holt die Blacklist
@@ -17,7 +20,7 @@ final readonly class BlacklistService
{
return $this->apiClient->request(Method::GET, 'blacklist', [], [
'page' => $page,
'per_page' => $perPage
'per_page' => $perPage,
]);
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail\Commands;
use App\Infrastructure\Api\RapidMail\RecipientListId;
final readonly class CreateRecipientCommand
{
public function __construct(
public string $email,
public ?string $firstname = null,
public ?string $lastname = null,
public ?string $title = null,
public ?string $company = null,
public ?string $zip = null,
public ?string $city = null,
public ?string $street = null,
public ?string $country = null,
public ?string $phone = null,
public ?string $birthdate = null,
public ?string $gender = null,
public ?string $status = null,
public ?RecipientListId $recipientListId = null,
public array $customFields = []
) {
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email format');
}
}
public function toArray(): array
{
$data = ['email' => $this->email];
if ($this->firstname !== null) {
$data['firstname'] = $this->firstname;
}
if ($this->lastname !== null) {
$data['lastname'] = $this->lastname;
}
if ($this->title !== null) {
$data['title'] = $this->title;
}
if ($this->company !== null) {
$data['company'] = $this->company;
}
if ($this->zip !== null) {
$data['zip'] = $this->zip;
}
if ($this->city !== null) {
$data['city'] = $this->city;
}
if ($this->street !== null) {
$data['street'] = $this->street;
}
if ($this->country !== null) {
$data['country'] = $this->country;
}
if ($this->phone !== null) {
$data['phone'] = $this->phone;
}
if ($this->birthdate !== null) {
$data['birthdate'] = $this->birthdate;
}
if ($this->gender !== null) {
$data['gender'] = $this->gender;
}
if ($this->status !== null) {
$data['status'] = $this->status;
}
if ($this->recipientListId !== null) {
$data['recipientlist_id'] = $this->recipientListId->value;
}
return array_merge($data, $this->customFields);
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail\Commands;
use App\Infrastructure\Api\RapidMail\RecipientId;
use App\Infrastructure\Api\RapidMail\RecipientListId;
final readonly class UpdateRecipientCommand
{
public function __construct(
public RecipientId $id,
public string $email,
public ?string $firstname = null,
public ?string $lastname = null,
public ?string $title = null,
public ?string $company = null,
public ?string $zip = null,
public ?string $city = null,
public ?string $street = null,
public ?string $country = null,
public ?string $phone = null,
public ?string $birthdate = null,
public ?string $gender = null,
public ?string $status = null,
public ?RecipientListId $recipientListId = null,
public array $customFields = []
) {
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email format');
}
}
public function toArray(): array
{
$data = ['email' => $this->email];
if ($this->firstname !== null) {
$data['firstname'] = $this->firstname;
}
if ($this->lastname !== null) {
$data['lastname'] = $this->lastname;
}
if ($this->title !== null) {
$data['title'] = $this->title;
}
if ($this->company !== null) {
$data['company'] = $this->company;
}
if ($this->zip !== null) {
$data['zip'] = $this->zip;
}
if ($this->city !== null) {
$data['city'] = $this->city;
}
if ($this->street !== null) {
$data['street'] = $this->street;
}
if ($this->country !== null) {
$data['country'] = $this->country;
}
if ($this->phone !== null) {
$data['phone'] = $this->phone;
}
if ($this->birthdate !== null) {
$data['birthdate'] = $this->birthdate;
}
if ($this->gender !== null) {
$data['gender'] = $this->gender;
}
if ($this->status !== null) {
$data['status'] = $this->status;
}
if ($this->recipientListId !== null) {
$data['recipientlist_id'] = $this->recipientListId->value;
}
return array_merge($data, $this->customFields);
}
}

View File

@@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail\Examples;
use App\Infrastructure\Api\RapidMail\Commands\CreateRecipientCommand;
use App\Infrastructure\Api\RapidMail\Factories\RecipientCommandFactory;
use App\Infrastructure\Api\RapidMail\RecipientId;
use App\Infrastructure\Api\RapidMail\RecipientListId;
use App\Infrastructure\Api\RapidMail\RecipientListService;
use App\Infrastructure\Api\RapidMail\RecipientService;
/**
* Usage examples for the refactored RapidMail API with Value Objects
*/
class UsageExamples
{
public function __construct(
private RecipientService $recipientService,
private RecipientListService $recipientListService
) {
}
/**
* Example 1: Creating a new recipient using Commands
*/
public function createRecipientExample(): void
{
// Create recipient list first
$recipientList = $this->recipientListService->create(
'Newsletter Subscribers',
'Main newsletter subscriber list'
);
// Create recipient using Command
$command = new CreateRecipientCommand(
email: 'john.doe@example.com',
firstname: 'John',
lastname: 'Doe',
company: 'Example Corp',
recipientListId: $recipientList->id,
customFields: [
'source' => 'website_signup',
'interests' => 'php,javascript',
]
);
$recipient = $this->recipientService->createWithCommand($command);
echo "Created recipient with ID: {$recipient->id->value}\n";
echo "Full name: {$recipient->getFullName()}\n";
echo "Is active: " . ($recipient->isActive() ? 'Yes' : 'No') . "\n";
}
/**
* Example 2: Creating from array data using Factory
*/
public function createFromArrayExample(): void
{
$formData = [
'email' => 'jane.smith@example.com',
'firstname' => 'Jane',
'lastname' => 'Smith',
'company' => 'Tech Solutions',
'newsletter_preference' => 'weekly',
'signup_date' => '2025-01-16',
];
$recipientListId = new RecipientListId(776);
$command = RecipientCommandFactory::createFromArray($formData, $recipientListId);
$recipient = $this->recipientService->createWithCommand($command);
echo "Created recipient: {$recipient->email}\n";
}
/**
* Example 3: Updating an existing recipient
*/
public function updateRecipientExample(): void
{
$recipientId = new RecipientId(123);
$recipient = $this->recipientService->get($recipientId);
// Update using factory with changes
$updateCommand = RecipientCommandFactory::updateFromRecipientWithChanges(
$recipient,
[
'company' => 'New Company Name',
'status' => 'active',
'customFields' => ['last_login' => '2025-01-16'],
]
);
$updatedRecipient = $this->recipientService->updateWithCommand($updateCommand);
echo "Updated recipient company to: {$updatedRecipient->company}\n";
}
/**
* Example 4: Working with RecipientLists
*/
public function recipientListExample(): void
{
// Get all recipient lists
$lists = $this->recipientListService->getAll();
foreach ($lists as $list) {
echo "List: {$list->name} (ID: {$list->id->value})\n";
echo " Recipients: {$list->recipientCount}\n";
echo " Is default: " . ($list->isDefault() ? 'Yes' : 'No') . "\n";
echo " Is empty: " . ($list->isEmpty() ? 'Yes' : 'No') . "\n";
}
}
/**
* Example 5: Searching recipients
*/
public function searchRecipientsExample(): void
{
// Search by email
$recipientListId = new RecipientListId(776);
$recipient = $this->recipientService->findByEmail(
'john.doe@example.com',
$recipientListId
);
if ($recipient !== null) {
echo "Found recipient: {$recipient->getFullName()}\n";
echo "Created at: {$recipient->createdAt?->format('Y-m-d H:i:s')}\n";
}
// Search with filters
$recipients = $this->recipientService->search([
'status' => 'active',
'recipientlist_id' => $recipientListId->value,
]);
echo "Found " . count($recipients) . " active recipients\n";
}
/**
* Example 6: Value Object benefits
*/
public function valueObjectBenefitsExample(): void
{
// Type safety - this prevents errors
$recipientId = new RecipientId(123);
$recipientListId = new RecipientListId(776);
// You can't accidentally mix up IDs
// $this->recipientService->get($recipientListId); // ❌ Type error
$recipient = $this->recipientService->get($recipientId); // ✅ Correct
// Value Objects have useful methods
$otherRecipientId = new RecipientId(123);
if ($recipientId->equals($otherRecipientId)) {
echo "Same recipient ID\n";
}
// Easy string conversion
echo "Recipient ID: {$recipientId}\n"; // Uses __toString()
}
/**
* Example 7: Error handling with Value Objects
*/
public function errorHandlingExample(): void
{
try {
// This will throw an exception
$invalidId = new RecipientId(-1);
} catch (\InvalidArgumentException $e) {
echo "Caught invalid ID: {$e->getMessage()}\n";
}
try {
// This will throw an exception if email is invalid
$command = new CreateRecipientCommand('invalid-email');
} catch (\InvalidArgumentException $e) {
echo "Caught invalid email: {$e->getMessage()}\n";
}
}
/**
* Example 8: Backward compatibility
*/
public function backwardCompatibilityExample(): void
{
// Old way still works (but is deprecated)
$legacyRecipient = $this->recipientService->getById(123);
// New way with Value Objects
$newRecipient = $this->recipientService->get(new RecipientId(123));
echo "Both approaches work, but new way is type-safe\n";
}
/**
* Example 9: Complex workflow
*/
public function complexWorkflowExample(): void
{
// 1. Create a new recipient list
$list = $this->recipientListService->create(
'VIP Customers',
'High-value customers for special offers'
);
// 2. Create multiple recipients
$customers = [
['email' => 'vip1@example.com', 'firstname' => 'Alice', 'company' => 'BigCorp'],
['email' => 'vip2@example.com', 'firstname' => 'Bob', 'company' => 'MegaInc'],
];
$createdRecipients = [];
foreach ($customers as $customerData) {
$command = RecipientCommandFactory::createFromArray($customerData, $list->id);
$recipient = $this->recipientService->createWithCommand($command);
$createdRecipients[] = $recipient;
}
// 3. Update all recipients to VIP status
foreach ($createdRecipients as $recipient) {
$updateCommand = RecipientCommandFactory::updateFromRecipientWithChanges(
$recipient,
['status' => 'active', 'customFields' => ['vip_level' => 'gold']]
);
$this->recipientService->updateWithCommand($updateCommand);
}
echo "Created VIP list with " . count($createdRecipients) . " customers\n";
}
/**
* Example 10: Migration from old to new API
*/
public function migrationExample(): void
{
// Old way (still works but deprecated)
$oldRecipients = $this->recipientService->searchLegacy(['status' => 'active']);
// New way with proper Value Objects
$newRecipients = $this->recipientService->search(['status' => 'active']);
echo "Old API returned array with meta data\n";
echo "New API returns direct array of Recipient ReadModels\n";
echo "New recipients have type-safe IDs and helpful methods\n";
foreach ($newRecipients as $recipient) {
echo "- {$recipient->getFullName()}: " .
($recipient->isActive() ? 'Active' : 'Inactive') . "\n";
}
}
}
/**
* Quick start guide for new users
*/
class QuickStartGuide
{
/**
* The minimal example to get started
*/
public static function quickStart(
RecipientService $recipientService,
RecipientListService $recipientListService
): void {
// 1. Get or create a recipient list
$lists = $recipientListService->getAll();
$list = $lists[0] ?? $recipientListService->create('My List', 'Default list');
// 2. Create a recipient
$command = new CreateRecipientCommand(
email: 'user@example.com',
firstname: 'John',
lastname: 'Doe',
recipientListId: $list->id
);
$recipient = $recipientService->createWithCommand($command);
echo "✅ Created recipient: {$recipient->getFullName()}\n";
echo " ID: {$recipient->id}\n";
echo " List: {$list->name}\n";
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail\Factories;
use App\Infrastructure\Api\RapidMail\Commands\CreateRecipientCommand;
use App\Infrastructure\Api\RapidMail\Commands\UpdateRecipientCommand;
use App\Infrastructure\Api\RapidMail\ReadModels\Recipient;
use App\Infrastructure\Api\RapidMail\RecipientListId;
final class RecipientCommandFactory
{
public static function createFromArray(array $data, ?RecipientListId $recipientListId = null): CreateRecipientCommand
{
return new CreateRecipientCommand(
email: $data['email'],
firstname: $data['firstname'] ?? null,
lastname: $data['lastname'] ?? null,
title: $data['title'] ?? null,
company: $data['company'] ?? null,
zip: $data['zip'] ?? null,
city: $data['city'] ?? null,
street: $data['street'] ?? null,
country: $data['country'] ?? null,
phone: $data['phone'] ?? null,
birthdate: $data['birthdate'] ?? null,
gender: $data['gender'] ?? null,
status: $data['status'] ?? null,
recipientListId: $recipientListId,
customFields: array_diff_key($data, array_flip([
'email', 'firstname', 'lastname', 'title', 'company',
'zip', 'city', 'street', 'country', 'phone', 'birthdate',
'gender', 'status',
]))
);
}
public static function updateFromRecipient(Recipient $recipient): UpdateRecipientCommand
{
return new UpdateRecipientCommand(
id: $recipient->id,
email: $recipient->email,
firstname: $recipient->firstname,
lastname: $recipient->lastname,
title: $recipient->title,
company: $recipient->company,
zip: $recipient->zip,
city: $recipient->city,
street: $recipient->street,
country: $recipient->country,
phone: $recipient->phone,
birthdate: $recipient->birthdate,
gender: $recipient->gender,
status: $recipient->status,
recipientListId: $recipient->recipientListId,
customFields: $recipient->customFields
);
}
public static function updateFromRecipientWithChanges(Recipient $recipient, array $changes): UpdateRecipientCommand
{
$merged = array_merge([
'email' => $recipient->email,
'firstname' => $recipient->firstname,
'lastname' => $recipient->lastname,
'title' => $recipient->title,
'company' => $recipient->company,
'zip' => $recipient->zip,
'city' => $recipient->city,
'street' => $recipient->street,
'country' => $recipient->country,
'phone' => $recipient->phone,
'birthdate' => $recipient->birthdate,
'gender' => $recipient->gender,
'status' => $recipient->status,
], $changes);
return new UpdateRecipientCommand(
id: $recipient->id,
email: $merged['email'],
firstname: $merged['firstname'],
lastname: $merged['lastname'],
title: $merged['title'],
company: $merged['company'],
zip: $merged['zip'],
city: $merged['city'],
street: $merged['street'],
country: $merged['country'],
phone: $merged['phone'],
birthdate: $merged['birthdate'],
gender: $merged['gender'],
status: $merged['status'],
recipientListId: $changes['recipientListId'] ?? $recipient->recipientListId,
customFields: array_merge($recipient->customFields, $changes['customFields'] ?? [])
);
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
final readonly class Mailing
@@ -15,7 +17,8 @@ final readonly class Mailing
public ?string $updatedAt = null,
public ?string $sentAt = null,
public ?array $links = null
) {}
) {
}
public static function fromArray(array $data): self
{

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
final readonly class MailingId
{
public function __construct(
public int $value
) {
if ($value <= 0) {
throw new \InvalidArgumentException('MailingId must be positive');
}
}
public static function fromInt(int $id): MailingId
{
return new MailingId($id);
}
public function equals(MailingId $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return (string) $this->value;
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\Http\Method;
@@ -8,7 +10,8 @@ final readonly class MailingService
{
public function __construct(
private RapidMailApiClient $apiClient
) {}
) {
}
/**
* Holt alle Mailings
@@ -17,10 +20,10 @@ final readonly class MailingService
{
$queryParams = [
'page' => $page,
'per_page' => $perPage
'per_page' => $perPage,
];
if (!empty($filter)) {
if (! empty($filter)) {
$queryParams['filter'] = $filter;
}
@@ -29,8 +32,8 @@ final readonly class MailingService
$mailings = $data['_embedded']['mailings'] ?? [];
return array_map(
fn(array $item) => Mailing::fromArray($item),
$mailings
fn (array $item) => Mailing::fromArray($item),
$mailings
);
}
@@ -38,10 +41,10 @@ final readonly class MailingService
{
$queryParams = [
'page' => $page,
'per_page' => $perPage
'per_page' => $perPage,
];
if (!empty($filter)) {
if (! empty($filter)) {
$queryParams = array_merge($queryParams, $filter);
}
@@ -51,12 +54,12 @@ final readonly class MailingService
return [
'mailings' => array_map(
fn(array $item) => Mailing::fromArray($item),
fn (array $item) => Mailing::fromArray($item),
$mailings
),
'page' => $apiResponse['page'] ?? $page,
'page_count' => $apiResponse['page_count'] ?? 1,
'total_count' => $apiResponse['total_count'] ?? count($mailings)
'total_count' => $apiResponse['total_count'] ?? count($mailings),
];
}
@@ -66,6 +69,7 @@ final readonly class MailingService
public function get(int $mailingId): Mailing
{
$data = $this->apiClient->request(Method::GET, "mailings/{$mailingId}");
return Mailing::fromArray($data);
}

View File

@@ -0,0 +1,252 @@
# RapidMail API Client - Refactored with Value Objects
## Übersicht
Diese refactorierte Version des RapidMail API Clients folgt Domain-Driven Design Prinzipien und trennt klar zwischen **Commands** (für Schreiboperationen) und **Read Models** (für Leseoperationen).
## 🆕 Neue Architektur
### Value Objects für IDs
```php
$recipientId = new RecipientId(123);
$recipientListId = new RecipientListId(776);
$mailingId = new MailingId(456);
```
### Commands für Create/Update
```php
// Neuen Recipient erstellen
$command = new CreateRecipientCommand(
email: 'user@example.com',
firstname: 'John',
lastname: 'Doe',
recipientListId: $recipientListId
);
// Bestehenden Recipient aktualisieren
$updateCommand = new UpdateRecipientCommand(
id: $recipientId,
email: 'updated@example.com',
firstname: 'Jane'
);
```
### Read Models für Abfragen
```php
// Read Models haben IMMER eine ID
$recipient = $recipientService->get($recipientId);
echo $recipient->id->value; // Garantiert vorhanden
echo $recipient->getFullName(); // Helper methods
echo $recipient->isActive(); // Business logic
```
## 🔧 Quick Start
```php
// 1. Services injizieren lassen (über DI Container)
$recipientService = $container->get(RecipientService::class);
$recipientListService = $container->get(RecipientListService::class);
// 2. Recipient List erstellen oder holen
$list = $recipientListService->create('Newsletter', 'Main subscriber list');
// 3. Recipient erstellen
$command = new CreateRecipientCommand(
email: 'test@example.com',
firstname: 'Test',
lastname: 'User',
recipientListId: $list->id
);
$recipient = $recipientService->createWithCommand($command);
echo "Created: {$recipient->getFullName()}";
```
## 📚 API Reference
### RecipientService
#### Neue Methoden (empfohlen)
```php
// Erstellen
$recipient = $recipientService->createWithCommand($createCommand);
// Lesen
$recipient = $recipientService->get($recipientId);
$recipients = $recipientService->search(['status' => 'active']);
$recipient = $recipientService->findByEmail('test@example.com', $recipientListId);
// Aktualisieren
$recipient = $recipientService->updateWithCommand($updateCommand);
// Löschen
$recipientService->delete($recipientId);
```
#### Legacy Methoden (deprecated, aber funktional)
```php
$recipient = $recipientService->getById(123); // ❌ Deprecated
$recipient = $recipientService->get(new RecipientId(123)); // ✅ Neu
```
### RecipientListService
```php
// Erstellen
$list = $recipientListService->create('Name', 'Description');
// Alle listen
$lists = $recipientListService->getAll();
// Einzelne Liste
$list = $recipientListService->get($recipientListId);
// Aktualisieren
$list = $recipientListService->update($recipientListId, 'New Name');
// Löschen
$recipientListService->delete($recipientListId);
```
### Command Factory
```php
// Aus Array erstellen
$command = RecipientCommandFactory::createFromArray([
'email' => 'test@example.com',
'firstname' => 'John'
], $recipientListId);
// Aus bestehendem Recipient
$updateCommand = RecipientCommandFactory::updateFromRecipient($recipient);
// Mit Änderungen
$updateCommand = RecipientCommandFactory::updateFromRecipientWithChanges(
$recipient,
['company' => 'New Company']
);
```
## 🎯 Vorteile der neuen Architektur
### 1. Type Safety
```php
// ❌ Alte API: Fehleranfällig
function processRecipient(int $id) {
// Ist das eine RecipientId oder RecipientListId?
}
// ✅ Neue API: Typsicher
function processRecipient(RecipientId $id) {
// Klar definiert, was erwartet wird
}
```
### 2. Klare Trennung
```php
// ❌ Alte API: Verwirrung
$recipient = new Recipient(); // Hat das eine ID oder nicht?
// ✅ Neue API: Eindeutig
$command = new CreateRecipientCommand(...); // Für CREATE
$recipient = $recipientService->get($id); // Hat IMMER eine ID
```
### 3. Business Logic
```php
$recipient = $recipientService->get($recipientId);
// Helper methods für bessere Lesbarkeit
echo $recipient->getFullName();
echo $recipient->isActive() ? 'Active' : 'Inactive';
echo $list->isDefault() ? 'Default list' : 'Custom list';
```
### 4. Validation
```php
// Value Objects validieren automatisch
$id = new RecipientId(-1); // ❌ Exception: ID muss positiv sein
$command = new CreateRecipientCommand('invalid-email'); // ❌ Exception: Ungültige E-Mail
```
## 🔄 Migration Guide
### Von alter API zu neuer API
**Alt:**
```php
$recipient = new Recipient(null, 'test@example.com', 'John');
$saved = $recipientService->create($recipient);
```
**Neu:**
```php
$command = new CreateRecipientCommand('test@example.com', 'John');
$saved = $recipientService->createWithCommand($command);
```
**Alt:**
```php
$recipient = $recipientService->get(123);
```
**Neu:**
```php
$recipient = $recipientService->get(new RecipientId(123));
```
## 🐛 Debugging & Troubleshooting
### Häufige Probleme
1. **"Recipient data must contain ID"**
- Lösung: Verwende Commands für CREATE, Read Models nur für Responses
2. **"RecipientId must be positive"**
- Lösung: Überprüfe, dass IDs gültig sind
3. **API-Struktur Probleme**
- Alte Services verwenden noch `$data['data']` statt `$data['_embedded']`
- Neue Services sind bereits korrigiert
### Debug Helpers
```php
// Value Object Informationen
echo "Recipient ID: {$recipientId}";
echo "Equal? " . $recipientId->equals($otherRecipientId);
// Read Model Informationen
echo "Full name: {$recipient->getFullName()}";
echo "Has list: " . $recipient->hasRecipientList();
```
## 📋 API Corrections Applied
### Fixed Issues:
1.**Data Structure**: `$data['_embedded']['recipientlists']` statt `$data['data']`
2.**Field Names**: `created`/`updated` statt `created_at`/`updated_at`
3.**Return Types**: Direkte Arrays von Objekten statt verschachtelte Strukturen
4.**Pagination**: Korrekte API-Struktur implementiert
5.**Value Objects**: Typisierte IDs für bessere Type Safety
6.**Command/Query**: Saubere Trennung zwischen Commands und Read Models
## 🔮 Roadmap
- [ ] MailingService refactoring
- [ ] StatisticsService improvement
- [ ] Event-driven architecture integration
- [ ] Caching layer
- [ ] Rate limiting
- [ ] Bulk operations
## 💡 Best Practices
1. **Verwende immer Value Objects für IDs**
2. **Commands für Schreiboperationen, Read Models für Leseoperationen**
3. **Factory Pattern für komplexe Command-Erstellung**
4. **Helper Methods in Read Models für Business Logic**
5. **Backward Compatibility während der Migration**
## 📖 Weitere Beispiele
Siehe `Examples/UsageExamples.php` für detaillierte Anwendungsbeispiele und `Examples/QuickStartGuide.php` für einen schnellen Einstieg.

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\Api\ApiException;
@@ -34,6 +36,7 @@ final readonly class RapidMailApiClient
array $queryParams = []
): array {
$response = $this->sendRawRequest($method, $endpoint, $data, $queryParams);
return $this->handleResponse($response);
}
@@ -49,11 +52,11 @@ final readonly class RapidMailApiClient
$url = $this->config->baseUrl . '/' . ltrim($endpoint, '/');
$options = $this->defaultOptions;
if (!empty($queryParams)) {
if (! empty($queryParams)) {
$options = $options->with(['query' => $queryParams]);
}
if (in_array($method, [Method::GET, Method::DELETE]) && !empty($data)) {
if (in_array($method, [Method::GET, Method::DELETE]) && ! empty($data)) {
$existingQuery = $options->query;
$options = $options->with(['query' => array_merge($existingQuery, $data)]);
$data = [];
@@ -65,7 +68,7 @@ final readonly class RapidMailApiClient
$response = $this->httpClient->send($request);
if (!$response->isSuccessful()) {
if (! $response->isSuccessful()) {
$this->throwApiException($response);
}
@@ -79,7 +82,7 @@ final readonly class RapidMailApiClient
{
return [
'send_activationmail' => $this->config->sendActivationMail ? 'yes' : 'no',
'test_mode' => $this->config->testMode ? 'yes' : 'no'
'test_mode' => $this->config->testMode ? 'yes' : 'no',
];
}
@@ -88,8 +91,9 @@ final readonly class RapidMailApiClient
*/
private function handleResponse(ClientResponse $response): array
{
if (!$response->isJson()) {
throw new ApiException('Expected JSON response, got: ' . $response->getContentType(), 0, $response);;
if (! $response->isJson()) {
throw new ApiException('Expected JSON response, got: ' . $response->getContentType(), 0, $response);
;
}
try {
@@ -115,6 +119,7 @@ final readonly class RapidMailApiClient
}
$message = $this->formatErrorMessage($data, $response);
throw new ApiException($message, $response->status->value, $response);
}
@@ -128,9 +133,9 @@ final readonly class RapidMailApiClient
if (isset($responseData['validation_messages'])) {
$message .= ' - Validation: ' . json_encode(
$responseData['validation_messages'],
JSON_UNESCAPED_UNICODE
);
$responseData['validation_messages'],
JSON_UNESCAPED_UNICODE
);
}
return $message;

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\HttpClient\HttpClient;
@@ -7,9 +9,13 @@ use App\Framework\HttpClient\HttpClient;
final readonly class RapidMailClient
{
public RecipientService $recipients;
public RecipientListService $recipientLists;
public MailingService $mailings;
public StatisticsService $statistics;
public BlacklistService $blacklist;
public function __construct(

View File

@@ -1,21 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\DI\Container;
use App\Framework\DI\Initializer;
use App\Framework\HttpClient\CurlHttpClient;
final readonly class RapidMailClientInitializer
{
#[Initializer]
public function __invoke(): RapidMailClient
public function __invoke(Container $container): RapidMailClient
{
$config = $container->get(\App\Framework\Config\External\ExternalApiConfig::class);
return new RapidMailClient(
new RapidMailConfig(
username: '3f60a5c15c3d49c631d0e75b7c1090a3859423a7',
password: '572d25dc36e620f14c89e9c75c02c1f3794ba3c0',
testMode: true,
username: $config->rapidMail->username,
password: $config->rapidMail->password,
testMode: $config->rapidMail->testMode,
),
new CurlHttpClient()
);

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
final readonly class RapidMailConfig
@@ -11,5 +13,6 @@ final readonly class RapidMailConfig
public bool $testMode = false,
public bool $sendActivationMail = true,
public float $timeout = 30.0
) {}
) {
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail\ReadModels;
use App\Infrastructure\Api\RapidMail\RecipientId;
use App\Infrastructure\Api\RapidMail\RecipientListId;
use DateTimeImmutable;
/**
* Read Model für Recipients - immer mit ID
* Für Create/Update Operations verwende stattdessen Commands
*/
final readonly class Recipient
{
public function __construct(
public RecipientId $id,
public string $email,
public ?string $firstname = null,
public ?string $lastname = null,
public ?string $title = null,
public ?string $company = null,
public ?string $zip = null,
public ?string $city = null,
public ?string $street = null,
public ?string $country = null,
public ?string $phone = null,
public ?string $birthdate = null,
public ?string $gender = null,
public ?string $status = null,
public ?RecipientListId $recipientListId = null,
public ?DateTimeImmutable $createdAt = null,
public ?DateTimeImmutable $updatedAt = null,
public array $customFields = [],
public ?array $links = null
) {
}
public static function fromArray(array $data): self
{
if (! isset($data['id'])) {
throw new \InvalidArgumentException('Recipient data must contain ID');
}
$knownFields = [
'id', 'email', 'firstname', 'lastname', 'title', 'company',
'zip', 'city', 'street', 'country', 'phone', 'birthdate',
'gender', 'status', 'recipientlist_id', 'created', 'updated', '_links',
];
$customFields = array_diff_key($data, array_flip($knownFields));
return new self(
id: new RecipientId($data['id']),
email: $data['email'] ?? '',
firstname: $data['firstname'] ?? null,
lastname: $data['lastname'] ?? null,
title: $data['title'] ?? null,
company: $data['company'] ?? null,
zip: $data['zip'] ?? null,
city: $data['city'] ?? null,
street: $data['street'] ?? null,
country: $data['country'] ?? null,
phone: $data['phone'] ?? null,
birthdate: $data['birthdate'] ?? null,
gender: $data['gender'] ?? null,
status: $data['status'] ?? null,
recipientListId: RecipientListId::tryFrom($data['recipientlist_id']),
/*recipientListId: isset($data['recipientlist_id'])
? new RecipientListId($data['recipientlist_id'])
: null,*/
createdAt: isset($data['created'])
? new DateTimeImmutable($data['created'])
: null,
updatedAt: isset($data['updated'])
? new DateTimeImmutable($data['updated'])
: null,
customFields: $customFields,
links: $data['_links'] ?? null
);
}
public function getFullName(): string
{
$parts = array_filter([$this->firstname, $this->lastname]);
return implode(' ', $parts);
}
public function isActive(): bool
{
return $this->status === 'active';
}
public function hasRecipientList(): bool
{
return $this->recipientListId !== null;
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail\ReadModels;
use App\Infrastructure\Api\RapidMail\RecipientListId;
/**
* Read Model für RecipientLists - immer mit ID
*/
final readonly class RecipientList
{
public function __construct(
public RecipientListId $id,
public string $name,
public ?string $description = null,
public ?int $recipientCount = null,
public ?\DateTimeImmutable $createdAt = null,
public ?\DateTimeImmutable $updatedAt = null,
public ?string $unsubscribeBlacklist = null,
public ?string $default = null,
public ?string $recipientSubscribeEmail = null,
public ?string $subscribeFormUrl = null,
public ?string $subscribeFormFieldKey = null,
public ?array $links = null
) {
}
public static function fromArray(array $data): self
{
if (! isset($data['id'])) {
throw new \InvalidArgumentException('RecipientList data must contain ID');
}
return new self(
id: RecipientListId::fromInt($data['id']),
name: $data['name'] ?? '',
description: $data['description'] ?? null,
recipientCount: $data['recipient_count'] ?? null,
createdAt: isset($data['created'])
? new \DateTimeImmutable($data['created'])
: null,
updatedAt: isset($data['updated'])
? new \DateTimeImmutable($data['updated'])
: null,
unsubscribeBlacklist: $data['unsubscribe_blacklist'] ?? null,
default: $data['default'] ?? null,
recipientSubscribeEmail: $data['recipient_subscribe_email'] ?? null,
subscribeFormUrl: $data['subscribe_form_url'] ?? null,
subscribeFormFieldKey: $data['subscribe_form_field_key'] ?? null,
links: $data['_links'] ?? null
);
}
public function isDefault(): bool
{
return $this->default === 'yes';
}
public function hasUnsubscribeBlacklist(): bool
{
return $this->unsubscribeBlacklist === 'yes';
}
public function hasRecipientSubscribeEmail(): bool
{
return $this->recipientSubscribeEmail === 'yes';
}
public function isEmpty(): bool
{
return $this->recipientCount === 0;
}
}

View File

@@ -1,13 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
final readonly class RecipientId
{
public function __construct(
public int $value,
)
{
) {
if ($value <= 0) {
throw new \InvalidArgumentException('RecipientId must be positive');
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
final readonly class RecipientListId
{
private function __construct(
public int $value
) {
if ($value <= 0) {
throw new \InvalidArgumentException('RecipientListId must be positive');
}
}
public static function fromInt(int $id): RecipientListId
{
return new RecipientListId($id);
}
public static function tryFrom(mixed $id): ?RecipientListId
{
if (is_int($id)) {
return new RecipientListId($id);
}
return null;
}
public function equals(RecipientListId $other): bool
{
return $this->value === $other->value;
}
public function __toString(): string
{
return (string) $this->value;
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\Http\Method;
@@ -9,7 +11,8 @@ final readonly class RecipientListService
{
public function __construct(
private RapidMailApiClient $apiClient
) {}
) {
}
/**
* Erstellt eine neue Empfängerliste
@@ -22,6 +25,7 @@ final readonly class RecipientListService
}
$result = $this->apiClient->request(Method::POST, 'recipientlists', $data);
return RecipientList::fromArray($result);
}
@@ -33,13 +37,13 @@ final readonly class RecipientListService
{
$apiResponse = $this->apiClient->request(Method::GET, 'recipientlists', [], [
'page' => $page,
'per_page' => $perPage
'per_page' => $perPage,
]);
$recipientlists = $apiResponse['_embedded']['recipientlists'] ?? [];
return array_map(
fn(array $item) => RecipientList::fromArray($item),
fn (array $item) => RecipientList::fromArray($item),
$recipientlists
);
}
@@ -50,6 +54,7 @@ final readonly class RecipientListService
public function get(RecipientListId $recipientlistId): RecipientList
{
$data = $this->apiClient->request(Method::GET, "recipientlists/{$recipientlistId->value}");
return RecipientList::fromArray($data);
}
@@ -64,6 +69,7 @@ final readonly class RecipientListService
}
$result = $this->apiClient->request(Method::PUT, "recipientlists/{$recipientlistId->value}", $data);
return RecipientList::fromArray($result);
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\Http\Method;
@@ -17,7 +19,8 @@ final readonly class RecipientService
{
public function __construct(
private RapidMailApiClient $apiClient
) {}
) {
}
/**
* Creates a new recipient using Command pattern (RECOMMENDED)
@@ -40,6 +43,7 @@ final readonly class RecipientService
public function get(RecipientId $recipientId): Recipient
{
$data = $this->apiClient->request(Method::GET, "recipients/{$recipientId->value}");
return Recipient::fromArray($data);
}
@@ -73,10 +77,10 @@ final readonly class RecipientService
{
$queryParams = [
'page' => $page,
'per_page' => $perPage
'per_page' => $perPage,
];
if (!empty($filter)) {
if (! empty($filter)) {
$queryParams = array_merge($queryParams, $filter);
}
@@ -85,7 +89,7 @@ final readonly class RecipientService
$recipients = $apiResponse['_embedded']['recipients'] ?? [];
return array_map(
fn(array $item) => Recipient::fromArray($item),
fn (array $item) => Recipient::fromArray($item),
$recipients
);
}
@@ -101,6 +105,7 @@ final readonly class RecipientService
}
$recipients = $this->search($filter, 1, 1);
return empty($recipients) ? null : $recipients[0];
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api\RapidMail;
use App\Framework\Http\Method;
@@ -8,7 +10,8 @@ final readonly class StatisticsService
{
public function __construct(
private RapidMailApiClient $apiClient
) {}
) {
}
/**
* Holt Statistiken für ein Mailing

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api;
use App\Framework\Api\ApiException;
use App\Framework\Api\ApiRequestTrait;
use App\Framework\Http\Method;
use App\Framework\HttpClient\AuthConfig;
@@ -43,8 +43,8 @@ final class RapidMailClient
options: $this->defaultOptions->with([
'query' => [
'send_activationmail' => 'yes',
'test_mode' => 'yes'
]
'test_mode' => 'yes',
],
])
);
@@ -86,8 +86,8 @@ final class RapidMailClient
data: [
'filter' => [
'email' => $email,
'recipientlist_id' => $recipientlistId
]
'recipientlist_id' => $recipientlistId,
],
]
);

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\Api;
use App\Framework\Api\ApiException;
use App\Framework\Api\ApiRequestTrait;
use App\Framework\Http\Method;
use App\Framework\HttpClient\AuthConfig;
@@ -37,7 +37,7 @@ final class ShopifyClient
$this->defaultOptions = new ClientOptions(
auth: AuthConfig::custom([
'headers' => ['X-Shopify-Access-Token' => $accessToken]
'headers' => ['X-Shopify-Access-Token' => $accessToken],
])
);
@@ -266,8 +266,8 @@ final class ShopifyClient
'webhook' => [
'topic' => $topic,
'address' => $address,
'format' => $format
]
'format' => $format,
],
]
);
@@ -370,6 +370,7 @@ final class ShopifyClient
if (preg_match('/^(\d+)\/(\d+)$/', $limitHeader, $matches)) {
$current = (int)$matches[1];
$limit = (int)$matches[2];
return $limit - $current;
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\GeoIp;
@@ -14,6 +15,9 @@ final readonly class Country
) {
}
/**
* @return array<string, string>
*/
public function toArray(): array
{
return [
@@ -21,7 +25,7 @@ final readonly class Country
'name_en' => $this->nameEn,
'name_de' => $this->nameDe,
'name_native' => $this->nameNative,
'updated_at' => $this->updatedAt
'updated_at' => $this->updatedAt,
];
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\GeoIp;
@@ -20,12 +21,12 @@ final class CountryDataService
echo "Lade Länderdaten von RestCountries API...\n";
$jsonData = file_get_contents(self::RESTCOUNTRIES_API_URL);
if (!$jsonData) {
if (! $jsonData) {
throw new RuntimeException('Konnte Länderdaten nicht herunterladen');
}
$countries = json_decode($jsonData, true);
if (!$countries) {
if (! $countries) {
throw new RuntimeException('Ungültige JSON-Daten erhalten');
}
@@ -43,7 +44,7 @@ final class CountryDataService
$country->nameEn,
$country->nameDe,
$country->nameNative,
$country->updatedAt
$country->updatedAt,
]);
$processed++;
}
@@ -57,11 +58,11 @@ final class CountryDataService
public function getCountryByCode(string $code): ?Country
{
$stmt = $this->database->prepare('SELECT * FROM countries WHERE code = ?');
$stmt = $this->database->prepare('SELECT code, name_en, name_de, name_native, updated_at FROM countries WHERE code = ?');
$stmt->execute([$code]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result) {
if (! $result) {
return null;
}
@@ -79,13 +80,13 @@ final class CountryDataService
$code = $countryData['cca2'] ?? null;
$nameEn = $countryData['name']['common'] ?? null;
if (!$code || !$nameEn) {
if (! $code || ! $nameEn) {
return null;
}
$nameDe = $countryData['translations']['deu']['common'] ?? $nameEn;
$nativeNames = $countryData['name']['nativeName'] ?? [];
$nameNative = !empty($nativeNames)
$nameNative = ! empty($nativeNames)
? $nativeNames[array_key_first($nativeNames)]['common'] ?? $nameEn
: $nameEn;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\GeoIp;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\GeoIp;
@@ -50,7 +51,7 @@ final readonly class DatabaseSetup
private function initializeDatabase(string $file): PDO
{
$directory = dirname($file);
if (!is_dir($directory)) {
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\GeoIp;
@@ -9,7 +10,9 @@ use PDO;
final class GeoIp
{
private readonly PDO $database;
private readonly IpRangeService $ipRangeService;
private readonly CountryDataService $countryDataService;
public function __construct(?string $databasePath = null)
@@ -47,7 +50,7 @@ final class GeoIp
{
$countryCode = $this->getCountryForString($ip);
if (!$countryCode) {
if (! $countryCode) {
return new CountryInfo($ip, null);
}
@@ -83,7 +86,7 @@ final class GeoIp
private function initializeDatabase(string $file): PDO
{
$directory = dirname($file);
if (!is_dir($directory)) {
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}

View File

@@ -1,10 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Infrastructure\GeoIp;
use PDO;
use Generator;
use PDO;
use RuntimeException;
final class IpRangeService
@@ -14,7 +15,7 @@ final class IpRangeService
'ARIN' => 'https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest', // Nordamerika
'APNIC' => 'https://ftp.apnic.net/pub/stats/apnic/delegated-apnic-latest', // Asien-Pazifik
'LACNIC' => 'https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest', // Lateinamerika
'AFRINIC' => 'https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest' // Afrika
'AFRINIC' => 'https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest', // Afrika
];
private const int BATCH_SIZE = 1000;
@@ -84,12 +85,12 @@ final class IpRangeService
{
$context = stream_context_create([
'http' => [
'timeout' => 300 // 5 Minuten Timeout
]
'timeout' => 300, // 5 Minuten Timeout
],
]);
$handle = fopen($url, 'r', false, $context);
if (!$handle) {
if (! $handle) {
throw new RuntimeException("Konnte URL nicht öffnen: {$url}");
}
@@ -118,7 +119,7 @@ final class IpRangeService
yield [
'ip_start' => $ipStart,
'ip_end' => $ipEnd,
'country' => $country
'country' => $country,
];
}
}

Binary file not shown.