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,345 @@
<?php
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Ulid\Ulid;
describe('JobId Value Object', function () {
describe('Creation and Validation', function () {
it('can generate unique JobIds', function () {
$id1 = JobId::generate();
$id2 = JobId::generate();
expect($id1)->toBeInstanceOf(JobId::class);
expect($id2)->toBeInstanceOf(JobId::class);
expect($id1->toString())->not->toBe($id2->toString());
});
it('can create from string', function () {
$idString = 'job_test_123';
$jobId = JobId::fromString($idString);
expect($jobId->toString())->toBe($idString);
expect($jobId->getValue())->toBe($idString);
});
it('can create from ULID object', function () {
$ulid = Ulid::generate();
$jobId = JobId::fromUlid($ulid);
expect($jobId->toString())->toBe($ulid->toString());
expect($jobId->toUlid()->toString())->toBe($ulid->toString());
});
it('rejects empty JobId', function () {
expect(fn() => JobId::fromString(''))
->toThrow(\InvalidArgumentException::class, 'JobId cannot be empty');
});
it('validates JobId format correctly', function () {
// Valid formats should work
expect(fn() => JobId::fromString('job_12345'))->not->toThrow();
expect(fn() => JobId::fromString('01FXYZ0123456789ABCDEF1234'))->not->toThrow(); // ULID format
expect(fn() => JobId::fromString('simple-id'))->not->toThrow();
// Any non-empty string is currently accepted
expect(fn() => JobId::fromString('a'))->not->toThrow();
expect(fn() => JobId::fromString('very-long-job-identifier-12345'))->not->toThrow();
});
it('is readonly and immutable', function () {
$jobId = JobId::fromString('test-job-123');
// Verify the class is readonly
$reflection = new ReflectionClass($jobId);
expect($reflection->isReadOnly())->toBeTrue();
// The value property should be readonly
$valueProperty = $reflection->getProperty('value');
expect($valueProperty->isReadOnly())->toBeTrue();
});
});
describe('String Representation', function () {
it('toString() returns the internal value', function () {
$value = 'test-job-456';
$jobId = JobId::fromString($value);
expect($jobId->toString())->toBe($value);
});
it('getValue() is alias for toString()', function () {
$value = 'another-test-job';
$jobId = JobId::fromString($value);
expect($jobId->getValue())->toBe($jobId->toString());
expect($jobId->getValue())->toBe($value);
});
it('__toString() magic method works', function () {
$value = 'magic-method-test';
$jobId = JobId::fromString($value);
expect((string) $jobId)->toBe($value);
expect("Job ID: {$jobId}")->toBe("Job ID: {$value}");
});
it('jsonSerialize() returns string value', function () {
$value = 'json-test-job';
$jobId = JobId::fromString($value);
expect($jobId->jsonSerialize())->toBe($value);
expect(json_encode($jobId))->toBe('"' . $value . '"');
});
});
describe('Equality and Comparison', function () {
it('equals() compares JobIds correctly', function () {
$id1 = JobId::fromString('same-id');
$id2 = JobId::fromString('same-id');
$id3 = JobId::fromString('different-id');
expect($id1->equals($id2))->toBeTrue();
expect($id1->equals($id3))->toBeFalse();
expect($id2->equals($id3))->toBeFalse();
});
it('isBefore() and isAfter() compare string values', function () {
$idA = JobId::fromString('aaa');
$idB = JobId::fromString('bbb');
$idC = JobId::fromString('ccc');
expect($idA->isBefore($idB))->toBeTrue();
expect($idB->isBefore($idC))->toBeTrue();
expect($idA->isBefore($idC))->toBeTrue();
expect($idC->isAfter($idB))->toBeTrue();
expect($idB->isAfter($idA))->toBeTrue();
expect($idC->isAfter($idA))->toBeTrue();
expect($idB->isBefore($idA))->toBeFalse();
expect($idA->isAfter($idB))->toBeFalse();
});
it('comparison works with numeric-like strings', function () {
$id1 = JobId::fromString('job_001');
$id2 = JobId::fromString('job_002');
$id10 = JobId::fromString('job_010');
expect($id1->isBefore($id2))->toBeTrue();
expect($id2->isBefore($id10))->toBeTrue();
expect($id10->isAfter($id1))->toBeTrue();
});
});
describe('ULID Integration', function () {
it('can convert to ULID when valid format', function () {
$originalUlid = Ulid::generate();
$jobId = JobId::fromUlid($originalUlid);
$convertedUlid = $jobId->toUlid();
expect($convertedUlid->toString())->toBe($originalUlid->toString());
});
it('getTimestamp() extracts timestamp from ULID', function () {
$ulid = Ulid::generate();
$jobId = JobId::fromUlid($ulid);
$timestamp = $jobId->getTimestamp();
expect($timestamp)->toBeInstanceOf(\DateTimeImmutable::class);
// Should be very recent
$now = new \DateTimeImmutable();
$diff = $now->getTimestamp() - $timestamp->getTimestamp();
expect($diff)->toBeLessThan(5); // Within 5 seconds
});
it('generateForQueue() creates ULID-based JobId', function () {
$jobId = JobId::generateForQueue('email-queue');
expect($jobId)->toBeInstanceOf(JobId::class);
expect(strlen($jobId->toString()))->toBe(26); // ULID length
});
it('getTimePrefix() extracts time portion', function () {
$jobId = JobId::generateForQueue('test-queue');
$timePrefix = $jobId->getTimePrefix();
expect($timePrefix)->toBeString();
expect(strlen($timePrefix))->toBe(10);
});
it('getRandomSuffix() extracts random portion', function () {
$jobId = JobId::generateForQueue('test-queue');
$randomSuffix = $jobId->getRandomSuffix();
expect($randomSuffix)->toBeString();
expect(strlen($randomSuffix))->toBe(16);
});
});
describe('Edge Cases and Error Handling', function () {
it('handles very long job IDs', function () {
$longId = str_repeat('a', 1000);
$jobId = JobId::fromString($longId);
expect($jobId->toString())->toBe($longId);
expect(strlen($jobId->toString()))->toBe(1000);
});
it('handles special characters in job IDs', function () {
$specialId = 'job-with_special.chars@123!';
$jobId = JobId::fromString($specialId);
expect($jobId->toString())->toBe($specialId);
});
it('handles unicode characters', function () {
$unicodeId = 'job-测试-🚀-123';
$jobId = JobId::fromString($unicodeId);
expect($jobId->toString())->toBe($unicodeId);
});
it('toUlid() may fail for non-ULID format strings', function () {
$nonUlidJobId = JobId::fromString('not-a-ulid-format');
// This should throw an exception since it's not a valid ULID
expect(fn() => $nonUlidJobId->toUlid())
->toThrow();
});
it('getTimestamp() may fail for non-ULID format', function () {
$nonUlidJobId = JobId::fromString('simple-job-id');
// This should throw an exception since it's not a valid ULID
expect(fn() => $nonUlidJobId->getTimestamp())
->toThrow();
});
});
describe('Performance and Uniqueness', function () {
it('generates unique IDs in rapid succession', function () {
$ids = [];
$count = 1000;
$start = microtime(true);
for ($i = 0; $i < $count; $i++) {
$id = JobId::generate();
$ids[] = $id->toString();
}
$elapsed = microtime(true) - $start;
// All IDs should be unique
$unique = array_unique($ids);
expect(count($unique))->toBe($count);
// Should generate quickly
expect($elapsed)->toBeLessThan(1.0); // Within 1 second
});
it('ULID-based IDs are time-ordered', function () {
$ids = [];
// Generate several IDs with small delays
for ($i = 0; $i < 5; $i++) {
$ids[] = JobId::generateForQueue('test');
if ($i < 4) {
usleep(1000); // 1ms delay
}
}
// Each ID should be "after" the previous one (time-ordered)
for ($i = 1; $i < count($ids); $i++) {
expect($ids[$i]->isAfter($ids[$i - 1]))->toBeTrue();
}
});
it('maintains consistent string representation', function () {
$jobId = JobId::fromString('consistent-test');
// Multiple calls should return the same result
expect($jobId->toString())->toBe($jobId->toString());
expect($jobId->getValue())->toBe($jobId->toString());
expect((string) $jobId)->toBe($jobId->toString());
expect($jobId->jsonSerialize())->toBe($jobId->toString());
});
});
});
describe('JobId in Queue Context', function () {
it('can be used as array keys', function () {
$id1 = JobId::fromString('job-1');
$id2 = JobId::fromString('job-2');
$jobs = [];
$jobs[$id1->toString()] = 'First Job';
$jobs[$id2->toString()] = 'Second Job';
expect($jobs[$id1->toString()])->toBe('First Job');
expect($jobs[$id2->toString()])->toBe('Second Job');
expect(count($jobs))->toBe(2);
});
it('works with job tracking scenarios', function () {
$processingJobs = [];
$completedJobs = [];
// Simulate job lifecycle
$jobId = JobId::generate();
// Job starts processing
$processingJobs[$jobId->toString()] = time();
expect(isset($processingJobs[$jobId->toString()]))->toBeTrue();
// Job completes
$completedJobs[$jobId->toString()] = [
'started_at' => $processingJobs[$jobId->toString()],
'completed_at' => time(),
'status' => 'success'
];
unset($processingJobs[$jobId->toString()]);
expect(isset($processingJobs[$jobId->toString()]))->toBeFalse();
expect(isset($completedJobs[$jobId->toString()]))->toBeTrue();
expect($completedJobs[$jobId->toString()]['status'])->toBe('success');
});
it('demonstrates time-based job identification', function () {
// Generate jobs for different queues
$emailJobId = JobId::generateForQueue('email');
$reportJobId = JobId::generateForQueue('reports');
$backgroundJobId = JobId::generateForQueue('background');
// All should be unique
expect($emailJobId->toString())->not->toBe($reportJobId->toString());
expect($reportJobId->toString())->not->toBe($backgroundJobId->toString());
expect($backgroundJobId->toString())->not->toBe($emailJobId->toString());
// All should have ULID format (26 characters)
expect(strlen($emailJobId->toString()))->toBe(26);
expect(strlen($reportJobId->toString()))->toBe(26);
expect(strlen($backgroundJobId->toString()))->toBe(26);
});
it('supports job priority scenarios', function () {
// Generate jobs with time component
$urgentJob = JobId::generateForQueue('urgent');
sleep(1); // Ensure different timestamp
$normalJob = JobId::generateForQueue('normal');
// Later job should have later timestamp
expect($normalJob->isAfter($urgentJob))->toBeTrue();
// Can use timestamps for ordering
$urgentTime = $urgentJob->getTimestamp();
$normalTime = $normalJob->getTimestamp();
expect($normalTime > $urgentTime)->toBeTrue();
});
});

