docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\ClickEvent;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
use App\Framework\Core\ValueObjects\Timestamp;
interface ClickEventRepository
{
public function save(ClickEvent $event): void;
public function findByLinkId(SmartLinkId $linkId, ?Timestamp $since = null): array;
public function getRecentClicksByIpHash(string $ipHash, int $minutes = 60): array;
public function countByLinkId(SmartLinkId $linkId): int;
public function countUniqueByLinkId(SmartLinkId $linkId): int;
public function getConversionsByLinkId(SmartLinkId $linkId): int;
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\ClickEvent;
use App\Domain\SmartLink\Enums\DeviceType;
use App\Domain\SmartLink\ValueObjects\ClickId;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
use App\Framework\Core\ValueObjects\CountryCode;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
final readonly class DatabaseClickEventRepository implements ClickEventRepository
{
public function __construct(
private ConnectionInterface $connection
) {}
public function save(ClickEvent $event): void
{
$sql = 'INSERT INTO click_events
(id, link_id, ip_hash, country_code, device_type, user_agent, referer, destination_service, converted, clicked_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
$query = SqlQuery::create($sql, [
$event->id->toString(),
$event->linkId->toString(),
$event->ipHash,
$event->countryCode->toString(),
$event->deviceType->value,
$event->userAgentString,
$event->referer,
$event->destinationService,
$event->converted ? 1 : 0,
$event->clickedAt->format('Y-m-d H:i:s'),
]);
$this->connection->execute($query);
}
public function findByLinkId(SmartLinkId $linkId, ?Timestamp $since = null): array
{
if ($since !== null) {
$sql = 'SELECT * FROM click_events WHERE link_id = ? AND clicked_at >= ? ORDER BY clicked_at DESC';
$query = SqlQuery::create($sql, [$linkId->toString(), $since->format('Y-m-d H:i:s')]);
} else {
$sql = 'SELECT * FROM click_events WHERE link_id = ? ORDER BY clicked_at DESC';
$query = SqlQuery::create($sql, [$linkId->toString()]);
}
$result = $this->connection->query($query);
$rows = $result->fetchAll();
return array_map(fn(array $row) => $this->hydrate($row), $rows);
}
public function getRecentClicksByIpHash(string $ipHash, int $minutes = 60): array
{
$since = Timestamp::now()->subtractMinutes($minutes);
$sql = 'SELECT * FROM click_events WHERE ip_hash = ? AND clicked_at >= ?';
$query = SqlQuery::create($sql, [$ipHash, $since->format('Y-m-d H:i:s')]);
$result = $this->connection->query($query);
$rows = $result->fetchAll();
return array_map(fn(array $row) => $this->hydrate($row), $rows);
}
public function countByLinkId(SmartLinkId $linkId): int
{
$sql = 'SELECT COUNT(*) as count FROM click_events WHERE link_id = ?';
$query = SqlQuery::create($sql, [$linkId->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
return (int) ($row['count'] ?? 0);
}
public function countUniqueByLinkId(SmartLinkId $linkId): int
{
$sql = 'SELECT COUNT(DISTINCT ip_hash) as count FROM click_events WHERE link_id = ?';
$query = SqlQuery::create($sql, [$linkId->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
return (int) ($row['count'] ?? 0);
}
public function getConversionsByLinkId(SmartLinkId $linkId): int
{
$sql = 'SELECT COUNT(*) as count FROM click_events WHERE link_id = ? AND converted = 1';
$query = SqlQuery::create($sql, [$linkId->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
return (int) ($row['count'] ?? 0);
}
private function hydrate(array $row): ClickEvent
{
return new ClickEvent(
id: ClickId::fromString($row['id']),
linkId: SmartLinkId::fromString($row['link_id']),
ipHash: $row['ip_hash'],
countryCode: CountryCode::fromString($row['country_code']),
deviceType: DeviceType::from($row['device_type']),
userAgentString: $row['user_agent'],
referer: $row['referer'],
destinationService: $row['destination_service'],
converted: (bool) $row['converted'],
clickedAt: Timestamp::fromDateTime(new \DateTimeImmutable($row['clicked_at']))
);
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\GeoRoutingRule;
use App\Domain\SmartLink\ValueObjects\GeoRuleId;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
use App\Framework\Core\ValueObjects\CountryCode;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
final readonly class DatabaseGeoRoutingRuleRepository implements GeoRoutingRuleRepository
{
public function __construct(
private ConnectionInterface $connection
) {}
public function save(GeoRoutingRule $rule): void
{
$sql = 'INSERT INTO geo_routing_rules
(id, link_id, country_code, destination_id, priority, created_at)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
country_code = VALUES(country_code),
destination_id = VALUES(destination_id),
priority = VALUES(priority)';
$query = SqlQuery::create($sql, [
$rule->id->toString(),
$rule->linkId->toString(),
$rule->countryCode->toString(),
$rule->destinationId,
$rule->priority,
$rule->createdAt->format('Y-m-d H:i:s'),
]);
$this->connection->execute($query);
}
public function findById(GeoRuleId $id): ?GeoRoutingRule
{
$sql = 'SELECT * FROM geo_routing_rules WHERE id = ?';
$query = SqlQuery::create($sql, [$id->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
if ($row === false) {
return null;
}
return $this->hydrate($row);
}
public function findByLinkAndCountry(SmartLinkId $linkId, CountryCode $countryCode): ?GeoRoutingRule
{
$sql = 'SELECT * FROM geo_routing_rules
WHERE link_id = ? AND country_code = ?
ORDER BY priority DESC
LIMIT 1';
$query = SqlQuery::create($sql, [
$linkId->toString(),
$countryCode->toString()
]);
$result = $this->connection->query($query);
$row = $result->fetch();
if ($row === false) {
return null;
}
return $this->hydrate($row);
}
public function findByLinkId(SmartLinkId $linkId): array
{
$sql = 'SELECT * FROM geo_routing_rules
WHERE link_id = ?
ORDER BY priority DESC, country_code ASC';
$query = SqlQuery::create($sql, [$linkId->toString()]);
$result = $this->connection->query($query);
$rows = $result->fetchAll();
return array_map(fn(array $row) => $this->hydrate($row), $rows);
}
public function delete(GeoRuleId $id): void
{
$sql = 'DELETE FROM geo_routing_rules WHERE id = ?';
$query = SqlQuery::create($sql, [$id->toString()]);
$this->connection->execute($query);
}
public function deleteByLinkId(SmartLinkId $linkId): void
{
$sql = 'DELETE FROM geo_routing_rules WHERE link_id = ?';
$query = SqlQuery::create($sql, [$linkId->toString()]);
$this->connection->execute($query);
}
private function hydrate(array $row): GeoRoutingRule
{
return new GeoRoutingRule(
id: GeoRuleId::fromString($row['id']),
linkId: SmartLinkId::fromString($row['link_id']),
countryCode: CountryCode::fromString($row['country_code']),
destinationId: $row['destination_id'],
priority: (int) $row['priority'],
createdAt: Timestamp::fromDateTime(new \DateTimeImmutable($row['created_at']))
);
}
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\LinkDestination;
use App\Domain\SmartLink\Enums\ServiceType;
use App\Domain\SmartLink\ValueObjects\DestinationUrl;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
final readonly class DatabaseLinkDestinationRepository implements LinkDestinationRepository
{
public function __construct(
private ConnectionInterface $connection
) {}
public function save(LinkDestination $destination): void
{
$sql = 'INSERT INTO link_destinations (id, link_id, service_type, url, priority, is_default, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
service_type = VALUES(service_type),
url = VALUES(url),
priority = VALUES(priority),
is_default = VALUES(is_default)';
$query = SqlQuery::create($sql, [
$destination->id,
$destination->linkId->toString(),
$destination->serviceType->value,
$destination->url->toString(),
$destination->priority,
$destination->isDefault,
$destination->createdAt->format('Y-m-d H:i:s'),
]);
$this->connection->execute($query);
}
public function findById(string $id): ?LinkDestination
{
$sql = 'SELECT * FROM link_destinations WHERE id = ? LIMIT 1';
$query = SqlQuery::create($sql, [$id]);
$result = $this->connection->query($query);
$row = $result->fetch();
return $row ? $this->hydrate($row) : null;
}
public function findByLinkId(SmartLinkId $linkId): array
{
$rows = $this->connection->fetchAll(
'SELECT * FROM link_destinations WHERE link_id = ? ORDER BY priority DESC, created_at ASC',
[$linkId->toString()]
);
return array_map(fn(array $row) => $this->hydrate($row), $rows);
}
public function findDefaultByLinkId(SmartLinkId $linkId): ?LinkDestination
{
$sql = 'SELECT * FROM link_destinations WHERE link_id = ? AND is_default = 1 LIMIT 1';
$query = SqlQuery::create($sql, [$linkId->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
return $row ? $this->hydrate($row) : null;
}
public function deleteByLinkId(SmartLinkId $linkId): void
{
$sql = 'DELETE FROM link_destinations WHERE link_id = ?';
$query = SqlQuery::create($sql, [$linkId->toString()]);
$this->connection->execute($query);
}
public function deleteById(string $id): void
{
$sql = 'DELETE FROM link_destinations WHERE id = ?';
$query = SqlQuery::create($sql, [$id]);
$this->connection->execute($query);
}
private function hydrate(array $row): LinkDestination
{
return new LinkDestination(
id: $row['id'],
linkId: SmartLinkId::fromString($row['link_id']),
serviceType: ServiceType::from($row['service_type']),
url: DestinationUrl::fromString($row['url']),
priority: (int) $row['priority'],
isDefault: (bool) $row['is_default'],
createdAt: Timestamp::fromDateTime(new \DateTimeImmutable($row['created_at']))
);
}
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\SmartLink;
use App\Domain\SmartLink\ValueObjects\LinkSettings;
use App\Domain\SmartLink\Enums\LinkStatus;
use App\Domain\SmartLink\Enums\LinkType;
use App\Domain\SmartLink\ValueObjects\LinkTitle;
use App\Domain\SmartLink\ValueObjects\ShortCode;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
final readonly class DatabaseSmartLinkRepository implements SmartLinkRepository
{
public function __construct(
private ConnectionInterface $connection
) {}
public function save(SmartLink $link): void
{
$sql = 'INSERT INTO smart_links (id, short_code, type, title, cover_image_url, status, user_id, settings, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
type = VALUES(type),
title = VALUES(title),
cover_image_url = VALUES(cover_image_url),
status = VALUES(status),
settings = VALUES(settings),
updated_at = VALUES(updated_at)';
$query = SqlQuery::create($sql, [
$link->id->toString(),
$link->shortCode->toString(),
$link->type->value,
$link->title->toString(),
$link->coverImageUrl,
$link->status->value,
$link->userId,
json_encode($link->settings->toArray()),
$link->createdAt->format('Y-m-d H:i:s'),
$link->updatedAt->format('Y-m-d H:i:s'),
]);
$this->connection->execute($query);
}
public function findById(SmartLinkId $id): ?SmartLink
{
$sql = 'SELECT * FROM smart_links WHERE id = ?';
$query = SqlQuery::create($sql, [$id->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
return $row ? $this->hydrate($row) : null;
}
public function findByShortCode(ShortCode $shortCode): ?SmartLink
{
$sql = 'SELECT * FROM smart_links WHERE short_code = ?';
$query = SqlQuery::create($sql, [$shortCode->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
return $row ? $this->hydrate($row) : null;
}
public function findByUserId(string $userId, ?LinkStatus $status = null): array
{
if ($status !== null) {
$sql = 'SELECT * FROM smart_links WHERE user_id = ? AND status = ? ORDER BY created_at DESC';
$query = SqlQuery::create($sql, [$userId, $status->value]);
} else {
$sql = 'SELECT * FROM smart_links WHERE user_id = ? ORDER BY created_at DESC';
$query = SqlQuery::create($sql, [$userId]);
}
$result = $this->connection->query($query);
$rows = $result->fetchAll();
return array_map(fn(array $row) => $this->hydrate($row), $rows);
}
public function existsShortCode(ShortCode $shortCode): bool
{
$sql = 'SELECT COUNT(*) as count FROM smart_links WHERE short_code = ?';
$query = SqlQuery::create($sql, [$shortCode->toString()]);
$result = $this->connection->query($query);
$count = $result->fetch();
return ($count['count'] ?? 0) > 0;
}
public function delete(SmartLinkId $id): void
{
$sql = 'DELETE FROM smart_links WHERE id = ?';
$query = SqlQuery::create($sql, [$id->toString()]);
$this->connection->execute($query);
}
public function findActiveLinks(): array
{
$sql = 'SELECT * FROM smart_links WHERE status = ? ORDER BY created_at DESC';
$query = SqlQuery::create($sql, [LinkStatus::ACTIVE->value]);
$result = $this->connection->query($query);
$rows = $result->fetchAll();
return array_map(fn(array $row) => $this->hydrate($row), $rows);
}
public function getTotalClicks(SmartLinkId $id): int
{
$sql = 'SELECT COUNT(*) as count FROM click_events WHERE link_id = ?';
$query = SqlQuery::create($sql, [$id->toString()]);
$result = $this->connection->query($query);
$row = $result->fetch();
return (int) ($row['count'] ?? 0);
}
private function hydrate(array $row): SmartLink
{
$settings = json_decode($row['settings'], true);
return new SmartLink(
id: SmartLinkId::fromString($row['id']),
shortCode: ShortCode::fromString($row['short_code']),
type: LinkType::from($row['type']),
title: LinkTitle::fromString($row['title']),
coverImageUrl: $row['cover_image_url'],
status: LinkStatus::from($row['status']),
userId: $row['user_id'],
settings: new LinkSettings(
trackClicks: (bool) $settings['track_clicks'],
enableGeoRouting: (bool) $settings['enable_geo_routing'],
showPreview: (bool) $settings['show_preview'],
customDomain: $settings['custom_domain'] ?? null,
clickLimit: $settings['click_limit'] ?? null,
password: $settings['password'] ?? null
),
createdAt: Timestamp::fromDateTime(new \DateTimeImmutable($row['created_at'])),
updatedAt: Timestamp::fromDateTime(new \DateTimeImmutable($row['updated_at']))
);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\GeoRoutingRule;
use App\Domain\SmartLink\ValueObjects\GeoRuleId;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
use App\Framework\Core\ValueObjects\CountryCode;
interface GeoRoutingRuleRepository
{
public function save(GeoRoutingRule $rule): void;
public function findById(GeoRuleId $id): ?GeoRoutingRule;
/**
* Findet Geo-Routing-Regel für Link + Land, sortiert nach Priorität
*/
public function findByLinkAndCountry(SmartLinkId $linkId, CountryCode $countryCode): ?GeoRoutingRule;
/**
* Findet alle Geo-Routing-Regeln für einen Link
*/
public function findByLinkId(SmartLinkId $linkId): array;
public function delete(GeoRuleId $id): void;
public function deleteByLinkId(SmartLinkId $linkId): void;
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\LinkDestination;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
interface LinkDestinationRepository
{
public function save(LinkDestination $destination): void;
public function findById(string $id): ?LinkDestination;
public function findByLinkId(SmartLinkId $linkId): array;
public function findDefaultByLinkId(SmartLinkId $linkId): ?LinkDestination;
public function deleteByLinkId(SmartLinkId $linkId): void;
public function deleteById(string $id): void;
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Domain\SmartLink\Repositories;
use App\Domain\SmartLink\Entities\SmartLink;
use App\Domain\SmartLink\Enums\LinkStatus;
use App\Domain\SmartLink\ValueObjects\ShortCode;
use App\Domain\SmartLink\ValueObjects\SmartLinkId;
interface SmartLinkRepository
{
public function save(SmartLink $link): void;
public function findById(SmartLinkId $id): ?SmartLink;
public function findByShortCode(ShortCode $shortCode): ?SmartLink;
public function findByUserId(string $userId, ?LinkStatus $status = null): array;
public function existsShortCode(ShortCode $shortCode): bool;
public function delete(SmartLinkId $id): void;
public function findActiveLinks(): array;
public function getTotalClicks(SmartLinkId $id): int;
}