Skip to content

Latest commit

 

History

History
104 lines (78 loc) · 3.87 KB

File metadata and controls

104 lines (78 loc) · 3.87 KB

ADR-005: OPcache-Friendly PHP Array Cache

Status: Accepted Date: 2024-02-15 Applies to: KaririCode\Dotenv 4.2+

Context

In production, .env files are static — they change only during deployments. Parsing the same file on every request wastes CPU cycles. The ideal cache format should:

  1. Eliminate file I/O and string parsing entirely after first load.
  2. Leverage existing PHP infrastructure (no external cache servers).
  3. Be atomic — no partial reads during deployment.
  4. Self-invalidate when source files change.

Decision

Format: var_export() PHP Array

The cache is a plain PHP file that returns an associative array:

<?php

// Auto-generated by KaririCode\Dotenv — do not edit manually.
// Regenerate with: php vendor/bin/kariricode-dotenv cache:dump

return array (
  '__metadata' =>
  array (
    'generated_at' => '2024-02-15T10:30:00+00:00',
    'source_hash' => 'a1b2c3d4e5f6...',
    'generator' => 'KaririCode\\Dotenv v4.x',
  ),
  'DB_HOST' => 'localhost',
  'DB_PORT' => '5432',
  'APP_DEBUG' => 'true',
);

When OPcache is enabled, PHP compiles this file once into shared memory. Subsequent include calls load directly from shared memory — no disk I/O, no parsing, no lexing. This is the fastest possible read path in PHP.

Atomic Writes

$tmpFile = $path . '.tmp.' . getmypid();
file_put_contents($tmpFile, $content);
rename($tmpFile, $path);
opcache_invalidate($path, true);
  1. Write to a temporary file (PID-suffixed to avoid collisions).
  2. Atomic rename() — POSIX guarantees this is atomic on the same filesystem.
  3. Invalidate OPcache for the target path so the new version is picked up.

Staleness Detection

The __metadata.source_hash is computed from file modification times and sizes of all source .env files:

$context = hash_init('md5');
foreach ($filePaths as $filePath) {
    hash_update($context, (string) filemtime($filePath));
    hash_update($context, (string) filesize($filePath));
}
return hash_final($context);

On load, if the stored hash doesn't match the current source hash, the cache is considered stale and bypassed. This avoids content hashing (expensive for large files) while still detecting changes.

Load Flow

load() called
  → cachePath configured?
    → No: parse .env files normally
    → Yes: compute source hash → load cache file → hash matches?
      → Yes: use cached variables (zero parse cost)
      → No: parse .env files normally (cache is stale)

Raw String Storage

The cache stores raw string values, not typed values. Type detection and casting run after cache load, ensuring that custom detectors/casters registered at runtime are applied consistently regardless of whether the source was cache or file.

Consequences

Positive:

  • Zero parse cost in production when OPcache is enabled — variables load from shared memory.
  • No external cache dependency (Redis, Memcached, APCu).
  • Atomic writes prevent partial reads during deployment.
  • Automatic staleness detection without manual cache clearing.
  • var_export() produces valid PHP that survives OPcache restarts.

Negative:

  • Requires filesystem write access in the deployment step.
  • OPcache must be enabled for the full performance benefit (still faster than parsing without OPcache due to include vs string parsing).
  • MD5-based hash is not cryptographically secure — but it only needs collision resistance for cache invalidation, not security.

Rejected alternatives:

  • APCu cache: Requires ext-apcu and doesn't survive OPcache resets.
  • JSON cache: Requires json_decode() on every request — slower than include.
  • Serialized PHP: unserialize() is slower than include for array data and has security implications.
  • Content hashing (SHA-256 of file contents): Correct but expensive — reading entire file contents to check if cache is valid defeats the purpose.