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,72 @@
<?php
use App\Framework\MagicLinks\Actions\ActionResult;
use App\Framework\MagicLinks\ValueObjects\ActionResultData;
use App\Framework\MagicLinks\ValueObjects\ErrorCollection;
describe('ActionResult', function () {
it('creates success result', function () {
$result = ActionResult::success('Operation successful');
expect($result->isSuccess())->toBeTrue();
expect($result->success)->toBeTrue();
expect($result->message)->toBe('Operation successful');
expect($result->data)->toBeInstanceOf(ActionResultData::class);
expect($result->data->isEmpty())->toBeTrue();
expect($result->errors)->toBeInstanceOf(ErrorCollection::class);
expect($result->errors->isEmpty())->toBeTrue();
});
it('creates success result with data', function () {
$data = ActionResultData::fromArray(['user_id' => 123]);
$result = ActionResult::success('Success', $data);
expect($result->isSuccess())->toBeTrue();
expect($result->data)->toBe($data);
expect($result->data->get('user_id'))->toBe(123);
});
it('creates success result with redirect', function () {
$result = ActionResult::success('Success', null, '/dashboard');
expect($result->isSuccess())->toBeTrue();
expect($result->redirectUrl)->toBe('/dashboard');
expect($result->hasRedirect())->toBeTrue();
});
it('creates failure result', function () {
$result = ActionResult::failure('Operation failed');
expect($result->isSuccess())->toBeFalse();
expect($result->success)->toBeFalse();
expect($result->message)->toBe('Operation failed');
expect($result->data->isEmpty())->toBeTrue();
expect($result->errors->isEmpty())->toBeTrue();
});
it('creates failure result with errors', function () {
$errors = ErrorCollection::fromArray(['Error 1', 'Error 2']);
$result = ActionResult::failure('Failed', $errors);
expect($result->isSuccess())->toBeFalse();
expect($result->errors)->toBe($errors);
expect($result->hasErrors())->toBeTrue();
expect($result->errors->count())->toBe(2);
});
it('checks for redirect', function () {
$withRedirect = ActionResult::success('Success', null, '/dashboard');
$withoutRedirect = ActionResult::success('Success');
expect($withRedirect->hasRedirect())->toBeTrue();
expect($withoutRedirect->hasRedirect())->toBeFalse();
});
it('checks for errors', function () {
$withErrors = ActionResult::failure('Failed', ErrorCollection::single('Error'));
$withoutErrors = ActionResult::failure('Failed');
expect($withErrors->hasErrors())->toBeTrue();
expect($withoutErrors->hasErrors())->toBeFalse();
});
});

View File

@@ -0,0 +1,37 @@
<?php
use App\Framework\MagicLinks\Commands\ExecuteMagicLinkCommand;
use App\Framework\MagicLinks\MagicLinkToken;
use App\Framework\MagicLinks\ValueObjects\ExecutionContext;
describe('ExecuteMagicLinkCommand', function () {
it('creates command with token and context', function () {
$token = new MagicLinkToken('test-token-12345678');
$context = ExecutionContext::fromArray(['ip' => '127.0.0.1']);
$command = new ExecuteMagicLinkCommand($token, $context);
expect($command->token)->toBe($token);
expect($command->context)->toBe($context);
});
it('creates command with factory method', function () {
$token = new MagicLinkToken('test-token-12345678');
$command = ExecuteMagicLinkCommand::withToken($token);
expect($command->token)->toBe($token);
expect($command->context)->toBeInstanceOf(ExecutionContext::class);
expect($command->context->isEmpty())->toBeTrue();
});
it('context is ExecutionContext value object', function () {
$token = new MagicLinkToken('test-token-12345678');
$context = ExecutionContext::fromArray(['test' => 'value']);
$command = new ExecuteMagicLinkCommand($token, $context);
expect($command->context)->toBeInstanceOf(ExecutionContext::class);
expect($command->context->toArray())->toBe(['test' => 'value']);
});
});

View File

@@ -0,0 +1,58 @@
<?php
use App\Framework\MagicLinks\Commands\GenerateMagicLinkCommand;
use App\Framework\MagicLinks\TokenAction;
use App\Framework\MagicLinks\TokenConfig;
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
describe('GenerateMagicLinkCommand', function () {
it('creates command with required parameters', function () {
$action = new TokenAction('email_verification');
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com']);
$command = new GenerateMagicLinkCommand(
action: $action,
payload: $payload
);
expect($command->action)->toBe($action);
expect($command->payload)->toBe($payload);
expect($command->config)->toBeNull();
expect($command->baseUrl)->toBeNull();
});
it('creates command with all parameters', function () {
$action = new TokenAction('email_verification');
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com']);
$config = new TokenConfig(expiryHours: 24);
$command = new GenerateMagicLinkCommand(
action: $action,
payload: $payload,
config: $config,
baseUrl: 'https://example.com',
createdByIp: '127.0.0.1',
userAgent: 'Test Agent'
);
expect($command->action)->toBe($action);
expect($command->payload)->toBe($payload);
expect($command->config)->toBe($config);
expect($command->baseUrl)->toBe('https://example.com');
expect($command->createdByIp)->toBe('127.0.0.1');
expect($command->userAgent)->toBe('Test Agent');
});
it('payload is MagicLinkPayload value object', function () {
$action = new TokenAction('test');
$payload = MagicLinkPayload::fromArray(['test' => 'value']);
$command = new GenerateMagicLinkCommand(
action: $action,
payload: $payload
);
expect($command->payload)->toBeInstanceOf(MagicLinkPayload::class);
expect($command->payload->toArray())->toBe(['test' => 'value']);
});
});

