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,172 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Repository;
use App\Framework\Database\EntityManager;
/**
* Service für Batch Loading Operationen (Komposition statt Vererbung)
*/
final readonly class BatchLoader
{
public function __construct(
private EntityManager $entityManager
) {
}
/**
* Findet Entities mit Batch Loading von Relations (verhindert N+1 Queries)
*
* @param string $entityClass
* @param array<string, mixed> $criteria
* @param array<string> $relations Relations die vorgeladen werden sollen
* @param array<string, string>|null $orderBy
* @param int|null $limit
* @return array<int, object>
*/
public function findWithRelations(
string $entityClass,
array $criteria = [],
array $relations = [],
?array $orderBy = null,
?int $limit = null
): array {
return $this->entityManager->findWithRelations(
$entityClass,
$criteria,
$relations,
$orderBy,
$limit
);
}
/**
* Findet paginierte Entities mit optionalem Batch Loading
*
* @param string $entityClass
* @param int $page Aktuelle Seite (1-basiert)
* @param int $limit Items pro Seite
* @param array<string, mixed> $criteria
* @param array<string> $relations Relations die vorgeladen werden sollen
* @param array<string, string>|null $orderBy
* @return PaginatedResult
*/
public function findPaginated(
string $entityClass,
int $page = 1,
int $limit = 20,
array $criteria = [],
array $relations = [],
?array $orderBy = null
): PaginatedResult {
if ($page < 1) {
$page = 1;
}
if ($limit < 1) {
$limit = 20;
}
// Berechne Offset
$offset = ($page - 1) * $limit;
// Hole Total Count für Pagination Info
$totalItems = $this->countBy($entityClass, $criteria);
// Hole die Items mit Batch Loading wenn Relations angegeben
if (! empty($relations)) {
// Verwende findWithRelations für Batch Loading
$items = $this->entityManager->findWithRelations(
$entityClass,
$criteria,
$relations,
$orderBy,
$limit
);
// EntityManager unterstützt kein Offset in findWithRelations,
// daher müssen wir manuell slicen (TODO: Optimierung auf DB-Ebene)
if ($offset > 0) {
$allItems = $this->entityManager->findWithRelations(
$entityClass,
$criteria,
$relations,
$orderBy,
null
);
$items = array_slice($allItems, $offset, $limit);
}
} else {
// Verwende normales findBy mit Limit/Offset
$items = $this->entityManager->findBy($entityClass, $criteria, $orderBy, $limit);
// Auch hier müssen wir manuell slicen
if ($offset > 0) {
$allItems = $this->entityManager->findBy($entityClass, $criteria, $orderBy);
$items = array_slice($allItems, $offset, $limit);
}
}
return PaginatedResult::fromQuery(
items: $items,
totalItems: $totalItems,
page: $page,
limit: $limit
);
}
/**
* Zählt Entities nach Kriterien
*
* @param string $entityClass
* @param array<string, mixed> $criteria
* @return int
*/
public function countBy(string $entityClass, array $criteria = []): int
{
// TODO: Optimierung - sollte COUNT Query verwenden statt alle zu laden
$items = $this->entityManager->findBy($entityClass, $criteria);
return count($items);
}
/**
* Batch-Save für mehrere Entities mit optimierter Performance
*
* @param array<int, object> $entities
* @param int $batchSize Anzahl Entities pro Batch
* @return array<int, object>
*/
public function saveBatch(array $entities, int $batchSize = 100): array
{
$saved = [];
// Verwende Transaktion für bessere Performance
$this->entityManager->transaction(function () use ($entities, $batchSize, &$saved) {
foreach (array_chunk($entities, $batchSize) as $batch) {
$saved = array_merge($saved, $this->entityManager->saveAll(...$batch));
}
});
return $saved;
}
/**
* Batch-Delete für mehrere Entities
*
* @param array<int, object> $entities
* @param int $batchSize Anzahl Entities pro Batch
*/
public function deleteBatch(array $entities, int $batchSize = 100): void
{
$this->entityManager->transaction(function () use ($entities, $batchSize) {
foreach (array_chunk($entities, $batchSize) as $batch) {
foreach ($batch as $entity) {
$this->entityManager->delete($entity);
}
}
});
}
}

