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,530 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheResult;
use App\Framework\Core\PathProvider;
use App\Framework\View\Caching\Analysis\CacheStrategy;
use App\Framework\View\Caching\Analysis\SmartTemplateAnalyzer;
use App\Framework\View\Loading\TemplateLoader;
describe('SmartTemplateAnalyzer', function () {
beforeEach(function () {
// Create test templates directory
$this->testDir = '/home/michael/dev/michaelschiemer/tests/tmp/smart_analyzer_' . uniqid();
if (!is_dir($this->testDir)) {
mkdir($this->testDir, 0755, true);
}
// Create real PathProvider
$this->pathProvider = new PathProvider($this->testDir);
// Create mocked Cache
$this->cache = Mockery::mock(Cache::class);
$this->cache->shouldReceive('get')->andReturn(CacheResult::fromItems(CacheItem::miss(CacheKey::fromString('test'))));
$this->cache->shouldReceive('set')->andReturn(true);
$this->cache->shouldReceive('clear')->andReturn(true);
// Create TemplateLoader with test directory as template path
$this->loader = new TemplateLoader(
pathProvider: $this->pathProvider,
cache: $this->cache,
templates: [],
templatePath: '', // Empty to use base path directly
cacheEnabled: false
);
$this->analyzer = new SmartTemplateAnalyzer($this->loader);
});
afterEach(function () {
Mockery::close();
// Clean up test files
if (is_dir($this->testDir)) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->testDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $file) {
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
}
rmdir($this->testDir);
}
});
describe('analyze()', function () {
it('recommends FULL_PAGE strategy for layouts with high static ratio', function () {
$content = '<html><head><title>Site</title></head><body>Mostly static content here</body></html>';
// Create template file
$filePath = $this->testDir . '/layouts/main.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('layouts/main');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FULL_PAGE);
expect($analysis->cacheability->staticContentRatio)->toBeGreaterThan(0.8);
});
it('recommends COMPONENT strategy for component templates', function () {
$content = '<button class="btn">{{ text }}</button>';
// Create template file
$filePath = $this->testDir . '/components/button.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('components/button');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::COMPONENT);
});
it('recommends COMPONENT strategy for partial templates', function () {
$content = '<nav class="main-navigation"><ul class="nav-list">{{ navigation_items }}</ul></nav>';
// Create template file
$filePath = $this->testDir . '/partials/navigation.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('partials/navigation');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::COMPONENT);
});
it('recommends NO_CACHE for templates with user content', function () {
$content = '<div>Welcome {{ user.name }}! <p>Your profile</p></div>';
// Create template file
$filePath = $this->testDir . '/pages/profile.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('pages/profile');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::NO_CACHE);
expect($analysis->cacheability->hasUserSpecificContent)->toBeTrue();
});
it('detects CSRF tokens and adjusts cacheability score', function () {
$content = '<form method="POST"><input type="hidden" name="_token" value="{{ csrf_token }}"></form>';
// Create template file
$filePath = $this->testDir . '/forms/contact.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('forms/contact');
// CSRF tokens detected but high static ratio (0.839) means still cacheable (0.839 - 0.2 = 0.639 > 0.5)
expect($analysis->cacheability->hasCsrfTokens)->toBeTrue();
expect($analysis->cacheability->getScore())->toBeGreaterThan(0.5);
});
it('detects timestamps and adjusts cacheability score', function () {
$content = '<div>Current time: {{ now }}</div>';
// Create template file
$filePath = $this->testDir . '/widgets/clock.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('widgets/clock');
// Timestamps detected but high static ratio means still cacheable
expect($analysis->cacheability->hasTimestamps)->toBeTrue();
expect($analysis->cacheability->getScore())->toBeGreaterThan(0.5);
});
it('recommends NO_CACHE for templates with random elements', function () {
$content = '<div>Random ID: {{ uuid }}</div>';
// Create template file
$filePath = $this->testDir . '/widgets/random.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('widgets/random');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::NO_CACHE);
expect($analysis->cacheability->hasRandomElements)->toBeTrue();
});
it('recommends FRAGMENT strategy for medium cacheability templates', function () {
$content = '<div class="content"><h1>Title</h1><p>Some content</p><span>{{ small_dynamic_part }}</span></div>';
// Create template file
$filePath = $this->testDir . '/pages/mixed.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('pages/mixed');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FRAGMENT);
expect($analysis->cacheability->getScore())->toBeGreaterThan(0.5);
});
it('creates fallback analysis when loader throws exception', function () {
$analysis = $this->analyzer->analyze('missing/template');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::NO_CACHE);
expect($analysis->recommendedTtl)->toBe(0);
expect($analysis->dependencies)->toBeEmpty();
});
it('adjusts TTL based on cacheability score', function () {
$staticContent = '<html><body>All static content here</body></html>';
// Create template file
$filePath = $this->testDir . '/static/page.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $staticContent);
$analysis = $this->analyzer->analyze('static/page');
expect($analysis->recommendedTtl)->toBeGreaterThan(0);
});
});
describe('getDependencies()', function () {
it('finds @include directives', function () {
$content = '@include("header") <div>Content</div> @include("footer")';
// Create template file
$filePath = $this->testDir . '/page/test.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$deps = $this->analyzer->getDependencies('page/test');
expect($deps)->toHaveKey('includes');
expect($deps['includes'])->toContain('header');
expect($deps['includes'])->toContain('footer');
});
it('finds <component> tags', function () {
$content = '<component name="button" /> <component name="card" />';
// Create template file
$filePath = $this->testDir . '/page/test2.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$deps = $this->analyzer->getDependencies('page/test2');
expect($deps)->toHaveKey('components');
expect($deps['components'])->toContain('button');
expect($deps['components'])->toContain('card');
});
it('finds both includes and components', function () {
$content = '@include("layout") <component name="nav" /> <component name="footer" />';
// Create template file
$filePath = $this->testDir . '/page/test3.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$deps = $this->analyzer->getDependencies('page/test3');
expect($deps)->toHaveKey('includes');
expect($deps)->toHaveKey('components');
expect($deps['includes'])->toContain('layout');
expect($deps['components'])->toContain('nav');
});
it('returns empty array when no dependencies found', function () {
$content = '<div>No dependencies here</div>';
// Create template file
$filePath = $this->testDir . '/page/simple.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$deps = $this->analyzer->getDependencies('page/simple');
expect($deps)->toBeEmpty();
});
it('returns empty array when loader throws exception', function () {
$deps = $this->analyzer->getDependencies('page/error');
expect($deps)->toBeEmpty();
});
});
describe('getCacheability()', function () {
it('detects user-specific content patterns', function () {
$content = '{{ user.name }} {{ current_user.email }} {{ auth.role }}';
// Create template file
$filePath = $this->testDir . '/test/user.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$score = $this->analyzer->getCacheability('test/user');
expect($score->hasUserSpecificContent)->toBeTrue();
expect($score->isCacheable())->toBeFalse();
});
it('detects CSRF token patterns', function () {
$content = '{{ csrf_token }} {{ _token }}';
// Create template file
$filePath = $this->testDir . '/test/csrf.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$score = $this->analyzer->getCacheability('test/csrf');
expect($score->hasCsrfTokens)->toBeTrue();
expect($score->isCacheable())->toBeFalse();
});
it('detects timestamp patterns', function () {
$content = '{{ now }} {{ timestamp }} {{ date() }}';
// Create template file
$filePath = $this->testDir . '/test/time.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$score = $this->analyzer->getCacheability('test/time');
expect($score->hasTimestamps)->toBeTrue();
expect($score->isCacheable())->toBeFalse();
});
it('detects random element patterns', function () {
$content = '{{ random }} {{ rand() }} {{ uuid }}';
// Create template file
$filePath = $this->testDir . '/test/random.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$score = $this->analyzer->getCacheability('test/random');
expect($score->hasRandomElements)->toBeTrue();
expect($score->isCacheable())->toBeFalse();
});
it('calculates high static content ratio for mostly static templates', function () {
$content = '<div>Static content</div><p>More static content here with lots of text</p><section>Even more static HTML content</section>{{ small_dynamic }}';
// Create template file
$filePath = $this->testDir . '/test/static.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$score = $this->analyzer->getCacheability('test/static');
expect($score->staticContentRatio)->toBeGreaterThan(0.8);
expect($score->isCacheable())->toBeTrue();
});
it('calculates low static content ratio for mostly dynamic templates', function () {
$content = '{{ var1 }} {{ var2 }} {{ var3 }} {{ var4 }} <p>static</p>';
// Create template file
$filePath = $this->testDir . '/test/dynamic.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$score = $this->analyzer->getCacheability('test/dynamic');
expect($score->staticContentRatio)->toBeLessThan(0.5);
});
it('returns default score when loader throws exception', function () {
$score = $this->analyzer->getCacheability('test/error');
// Default CacheabilityScore has staticContentRatio = 0.0
expect($score->staticContentRatio)->toBe(0.0);
expect($score->hasUserSpecificContent)->toBeFalse();
expect($score->hasCsrfTokens)->toBeFalse();
expect($score->hasTimestamps)->toBeFalse();
expect($score->hasRandomElements)->toBeFalse();
});
it('handles empty content gracefully', function () {
// Create empty template file
$filePath = $this->testDir . '/test/empty.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, '');
$score = $this->analyzer->getCacheability('test/empty');
expect($score->staticContentRatio)->toBe(0.0);
});
});
describe('integration scenarios', function () {
it('correctly analyzes a typical blog post template', function () {
$content = <<<HTML
<article class="post">
<header>
<h1>{{ post.title }}</h1>
<time>{{ post.published_at }}</time>
</header>
<div class="content">
{{ post.content }}
</div>
<footer>
<p>Read more articles</p>
</footer>
</article>
HTML;
// Create template file
$filePath = $this->testDir . '/blog/post.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('blog/post');
expect($analysis->cacheability->hasTimestamps)->toBeFalse();
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FRAGMENT);
});
it('correctly analyzes a static landing page', function () {
$content = <<<HTML
<html>
<head><title>Welcome</title></head>
<body>
<header><h1>Welcome to our site</h1></header>
<main>
<section>Static marketing content</section>
<section>More static content</section>
</main>
<footer>Copyright 2024</footer>
</body>
</html>
HTML;
// Create template file
$filePath = $this->testDir . '/pages/landing.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('pages/landing');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FULL_PAGE);
expect($analysis->cacheability->staticContentRatio)->toBeGreaterThan(0.9);
});
it('correctly analyzes a reusable button component', function () {
$content = '<button class="btn btn-{{ variant }}">{{ text }}</button>';
// Create template file
$filePath = $this->testDir . '/components/button2.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('components/button2');
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::COMPONENT);
expect($analysis->recommendedTtl)->toBeGreaterThan(0);
});
it('correctly analyzes a form with CSRF protection', function () {
$content = <<<HTML
<form method="POST" action="/submit">
{{ csrf_token }}
<input type="text" name="email" />
<button type="submit">Submit</button>
</form>
HTML;
// Create template file
$filePath = $this->testDir . '/forms/contact2.view.php';
$dir = dirname($filePath);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($filePath, $content);
$analysis = $this->analyzer->analyze('forms/contact2');
// CSRF detected and high static ratio means still cacheable, recommends FULL_PAGE
expect($analysis->cacheability->hasCsrfTokens)->toBeTrue();
expect($analysis->cacheability->staticContentRatio)->toBeGreaterThan(0.8);
expect($analysis->recommendedStrategy)->toBe(CacheStrategy::FULL_PAGE);
});
});
});