feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
use App\Framework\OutputBuffer\BufferHandle;
use App\Framework\OutputBuffer\OutputBuffer;
use App\Framework\OutputBuffer\OutputBufferConfig;
use App\Framework\OutputBuffer\OutputBufferException;
describe('BufferHandle', function () {
beforeEach(function () {
while (ob_get_level() > 0) {
ob_end_clean();
}
});
afterEach(function () {
while (ob_get_level() > 0) {
ob_end_clean();
}
});
describe('clean()', function () {
it('cleans buffer and returns content', function () {
$handle = OutputBuffer::start();
echo "Test content";
$content = $handle->clean();
expect($content)->toBe("Test content");
expect(ob_get_level())->toBe(0);
});
it('throws exception if buffer level mismatch', function () {
$handle = OutputBuffer::start();
ob_start(); // Create additional buffer
expect(fn() => $handle->clean())
->toThrow(OutputBufferException::class);
ob_end_clean();
$handle->end();
});
});
describe('end()', function () {
it('ends buffer without returning content', function () {
$handle = OutputBuffer::start();
echo "Test content";
$handle->end();
expect(ob_get_level())->toBe(0);
});
it('validates buffer level before ending', function () {
$handle = OutputBuffer::start();
$handle->end();
expect(fn() => $handle->end())
->toThrow(OutputBufferException::class);
});
});
describe('flush()', function () {
it('flushes buffer contents', function () {
$handle = OutputBuffer::start();
echo "Test";
// Flush should work (though output goes nowhere in tests)
expect(fn() => $handle->flush())->not->toThrow(Exception::class);
$handle->end();
});
});
describe('getContents()', function () {
it('gets contents without clearing buffer', function () {
$handle = OutputBuffer::start();
echo "Test";
$content = $handle->getContents();
expect($content)->toBe("Test");
expect($handle->isActive())->toBeTrue();
$handle->end();
});
it('can be called multiple times', function () {
$handle = OutputBuffer::start();
echo "Test";
$content1 = $handle->getContents();
echo " More";
$content2 = $handle->getContents();
expect($content1)->toBe("Test");
expect($content2)->toBe("Test More");
$handle->end();
});
});
describe('getLength()', function () {
it('returns buffer content length', function () {
$handle = OutputBuffer::start();
expect($handle->getLength())->toBe(0);
echo "Test";
expect($handle->getLength())->toBe(4);
echo "ing";
expect($handle->getLength())->toBe(7);
$handle->end();
});
});
describe('isActive()', function () {
it('returns true when buffer is active', function () {
$handle = OutputBuffer::start();
expect($handle->isActive())->toBeTrue();
$handle->end();
});
it('returns false after buffer is cleaned', function () {
$handle = OutputBuffer::start();
$handle->end();
expect($handle->isActive())->toBeFalse();
});
});
describe('level validation', function () {
it('validates buffer level for all operations', function () {
$handle = OutputBuffer::start();
$handle->end();
// All operations should fail after buffer is ended
expect(fn() => $handle->clean())->toThrow(OutputBufferException::class);
expect(fn() => $handle->getContents())->toThrow(OutputBufferException::class);
expect(fn() => $handle->flush())->toThrow(OutputBufferException::class);
});
it('detects level mismatch', function () {
$handle1 = OutputBuffer::start();
$handle2 = OutputBuffer::start();
// Try to clean handle1 while handle2 is active
expect(fn() => $handle1->clean())
->toThrow(OutputBufferException::class);
$handle2->end();
$handle1->end();
});
});
});

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
use App\Framework\OutputBuffer\CapturedOutput;
describe('CapturedOutput', function () {
it('stores content and result', function () {
$captured = new CapturedOutput(
content: "test output",
result: 42,
level: 1
);
expect($captured->content)->toBe("test output");
expect($captured->result)->toBe(42);
expect($captured->level)->toBe(1);
});
describe('isEmpty()', function () {
it('returns true for empty content', function () {
$captured = new CapturedOutput("", null, 1);
expect($captured->isEmpty())->toBeTrue();
});
it('returns true for whitespace-only content', function () {
$captured = new CapturedOutput(" \n\t ", null, 1);
expect($captured->isEmpty())->toBeTrue();
});
it('returns false for non-empty content', function () {
$captured = new CapturedOutput("test", null, 1);
expect($captured->isEmpty())->toBeFalse();
});
});
describe('hasContent()', function () {
it('returns false for empty content', function () {
$captured = new CapturedOutput("", null, 1);
expect($captured->hasContent())->toBeFalse();
});
it('returns true for non-empty content', function () {
$captured = new CapturedOutput("test", null, 1);
expect($captured->hasContent())->toBeTrue();
});
});
describe('lines()', function () {
it('returns empty array for empty content', function () {
$captured = new CapturedOutput("", null, 1);
expect($captured->lines())->toBe([]);
});
it('splits content into lines', function () {
$captured = new CapturedOutput("Line 1\nLine 2\nLine 3", null, 1);
expect($captured->lines())->toBe(['Line 1', 'Line 2', 'Line 3']);
});
it('filters empty lines', function () {
$captured = new CapturedOutput("Line 1\n\nLine 2\n\n", null, 1);
expect($captured->lines())->toBe(['Line 1', 'Line 2']);
});
it('trims whitespace from lines', function () {
$captured = new CapturedOutput(" Line 1 \n Line 2 ", null, 1);
expect($captured->lines())->toBe(['Line 1', 'Line 2']);
});
});
describe('lineCount()', function () {
it('returns 0 for empty content', function () {
$captured = new CapturedOutput("", null, 1);
expect($captured->lineCount())->toBe(0);
});
it('returns correct line count', function () {
$captured = new CapturedOutput("Line 1\nLine 2\nLine 3", null, 1);
expect($captured->lineCount())->toBe(3);
});
it('excludes empty lines from count', function () {
$captured = new CapturedOutput("Line 1\n\n\nLine 2", null, 1);
expect($captured->lineCount())->toBe(2);
});
});
describe('length()', function () {
it('returns 0 for empty content', function () {
$captured = new CapturedOutput("", null, 1);
expect($captured->length())->toBe(0);
});
it('returns byte length of content', function () {
$captured = new CapturedOutput("test", null, 1);
expect($captured->length())->toBe(4);
});
it('counts multibyte characters correctly', function () {
$captured = new CapturedOutput("café", null, 1);
expect($captured->length())->toBe(5); // UTF-8 encoding
});
});
describe('contains()', function () {
it('returns false for empty content', function () {
$captured = new CapturedOutput("", null, 1);
expect($captured->contains("test"))->toBeFalse();
});
it('returns true when content contains string', function () {
$captured = new CapturedOutput("Hello World", null, 1);
expect($captured->contains("World"))->toBeTrue();
});
it('returns false when content does not contain string', function () {
$captured = new CapturedOutput("Hello World", null, 1);
expect($captured->contains("Goodbye"))->toBeFalse();
});
it('is case-sensitive', function () {
$captured = new CapturedOutput("Hello World", null, 1);
expect($captured->contains("world"))->toBeFalse();
});
});
describe('trimmed()', function () {
it('returns empty string for empty content', function () {
$captured = new CapturedOutput("", null, 1);
expect($captured->trimmed())->toBe("");
});
it('trims whitespace from content', function () {
$captured = new CapturedOutput(" test \n", null, 1);
expect($captured->trimmed())->toBe("test");
});
it('does not modify original content', function () {
$captured = new CapturedOutput(" test ", null, 1);
$trimmed = $captured->trimmed();
expect($captured->content)->toBe(" test ");
expect($trimmed)->toBe("test");
});
});
describe('result handling', function () {
it('stores null result', function () {
$captured = new CapturedOutput("test", null, 1);
expect($captured->result)->toBeNull();
});
it('stores scalar result', function () {
$captured = new CapturedOutput("test", 42, 1);
expect($captured->result)->toBe(42);
});
it('stores array result', function () {
$captured = new CapturedOutput("test", ['a' => 1], 1);
expect($captured->result)->toBe(['a' => 1]);
});
it('stores object result', function () {
$obj = new stdClass();
$captured = new CapturedOutput("test", $obj, 1);
expect($captured->result)->toBe($obj);
});
});
});

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
use App\Framework\OutputBuffer\OutputBufferFlag;
use App\Framework\OutputBuffer\OutputBufferFlags;
describe('OutputBufferFlags', function () {
describe('variadic constructor', function () {
it('accepts no flags', function () {
$flags = new OutputBufferFlags();
expect($flags->toBitmask())->toBe(0);
});
it('accepts single flag', function () {
$flags = new OutputBufferFlags(OutputBufferFlag::CLEANABLE);
expect($flags->has(OutputBufferFlag::CLEANABLE))->toBeTrue();
});
it('accepts multiple flags', function () {
$flags = new OutputBufferFlags(
OutputBufferFlag::CLEANABLE,
OutputBufferFlag::FLUSHABLE
);
expect($flags->has(OutputBufferFlag::CLEANABLE))->toBeTrue();
expect($flags->has(OutputBufferFlag::FLUSHABLE))->toBeTrue();
});
});
describe('factory methods', function () {
it('creates standard flags', function () {
$flags = OutputBufferFlags::standard();
expect($flags->has(OutputBufferFlag::STDFLAGS))->toBeTrue();
});
it('creates default flags', function () {
$flags = OutputBufferFlags::default();
expect($flags->has(OutputBufferFlag::CLEANABLE))->toBeTrue();
});
it('creates cleanable flags', function () {
$flags = OutputBufferFlags::cleanable();
expect($flags->has(OutputBufferFlag::CLEANABLE))->toBeTrue();
});
it('creates cleanable and flushable flags', function () {
$flags = OutputBufferFlags::cleanableAndFlushable();
expect($flags->has(OutputBufferFlag::CLEANABLE))->toBeTrue();
expect($flags->has(OutputBufferFlag::FLUSHABLE))->toBeTrue();
});
});
describe('toBitmask()', function () {
it('returns 0 for no flags', function () {
$flags = new OutputBufferFlags();
expect($flags->toBitmask())->toBe(0);
});
it('returns correct value for single flag', function () {
$flags = new OutputBufferFlags(OutputBufferFlag::CLEANABLE);
expect($flags->toBitmask())->toBe(PHP_OUTPUT_HANDLER_CLEANABLE);
});
it('combines multiple flags with OR', function () {
$flags = new OutputBufferFlags(
OutputBufferFlag::CLEANABLE,
OutputBufferFlag::FLUSHABLE
);
$expected = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_FLUSHABLE;
expect($flags->toBitmask())->toBe($expected);
});
it('handles STDFLAGS correctly', function () {
$flags = new OutputBufferFlags(OutputBufferFlag::STDFLAGS);
expect($flags->toBitmask())->toBe(PHP_OUTPUT_HANDLER_STDFLAGS);
});
});
describe('has()', function () {
it('returns false when flag is not set', function () {
$flags = new OutputBufferFlags(OutputBufferFlag::CLEANABLE);
expect($flags->has(OutputBufferFlag::FLUSHABLE))->toBeFalse();
});
it('returns true when flag is set', function () {
$flags = new OutputBufferFlags(OutputBufferFlag::CLEANABLE);
expect($flags->has(OutputBufferFlag::CLEANABLE))->toBeTrue();
});
it('works with multiple flags', function () {
$flags = new OutputBufferFlags(
OutputBufferFlag::CLEANABLE,
OutputBufferFlag::FLUSHABLE,
OutputBufferFlag::REMOVABLE
);
expect($flags->has(OutputBufferFlag::CLEANABLE))->toBeTrue();
expect($flags->has(OutputBufferFlag::FLUSHABLE))->toBeTrue();
expect($flags->has(OutputBufferFlag::REMOVABLE))->toBeTrue();
});
});
describe('toArray()', function () {
it('returns empty array for no flags', function () {
$flags = new OutputBufferFlags();
expect($flags->toArray())->toBe([]);
});
it('returns all flags as array', function () {
$flags = new OutputBufferFlags(
OutputBufferFlag::CLEANABLE,
OutputBufferFlag::FLUSHABLE
);
$array = $flags->toArray();
expect($array)->toHaveCount(2);
expect($array)->toContain(OutputBufferFlag::CLEANABLE);
expect($array)->toContain(OutputBufferFlag::FLUSHABLE);
});
});
});

