- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
22 KiB
Troubleshooting
Comprehensive guide for diagnosing and fixing common issues in the Custom PHP Framework.
Common Errors
Class Not Found Errors
Symptom: Class 'App\...' not found
Causes:
- Autoloader not regenerated after creating new classes
- Incorrect namespace
- File not saved in correct directory
Solutions:
# Regenerate autoloader
composer reload
# Or in Docker
make reload
# Check namespace matches directory structure
# src/Domain/User/Services/UserService.php
namespace App\Domain\User\Services;
Prevention:
- Always run
composer reloadafter creating new classes - Use PSR-4 autoloading standards
- Namespace must match directory structure
Container Binding Missing
Symptom: No binding found for interface X
Causes:
- Missing
#[Initializer]attribute - Initializer not discovered (cache issue)
- Circular dependency
Solutions:
// 1. Add Initializer attribute
final readonly class ServiceInitializer
{
#[Initializer]
public function initialize(Container $container): void
{
$container->singleton(
UserRepository::class,
new DatabaseUserRepository($container->get(Connection::class))
);
}
}
// 2. Clear discovery cache
rm -rf storage/cache/discovery_*
// 3. Check for circular dependencies in constructor
// ❌ Bad: A depends on B, B depends on A
final readonly class ServiceA
{
public function __construct(private readonly ServiceB $b) {}
}
final readonly class ServiceB
{
public function __construct(private readonly ServiceA $a) {} // Circular!
}
Prevention:
- Use
#[Initializer]for all service registrations - Avoid circular dependencies through interface abstraction
- Use method injection instead of constructor injection when needed
Route Not Found (404)
Symptom: Route returns 404 even though controller exists
Causes:
- Missing
#[Route]attribute - Route cache out of date
- HTTP method mismatch
- Middleware blocking request
Solutions:
# 1. Check route is registered
docker exec php php console.php routes:list
# 2. Clear route cache
rm -rf storage/cache/routes_*
# 3. Verify Route attribute
#[Route(path: '/api/users', method: Method::GET)]
public function getUsers(): JsonResult
# 4. Check middleware chain
docker exec php php -r "
use App\Framework\Discovery\UnifiedDiscoveryService;
\$discovery = new UnifiedDiscoveryService();
\$routes = \$discovery->discoverRoutes();
print_r(\$routes);
"
Common Mistakes:
// ❌ Wrong: Method mismatch
#[Route(path: '/api/users', method: Method::GET)]
// Request: POST /api/users → 404
// ✅ Correct: Match HTTP method
#[Route(path: '/api/users', method: Method::POST)]
HTTPS Required Error
Symptom: HTTPS is required for this operation
Causes:
- Accessing framework via HTTP instead of HTTPS
- Missing SSL certificates in development
- Reverse proxy not forwarding HTTPS headers
Solutions:
# Development: Use HTTPS
https://localhost/ # ✅ Correct
http://localhost/ # ❌ Wrong - will be rejected
# Check SSL certificates exist
ls -la docker/ssl/
# Should see: localhost.crt, localhost.key
# Regenerate if missing
cd docker/ssl
./generate-ssl.sh
Production Fix:
# Nginx: Forward HTTPS headers
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Ssl on;
User Agent Required Error
Symptom: Valid User-Agent header is required
Causes:
- Missing User-Agent in cURL/API requests
- Bot/Scanner detection blocking legitimate requests
Solutions:
# Add User-Agent to cURL requests
curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \
https://localhost/api/endpoint
# In API client code
$client = new GuzzleHttp\Client([
'headers' => [
'User-Agent' => 'MyApp/1.0'
]
]);
Container/DI Issues
Cannot Resolve Dependency
Symptom: Cannot resolve parameter 'X' for class Y
Causes:
- Constructor parameter has no type hint
- Scalar parameter without default value
- Interface not bound in container
Solutions:
// ❌ Problem: No type hint
public function __construct($logger) {}
// ✅ Solution: Add type hint
public function __construct(private readonly Logger $logger) {}
// ❌ Problem: Scalar without default
public function __construct(string $apiKey) {}
// ✅ Solution: Use Value Object or default
public function __construct(
private readonly ApiKey $apiKey
) {}
// OR provide default
public function __construct(
private readonly string $apiKey = ''
) {}
Interface Binding:
// Bind interface to implementation
#[Initializer]
public function initialize(Container $container): void
{
$container->bind(
LoggerInterface::class,
fn() => new FileLogger('/var/log/app.log')
);
}
Circular Dependency Detected
Symptom: Circular dependency detected: A -> B -> A
Solutions:
// ❌ Problem: Direct circular dependency
final readonly class OrderService
{
public function __construct(
private readonly InvoiceService $invoices
) {}
}
final readonly class InvoiceService
{
public function __construct(
private readonly OrderService $orders // Circular!
) {}
}
// ✅ Solution 1: Extract shared logic to third service
final readonly class OrderService
{
public function __construct(
private readonly PriceCalculator $calculator
) {}
}
final readonly class InvoiceService
{
public function __construct(
private readonly PriceCalculator $calculator
) {}
}
// ✅ Solution 2: Use events for decoupling
final readonly class OrderService
{
public function __construct(
private readonly EventDispatcher $events
) {}
public function completeOrder(Order $order): void
{
// Process order
$this->events->dispatch(new OrderCompletedEvent($order));
}
}
final readonly class InvoiceService
{
#[EventHandler]
public function onOrderCompleted(OrderCompletedEvent $event): void
{
// Generate invoice without direct dependency
$this->generateInvoice($event->order);
}
}
Service Not Singleton
Symptom: Multiple instances created when singleton expected
Solutions:
// ❌ Wrong: Regular binding creates new instance each time
$container->bind(
DatabaseConnection::class,
fn() => new DatabaseConnection($config)
);
// ✅ Correct: Singleton creates one instance
$container->singleton(
DatabaseConnection::class,
new DatabaseConnection($config)
);
// ✅ Alternative: Lazy singleton
$container->singleton(
DatabaseConnection::class,
fn() => new DatabaseConnection($config)
);
Routing Problems
Route Conflicts
Symptom: Wrong controller handling request
Causes:
- Multiple routes match same pattern
- Route priority issues
- Catch-all route defined too early
Solutions:
# List all routes to find conflicts
docker exec php php console.php routes:list | grep '/api/users'
# Check route priority order
docker exec php php console.php routes:list --sort=priority
Fix Route Conflicts:
// ❌ Problem: Routes conflict
#[Route(path: '/api/users/{id}', method: Method::GET)]
public function getUser(string $id): JsonResult {}
#[Route(path: '/api/users/me', method: Method::GET)]
public function getCurrentUser(): JsonResult {} // Never matched!
// ✅ Solution: Most specific routes first
#[Route(path: '/api/users/me', method: Method::GET)]
public function getCurrentUser(): JsonResult {}
#[Route(path: '/api/users/{id}', method: Method::GET)]
public function getUser(string $id): JsonResult {}
Route Parameters Not Working
Symptom: Route parameter is null or empty
Solutions:
// ✅ Parameter name must match route
#[Route(path: '/api/users/{userId}', method: Method::GET)]
public function getUser(string $userId): JsonResult // Parameter name matches!
{
return new JsonResult(['userId' => $userId]);
}
// ❌ Wrong: Parameter name mismatch
#[Route(path: '/api/users/{userId}', method: Method::GET)]
public function getUser(string $id): JsonResult // Mismatch!
{
// $id will be null
}
Middleware Not Executing
Symptom: Middleware bypassed or not running
Causes:
- Missing
#[MiddlewarePriority]attribute - Wrong middleware registration
- Middleware exception not caught
Solutions:
// Add priority attribute
#[MiddlewarePriority(100)]
final readonly class AuthMiddleware implements Middleware
{
public function process(Request $request, callable $next): Response
{
// Auth logic
return $next($request);
}
}
// Check middleware is discovered
docker exec php php console.php debug:middleware
Middleware Chain Debug:
// Add logging to trace execution
final readonly class DebugMiddleware implements Middleware
{
public function process(Request $request, callable $next): Response
{
Logger::debug('[Middleware] Before: ' . get_class($this));
$response = $next($request);
Logger::debug('[Middleware] After: ' . get_class($this));
return $response;
}
}
Performance Issues
Slow Page Load Times
Diagnosis Steps:
# 1. Enable performance profiling
docker exec php php console.php performance:enable
# 2. Check slow query log
docker exec php tail -f /var/log/mysql/slow-queries.log
# 3. Profile specific endpoint
docker exec php php console.php performance:profile /api/users
# 4. Check N+1 queries
docker exec php php console.php db:explain
Common Causes & Fixes:
N+1 Query Problem
// ❌ Problem: N+1 queries
$users = $this->userRepository->findAll();
foreach ($users as $user) {
// Triggers separate query for each user!
$profile = $user->getProfile();
}
// ✅ Solution: Eager loading
$users = $this->userRepository->findAllWithProfiles();
// Single query with JOIN
Missing Indexes
# Check missing indexes
docker exec php php console.php db:analyze-indexes
# Add index via migration
$table->index('email'); // Single column
$table->index(['user_id', 'created_at']); // Composite
Cache Not Used
// ✅ Cache expensive operations
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
$cacheKey = CacheKey::fromString('users_list');
$users = $this->cache->remember(
$cacheKey,
fn() => $this->repository->findAll(),
Duration::fromMinutes(10)
);
Memory Exhaustion
Symptom: Allowed memory size exhausted
Causes:
- Large result sets loaded into memory
- Memory leaks in loops
- Circular references
Solutions:
// ❌ Problem: Loading entire table
$allUsers = $this->connection->query('SELECT * FROM users');
// 1 million rows = memory exhausted
// ✅ Solution: Batch processing
$batchSize = 1000;
$offset = 0;
do {
$batch = $this->connection->query(
"SELECT * FROM users LIMIT {$batchSize} OFFSET {$offset}"
);
foreach ($batch as $user) {
$this->processUser($user);
}
$offset += $batchSize;
// Force garbage collection
gc_collect_cycles();
} while (count($batch) === $batchSize);
Memory Leak Prevention:
// ✅ Clear references in loops
foreach ($largeDataset as $item) {
$result = $this->process($item);
// Clear large objects
unset($result, $item);
if ($iteration % 100 === 0) {
gc_collect_cycles();
}
}
Database Connection Issues
Connection Refused
Symptom: SQLSTATE[HY000] [2002] Connection refused
Solutions:
# 1. Check MySQL container is running
docker ps | grep mysql
# 2. Check database credentials in .env
DB_HOST=mysql # Use service name, not 'localhost'
DB_PORT=3306
DB_NAME=your_database
DB_USER=root
DB_PASS=your_password
# 3. Test connection directly
docker exec mysql mysql -u root -p your_database
# 4. Check MySQL logs
docker logs mysql
Common Mistakes:
# ❌ Wrong: localhost (from container perspective)
DB_HOST=localhost
# ✅ Correct: Docker service name
DB_HOST=mysql
Too Many Connections
Symptom: SQLSTATE[HY000] [1040] Too many connections
Causes:
- Connection leaks (not closing connections)
- Too many concurrent requests
- Connection pool exhausted
Solutions:
// ✅ Always use try-finally for connections
$connection = $this->connectionPool->getConnection();
try {
$connection->beginTransaction();
// Database operations
$connection->commit();
} catch (\Exception $e) {
$connection->rollback();
throw $e;
} finally {
// Always release connection
$this->connectionPool->releaseConnection($connection);
}
Increase Connection Limit:
# docker/mysql/my.cnf
[mysqld]
max_connections = 200
Deadlock Detected
Symptom: SQLSTATE[40001] Deadlock found when trying to get lock
Causes:
- Transactions accessing tables in different order
- Long-running transactions
- Missing indexes causing lock escalation
Solutions:
// ✅ Always access tables in consistent order
final readonly class PaymentService
{
public function transfer(Account $from, Account $to, Money $amount): void
{
$this->connection->beginTransaction();
try {
// ALWAYS lock in same order (e.g., by ID)
$accounts = [$from, $to];
usort($accounts, fn($a, $b) => $a->id <=> $b->id);
foreach ($accounts as $account) {
$this->lockAccount($account);
}
// Process transfer
$from->debit($amount);
$to->credit($amount);
$this->connection->commit();
} catch (\Exception $e) {
$this->connection->rollback();
throw $e;
}
}
}
Retry Strategy:
// ✅ Retry on deadlock
public function executeWithRetry(callable $operation, int $maxAttempts = 3): mixed
{
$attempt = 0;
while ($attempt < $maxAttempts) {
try {
return $operation();
} catch (DeadlockException $e) {
$attempt++;
if ($attempt >= $maxAttempts) {
throw $e;
}
// Exponential backoff
usleep(pow(2, $attempt) * 100000); // 200ms, 400ms, 800ms
}
}
}
Cache Problems
Cache Not Working
Symptom: Data not cached, fetched every time
Diagnosis:
# 1. Check cache driver in .env
CACHE_DRIVER=redis # or file, array, memcached
# 2. Test cache directly
docker exec php php -r "
\$cache = new App\Framework\Cache\SmartCache();
\$cache->set('test', 'value', 60);
var_dump(\$cache->get('test'));
"
# 3. Check Redis connection (if using Redis)
docker exec redis redis-cli PING
# Should return: PONG
Common Issues:
// ❌ Problem: TTL too short
$cache->set($key, $value, 1); // 1 second - expires immediately
// ✅ Solution: Appropriate TTL
use App\Framework\Core\ValueObjects\Duration;
$cache->set($key, $value, Duration::fromMinutes(10)->toSeconds());
// ✅ Better: Use CacheItem with Duration
$cacheItem = CacheItem::forSetting(
key: $key,
value: $value,
ttl: Duration::fromHours(1)
);
$cache->set($cacheItem);
Cache Stampede
Symptom: Multiple processes regenerating same cached value simultaneously
Solutions:
// ✅ Use cache locking
use App\Framework\Cache\CacheLock;
public function getExpensiveData(string $key): array
{
$cacheKey = CacheKey::fromString($key);
// Try to get from cache
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
// Acquire lock to prevent stampede
$lock = $this->cache->lock($cacheKey, Duration::fromSeconds(10));
if (!$lock->acquire()) {
// Another process is regenerating, wait for it
sleep(1);
return $this->cache->get($cacheKey) ?? [];
}
try {
// Regenerate data
$data = $this->expensiveOperation();
// Cache it
$this->cache->set(
CacheItem::forSetting($cacheKey, $data, Duration::fromMinutes(10))
);
return $data;
} finally {
$lock->release();
}
}
Cache Invalidation Issues
Symptom: Stale data served from cache
Solutions:
// ✅ Tag-based invalidation
use App\Framework\Cache\CacheTag;
// Cache with tags
$userTag = CacheTag::fromString("user_{$userId}");
$teamTag = CacheTag::fromString("team_{$teamId}");
$cacheItem = CacheItem::forSetting(
key: CacheKey::fromString("user_profile_{$userId}"),
value: $userProfile,
ttl: Duration::fromHours(1),
tags: [$userTag, $teamTag]
);
$this->cache->set($cacheItem);
// Invalidate all user-related caches
$this->cache->forget($userTag);
// Invalidate all team-related caches
$this->cache->forget($teamTag);
Event-Based Invalidation:
// ✅ Automatic invalidation via events
final readonly class CacheInvalidationListener
{
#[EventHandler]
public function onUserUpdated(UserUpdatedEvent $event): void
{
$userTag = CacheTag::fromString("user_{$event->userId}");
$this->cache->forget($userTag);
}
}
Debug Tools
Framework Debug Mode
# Enable debug mode in .env
APP_ENV=development
APP_DEBUG=true
# Enhanced error pages with:
# - Full stack traces
# - Variable dumps
# - SQL query logs
# - Performance metrics
Performance Profiling
# Enable performance collector
docker exec php php console.php performance:enable
# Profile specific request
docker exec php php console.php performance:profile /api/users
# View performance report
docker exec php php console.php performance:report
# Disable profiling
docker exec php php console.php performance:disable
Database Query Logging
// Enable query logging in code
use App\Framework\Database\QueryLogger;
$queryLogger = new QueryLogger();
$connection->setQueryLogger($queryLogger);
// Log queries
$users = $connection->query('SELECT * FROM users');
// View logged queries
foreach ($queryLogger->getQueries() as $query) {
Logger::debug('[SQL]', [
'query' => $query['sql'],
'bindings' => $query['bindings'],
'duration' => $query['duration']
]);
}
Detect N+1 Queries:
# Run N+1 detection
docker exec php php console.php db:detect-n-plus-one /api/users
MCP Server Debugging
# Test MCP server connection
echo '{"jsonrpc": "2.0", "method": "initialize", "params": {}}' | \
docker exec -i php php console.php mcp:server
# Analyze routes via MCP
docker exec php php console.php mcp:analyze routes
# Analyze container bindings
docker exec php php console.php mcp:analyze container
# Check framework health
docker exec php php console.php mcp:health
Request Debugging
// Log full request details
use App\Framework\Http\Request;
Logger::debug('[Request]', [
'method' => $request->method->value,
'path' => $request->path,
'query' => $request->queryParameters ?? [],
'body' => $request->parsedBody ?? [],
'headers' => $request->headers->toArray(),
'server' => [
'ip' => $request->server->getRemoteAddr(),
'user_agent' => $request->server->getUserAgent()
]
]);
Event System Debugging
// Log all dispatched events
final readonly class EventDebugListener
{
#[EventHandler]
public function onAnyEvent(object $event): void
{
Logger::debug('[Event]', [
'type' => get_class($event),
'data' => $event
]);
}
}
Emergency Procedures
Application Down
Quick Recovery Steps:
# 1. Check Docker containers
docker ps -a
# 2. Restart all services
make down && make up
# 3. Check logs
make logs
# 4. Verify database connection
docker exec mysql mysql -u root -p
# 5. Clear all caches
rm -rf storage/cache/*
composer reload
# 6. Test health endpoint
curl -k https://localhost/health
Database Corruption
# 1. Stop application
make down
# 2. Backup database immediately
docker exec mysql mysqldump -u root -p your_database > backup.sql
# 3. Check and repair tables
docker exec mysql mysqlcheck -u root -p --auto-repair your_database
# 4. Restore from backup if needed
docker exec -i mysql mysql -u root -p your_database < backup.sql
# 5. Restart application
make up
Cache Corruption
# 1. Flush all caches
docker exec redis redis-cli FLUSHALL # If using Redis
rm -rf storage/cache/* # File cache
# 2. Restart services
make restart
# 3. Warm up critical caches
docker exec php php console.php cache:warm
Getting Help
Framework Support Channels
Issues & Bug Reports:
- GitHub Issues: Report bugs with reproduction steps
- Include: PHP version, Docker logs, error messages
Documentation:
/docs/claude/- Complete framework documentation- CLAUDE.md - AI assistant integration guide
Debugging Resources:
- MCP Server - AI-powered framework analysis
- Performance Profiler - Built-in performance monitoring
- Query Logger - SQL debugging tool
Prevention Best Practices
Development Checklist
- Run
composer reloadafter creating new classes - Clear caches after config changes
- Test with HTTPS in development
- Add User-Agent to API requests
- Use Value Objects instead of primitives
- Implement proper error handling
- Add logging to critical operations
- Test with production-like data volumes
- Profile performance before deployment
- Review query execution plans
Code Review Checklist
- No circular dependencies
- Proper exception handling
- Database transactions properly closed
- Cache keys properly namespaced
- Routes don't conflict
- Middleware properly ordered
- Security best practices followed
- Performance considerations addressed
- Tests cover critical paths
- Documentation updated
Appendix: Common Error Codes
| Error Code | Meaning | Common Cause |
|---|---|---|
| 403 | Forbidden | WAF blocking, missing Auth, IP restriction |
| 404 | Not Found | Route not registered, wrong path |
| 405 | Method Not Allowed | HTTP method mismatch in Route attribute |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unhandled exception, see logs |
| 502 | Bad Gateway | PHP-FPM down, connection refused |
| 503 | Service Unavailable | Database down, cache unavailable |
Last Updated: 2025-01-28 Framework Version: 2.x