View File

@@ -0,0 +1,53 @@
<?php
use App\Framework\MagicLinks\ValueObjects\ActionResultData;
describe('ActionResultData', function () {
it('creates empty result data', function () {
$data = ActionResultData::empty();
expect($data->isEmpty())->toBeTrue();
expect($data->toArray())->toBe([]);
});
it('creates from array', function () {
$data = ActionResultData::fromArray(['user_id' => 123, 'email' => 'test@example.com']);
expect($data->isEmpty())->toBeFalse();
expect($data->toArray())->toBe(['user_id' => 123, 'email' => 'test@example.com']);
});
it('adds values immutably', function () {
$data = ActionResultData::empty();
$updated = $data->with('status', 'success');
expect($data->has('status'))->toBeFalse();
expect($updated->has('status'))->toBeTrue();
expect($updated->get('status'))->toBe('success');
});
it('retrieves values with defaults', function () {
$data = ActionResultData::fromArray(['status' => 'success']);
expect($data->get('status'))->toBe('success');
expect($data->get('missing', 'default'))->toBe('default');
});
it('merges data', function () {
$data1 = ActionResultData::fromArray(['user_id' => 123]);
$data2 = ActionResultData::fromArray(['email' => 'test@example.com']);
$merged = $data1->merge($data2);
expect($merged->toArray())->toBe([
'user_id' => 123,
'email' => 'test@example.com',
]);
});
it('checks key existence', function () {
$data = ActionResultData::fromArray(['user_id' => 123]);
expect($data->has('user_id'))->toBeTrue();
expect($data->has('missing'))->toBeFalse();
});
});

View File

@@ -0,0 +1,64 @@
<?php
use App\Framework\MagicLinks\ValueObjects\ErrorCollection;
describe('ErrorCollection', function () {
it('creates empty collection', function () {
$errors = ErrorCollection::empty();
expect($errors->isEmpty())->toBeTrue();
expect($errors->hasErrors())->toBeFalse();
expect($errors->count())->toBe(0);
});
it('creates collection from array', function () {
$errors = ErrorCollection::fromArray(['Error 1', 'Error 2']);
expect($errors->isEmpty())->toBeFalse();
expect($errors->hasErrors())->toBeTrue();
expect($errors->count())->toBe(2);
});
it('creates single error collection', function () {
$errors = ErrorCollection::single('Test error');
expect($errors->count())->toBe(1);
expect($errors->first())->toBe('Test error');
});
it('adds error immutably', function () {
$errors = ErrorCollection::empty();
$updated = $errors->add('New error');
expect($errors->count())->toBe(0);
expect($updated->count())->toBe(1);
expect($updated->first())->toBe('New error');
});
it('adds multiple errors', function () {
$errors = ErrorCollection::single('Error 1');
$updated = $errors->addMultiple(['Error 2', 'Error 3']);
expect($updated->count())->toBe(3);
expect($updated->toArray())->toBe(['Error 1', 'Error 2', 'Error 3']);
});
it('converts to string', function () {
$errors = ErrorCollection::fromArray(['Error 1', 'Error 2', 'Error 3']);
expect($errors->toString())->toBe('Error 1, Error 2, Error 3');
expect($errors->toString(' | '))->toBe('Error 1 | Error 2 | Error 3');
});
it('returns first error', function () {
$errors = ErrorCollection::fromArray(['First', 'Second']);
expect($errors->first())->toBe('First');
});
it('returns null for empty collection first', function () {
$errors = ErrorCollection::empty();
expect($errors->first())->toBeNull();
});
});

View File

