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