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,151 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Search;
use App\Framework\Search\SearchDocument;
use App\Framework\Search\ValueObjects\DocumentData;
use App\Framework\Search\ValueObjects\DocumentMetadata;
use App\Framework\Search\ValueObjects\EntityId;
use App\Framework\Search\ValueObjects\EntityType;
describe('SearchDocument Value Object', function () {
it('can be created with Value Objects', function () {
$document = new SearchDocument(
EntityId::fromString('user_123'),
EntityType::user(),
DocumentData::fromArray(['name' => 'John', 'email' => 'john@test.com']),
DocumentMetadata::withTimestamps()
);
expect($document->id->toString())->toBe('user_123');
expect($document->entityType->toString())->toBe('user');
expect($document->data->get('name'))->toBe('John');
expect($document->hasMetadata())->toBeTrue();
});
it('can be created with minimal data', function () {
$document = new SearchDocument(
EntityId::fromString('product_456'),
EntityType::product(),
DocumentData::fromArray(['title' => 'Test Product'])
);
expect($document->id->toString())->toBe('product_456');
expect($document->hasMetadata())->toBeFalse();
});
it('supports immutable metadata updates', function () {
$document = new SearchDocument(
EntityId::fromString('test_1'),
EntityType::fromString('test'),
DocumentData::empty()
);
$updated = $document->withMetadata('version', '1.0');
expect($document->hasMetadata())->toBeFalse(); // Original unchanged
expect($updated->hasMetadata())->toBeTrue();
expect($updated->metadata->get('version'))->toBe('1.0');
});
it('supports immutable data updates', function () {
$document = new SearchDocument(
EntityId::fromString('test_1'),
EntityType::fromString('test'),
DocumentData::fromArray(['title' => 'Original'])
);
$updated = $document->withDocumentData(
DocumentData::fromArray(['title' => 'Updated', 'content' => 'New content'])
);
expect($document->data->get('title'))->toBe('Original'); // Original unchanged
expect($updated->data->get('title'))->toBe('Updated');
expect($updated->data->get('content'))->toBe('New content');
});
it('supports field-level updates', function () {
$document = new SearchDocument(
EntityId::fromString('test_1'),
EntityType::fromString('test'),
DocumentData::fromArray(['title' => 'Original', 'status' => 'draft'])
);
$updated = $document->withField('status', 'published');
expect($document->data->get('status'))->toBe('draft'); // Original unchanged
expect($updated->data->get('status'))->toBe('published');
expect($updated->data->get('title'))->toBe('Original'); // Other fields preserved
});
it('supports field removal', function () {
$document = new SearchDocument(
EntityId::fromString('test_1'),
EntityType::fromString('test'),
DocumentData::fromArray(['title' => 'Test', 'temp' => 'remove me'])
);
$updated = $document->withoutField('temp');
expect($document->data->has('temp'))->toBeTrue(); // Original unchanged
expect($updated->data->has('temp'))->toBeFalse();
expect($updated->data->has('title'))->toBeTrue(); // Other fields preserved
});
it('converts to array correctly', function () {
$document = new SearchDocument(
EntityId::fromString('user_123'),
EntityType::user(),
DocumentData::fromArray(['name' => 'John']),
DocumentMetadata::fromArray(['version' => '1.0'])
);
$array = $document->toArray();
expect($array)->toBe([
'id' => 'user_123',
'entity_type' => 'user',
'data' => ['name' => 'John'],
'metadata' => ['version' => '1.0'],
]);
});
it('converts to index array with special fields', function () {
$document = new SearchDocument(
EntityId::fromString('product_456'),
EntityType::product(),
DocumentData::fromArray(['title' => 'Test Product', 'price' => 99.99]),
DocumentMetadata::fromArray(['version' => '2.0', 'language' => 'en'])
);
$indexArray = $document->toIndexArray();
expect($indexArray)->toBe([
'title' => 'Test Product',
'price' => 99.99,
'_id' => 'product_456',
'_type' => 'product',
'_meta_version' => '2.0',
'_meta_language' => 'en',
]);
});
it('handles empty metadata correctly', function () {
$document = new SearchDocument(
EntityId::fromString('test_1'),
EntityType::fromString('test'),
DocumentData::fromArray(['title' => 'Test'])
);
expect($document->hasMetadata())->toBeFalse();
$indexArray = $document->toIndexArray();
expect($indexArray)->toBe([
'title' => 'Test',
'_id' => 'test_1',
'_type' => 'test',
]);
});
});

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Search\ValueObjects;
use App\Framework\Search\ValueObjects\DocumentData;
use InvalidArgumentException;
describe('DocumentData Value Object', function () {
it('can be created empty', function () {
$data = DocumentData::empty();
expect($data->isEmpty())->toBeTrue();
expect($data->toArray())->toBe([]);
});
it('can be created from array', function () {
$fields = ['title' => 'Test', 'content' => 'Content'];
$data = DocumentData::fromArray($fields);
expect($data->toArray())->toBe($fields);
expect($data->isEmpty())->toBeFalse();
});
it('can add fields immutably', function () {
$data = DocumentData::empty();
$newData = $data->with('title', 'Test Title');
expect($data->isEmpty())->toBeTrue(); // Original unchanged
expect($newData->get('title'))->toBe('Test Title');
expect($newData->has('title'))->toBeTrue();
});
it('can remove fields immutably', function () {
$data = DocumentData::fromArray(['title' => 'Test', 'content' => 'Content']);
$newData = $data->without('title');
expect($data->has('title'))->toBeTrue(); // Original unchanged
expect($newData->has('title'))->toBeFalse();
expect($newData->has('content'))->toBeTrue();
});
it('gets field values with defaults', function () {
$data = DocumentData::fromArray(['title' => 'Test']);
expect($data->get('title'))->toBe('Test');
expect($data->get('missing'))->toBeNull();
expect($data->get('missing', 'default'))->toBe('default');
});
it('extracts text fields only', function () {
$data = DocumentData::fromArray([
'title' => 'Text Title',
'content' => 'Text Content',
'price' => 123.45,
'active' => true,
'empty_text' => '',
'whitespace' => ' ',
]);
$textFields = $data->getTextFields();
expect($textFields)->toBe([
'title' => 'Text Title',
'content' => 'Text Content',
]);
});
it('extracts facet fields only', function () {
$data = DocumentData::fromArray([
'title' => 'Text Title',
'price' => 123.45,
'active' => true,
'count' => 42,
'nested' => ['key' => 'value'],
]);
$facetFields = $data->getFacetFields();
expect($facetFields)->toBe([
'title' => 'Text Title',
'price' => 123.45,
'active' => true,
'count' => 42,
]);
});
it('validates field keys', function () {
expect(fn () => DocumentData::fromArray(['' => 'value']))->toThrow(InvalidArgumentException::class);
expect(fn () => DocumentData::fromArray([' ' => 'value']))->toThrow(InvalidArgumentException::class);
expect(fn () => DocumentData::fromArray([123 => 'value']))->toThrow(InvalidArgumentException::class);
});
it('validates field key length', function () {
$longKey = str_repeat('a', 256);
expect(fn () => DocumentData::fromArray([$longKey => 'value']))->toThrow(InvalidArgumentException::class);
});
it('validates field key format', function () {
expect(fn () => DocumentData::fromArray(['1invalid' => 'value']))->toThrow(InvalidArgumentException::class);
expect(fn () => DocumentData::fromArray(['invalid-key' => 'value']))->toThrow(InvalidArgumentException::class);
expect(fn () => DocumentData::fromArray(['invalid key' => 'value']))->toThrow(InvalidArgumentException::class);
});
it('allows valid field keys', function () {
expect(fn () => DocumentData::fromArray(['valid' => 'value']))->not->toThrow(InvalidArgumentException::class);
expect(fn () => DocumentData::fromArray(['valid_key' => 'value']))->not->toThrow(InvalidArgumentException::class);
expect(fn () => DocumentData::fromArray(['valid123' => 'value']))->not->toThrow(InvalidArgumentException::class);
expect(fn () => DocumentData::fromArray(['a' => 'value']))->not->toThrow(InvalidArgumentException::class);
});
it('validates field values for serializability', function () {
$resource = fopen('php://memory', 'r');
expect(fn () => DocumentData::fromArray(['resource' => $resource]))->toThrow(InvalidArgumentException::class);
fclose($resource);
});
it('allows serializable values', function () {
$data = DocumentData::fromArray([
'string' => 'text',
'int' => 123,
'float' => 12.34,
'bool' => true,
'null' => null,
'array' => [1, 2, 3],
]);
expect($data->toArray())->toHaveCount(6);
});
});

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Search\ValueObjects;
use App\Framework\Search\ValueObjects\EntityId;
use InvalidArgumentException;
describe('EntityId Value Object', function () {
it('can be created with valid ID', function () {
$id = new EntityId('user_123');
expect($id->value)->toBe('user_123');
});
it('can be created from string', function () {
$id = EntityId::fromString('product_456');
expect($id->toString())->toBe('product_456');
});
it('can generate unique ID', function () {
$id1 = EntityId::generate();
$id2 = EntityId::generate();
expect($id1->toString())->not->toBe($id2->toString());
expect($id1->toString())->toStartWith('entity_');
});
it('converts to string', function () {
$id = new EntityId('test_789');
expect($id->__toString())->toBe('test_789');
});
it('compares equality correctly', function () {
$id1 = new EntityId('same_id');
$id2 = new EntityId('same_id');
$id3 = new EntityId('different_id');
expect($id1->equals($id2))->toBeTrue();
expect($id1->equals($id3))->toBeFalse();
});
it('throws exception for empty ID', function () {
expect(fn () => new EntityId(''))->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityId(' '))->toThrow(InvalidArgumentException::class);
});
it('throws exception for too long ID', function () {
$longId = str_repeat('a', 256);
expect(fn () => new EntityId($longId))->toThrow(InvalidArgumentException::class);
});
it('throws exception for invalid characters', function () {
expect(fn () => new EntityId('invalid@id'))->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityId('invalid id'))->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityId('invalid/id'))->toThrow(InvalidArgumentException::class);
});
it('allows valid characters', function () {
expect(fn () => new EntityId('valid_id.123-test'))->not->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityId('123'))->not->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityId('a'))->not->toThrow(InvalidArgumentException::class);
});
});

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\Search\ValueObjects;
use App\Framework\Search\ValueObjects\EntityType;
use InvalidArgumentException;
describe('EntityType Value Object', function () {
it('can be created with valid type', function () {
$type = new EntityType('user');
expect($type->value)->toBe('user');
});
it('provides common entity types', function () {
expect(EntityType::user()->toString())->toBe('user');
expect(EntityType::product()->toString())->toBe('product');
expect(EntityType::article()->toString())->toBe('article');
expect(EntityType::category()->toString())->toBe('category');
expect(EntityType::order()->toString())->toBe('order');
});
it('can be created from string', function () {
$type = EntityType::fromString('custom_type');
expect($type->toString())->toBe('custom_type');
});
it('converts to string', function () {
$type = new EntityType('product');
expect($type->__toString())->toBe('product');
});
it('generates index name', function () {
$type = new EntityType('user');
expect($type->getIndexName())->toBe('search_user');
});
it('compares equality correctly', function () {
$type1 = new EntityType('user');
$type2 = new EntityType('user');
$type3 = new EntityType('product');
expect($type1->equals($type2))->toBeTrue();
expect($type1->equals($type3))->toBeFalse();
});
it('throws exception for empty type', function () {
expect(fn () => new EntityType(''))->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityType(' '))->toThrow(InvalidArgumentException::class);
});
it('throws exception for too long type', function () {
$longType = str_repeat('a', 101);
expect(fn () => new EntityType($longType))->toThrow(InvalidArgumentException::class);
});
it('throws exception for invalid format', function () {
expect(fn () => new EntityType('User'))->toThrow(InvalidArgumentException::class); // uppercase
expect(fn () => new EntityType('user-type'))->toThrow(InvalidArgumentException::class); // hyphen
expect(fn () => new EntityType('user type'))->toThrow(InvalidArgumentException::class); // space
expect(fn () => new EntityType('1user'))->toThrow(InvalidArgumentException::class); // starts with number
});
it('allows valid format', function () {
expect(fn () => new EntityType('user'))->not->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityType('user_type'))->not->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityType('user123'))->not->toThrow(InvalidArgumentException::class);
expect(fn () => new EntityType('a'))->not->toThrow(InvalidArgumentException::class);
});
});