View File

@@ -7,52 +7,51 @@ namespace App\Framework\Database\Repository;
use App\Framework\Database\EntityManager;
/**
* Basis-Repository für Entities
* Base Repository Service für Entities (Komposition statt Vererbung)
* Wird als Dependency in Domain Repositories injiziert
*/
abstract class EntityRepository
final readonly class EntityRepository
{
protected string $entityClass;
private BatchLoader $batchLoader;
public function __construct(
protected EntityManager $entityManager
private EntityManager $entityManager
) {
if (! isset($this->entityClass)) {
throw new \LogicException(static::class . " must define \$entityClass property");
}
$this->batchLoader = new BatchLoader($entityManager);
}
/**
* Findet Entity nach ID
*/
public function find(string $id): ?object
public function find(string $entityClass, string $id): ?object
{
return $this->entityManager->find($this->entityClass, $id);
return $this->entityManager->find($entityClass, $id);
}
/**
* Findet Entity nach ID (eager loading)
*/
public function findEager(string $id): ?object
public function findEager(string $entityClass, string $id): ?object
{
return $this->entityManager->findEager($this->entityClass, $id);
return $this->entityManager->findEager($entityClass, $id);
}
/**
* Findet alle Entities
* @return array<int, object>
*/
public function findAll(): array
public function findAll(string $entityClass): array
{
return $this->entityManager->findAll($this->entityClass);
return $this->entityManager->findAll($entityClass);
}
/**
* Findet alle Entities (eager loading)
* @return array<int, object>
*/
public function findAllEager(): array
public function findAllEager(string $entityClass): array
{
return $this->entityManager->findAllEager($this->entityClass);
return $this->entityManager->findAllEager($entityClass);
}
/**
@@ -61,18 +60,18 @@ abstract class EntityRepository
* @param array<string, string>|null $orderBy
* @return array<int, object>
*/
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null): array
public function findBy(string $entityClass, array $criteria, ?array $orderBy = null, ?int $limit = null): array
{
return $this->entityManager->findBy($this->entityClass, $criteria, $orderBy, $limit);
return $this->entityManager->findBy($entityClass, $criteria, $orderBy, $limit);
}
/**
* Findet eine Entity nach Kriterien
* @param array<string, mixed> $criteria
*/
public function findOneBy(array $criteria): ?object
public function findOneBy(string $entityClass, array $criteria): ?object
{
return $this->entityManager->findOneBy($this->entityClass, $criteria);
return $this->entityManager->findOneBy($entityClass, $criteria);
}
/**
@@ -108,4 +107,102 @@ abstract class EntityRepository
{
return $this->entityManager->transaction($callback);
}
/**
* Findet Entities mit Batch Loading von Relations (verhindert N+1 Queries)
*
* @param string $entityClass
* @param array<string, mixed> $criteria
* @param array<string> $relations Relations die vorgeladen werden sollen
* @param array<string, string>|null $orderBy
* @param int|null $limit
* @return array<int, object>
*/
public function findWithRelations(
string $entityClass,
array $criteria = [],
array $relations = [],
?array $orderBy = null,
?int $limit = null
): array {
return $this->batchLoader->findWithRelations(
$entityClass,
$criteria,
$relations,
$orderBy,
$limit
);
}
/**
* Findet paginierte Entities mit optionalem Batch Loading
*
* @param string $entityClass
* @param int $page Aktuelle Seite (1-basiert)
* @param int $limit Items pro Seite
* @param array<string, mixed> $criteria
* @param array<string> $relations Relations die vorgeladen werden sollen
* @param array<string, string>|null $orderBy
* @return PaginatedResult
*/
public function findPaginated(
string $entityClass,
int $page = 1,
int $limit = 20,
array $criteria = [],
array $relations = [],
?array $orderBy = null
): PaginatedResult {
return $this->batchLoader->findPaginated(
$entityClass,
$page,
$limit,
$criteria,
$relations,
$orderBy
);
}
/**
* Zählt Entities nach Kriterien
*
* @param string $entityClass
* @param array<string, mixed> $criteria
* @return int
*/
public function countBy(string $entityClass, array $criteria = []): int
{
return $this->batchLoader->countBy($entityClass, $criteria);
}
/**
* Batch-Save für mehrere Entities mit optimierter Performance
*
* @param array<int, object> $entities
* @param int $batchSize Anzahl Entities pro Batch
* @return array<int, object>
*/
public function saveBatch(array $entities, int $batchSize = 100): array
{
return $this->batchLoader->saveBatch($entities, $batchSize);
}
/**
* Batch-Delete für mehrere Entities
*
* @param array<int, object> $entities
* @param int $batchSize Anzahl Entities pro Batch
*/
public function deleteBatch(array $entities, int $batchSize = 100): void
{
$this->batchLoader->deleteBatch($entities, $batchSize);
}
/**
* Gibt den BatchLoader für erweiterte Operationen zurück
*/
public function getBatchLoader(): BatchLoader
{
return $this->batchLoader;
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Repository;
/**
* Value Object für paginierte Ergebnisse
*/
final readonly class PaginatedResult
{
public function __construct(
public array $items,
public int $totalItems,
public int $currentPage,
public int $itemsPerPage,
public int $totalPages
) {
if ($currentPage < 1) {
throw new \InvalidArgumentException('Current page must be at least 1');
}
if ($itemsPerPage < 1) {
throw new \InvalidArgumentException('Items per page must be at least 1');
}
if ($totalItems < 0) {
throw new \InvalidArgumentException('Total items cannot be negative');
}
if ($totalPages < 0) {
throw new \InvalidArgumentException('Total pages cannot be negative');
}
}
/**
* Factory method für die Erstellung aus Query-Daten
*/
public static function fromQuery(
array $items,
int $totalItems,
int $page,
int $limit
): self {
$totalPages = (int) ceil($totalItems / $limit);
return new self(
items: $items,
totalItems: $totalItems,
currentPage: $page,
itemsPerPage: $limit,
totalPages: $totalPages
);
}
/**
* Prüft ob es eine nächste Seite gibt
*/
public function hasNextPage(): bool
{
return $this->currentPage < $this->totalPages;
}
/**
* Prüft ob es eine vorherige Seite gibt
*/
public function hasPreviousPage(): bool
{
return $this->currentPage > 1;
}
/**
* Berechnet den Offset für die aktuelle Seite
*/
public function getOffset(): int
{
return ($this->currentPage - 1) * $this->itemsPerPage;
}
/**
* Gibt die Range der angezeigten Items zurück (z.B. "1-10 of 100")
*/
public function getDisplayRange(): string
{
if ($this->totalItems === 0) {
return '0-0 of 0';
}
$start = $this->getOffset() + 1;
$end = min($this->currentPage * $this->itemsPerPage, $this->totalItems);
return "{$start}-{$end} of {$this->totalItems}";
}
/**
* Konvertiert zu Array für JSON-Serialisierung
*/
public function toArray(): array
{
return [
'items' => $this->items,
'pagination' => [
'total_items' => $this->totalItems,
'current_page' => $this->currentPage,
'items_per_page' => $this->itemsPerPage,
'total_pages' => $this->totalPages,
'has_next_page' => $this->hasNextPage(),
'has_previous_page' => $this->hasPreviousPage(),
'display_range' => $this->getDisplayRange(),
],
];
}
}