View File

@@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
use App\Framework\OutputBuffer\OutputBuffer;
use App\Framework\OutputBuffer\OutputBufferConfig;
use App\Framework\OutputBuffer\OutputBufferException;
use App\Framework\OutputBuffer\OutputBufferFlag;
use App\Framework\OutputBuffer\OutputBufferFlags;
use App\Framework\OutputBuffer\CapturedOutput;
describe('OutputBuffer', function () {
beforeEach(function () {
// Ensure clean state
while (ob_get_level() > 0) {
ob_end_clean();
}
});
afterEach(function () {
// Cleanup after tests
while (ob_get_level() > 0) {
ob_end_clean();
}
});
describe('capture()', function () {
it('captures output from callback', function () {
$captured = OutputBuffer::capture(function () {
echo "Hello World";
return 42;
});
expect($captured)->toBeInstanceOf(CapturedOutput::class);
expect($captured->content)->toBe("Hello World");
expect($captured->result)->toBe(42);
});
it('captures empty output', function () {
$captured = OutputBuffer::capture(function () {
return "result";
});
expect($captured->content)->toBe("");
expect($captured->result)->toBe("result");
expect($captured->isEmpty())->toBeTrue();
});
it('captures multiline output', function () {
$captured = OutputBuffer::capture(function () {
echo "Line 1\n";
echo "Line 2\n";
echo "Line 3";
});
expect($captured->content)->toBe("Line 1\nLine 2\nLine 3");
expect($captured->lineCount())->toBe(3);
expect($captured->lines())->toBe(['Line 1', 'Line 2', 'Line 3']);
});
it('cleans up buffer on success', function () {
$levelBefore = ob_get_level();
OutputBuffer::capture(function () {
echo "test";
});
expect(ob_get_level())->toBe($levelBefore);
});
it('cleans up buffer on exception', function () {
$levelBefore = ob_get_level();
try {
OutputBuffer::capture(function () {
echo "test";
throw new RuntimeException('Test exception');
});
} catch (RuntimeException $e) {
// Expected exception
}
expect(ob_get_level())->toBe($levelBefore);
});
it('supports custom configuration', function () {
$callbackExecuted = false;
$config = OutputBufferConfig::withCallback(function (string $buffer) use (&$callbackExecuted) {
$callbackExecuted = true;
return strtoupper($buffer);
});
$captured = OutputBuffer::capture(function () {
echo "hello";
}, $config);
expect($callbackExecuted)->toBeTrue();
expect($captured->content)->toBe("HELLO");
});
it('handles nested captures', function () {
$captured1 = OutputBuffer::capture(function () {
echo "Outer Start\n";
$captured2 = OutputBuffer::capture(function () {
echo "Inner";
return "inner-result";
});
echo $captured2->content . " processed\n";
echo "Outer End";
return "outer-result";
});
expect($captured1->content)->toBe("Outer Start\nInner processed\nOuter End");
expect($captured1->result)->toBe("outer-result");
});
});
describe('start()', function () {
it('starts new buffer and returns handle', function () {
$levelBefore = ob_get_level();
$handle = OutputBuffer::start();
expect(ob_get_level())->toBe($levelBefore + 1);
expect($handle->level)->toBe($levelBefore + 1);
expect($handle->isActive())->toBeTrue();
$handle->end();
});
it('allows manual buffer management', function () {
$handle = OutputBuffer::start();
echo "Test output";
$content = $handle->clean();
expect($content)->toBe("Test output");
expect(ob_get_level())->toBe(0);
});
it('supports custom configuration', function () {
$config = OutputBufferConfig::forCapture();
$handle = OutputBuffer::start($config);
expect($handle->config)->toBe($config);
$handle->end();
});
});
describe('getCurrentLevel()', function () {
it('returns current buffer level', function () {
expect(OutputBuffer::getCurrentLevel())->toBe(0);
ob_start();
expect(OutputBuffer::getCurrentLevel())->toBe(1);
ob_start();
expect(OutputBuffer::getCurrentLevel())->toBe(2);
ob_end_clean();
ob_end_clean();
});
});
describe('isActive()', function () {
it('returns false when no buffers active', function () {
expect(OutputBuffer::isActive())->toBeFalse();
});
it('returns true when buffer is active', function () {
ob_start();
expect(OutputBuffer::isActive())->toBeTrue();
ob_end_clean();
});
});
describe('cleanAll()', function () {
it('cleans all active buffers', function () {
ob_start();
ob_start();
ob_start();
expect(ob_get_level())->toBe(3);
$count = OutputBuffer::cleanAll();
expect($count)->toBe(3);
expect(ob_get_level())->toBe(0);
});
it('returns 0 when no buffers active', function () {
$count = OutputBuffer::cleanAll();
expect($count)->toBe(0);
});
});
describe('getStatus()', function () {
it('returns status of all buffers', function () {
$status = OutputBuffer::getStatus();
expect($status)->toBeArray();
});
it('shows buffer information when active', function () {
ob_start();
$status = OutputBuffer::getStatus();
expect($status)->toBeArray();
expect($status)->toHaveCount(1);
ob_end_clean();
});
});
describe('withoutBuffering()', function () {
it('executes callback without buffering', function () {
$executedWithoutBuffer = false;
OutputBuffer::capture(function () use (&$executedWithoutBuffer) {
OutputBuffer::withoutBuffering(function () use (&$executedWithoutBuffer) {
$executedWithoutBuffer = ob_get_level() === 0;
});
});
expect($executedWithoutBuffer)->toBeTrue();
});
it('restores buffers after callback', function () {
$captured = OutputBuffer::capture(function () {
echo "Before";
OutputBuffer::withoutBuffering(function () {
// This output should not be captured
echo "During";
});
echo "After";
});
expect($captured->content)->toBe("BeforeAfter");
});
it('returns callback result', function () {
$result = OutputBuffer::withoutBuffering(function () {
return 42;
});
expect($result)->toBe(42);
});
});
describe('exception handling', function () {
it('throws exception when capture fails', function () {
// This is hard to test as ob_start rarely fails
// but we test the exception exists
expect(OutputBufferException::failedToCapture())
->toBeInstanceOf(OutputBufferException::class);
});
it('propagates callback exceptions', function () {
expect(function () {
OutputBuffer::capture(function () {
throw new RuntimeException('Test');
});
})->toThrow(RuntimeException::class);
});
});
});