@@ -0,0 +1,51 @@
<?php
use App\Framework\MagicLinks\ValueObjects\ExecutionContext;
describe('ExecutionContext', function () {
it('creates empty context', function () {
$context = ExecutionContext::empty();
expect($context->isEmpty())->toBeTrue();
expect($context->toArray())->toBe([]);
});
it('creates context from array', function () {
$context = ExecutionContext::fromArray(['ip' => '127.0.0.1', 'user_agent' => 'Test']);
expect($context->isEmpty())->toBeFalse();
expect($context->toArray())->toBe(['ip' => '127.0.0.1', 'user_agent' => 'Test']);
});
it('adds values immutably', function () {
$context = ExecutionContext::empty();
$updated = $context->with('ip', '127.0.0.1');
expect($context->has('ip'))->toBeFalse();
expect($updated->has('ip'))->toBeTrue();
expect($updated->get('ip'))->toBe('127.0.0.1');
});
it('retrieves values with defaults', function () {
$context = ExecutionContext::fromArray(['ip' => '127.0.0.1']);
expect($context->get('ip'))->toBe('127.0.0.1');
expect($context->get('missing', 'default'))->toBe('default');
});
it('merges contexts', function () {
$context1 = ExecutionContext::fromArray(['ip' => '127.0.0.1']);
$context2 = ExecutionContext::fromArray(['user_agent' => 'Test']);
$merged = $context1->merge($context2);
expect($merged->toArray())->toBe(['ip' => '127.0.0.1', 'user_agent' => 'Test']);
});
it('merges with override', function () {
$context1 = ExecutionContext::fromArray(['ip' => '127.0.0.1']);
$context2 = ExecutionContext::fromArray(['ip' => '192.168.1.1']);
$merged = $context1->merge($context2);
expect($merged->get('ip'))->toBe('192.168.1.1');
});
});

View File

@@ -0,0 +1,61 @@
<?php
use App\Framework\MagicLinks\ValueObjects\MagicLinkPayload;
describe('MagicLinkPayload', function () {
it('creates payload from array', function () {
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com', 'user_id' => 123]);
expect($payload->toArray())->toBe(['email' => 'test@example.com', 'user_id' => 123]);
});
it('throws exception for empty payload', function () {
expect(fn() => MagicLinkPayload::fromArray([]))
->toThrow(InvalidArgumentException::class, 'Payload cannot be empty');
});
it('retrieves value with get method', function () {
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com']);
expect($payload->get('email'))->toBe('test@example.com');
expect($payload->get('missing', 'default'))->toBe('default');
});
it('checks if key exists', function () {
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com']);
expect($payload->has('email'))->toBeTrue();
expect($payload->has('missing'))->toBeFalse();
});
it('creates new instance with added value', function () {
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com']);
$updated = $payload->with('user_id', 123);
expect($payload->has('user_id'))->toBeFalse();
expect($updated->has('user_id'))->toBeTrue();
expect($updated->get('user_id'))->toBe(123);
});
it('creates new instance without key', function () {
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com', 'user_id' => 123]);
$updated = $payload->without('user_id');
expect($payload->has('user_id'))->toBeTrue();
expect($updated->has('user_id'))->toBeFalse();
});
it('throws exception when removing last key', function () {
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com']);
expect(fn() => $payload->without('email'))
->toThrow(InvalidArgumentException::class, 'Payload cannot be empty after removal');
});
it('returns keys and values', function () {
$payload = MagicLinkPayload::fromArray(['email' => 'test@example.com', 'user_id' => 123]);
expect($payload->keys())->toBe(['email', 'user_id']);
expect($payload->values())->toBe(['test@example.com', 123]);
});
});

View File

@@ -0,0 +1,51 @@
<?php
use App\Framework\MagicLinks\ValueObjects\Metadata;
describe('Metadata', function () {
it('creates empty metadata', function () {
$metadata = Metadata::empty();
expect($metadata->isEmpty())->toBeTrue();
expect($metadata->toArray())->toBe([]);
});
it('creates from array', function () {
$metadata = Metadata::fromArray(['source' => 'email', 'campaign' => 'welcome']);
expect($metadata->isEmpty())->toBeFalse();
expect($metadata->toArray())->toBe(['source' => 'email', 'campaign' => 'welcome']);
});
it('adds values immutably', function () {
$metadata = Metadata::empty();
$updated = $metadata->with('source', 'email');
expect($metadata->has('source'))->toBeFalse();
expect($updated->has('source'))->toBeTrue();
expect($updated->get('source'))->toBe('email');
});
it('removes values immutably', function () {
$metadata = Metadata::fromArray(['source' => 'email', 'campaign' => 'welcome']);
$updated = $metadata->without('campaign');
expect($metadata->has('campaign'))->toBeTrue();
expect($updated->has('campaign'))->toBeFalse();
expect($updated->has('source'))->toBeTrue();
});
it('merges metadata', function () {
$meta1 = Metadata::fromArray(['source' => 'email']);
$meta2 = Metadata::fromArray(['campaign' => 'welcome']);
$merged = $meta1->merge($meta2);
expect($merged->toArray())->toBe(['source' => 'email', 'campaign' => 'welcome']);
});
it('returns keys', function () {
$metadata = Metadata::fromArray(['source' => 'email', 'campaign' => 'welcome']);
expect($metadata->keys())->toBe(['source', 'campaign']);
});
});