Production-Grade PHP: Advanced Testing, Security Protocols, and Performance Caching
Code correctness and system integrity are non-negotiable in enterprise software. This module bridges the gap between development and production. We explore rigorous testing methodologies (from TDD to Mutation Testing), implement defense-in-depth security measures (Sodium encryption, CSRF protection), and architect high-performance caching layers using Redis and PSR standards.
Testing
PHPUnit Setup and Configuration
Install via Composer (composer require --dev phpunit/phpunit) and configure with phpunit.xml to define test suites, bootstrap files, and coverage settings.
<!-- phpunit.xml --> <phpunit bootstrap="vendor/autoload.php" colors="true"> <testsuites> <testsuite name="Unit"> <directory>tests/Unit</directory> </testsuite> </testsuites> </phpunit>
Writing Test Cases
Test classes extend TestCase, methods prefixed with test or annotated with @test, following Arrange-Act-Assert pattern.
class CalculatorTest extends TestCase { public function testAddition(): void { $calc = new Calculator(); // Arrange $result = $calc->add(2, 3); // Act $this->assertEquals(5, $result); // Assert } }
Assertions
PHPUnit provides 60+ assertion methods to verify expected outcomes including assertEquals, assertTrue, assertCount, assertInstanceOf, assertArrayHasKey, and assertThrows.
$this->assertEquals($expected, $actual); $this->assertCount(3, $array); $this->assertInstanceOf(User::class, $user); $this->assertStringContainsString('hello', $text); $this->expectException(InvalidArgumentException::class);
Test Fixtures
setUp() runs before each test and tearDown() after, while setUpBeforeClass() and tearDownAfterClass() run once per test class for shared resources.
class DatabaseTest extends TestCase { protected PDO $db; protected function setUp(): void { $this->db = new PDO('sqlite::memory:'); $this->db->exec('CREATE TABLE users (id INT, name TEXT)'); } protected function tearDown(): void { $this->db = null; } }
Data Providers
Methods that supply multiple datasets to a single test, reducing duplication and enabling parameterized testing.
#[DataProvider('additionProvider')] public function testAdd(int $a, int $b, int $expected): void { $this->assertEquals($expected, $a + $b); } public static function additionProvider(): array { return [ 'positive' => [1, 2, 3], 'negative' => [-1, -2, -3], 'zero' => [0, 0, 0], ]; }
Mocking and Stubbing
Stubs provide predetermined responses while mocks also verify interactions. PHPUnit's createMock() and createStub() create test doubles.
$mailer = $this->createMock(Mailer::class); $mailer->expects($this->once()) // Verify called once ->method('send') ->with($this->equalTo($email)) ->willReturn(true); // Stub return value
Test Doubles
Umbrella term for fake objects: Dummy (placeholder), Stub (canned answers), Spy (records calls), Mock (verifies expectations), Fake (working implementation).
┌──────────────────────────────────────────────────┐
│ Test Doubles Spectrum │
├──────────────────────────────────────────────────┤
│ Dummy → Stub → Spy → Mock → Fake │
│ (Less behavior) ←───────→ (More behavior) │
└──────────────────────────────────────────────────┘
Code Coverage
Measures which lines/branches are executed during tests. Enable with --coverage-html flag, requires Xdebug or PCOV.
vendor/bin/phpunit --coverage-html coverage/ # Target: 80%+ line coverage, but quality > quantity
Test-Driven Development (TDD)
Write failing tests first, then minimal code to pass, then refactor. Red-Green-Refactor cycle drives design.
┌─────┐ ┌───────┐ ┌──────────┐
│ RED │ ───→ │ GREEN │ ───→ │ REFACTOR │
└─────┘ └───────┘ └──────────┘
↑ │
└──────────────────────────────┘
Behavior-Driven Development (BDD)
Focuses on behavior specifications using Given-When-Then syntax. PHP implementations include Behat (story-level) and PHPSpec (spec-level).
# features/login.feature Feature: User Login Scenario: Successful login Given I am on the login page When I enter valid credentials Then I should see the dashboard
Pest PHP
Modern PHP testing framework with elegant syntax, built on PHPUnit. Less boilerplate, more expressive.
test('user can be created', function () { $user = User::create(['name' => 'John']); expect($user)->toBeInstanceOf(User::class) ->name->toBe('John'); }); it('calculates total correctly', fn() => expect((new Cart())->total())->toBe(100) );
Codeception
Full-stack testing framework supporting unit, functional, and acceptance testing with built-in modules for popular frameworks.
// tests/Acceptance/LoginCest.php public function tryToLogin(AcceptanceTester $I): void { $I->amOnPage('/login'); $I->fillField('email', 'test@example.com'); $I->click('Login'); $I->see('Welcome'); }
Integration Testing
Tests multiple components working together, often with real databases or services. Slower than unit tests but catches integration issues.
class OrderIntegrationTest extends TestCase { public function testCompleteOrderFlow(): void { $order = new Order(new RealDatabase(), new RealPaymentGateway()); $result = $order->process($cart); $this->assertDatabaseHas('orders', ['id' => $result->id]); } }
Functional Testing
Tests complete features from user perspective without UI—HTTP requests/responses, ensuring endpoints work correctly.
public function testApiReturnsUserList(): void { $response = $this->get('/api/users'); $response->assertStatus(200) ->assertJsonStructure(['data' => [['id', 'name', 'email']]]); }
Mutation Testing (Infection)
Tests your tests by introducing small code changes (mutants); if tests still pass, they're weak. Install with composer require --dev infection/infection.
vendor/bin/infection --min-msi=70 # Mutant example: changes $a + $b to $a - $b # If tests pass → mutant "escaped" → weak tests!
Security
SQL Injection Prevention
Always use prepared statements with bound parameters—never concatenate user input into queries.
// ❌ VULNERABLE $sql = "SELECT * FROM users WHERE id = " . $_GET['id']; // ✅ SAFE - Prepared Statement $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$_GET['id']]);
Cross-Site Scripting (XSS) Prevention
Escape output based on context (HTML, JavaScript, URL, CSS). Use htmlspecialchars() for HTML context.
// Always escape output <p>Hello, <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></p> // Or use templating engines with auto-escaping (Blade, Twig) {{ $name }} // Auto-escaped in Laravel Blade
Cross-Site Request Forgery (CSRF) Prevention
Include unique tokens in forms that are validated server-side, ensuring requests originate from your application.
// Generate token $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // In form <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>"> // Validate if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { die('CSRF validation failed'); }
Password Hashing (password_hash, password_verify)
Use PHP's built-in functions with bcrypt (default) or Argon2—never MD5/SHA1. Cost factor adjusts automatically.
// Hash password (store this) $hash = password_hash($password, PASSWORD_DEFAULT); // Verify login if (password_verify($inputPassword, $storedHash)) { // Login successful } // Check if rehash needed (algorithm upgraded) if (password_needs_rehash($hash, PASSWORD_DEFAULT)) { $newHash = password_hash($password, PASSWORD_DEFAULT); }
Encryption and Decryption (OpenSSL, Sodium)
Sodium (libsodium) is modern and recommended; OpenSSL for legacy. Use for sensitive data at rest.
// Sodium (recommended) $key = sodium_crypto_secretbox_keygen(); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $encrypted = sodium_crypto_secretbox($message, $nonce, $key); $decrypted = sodium_crypto_secretbox_open($encrypted, $nonce, $key); // OpenSSL $encrypted = openssl_encrypt($data, 'aes-256-gcm', $key, 0, $iv, $tag);
Input Validation and Sanitization
Validate (check format) and sanitize (clean) all input. Use filter_var() or validation libraries.
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL); $age = filter_var($_POST['age'], FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 1, 'max_range' => 120] ]); if ($email === false) { throw new InvalidArgumentException('Invalid email'); }
Output Encoding
Encode data appropriately for the output context to prevent injection attacks.
// HTML context htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); // URL context urlencode($data); // JavaScript context json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP); // CSS context - avoid user input in CSS
Secure Headers
HTTP headers that enhance security by controlling browser behavior.
header('X-Content-Type-Options: nosniff'); header('X-Frame-Options: DENY'); header('X-XSS-Protection: 1; mode=block'); header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); header('Referrer-Policy: strict-origin-when-cross-origin');
HTTPS and SSL/TLS
Encrypt all traffic using TLS 1.2+. Force HTTPS via redirects and HSTS header.
// Force HTTPS redirect if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') { header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], true, 301); exit; } // Set secure cookie setcookie('session', $value, ['secure' => true, 'httponly' => true, 'samesite' => 'Strict']);
Authentication Best Practices
Multi-factor auth, secure session management, rate limiting, and secure password reset flows.
┌────────────────────────────────────────────────────────┐
│ Authentication Checklist │
├────────────────────────────────────────────────────────┤
│ ✓ Use password_hash/verify │
│ ✓ Implement MFA/2FA │
│ ✓ Rate limit login attempts │
│ ✓ Secure session regeneration │
│ ✓ Constant-time comparison for tokens │
│ ✓ Secure "remember me" tokens │
└────────────────────────────────────────────────────────┘
Authorization and Access Control
Implement RBAC (Role-Based) or ABAC (Attribute-Based) access control; check permissions on every request.
// Simple RBAC example class Gate { public static function allows(User $user, string $action, $resource): bool { return match($action) { 'edit' => $user->id === $resource->user_id || $user->isAdmin(), 'delete' => $user->isAdmin(), default => false }; } } if (!Gate::allows($user, 'edit', $post)) abort(403);
OWASP Top 10
Industry-standard awareness document listing critical web security risks: Injection, Broken Auth, Sensitive Data Exposure, XXE, Broken Access Control, Misconfig, XSS, Insecure Deserialization, Vulnerable Components, Insufficient Logging.
┌─────────────────────────────────────┐
│ OWASP Top 10 (2021) │
├─────────────────────────────────────┤
│ A01: Broken Access Control │
│ A02: Cryptographic Failures │
│ A03: Injection │
│ A04: Insecure Design │
│ A05: Security Misconfiguration │
│ A06: Vulnerable Components │
│ A07: Auth Failures │
│ A08: Data Integrity Failures │
│ A09: Logging Failures │
│ A10: SSRF │
└─────────────────────────────────────┘
Security Auditing
Regular code reviews, automated scanning (SAST/DAST), dependency checking, and penetration testing.
# Static analysis tools composer require --dev vimeo/psalm ./vendor/bin/psalm --taint-analysis # Security-focused # Dependency vulnerability check composer audit
Rate Limiting
Prevent brute force and DoS attacks by limiting requests per time window using token bucket or sliding window algorithms.
class RateLimiter { public function attempt(string $key, int $maxAttempts, int $decaySeconds): bool { $attempts = $this->cache->get($key, 0); if ($attempts >= $maxAttempts) return false; $this->cache->put($key, $attempts + 1, $decaySeconds); return true; } } // Usage: 5 login attempts per minute if (!$limiter->attempt('login:'.$ip, 5, 60)) abort(429);
Content Security Policy
HTTP header that controls which resources browsers can load, mitigating XSS and data injection.
header("Content-Security-Policy: " . "default-src 'self'; " . "script-src 'self' 'nonce-" . $nonce . "'; " . "style-src 'self' 'unsafe-inline'; " . "img-src 'self' data: https:; " . "frame-ancestors 'none';" );
Caching
OpCode Caching (OPcache)
Caches compiled PHP bytecode in shared memory, eliminating parsing/compilation on each request. Enabled by default in PHP 7+; configure in php.ini.
; php.ini opcache.enable=1 opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 ; Set 0 in production
File-based Caching
Simple caching to filesystem; good for single-server setups without external dependencies.
class FileCache { public function get(string $key): mixed { $file = "/tmp/cache/" . md5($key); if (!file_exists($file)) return null; $data = unserialize(file_get_contents($file)); return $data['expires'] > time() ? $data['value'] : null; } public function set(string $key, mixed $value, int $ttl): void { file_put_contents("/tmp/cache/" . md5($key), serialize(['value' => $value, 'expires' => time() + $ttl])); } }
APCu
User-space caching in shared memory—fast, single-server only. Good for frequently accessed data.
// Store value for 1 hour apcu_store('user_123', $userData, 3600); // Retrieve with fallback $user = apcu_fetch('user_123', $success); if (!$success) { $user = $db->fetchUser(123); apcu_store('user_123', $user, 3600); }
Memcached
Distributed memory caching system; ideal for multi-server environments, simple key-value storage.
$mc = new Memcached(); $mc->addServer('localhost', 11211); $mc->set('user:123', $userData, 3600); $data = $mc->get('user:123'); // Multi-get for efficiency $users = $mc->getMulti(['user:1', 'user:2', 'user:3']);
Redis
In-memory data structure store supporting strings, lists, sets, hashes, sorted sets. More features than Memcached including persistence.
$redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->setex('session:abc', 3600, serialize($data)); // Advanced data structures $redis->hSet('user:1', 'name', 'John'); $redis->lPush('queue:jobs', json_encode($job)); $redis->zAdd('leaderboard', 100, 'player:1');
HTTP Caching
Browser and CDN caching via Cache-Control, ETag, and Last-Modified headers. Reduces server load significantly.
$etag = md5($content); $lastModified = gmdate('D, d M Y H:i:s', $timestamp) . ' GMT'; header("Cache-Control: public, max-age=3600"); header("ETag: \"$etag\""); header("Last-Modified: $lastModified"); // Check if client has fresh copy if ($_SERVER['HTTP_IF_NONE_MATCH'] === "\"$etag\"") { http_response_code(304); exit; }
Cache Strategies
Patterns for managing cache writes and reads depending on consistency requirements.
┌─────────────────────────────────────────────────────────────┐
│ Cache-Aside │ App manages cache; read DB on miss │
│ │ [App] ─→ [Cache] ─miss→ [DB] │
├─────────────────────────────────────────────────────────────┤
│ Write-Through │ Write to cache AND DB synchronously │
│ │ [App] ─→ [Cache] ─→ [DB] │
├─────────────────────────────────────────────────────────────┤
│ Write-Behind │ Write cache, async persist to DB │
│ │ [App] ─→ [Cache] ═async═→ [DB] │
└─────────────────────────────────────────────────────────────┘
PSR-6 and PSR-16 Caching Interface
Standard interfaces for cache interoperability. PSR-6 is feature-rich (pools/items), PSR-16 is simple (get/set).
// PSR-16 Simple Cache interface CacheInterface { public function get(string $key, mixed $default = null): mixed; public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool; public function delete(string $key): bool; public function clear(): bool; } // Use symfony/cache or any PSR-16 implementation $cache = new RedisAdapter($redis);