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,125 @@
<?php
declare(strict_types=1);
namespace App\Framework\Pagination\ValueObjects;
use App\Framework\Serializer\Json\JsonSerializer;
use App\Framework\Serializer\Exception\SerializeException;
use App\Framework\Serializer\Exception\DeserializeException;
use InvalidArgumentException;
/**
* Cursor for cursor-based pagination
*/
final readonly class Cursor
{
public function __construct(
public string $value,
public Direction $direction = Direction::ASC
) {
if (empty($this->value)) {
throw new InvalidArgumentException('Cursor value cannot be empty');
}
}
/**
* Create cursor from base64 encoded value
*/
public static function fromEncoded(string $encodedValue, Direction $direction = Direction::ASC): self
{
$decoded = base64_decode($encodedValue, true);
if ($decoded === false) {
throw new InvalidArgumentException('Invalid cursor encoding');
}
return new self($decoded, $direction);
}
/**
* Create cursor from field value
*/
public static function fromField(string $field, mixed $value, Direction $direction = Direction::ASC): self
{
$serializer = new JsonSerializer();
try {
$cursorValue = $serializer->serialize([$field => $value]);
} catch (SerializeException $e) {
throw new InvalidArgumentException('Cannot encode cursor value: ' . $e->getMessage(), 0, $e);
}
return new self($cursorValue, $direction);
}
/**
* Get base64 encoded cursor value
*/
public function toEncoded(): string
{
return base64_encode($this->value);
}
/**
* Parse cursor value as array
*/
public function parseValue(): array
{
$serializer = new JsonSerializer();
try {
$parsed = $serializer->deserialize($this->value);
} catch (DeserializeException $e) {
throw new InvalidArgumentException('Cursor value is not valid JSON: ' . $e->getMessage(), 0, $e);
}
if (!is_array($parsed)) {
throw new InvalidArgumentException('Cursor value is not an array');
}
return $parsed;
}
/**
* Get field value from cursor
*/
public function getFieldValue(string $field): mixed
{
$parsed = $this->parseValue();
return $parsed[$field] ?? null;
}
/**
* Check if cursor is for ascending direction
*/
public function isAscending(): bool
{
return $this->direction->isAscending();
}
/**
* Check if cursor is for descending direction
*/
public function isDescending(): bool
{
return $this->direction->isDescending();
}
/**
* Create new cursor with different direction
*/
public function withDirection(Direction $direction): self
{
return new self($this->value, $direction);
}
public function toString(): string
{
return $this->toEncoded();
}
public function __toString(): string
{
return $this->toString();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Framework\Pagination\ValueObjects;
/**
* Pagination direction for cursor-based pagination
*/
enum Direction: string
{
case ASC = 'asc';
case DESC = 'desc';
/**
* Check if direction is ascending
*/
public function isAscending(): bool
{
return $this === self::ASC;
}
/**
* Check if direction is descending
*/
public function isDescending(): bool
{
return $this === self::DESC;
}
/**
* Get opposite direction
*/
public function opposite(): self
{
return match ($this) {
self::ASC => self::DESC,
self::DESC => self::ASC,
};
}
/**
* Get SQL ORDER BY clause value
*/
public function toSql(): string
{
return strtoupper($this->value);
}
}

View File

@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
namespace App\Framework\Pagination\ValueObjects;
/**
* Pagination metadata
*/
final readonly class PaginationMeta
{
public function __construct(
public int $resultCount,
public int $limit,
public bool $hasMore,
public bool $hasPrevious,
public PaginationType $type,
public ?int $totalCount = null,
public ?int $totalPages = null,
public ?int $currentPage = null,
public ?Cursor $nextCursor = null,
public ?Cursor $previousCursor = null,
public ?string $sortField = null,
public Direction $direction = Direction::ASC
) {
}
/**
* Create metadata for offset-based pagination
*/
public static function offset(
PaginationRequest $request,
int $totalCount,
int $resultCount
): self {
$totalPages = $request->limit > 0 ? (int) ceil($totalCount / $request->limit) : 1;
$currentPage = $request->getPage();
return new self(
resultCount: $resultCount,
limit: $request->limit,
hasMore: ($request->offset + $request->limit) < $totalCount,
hasPrevious: $request->offset > 0,
type: PaginationType::OFFSET,
totalCount: $totalCount,
totalPages: $totalPages,
currentPage: $currentPage,
nextCursor: null,
previousCursor: null,
sortField: $request->sortField,
direction: $request->direction
);
}
/**
* Create metadata for cursor-based pagination
*/
public static function cursor(
PaginationRequest $request,
int $resultCount,
?Cursor $nextCursor = null,
?Cursor $previousCursor = null
): self {
return new self(
resultCount: $resultCount,
limit: $request->limit,
hasMore: $nextCursor !== null,
hasPrevious: $previousCursor !== null,
type: PaginationType::CURSOR,
totalCount: null,
totalPages: null,
currentPage: null,
nextCursor: $nextCursor,
previousCursor: $previousCursor,
sortField: $request->sortField,
direction: $request->direction
);
}
/**
* Check if this is offset-based pagination
*/
public function isOffsetBased(): bool
{
return $this->type->isOffset();
}
/**
* Check if this is cursor-based pagination
*/
public function isCursorBased(): bool
{
return $this->type->isCursor();
}
/**
* Create new metadata with different result count
*/
public function withResultCount(int $resultCount): self
{
return new self(
resultCount: $resultCount,
limit: $this->limit,
hasMore: $this->hasMore,
hasPrevious: $this->hasPrevious,
type: $this->type,
totalCount: $this->totalCount,
totalPages: $this->totalPages,
currentPage: $this->currentPage,
nextCursor: $this->nextCursor,
previousCursor: $this->previousCursor,
sortField: $this->sortField,
direction: $this->direction
);
}
/**
* Get navigation information for offset-based pagination
*/
public function getNavigationInfo(): array
{
if (!$this->isOffsetBased()) {
return [];
}
return [
'first_page' => 1,
'last_page' => $this->totalPages,
'current_page' => $this->currentPage,
'next_page' => $this->hasMore ? $this->currentPage + 1 : null,
'previous_page' => $this->hasPrevious ? $this->currentPage - 1 : null,
];
}
/**
* Get cursor navigation information
*/
public function getCursorInfo(): array
{
if (!$this->isCursorBased()) {
return [];
}
return [
'next_cursor' => $this->nextCursor?->toEncoded(),
'previous_cursor' => $this->previousCursor?->toEncoded(),
];
}
/**
* Convert to array for JSON serialization
*/
public function toArray(): array
{
$base = [
'result_count' => $this->resultCount,
'limit' => $this->limit,
'has_more' => $this->hasMore,
'has_previous' => $this->hasPrevious,
'type' => $this->type->value,
'sort_field' => $this->sortField,
'direction' => $this->direction->value,
];
if ($this->isOffsetBased()) {
$base = array_merge($base, [
'total_count' => $this->totalCount,
'total_pages' => $this->totalPages,
'current_page' => $this->currentPage,
], $this->getNavigationInfo());
}
if ($this->isCursorBased()) {
$base = array_merge($base, $this->getCursorInfo());
}
return $base;
}
/**
* Check if results are at the maximum limit
*/
public function isAtLimit(): bool
{
return $this->resultCount === $this->limit;
}
/**
* Get pagination type
*/
public function getType(): PaginationType
{
return $this->type;
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace App\Framework\Pagination\ValueObjects;
use InvalidArgumentException;
/**
* Pagination request parameters
*/
final readonly class PaginationRequest
{
public function __construct(
public int $limit,
public int $offset = 0,
public ?Cursor $cursor = null,
public Direction $direction = Direction::ASC,
public ?string $sortField = null
) {
if ($this->limit <= 0) {
throw new InvalidArgumentException('Limit must be positive');
}
if ($this->limit > 1000) {
throw new InvalidArgumentException('Limit cannot exceed 1000');
}
if ($this->offset < 0) {
throw new InvalidArgumentException('Offset cannot be negative');
}
}
/**
* Create offset-based pagination request
*/
public static function offset(int $limit, int $offset = 0, ?string $sortField = null, Direction $direction = Direction::ASC): self
{
return new self(
limit: $limit,
offset: $offset,
cursor: null,
direction: $direction,
sortField: $sortField
);
}
/**
* Create cursor-based pagination request
*/
public static function cursor(int $limit, Cursor $cursor, ?string $sortField = null): self
{
return new self(
limit: $limit,
offset: 0,
cursor: $cursor,
direction: $cursor->direction,
sortField: $sortField
);
}
/**
* Create first page request
*/
public static function first(int $limit, ?string $sortField = null, Direction $direction = Direction::ASC): self
{
return new self(
limit: $limit,
offset: 0,
cursor: null,
direction: $direction,
sortField: $sortField
);
}
/**
* Check if this is cursor-based pagination
*/
public function isCursorBased(): bool
{
return $this->cursor !== null;
}
/**
* Check if this is offset-based pagination
*/
public function isOffsetBased(): bool
{
return $this->cursor === null;
}
/**
* Get page number for offset-based pagination
*/
public function getPage(): int
{
if ($this->limit === 0) {
return 1;
}
return (int) floor($this->offset / $this->limit) + 1;
}
/**
* Create next page request for offset-based pagination
*/
public function nextPage(): self
{
if ($this->isCursorBased()) {
throw new InvalidArgumentException('Cannot get next page for cursor-based pagination');
}
return new self(
limit: $this->limit,
offset: $this->offset + $this->limit,
cursor: null,
direction: $this->direction,
sortField: $this->sortField
);
}
/**
* Create previous page request for offset-based pagination
*/
public function previousPage(): self
{
if ($this->isCursorBased()) {
throw new InvalidArgumentException('Cannot get previous page for cursor-based pagination');
}
$newOffset = max(0, $this->offset - $this->limit);
return new self(
limit: $this->limit,
offset: $newOffset,
cursor: null,
direction: $this->direction,
sortField: $this->sortField
);
}
/**
* Create request with different limit
*/
public function withLimit(int $limit): self
{
return new self(
limit: $limit,
offset: $this->offset,
cursor: $this->cursor,
direction: $this->direction,
sortField: $this->sortField
);
}
/**
* Create request with different sort field
*/
public function withSort(string $sortField, Direction $direction = Direction::ASC): self
{
return new self(
limit: $this->limit,
offset: $this->offset,
cursor: $this->cursor?->withDirection($direction),
direction: $direction,
sortField: $sortField
);
}
}

View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace App\Framework\Pagination\ValueObjects;
use App\Framework\Serializer\Json\JsonSerializer;
/**
* Pagination response with data and metadata
*/
final readonly class PaginationResponse
{
/**
* @param array<mixed> $data
*/
public function __construct(
public array $data,
public PaginationMeta $meta
) {
}
/**
* Create response for offset-based pagination
*
* @param array<mixed> $data
*/
public static function offset(
array $data,
PaginationRequest $request,
int $totalCount
): self {
$meta = PaginationMeta::offset(
request: $request,
totalCount: $totalCount,
resultCount: count($data)
);
return new self($data, $meta);
}
/**
* Create response for cursor-based pagination
*
* @param array<mixed> $data
*/
public static function cursor(
array $data,
PaginationRequest $request,
?Cursor $nextCursor = null,
?Cursor $previousCursor = null
): self {
$meta = PaginationMeta::cursor(
request: $request,
resultCount: count($data),
nextCursor: $nextCursor,
previousCursor: $previousCursor
);
return new self($data, $meta);
}
/**
* Check if there are more results
*/
public function hasMore(): bool
{
return $this->meta->hasMore();
}
/**
* Check if there are previous results
*/
public function hasPrevious(): bool
{
return $this->meta->hasPrevious();
}
/**
* Get data count
*/
public function count(): int
{
return count($this->data);
}
/**
* Check if response is empty
*/
public function isEmpty(): bool
{
return empty($this->data);
}
/**
* Convert to array for JSON serialization
*/
public function toArray(): array
{
return [
'data' => $this->data,
'meta' => $this->meta->toArray(),
];
}
/**
* Convert to JSON string
*/
public function toJson(): string
{
$serializer = new JsonSerializer();
return $serializer->serialize($this->toArray());
}
/**
* Get first item from data
*/
public function first(): mixed
{
return $this->data[0] ?? null;
}
/**
* Get last item from data
*/
public function last(): mixed
{
if (empty($this->data)) {
return null;
}
return $this->data[array_key_last($this->data)];
}
/**
* Map data through a callback
*
* @param callable(mixed): mixed $callback
*/
public function map(callable $callback): self
{
$mappedData = array_map($callback, $this->data);
return new self($mappedData, $this->meta);
}
/**
* Filter data through a callback
*
* @param callable(mixed): bool $callback
*/
public function filter(callable $callback): self
{
$filteredData = array_values(array_filter($this->data, $callback));
// Update meta with new result count
$newMeta = $this->meta->withResultCount(count($filteredData));
return new self($filteredData, $newMeta);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Framework\Pagination\ValueObjects;
/**
* Pagination type enumeration
*/
enum PaginationType: string
{
case OFFSET = 'offset';
case CURSOR = 'cursor';
/**
* Check if this is offset-based pagination
*/
public function isOffset(): bool
{
return $this === self::OFFSET;
}
/**
* Check if this is cursor-based pagination
*/
public function isCursor(): bool
{
return $this === self::CURSOR;
}
/**
* Get human-readable description
*/
public function getDescription(): string
{
return match ($this) {
self::OFFSET => 'Offset-based pagination with page numbers',
self::CURSOR => 'Cursor-based pagination for large datasets',
};
}
/**
* Get recommended use case
*/
public function getUseCase(): string
{
return match ($this) {
self::OFFSET => 'Small to medium datasets where total count is needed',
self::CURSOR => 'Large datasets or real-time data where performance is critical',
};
}
}