- 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.
260 lines
9.1 KiB
PHP
260 lines
9.1 KiB
PHP
<?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\View\Caching\Analysis\CacheabilityScore;
|
|
use App\Framework\View\Caching\Analysis\CacheStrategy;
|
|
use App\Framework\View\Caching\Analysis\TemplateAnalysis;
|
|
use App\Framework\View\Caching\Analysis\TemplateAnalyzer;
|
|
use App\Framework\View\Caching\CacheManager;
|
|
use App\Framework\View\Caching\FragmentCache;
|
|
use App\Framework\View\Caching\TemplateContext;
|
|
|
|
// Helper function to create TemplateAnalysis
|
|
function createAnalysis(
|
|
string $template,
|
|
CacheStrategy $strategy,
|
|
float $staticRatio = 1.0,
|
|
bool $hasUserContent = false,
|
|
bool $hasCsrf = false,
|
|
bool $hasTimestamps = false,
|
|
bool $hasRandom = false
|
|
): TemplateAnalysis {
|
|
$cacheability = new CacheabilityScore();
|
|
$cacheability->staticContentRatio = $staticRatio;
|
|
$cacheability->hasUserSpecificContent = $hasUserContent;
|
|
$cacheability->hasCsrfTokens = $hasCsrf;
|
|
$cacheability->hasTimestamps = $hasTimestamps;
|
|
$cacheability->hasRandomElements = $hasRandom;
|
|
|
|
return new TemplateAnalysis(
|
|
template: $template,
|
|
recommendedStrategy: $strategy,
|
|
recommendedTtl: $strategy->getTtl(),
|
|
dependencies: [],
|
|
cacheability: $cacheability,
|
|
fragments: []
|
|
);
|
|
}
|
|
|
|
describe('CacheManager', function () {
|
|
beforeEach(function () {
|
|
$this->cache = Mockery::mock(Cache::class);
|
|
$this->analyzer = Mockery::mock(TemplateAnalyzer::class);
|
|
$this->fragmentCache = Mockery::mock(FragmentCache::class);
|
|
|
|
$this->cacheManager = new CacheManager(
|
|
$this->cache,
|
|
$this->analyzer,
|
|
$this->fragmentCache
|
|
);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
describe('render() with cacheable strategies', function () {
|
|
it('renders and caches on cache miss for FULL_PAGE strategy', function () {
|
|
$context = new TemplateContext(
|
|
template: 'pages/contact',
|
|
data: ['email' => 'contact@example.com']
|
|
);
|
|
|
|
$analysis = createAnalysis(
|
|
'pages/contact',
|
|
CacheStrategy::FULL_PAGE,
|
|
staticRatio: 0.9
|
|
);
|
|
$this->analyzer->shouldReceive('analyze')
|
|
->once()
|
|
->with('pages/contact')
|
|
->andReturn($analysis);
|
|
|
|
// Create actual CacheResult with cache miss
|
|
$cacheKey = CacheKey::fromString('page:pages/contact:' . md5(serialize(['email' => 'contact@example.com'])));
|
|
$missItem = CacheItem::miss($cacheKey);
|
|
$cacheResult = CacheResult::fromItems($missItem);
|
|
|
|
$this->cache->shouldReceive('get')
|
|
->once()
|
|
->andReturn($cacheResult);
|
|
|
|
// Expect cache set with rendered content
|
|
$renderedContent = '<html>Contact Page</html>';
|
|
$this->cache->shouldReceive('set')
|
|
->once()
|
|
->with(Mockery::type(CacheItem::class))
|
|
->andReturn(true);
|
|
|
|
$result = $this->cacheManager->render($context, function () use ($renderedContent) {
|
|
return $renderedContent;
|
|
});
|
|
|
|
expect($result)->toBe($renderedContent);
|
|
});
|
|
|
|
it('caches component templates with COMPONENT strategy', function () {
|
|
$context = new TemplateContext(
|
|
template: 'components/button',
|
|
data: ['text' => 'Click me']
|
|
);
|
|
|
|
$analysis = createAnalysis(
|
|
'components/button',
|
|
CacheStrategy::COMPONENT,
|
|
staticRatio: 1.0
|
|
);
|
|
$this->analyzer->shouldReceive('analyze')
|
|
->once()
|
|
->andReturn($analysis);
|
|
|
|
// Create actual CacheResult with cache miss
|
|
$cacheKey = CacheKey::fromString('component:button:' . md5(serialize(['text' => 'Click me'])));
|
|
$missItem = CacheItem::miss($cacheKey);
|
|
$cacheResult = CacheResult::fromItems($missItem);
|
|
|
|
$this->cache->shouldReceive('get')->once()->andReturn($cacheResult);
|
|
$this->cache->shouldReceive('set')->once()->andReturn(true);
|
|
|
|
$renderedContent = '<button>Click me</button>';
|
|
$result = $this->cacheManager->render($context, fn() => $renderedContent);
|
|
|
|
expect($result)->toBe($renderedContent);
|
|
});
|
|
|
|
it('caches fragment with FRAGMENT strategy', function () {
|
|
$context = new TemplateContext(
|
|
template: 'pages/dashboard',
|
|
data: ['stats' => []],
|
|
metadata: ['fragment_id' => 'user-stats']
|
|
);
|
|
|
|
$analysis = createAnalysis(
|
|
'pages/dashboard',
|
|
CacheStrategy::FRAGMENT,
|
|
staticRatio: 0.6
|
|
);
|
|
$this->analyzer->shouldReceive('analyze')
|
|
->once()
|
|
->andReturn($analysis);
|
|
|
|
// Create actual CacheResult with cache miss
|
|
$cacheKey = CacheKey::fromString('fragment:pages/dashboard:user-stats:' . md5(serialize(['stats' => []])));
|
|
$missItem = CacheItem::miss($cacheKey);
|
|
$cacheResult = CacheResult::fromItems($missItem);
|
|
|
|
$this->cache->shouldReceive('get')->once()->andReturn($cacheResult);
|
|
$this->cache->shouldReceive('set')->once()->andReturn(true);
|
|
|
|
$renderedContent = '<div class="stats">Stats Fragment</div>';
|
|
$result = $this->cacheManager->render($context, fn() => $renderedContent);
|
|
|
|
expect($result)->toBe($renderedContent);
|
|
});
|
|
});
|
|
|
|
describe('render() with NO_CACHE strategy', function () {
|
|
it('skips caching for non-cacheable content', function () {
|
|
$context = new TemplateContext(
|
|
template: 'pages/dynamic',
|
|
data: ['user' => ['id' => 1], 'timestamp' => time()]
|
|
);
|
|
|
|
$analysis = createAnalysis(
|
|
'pages/dynamic',
|
|
CacheStrategy::NO_CACHE,
|
|
staticRatio: 0.2,
|
|
hasUserContent: true,
|
|
hasTimestamps: true
|
|
);
|
|
$this->analyzer->shouldReceive('analyze')
|
|
->once()
|
|
->andReturn($analysis);
|
|
|
|
// No cache operations expected
|
|
$this->cache->shouldReceive('get')->never();
|
|
$this->cache->shouldReceive('set')->never();
|
|
|
|
$renderedContent = '<html>Dynamic Content</html>';
|
|
$result = $this->cacheManager->render($context, fn() => $renderedContent);
|
|
|
|
expect($result)->toBe($renderedContent);
|
|
});
|
|
});
|
|
|
|
describe('invalidateTemplate()', function () {
|
|
it('invalidates cache for given template', function () {
|
|
// clear() is called once per strategy that canInvalidate
|
|
$this->cache->shouldReceive('clear')
|
|
->atLeast()->once()
|
|
->andReturn(true);
|
|
|
|
$invalidated = $this->cacheManager->invalidateTemplate('pages/about');
|
|
|
|
// Returns count of invalidated caches
|
|
expect($invalidated)->toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('builds correct invalidation patterns for different strategies', function () {
|
|
// This test verifies the pattern building logic by invalidating
|
|
$this->cache->shouldReceive('clear')
|
|
->atLeast()->once()
|
|
->andReturn(true);
|
|
|
|
// Invalidate various template types
|
|
$this->cacheManager->invalidateTemplate('pages/home');
|
|
$this->cacheManager->invalidateTemplate('components/button');
|
|
$this->cacheManager->invalidateTemplate('fragments/stats');
|
|
|
|
expect(true)->toBeTrue(); // Verify no exceptions thrown
|
|
});
|
|
});
|
|
|
|
describe('edge cases', function () {
|
|
it('handles empty template content', function () {
|
|
$context = new TemplateContext(
|
|
template: 'pages/empty',
|
|
data: []
|
|
);
|
|
|
|
$analysis = createAnalysis(
|
|
'pages/empty',
|
|
CacheStrategy::NO_CACHE
|
|
);
|
|
$this->analyzer->shouldReceive('analyze')
|
|
->once()
|
|
->andReturn($analysis);
|
|
|
|
$result = $this->cacheManager->render($context, fn() => '');
|
|
|
|
expect($result)->toBe('');
|
|
});
|
|
|
|
it('handles large rendered content', function () {
|
|
$context = new TemplateContext(
|
|
template: 'pages/large',
|
|
data: []
|
|
);
|
|
|
|
$analysis = createAnalysis(
|
|
'pages/large',
|
|
CacheStrategy::NO_CACHE
|
|
);
|
|
$this->analyzer->shouldReceive('analyze')
|
|
->once()
|
|
->andReturn($analysis);
|
|
|
|
$largeContent = str_repeat('<p>Content</p>', 1000);
|
|
$result = $this->cacheManager->render($context, fn() => $largeContent);
|
|
|
|
expect($result)->toBe($largeContent);
|
|
expect(strlen($result))->toBeGreaterThan(10000);
|
|
});
|
|
});
|
|
});
|