View File

@@ -0,0 +1,484 @@
<?php
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Queue\ValueObjects\JobMetadata;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Retry\Strategies\ExponentialBackoffStrategy;
use App\Framework\Retry\Strategies\FixedDelayStrategy;
describe('JobPayload Value Object', function () {
beforeEach(function () {
$this->simpleJob = new class {
public function handle(): string {
return 'executed';
}
};
$this->complexJob = new class {
public function __construct(
public string $id = 'test-123',
public array $data = ['key' => 'value']
) {}
public function process(): array {
return $this->data;
}
};
});
describe('Basic Construction', function () {
it('can be created with minimal parameters', function () {
$payload = JobPayload::create($this->simpleJob);
expect($payload->job)->toBe($this->simpleJob);
expect($payload->priority->isNormal())->toBeTrue();
expect($payload->delay->toSeconds())->toBe(0);
expect($payload->timeout)->toBeNull();
expect($payload->retryStrategy)->toBeNull();
expect($payload->metadata)->not->toBeNull();
});
it('accepts all configuration parameters', function () {
$priority = QueuePriority::high();
$delay = Duration::fromMinutes(5);
$timeout = Duration::fromSeconds(30);
$retryStrategy = new ExponentialBackoffStrategy(maxAttempts: 3);
$metadata = JobMetadata::create(['user_id' => 123]);
$payload = JobPayload::create(
$this->complexJob,
$priority,
$delay,
$timeout,
$retryStrategy,
$metadata
);
expect($payload->job)->toBe($this->complexJob);
expect($payload->priority)->toBe($priority);
expect($payload->delay)->toBe($delay);
expect($payload->timeout)->toBe($timeout);
expect($payload->retryStrategy)->toBe($retryStrategy);
expect($payload->metadata)->toBe($metadata);
});
it('is immutable - readonly properties cannot be changed', function () {
$payload = JobPayload::create($this->simpleJob);
// This would cause a PHP error if attempted:
// $payload->job = new stdClass(); // Fatal error: Cannot modify readonly property
// $payload->priority = QueuePriority::high(); // Fatal error: Cannot modify readonly property
// Test that the properties are indeed readonly
$reflection = new ReflectionClass($payload);
foreach (['job', 'priority', 'delay', 'timeout', 'retryStrategy', 'metadata'] as $prop) {
$property = $reflection->getProperty($prop);
expect($property->isReadOnly())->toBeTrue("Property {$prop} should be readonly");
}
});
});
describe('Factory Methods', function () {
it('creates immediate jobs with high priority and no delay', function () {
$payload = JobPayload::immediate($this->simpleJob);
expect($payload->priority->isHigh())->toBeTrue();
expect($payload->delay->toSeconds())->toBe(0);
expect($payload->isReady())->toBeTrue();
});
it('creates delayed jobs with specified delay', function () {
$delay = Duration::fromMinutes(15);
$payload = JobPayload::delayed($this->simpleJob, $delay);
expect($payload->delay)->toBe($delay);
expect($payload->isDelayed())->toBeTrue();
expect($payload->isReady())->toBeFalse();
});
it('creates critical jobs with critical priority and short timeout', function () {
$payload = JobPayload::critical($this->simpleJob);
expect($payload->priority->isCritical())->toBeTrue();
expect($payload->delay->toSeconds())->toBe(0);
expect($payload->timeout->toSeconds())->toBe(30);
expect($payload->hasTimeout())->toBeTrue();
});
it('creates background jobs with low priority and retry strategy', function () {
$payload = JobPayload::background($this->simpleJob);
expect($payload->priority->isLow())->toBeTrue();
expect($payload->timeout->toMinutes())->toBe(30);
expect($payload->hasRetryStrategy())->toBeTrue();
expect($payload->retryStrategy->getMaxAttempts())->toBe(5);
});
});
describe('Immutable Transformations', function () {
beforeEach(function () {
$this->originalPayload = JobPayload::create(
$this->simpleJob,
QueuePriority::normal(),
Duration::zero()
);
});
it('withPriority() creates new instance with different priority', function () {
$newPriority = QueuePriority::high();
$newPayload = $this->originalPayload->withPriority($newPriority);
expect($newPayload)->not->toBe($this->originalPayload);
expect($newPayload->priority)->toBe($newPriority);
expect($newPayload->job)->toBe($this->originalPayload->job);
expect($newPayload->delay)->toBe($this->originalPayload->delay);
// Original should be unchanged
expect($this->originalPayload->priority->isNormal())->toBeTrue();
});
it('withDelay() creates new instance with different delay', function () {
$newDelay = Duration::fromMinutes(10);
$newPayload = $this->originalPayload->withDelay($newDelay);
expect($newPayload)->not->toBe($this->originalPayload);
expect($newPayload->delay)->toBe($newDelay);
expect($newPayload->job)->toBe($this->originalPayload->job);
expect($newPayload->priority)->toBe($this->originalPayload->priority);
// Original should be unchanged
expect($this->originalPayload->delay->toSeconds())->toBe(0);
});
it('withTimeout() creates new instance with timeout', function () {
$timeout = Duration::fromSeconds(45);
$newPayload = $this->originalPayload->withTimeout($timeout);
expect($newPayload)->not->toBe($this->originalPayload);
expect($newPayload->timeout)->toBe($timeout);
expect($newPayload->hasTimeout())->toBeTrue();
// Original should be unchanged
expect($this->originalPayload->timeout)->toBeNull();
});
it('withRetryStrategy() creates new instance with retry strategy', function () {
$retryStrategy = new FixedDelayStrategy(Duration::fromSeconds(30), 3);
$newPayload = $this->originalPayload->withRetryStrategy($retryStrategy);
expect($newPayload)->not->toBe($this->originalPayload);
expect($newPayload->retryStrategy)->toBe($retryStrategy);
expect($newPayload->hasRetryStrategy())->toBeTrue();
// Original should be unchanged
expect($this->originalPayload->retryStrategy)->toBeNull();
});
it('withMetadata() creates new instance with metadata', function () {
$metadata = JobMetadata::create(['source' => 'test', 'version' => '1.0']);
$newPayload = $this->originalPayload->withMetadata($metadata);
expect($newPayload)->not->toBe($this->originalPayload);
expect($newPayload->metadata)->toBe($metadata);
// Original should be unchanged
expect($this->originalPayload->metadata)->not->toBe($metadata);
});
it('can chain multiple transformations', function () {
$finalPayload = $this->originalPayload
->withPriority(QueuePriority::critical())
->withDelay(Duration::fromSeconds(30))
->withTimeout(Duration::fromMinutes(5))
->withRetryStrategy(new ExponentialBackoffStrategy(maxAttempts: 3));
expect($finalPayload->priority->isCritical())->toBeTrue();
expect($finalPayload->delay->toSeconds())->toBe(30);
expect($finalPayload->timeout->toMinutes())->toBe(5);
expect($finalPayload->hasRetryStrategy())->toBeTrue();
// Original should be completely unchanged
expect($this->originalPayload->priority->isNormal())->toBeTrue();
expect($this->originalPayload->delay->toSeconds())->toBe(0);
expect($this->originalPayload->timeout)->toBeNull();
expect($this->originalPayload->retryStrategy)->toBeNull();
});
});
describe('Status Checking Methods', function () {
it('isReady() returns true for jobs with no delay', function () {
$payload = JobPayload::create($this->simpleJob, delay: Duration::zero());
expect($payload->isReady())->toBeTrue();
$immediate = JobPayload::immediate($this->simpleJob);
expect($immediate->isReady())->toBeTrue();
});
it('isReady() returns false for delayed jobs', function () {
$payload = JobPayload::delayed($this->simpleJob, Duration::fromSeconds(30));
expect($payload->isReady())->toBeFalse();
});
it('isDelayed() returns true for jobs with delay', function () {
$payload = JobPayload::delayed($this->simpleJob, Duration::fromMinutes(1));
expect($payload->isDelayed())->toBeTrue();
});
it('isDelayed() returns false for immediate jobs', function () {
$payload = JobPayload::immediate($this->simpleJob);
expect($payload->isDelayed())->toBeFalse();
});
it('hasRetryStrategy() reflects retry strategy presence', function () {
$withoutRetry = JobPayload::create($this->simpleJob);
expect($withoutRetry->hasRetryStrategy())->toBeFalse();
$withRetry = JobPayload::background($this->simpleJob);
expect($withRetry->hasRetryStrategy())->toBeTrue();
});
it('hasTimeout() reflects timeout presence', function () {
$withoutTimeout = JobPayload::create($this->simpleJob);
expect($withoutTimeout->hasTimeout())->toBeFalse();
$withTimeout = JobPayload::critical($this->simpleJob);
expect($withTimeout->hasTimeout())->toBeTrue();
});
});
describe('Time Calculations', function () {
it('getAvailableTime() returns current time for immediate jobs', function () {
$payload = JobPayload::immediate($this->simpleJob);
$available = $payload->getAvailableTime();
$now = time();
expect($available)->toBeGreaterThanOrEqual($now - 1);
expect($available)->toBeLessThanOrEqual($now + 1);
});
it('getAvailableTime() returns future time for delayed jobs', function () {
$delay = Duration::fromSeconds(300); // 5 minutes
$payload = JobPayload::delayed($this->simpleJob, $delay);
$available = $payload->getAvailableTime();
$expected = time() + 300;
expect($available)->toBeGreaterThanOrEqual($expected - 1);
expect($available)->toBeLessThanOrEqual($expected + 1);
});
});
describe('Serialization and Array Conversion', function () {
it('can serialize job objects', function () {
$payload = JobPayload::create($this->complexJob);
$serialized = $payload->serialize();
expect($serialized)->toBeString();
expect(strlen($serialized))->toBeGreaterThan(0);
// Should be able to unserialize
$unserialized = unserialize($serialized);
expect($unserialized)->toBeInstanceOf(get_class($this->complexJob));
expect($unserialized->id)->toBe('test-123');
});
it('getJobClass() returns correct class name', function () {
$payload = JobPayload::create($this->complexJob);
$className = $payload->getJobClass();
expect($className)->toBeString();
expect($className)->toContain('class@anonymous');
});
it('toArray() provides comprehensive job information', function () {
$retryStrategy = new ExponentialBackoffStrategy(maxAttempts: 5);
$payload = JobPayload::create(
$this->complexJob,
QueuePriority::high(),
Duration::fromSeconds(120),
Duration::fromMinutes(10),
$retryStrategy,
JobMetadata::create(['source' => 'api'])
);
$array = $payload->toArray();
expect($array)->toHaveKey('job_class');
expect($array)->toHaveKey('priority');
expect($array)->toHaveKey('priority_value');
expect($array)->toHaveKey('delay_seconds');
expect($array)->toHaveKey('timeout_seconds');
expect($array)->toHaveKey('has_retry_strategy');
expect($array)->toHaveKey('max_attempts');
expect($array)->toHaveKey('available_at');
expect($array)->toHaveKey('metadata');
expect($array['priority'])->toBe('high');
expect($array['priority_value'])->toBe(100);
expect($array['delay_seconds'])->toBe(120);
expect($array['timeout_seconds'])->toBe(600);
expect($array['has_retry_strategy'])->toBeTrue();
expect($array['max_attempts'])->toBe(5);
expect($array['available_at'])->toBeInt();
expect($array['metadata'])->toBeArray();
});
it('toArray() handles null values correctly', function () {
$payload = JobPayload::create($this->simpleJob);
$array = $payload->toArray();
expect($array['timeout_seconds'])->toBeNull();
expect($array['has_retry_strategy'])->toBeFalse();
expect($array['max_attempts'])->toBeNull();
});
});
describe('Edge Cases and Error Handling', function () {
it('maintains object reference integrity', function () {
$job = new stdClass();
$job->property = 'value';
$payload = JobPayload::create($job);
// Same object reference should be maintained
expect($payload->job)->toBe($job);
expect($payload->job->property)->toBe('value');
// Modifying original object should affect payload job (reference)
$job->property = 'modified';
expect($payload->job->property)->toBe('modified');
});
it('handles complex job objects with dependencies', function () {
$complexJob = new class {
public array $config;
public \DateTime $created;
public function __construct() {
$this->config = ['timeout' => 30, 'retries' => 3];
$this->created = new \DateTime();
}
public function getData(): array {
return [
'config' => $this->config,
'created' => $this->created->format('Y-m-d H:i:s')
];
}
};
$payload = JobPayload::create($complexJob);
expect($payload->job->getData())->toBeArray();
expect($payload->job->config['timeout'])->toBe(30);
});
it('preserves metadata across transformations', function () {
$originalMetadata = JobMetadata::create(['initial' => 'data']);
$payload = JobPayload::create($this->simpleJob, metadata: $originalMetadata);
// Transform without changing metadata
$newPayload = $payload->withPriority(QueuePriority::critical());
expect($newPayload->metadata)->toBe($originalMetadata);
});
});
});
describe('JobPayload Integration Scenarios', function () {
beforeEach(function () {
$this->emailJob = new class {
public function __construct(
public string $to = 'test@example.com',
public string $subject = 'Test Email',
public string $body = 'Hello World'
) {}
public function send(): bool {
// Simulate email sending
return true;
}
};
$this->reportJob = new class {
public function __construct(
public array $criteria = ['period' => 'monthly'],
public string $format = 'pdf'
) {}
public function generate(): string {
return "Report generated with format: {$this->format}";
}
};
});
it('handles email job scenarios', function () {
// Immediate notification
$urgent = JobPayload::immediate($this->emailJob);
expect($urgent->priority->isHigh())->toBeTrue();
expect($urgent->isReady())->toBeTrue();
// Delayed newsletter
$newsletter = JobPayload::delayed($this->emailJob, Duration::fromHours(2));
expect($newsletter->isDelayed())->toBeTrue();
// Critical alert with timeout
$alert = JobPayload::critical($this->emailJob);
expect($alert->priority->isCritical())->toBeTrue();
expect($alert->hasTimeout())->toBeTrue();
});
it('handles report generation scenarios', function () {
// Background monthly report
$monthlyReport = JobPayload::background($this->reportJob);
expect($monthlyReport->priority->isLow())->toBeTrue();
expect($monthlyReport->hasRetryStrategy())->toBeTrue();
// Add custom metadata for tracking
$metadata = JobMetadata::create([
'user_id' => 123,
'report_type' => 'financial',
'department' => 'accounting'
]);
$customReport = $monthlyReport->withMetadata($metadata);
expect($customReport->metadata->get('user_id'))->toBe(123);
expect($customReport->metadata->get('report_type'))->toBe('financial');
});
it('handles job priority escalation scenarios', function () {
// Start as normal priority
$job = JobPayload::create($this->emailJob, QueuePriority::normal());
expect($job->priority->isNormal())->toBeTrue();
// Escalate to high priority (customer complaint)
$escalated = $job->withPriority(QueuePriority::high());
expect($escalated->priority->isHigh())->toBeTrue();
// Further escalate to critical (system outage notification)
$critical = $escalated->withPriority(QueuePriority::critical());
expect($critical->priority->isCritical())->toBeTrue();
// Original should remain unchanged
expect($job->priority->isNormal())->toBeTrue();
});
it('demonstrates retry strategy configuration', function () {
// Simple retry for transient failures
$simpleRetry = new FixedDelayStrategy(Duration::fromSeconds(30), 3);
$payload = JobPayload::create($this->emailJob)->withRetryStrategy($simpleRetry);
expect($payload->retryStrategy->getMaxAttempts())->toBe(3);
// Exponential backoff for rate limiting
$exponentialRetry = new ExponentialBackoffStrategy(maxAttempts: 5);
$rateLimitedPayload = $payload->withRetryStrategy($exponentialRetry);
expect($rateLimitedPayload->retryStrategy->getMaxAttempts())->toBe(5);
expect($rateLimitedPayload->retryStrategy)->toBeInstanceOf(ExponentialBackoffStrategy::class);
});
});

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\LockKey;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\QueueName;
describe('LockKey Value Object', function () {
it('can create lock keys from strings', function () {
$key = 'test.lock.key';
$lockKey = LockKey::fromString($key);
expect($lockKey->toString())->toBe($key);
expect((string) $lockKey)->toBe($key);
});
it('validates lock key constraints', function () {
// Empty key
expect(fn() => LockKey::fromString(''))
->toThrow(\InvalidArgumentException::class, 'Lock key cannot be empty');
// Too long
expect(fn() => LockKey::fromString(str_repeat('a', 256)))
->toThrow(\InvalidArgumentException::class, 'Lock key cannot exceed 255 characters');
// Invalid characters
expect(fn() => LockKey::fromString('invalid@key!'))
->toThrow(\InvalidArgumentException::class, 'Lock key contains invalid characters');
expect(fn() => LockKey::fromString('key with spaces'))
->toThrow(\InvalidArgumentException::class, 'Lock key contains invalid characters');
});
it('allows valid characters only', function () {
$validKeys = [
'simple-key',
'key_with_underscores',
'key.with.dots',
'key123',
'UPPERCASE-key',
'mixed-Key_123.test'
];
foreach ($validKeys as $key) {
$lockKey = LockKey::fromString($key);
expect($lockKey->toString())->toBe($key);
}
});
it('can create job-specific lock keys', function () {
$jobId = JobId::generate();
$lockKey = LockKey::forJob($jobId);
expect($lockKey->toString())->toStartWith('job.');
expect($lockKey->toString())->toContain($jobId->toString());
});
it('can create queue-specific lock keys', function () {
$queueName = QueueName::defaultQueue();
$lockKey = LockKey::forQueue($queueName);
expect($lockKey->toString())->toStartWith('queue.');
expect($lockKey->toString())->toContain($queueName->toString());
});
it('can create worker-specific lock keys', function () {
$workerId = WorkerId::generate();
$lockKey = LockKey::forWorker($workerId);
expect($lockKey->toString())->toStartWith('worker.');
expect($lockKey->toString())->toContain($workerId->toString());
});
it('can create resource-specific lock keys', function () {
$lockKey = LockKey::forResource('database', 'user-table');
expect($lockKey->toString())->toBe('database.user-table');
});
it('can create batch-specific lock keys', function () {
$batchId = 'batch-123-abc';
$lockKey = LockKey::forBatch($batchId);
expect($lockKey->toString())->toBe('batch.' . $batchId);
});
it('supports prefix modification', function () {
$lockKey = LockKey::fromString('original.key');
$prefixed = $lockKey->withPrefix('tenant-1');
expect($prefixed->toString())->toBe('tenant-1.original.key');
expect($lockKey->toString())->toBe('original.key'); // Original unchanged
});
it('supports suffix modification', function () {
$lockKey = LockKey::fromString('original.key');
$suffixed = $lockKey->withSuffix('processing');
expect($suffixed->toString())->toBe('original.key.processing');
expect($lockKey->toString())->toBe('original.key'); // Original unchanged
});
it('supports pattern matching', function () {
$lockKey = LockKey::fromString('job.email-queue.123');
expect($lockKey->matches('job.*'))->toBeTrue();
expect($lockKey->matches('job.email-queue.*'))->toBeTrue();
expect($lockKey->matches('worker.*'))->toBeFalse();
expect($lockKey->matches('*.123'))->toBeTrue();
});
it('supports equality comparison', function () {
$key = 'test.lock.key';
$lockKey1 = LockKey::fromString($key);
$lockKey2 = LockKey::fromString($key);
$lockKey3 = LockKey::fromString('different.key');
expect($lockKey1->equals($lockKey2))->toBeTrue();
expect($lockKey1->equals($lockKey3))->toBeFalse();
});
it('supports JSON serialization', function () {
$key = 'serializable.lock.key';
$lockKey = LockKey::fromString($key);
expect($lockKey->jsonSerialize())->toBe($key);
expect(json_encode($lockKey))->toBe('"' . $key . '"');
});
it('can chain modifications', function () {
$lockKey = LockKey::fromString('base.key')
->withPrefix('tenant-1')
->withSuffix('processing')
->withSuffix('active');
expect($lockKey->toString())->toBe('tenant-1.base.key.processing.active');
});
it('handles complex resource hierarchies', function () {
// Simulate nested resource locks
$databaseLock = LockKey::forResource('database', 'users');
$tableLock = $databaseLock->withSuffix('table-lock');
$rowLock = $tableLock->withSuffix('row-123');
expect($databaseLock->toString())->toBe('database.users');
expect($tableLock->toString())->toBe('database.users.table-lock');
expect($rowLock->toString())->toBe('database.users.table-lock.row-123');
// Pattern matching for hierarchical locks
expect($rowLock->matches('database.users.*'))->toBeTrue();
expect($rowLock->matches('*.row-123'))->toBeTrue();
});
});

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\WorkerId;
describe('WorkerId Value Object', function () {
it('can generate unique worker IDs', function () {
$workerId1 = WorkerId::generate();
$workerId2 = WorkerId::generate();
expect($workerId1->toString())->not->toBe($workerId2->toString());
expect($workerId1->equals($workerId2))->toBeFalse();
});
it('can create deterministic IDs for host and process combinations', function () {
$workerId1 = WorkerId::forHost('app-server-1', 1001);
$workerId2 = WorkerId::forHost('app-server-1', 1001);
$workerId3 = WorkerId::forHost('app-server-2', 1001);
// Same host/PID should create different IDs (due to ULID component)
expect($workerId1->toString())->not->toBe($workerId2->toString());
// Different hosts should create different IDs
expect($workerId1->toString())->not->toBe($workerId3->toString());
});
it('can create worker ID from existing string', function () {
$originalId = 'test-worker-id-123';
$workerId = WorkerId::fromString($originalId);
expect($workerId->toString())->toBe($originalId);
expect($workerId->getValue())->toBe($originalId);
});
it('validates worker ID is not empty', function () {
expect(fn() => WorkerId::fromString(''))
->toThrow(\InvalidArgumentException::class, 'WorkerId cannot be empty');
});
it('supports equality comparison', function () {
$id = 'same-worker-id';
$workerId1 = WorkerId::fromString($id);
$workerId2 = WorkerId::fromString($id);
$workerId3 = WorkerId::fromString('different-id');
expect($workerId1->equals($workerId2))->toBeTrue();
expect($workerId1->equals($workerId3))->toBeFalse();
});
it('provides string conversion methods', function () {
$id = 'test-worker-id';
$workerId = WorkerId::fromString($id);
expect($workerId->toString())->toBe($id);
expect($workerId->getValue())->toBe($id);
expect((string) $workerId)->toBe($id);
});
it('supports JSON serialization', function () {
$id = 'json-serializable-worker-id';
$workerId = WorkerId::fromString($id);
expect($workerId->jsonSerialize())->toBe($id);
expect(json_encode($workerId))->toBe('"' . $id . '"');
});
});