Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions CHANGELOG-6.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Changelog - Version 6.0

## Overview

Version 6.0 represents a major update focused on modernizing the codebase for PHP 8.3+ compatibility, improving code quality with strict typing, and enhancing developer experience with updated tooling.

## Breaking Changes

| Area | Before (5.x) | After (6.0) | Description |
|------|--------------|-------------|-------------|
| **PHP Version** | `>=8.1 <8.4` | `>=8.3 <8.6` | Minimum PHP version raised from 8.1 to 8.3. PHP 8.1 and 8.2 are no longer supported. Added support for PHP 8.4 and 8.5. |
| **PHPUnit** | `^9.6` | `^10.5\|^11.5` | PHPUnit 9.6 support dropped. Minimum version is now 10.5. Added support for PHPUnit 11.5. |
| **PSR-3 Logger** | `^1.0\|^1.1\|^2.0` | `^1.0\|^2.0\|^3.0` | Added PSR-3 version 3.0 support. Removed version 1.1 constraint (covered by 1.0). |
| **Psalm** | `^5.9` | `^5.9\|^6.13` | Added support for Psalm 6.13 while maintaining backward compatibility with 5.9. |

## New Features

### Code Quality Enhancements
- **PHP 8.3+ Override Attribute**: Added `#[Override]` attributes to all overridden methods across the codebase for better code clarity and IDE support
- **Strict Typing**: Enhanced type declarations throughout the codebase for improved type safety
- **Psalm SARIF Reporting**: Added SARIF (Static Analysis Results Interchange Format) output support for better CI/CD integration

### Developer Experience
- **Composer Scripts**: Added convenient composer scripts:
- `composer test` - Run PHPUnit tests
- `composer psalm` - Run Psalm static analysis with single thread for stability
- **Gitpod Support**: Added `.gitpod.yml` configuration for cloud-based development environment
- **VSCode Configuration**: Added `.vscode/launch.json` with debugging configurations
- **Improved Documentation**: Enhanced all documentation files with better examples and clearer explanations

### Testing Improvements
- **Test Class Refactoring**: Reorganized test class hierarchy for better maintainability
- Introduced `TestBase` class as the foundation for all cache tests
- Renamed test classes for clarity and consistency
- Migrated PHPUnit data providers to PHP 8.1+ syntax
- **Enhanced CI/CD**: Updated GitHub Actions workflows with better PHP version matrix testing

### Engine Improvements
- **Consistent Key Handling**: Fixed key consistency issues in `MemcachedEngine` for more reliable caching
- **FileSystemCacheEngine**: Improved path handling and directory creation logic

## Bug Fixes

- Fixed unit test issues related to session handling in GitHub Actions environment
- Improved Memcached availability testing in CI/CD pipelines
- Fixed test execution issues requiring `--stderr` parameter for SessionCacheEngine tests
- Enhanced error handling and edge cases in various cache engines

## Documentation Updates

All documentation files have been updated to reflect version 6.0 changes:
- Updated code examples to use PHP 8.3+ syntax
- Improved atomic operations documentation
- Enhanced PSR-16 and PSR-6 usage guides
- Updated all engine-specific documentation pages
- Refreshed README with clearer quick start examples
- Added mermaid diagrams for dependency visualization

## Migration Path from 5.x to 6.0

### Step 1: Update PHP Version
Ensure your environment is running PHP 8.3 or later:
```bash
php -v # Should show 8.3.x, 8.4.x, or 8.5.x
```

If you're on PHP 8.1 or 8.2, you must upgrade your PHP version before migrating to version 6.0.

### Step 2: Update Dependencies
Update your `composer.json`:
```bash
composer require byjg/cache-engine:^6.0
composer update
```

### Step 3: Update Development Dependencies (Optional)
If you're using PHPUnit or Psalm in your project:

**For PHPUnit:**
```bash
composer require --dev phpunit/phpunit:^10.5
# or
composer require --dev phpunit/phpunit:^11.5
```

**For Psalm:**
```bash
composer require --dev vimeo/psalm:^6.13
```

### Step 4: Test Your Application
Run your existing tests to ensure compatibility:
```bash
vendor/bin/phpunit
```

### Step 5: Optional Enhancements
Consider adding the `#[Override]` attribute to your own classes that extend cache engines for better IDE support and code clarity:

```php
class MyCustomCache extends BaseCacheEngine
{
#[Override]
public function get(string $key, mixed $default = null): mixed
{
// Your implementation
}
}
```

### Step 6: Update CI/CD Pipelines
Update your CI/CD configuration to use PHP 8.3+ in your testing matrix. Remove PHP 8.1 and 8.2 from your test matrix.

### Common Migration Issues

**Issue**: Application fails with "PHP version requirement not satisfied"
**Solution**: Upgrade your PHP version to 8.3 or later

**Issue**: PHPUnit tests fail to run
**Solution**: Upgrade PHPUnit to version 10.5 or later: `composer require --dev phpunit/phpunit:^10.5`

**Issue**: Psalm reports new errors
**Solution**: If using Psalm 6.x, review and address the stricter type checking. You can temporarily stay on Psalm 5.9 during migration.

### Testing Your Migration
After completing the migration steps, verify everything works:

```bash
# Run tests
composer test

# Run static analysis
composer psalm

# If using docker-compose
docker compose up -d
composer test
docker compose down
```

## Notes

- All cache engines maintain backward compatibility at the API level
- No changes to PSR-6 or PSR-16 interface implementations
- Existing cache data remains compatible across versions
- The upgrade primarily affects development-time requirements (PHP version, testing tools)

## Links

- [Full Commit History](https://github.com/byjg/php-cache-engine/compare/5.0.4...6.0.0)
- [Documentation](https://github.com/byjg/php-cache-engine/tree/master/docs)
- [Report Issues](https://github.com/byjg/php-cache-engine/issues)
212 changes: 212 additions & 0 deletions CHANGELOG-7.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Changelog - Version 7.0

## Overview

Version 7.0 adds `CompareAndSwapInterface`, the conditional-write primitive needed to build correct
distributed locks on top of a cache engine, and repairs three genuine race conditions in the
existing atomic operations.

The races were not theoretical. A forked-process test reproduces each one against 6.x: twenty
concurrent `increment()` calls on Memcached ended at **8** instead of 20, and twenty concurrent
`add()` calls on Redis duplicated the seed value **16 times** while dropping appends.

## Breaking Changes

| Area | Before (6.x) | After (7.0) | Description |
|------|--------------|-------------|-------------|
| **Redis atomic operations** | `INCR`/`RPUSH` + separate `EXPIRE` | Lua scripts (`EVAL`) | `increment()`, `decrement()` and `add()` are now single scripts so the value and its TTL cannot be split apart. **Requires Lua scripting to be enabled on the Redis server**; some managed offerings restrict `EVAL`. |
| **Redis `add()` TTL** | `$ttl` accepted and silently ignored | `$ttl` applied | A list built with `add($key, $value, 60)` now actually expires. Code that relied on the list living forever despite passing a TTL will see it expire. |
| **FileSystem atomic TTL** | raw `$ttl` written as the expiry timestamp | converted with `addToNow()` | `increment($key, 1, 60)` wrote `60` into the expiry file — a moment in 1970 — so the entry was already expired when written. It now means 60 seconds from now, and the value survives as intended. |

## New Features

### CompareAndSwapInterface

A new interface for conditional writes that the storage engine resolves in one indivisible step:

- `setIfAbsent(string $key, mixed $value, DateInterval|int|null $ttl = null): bool` — stores only if
the key is free; exactly one of any number of concurrent callers gets `true`. The TTL is applied
in the same step as the write, so a process that dies mid-operation cannot leave a key with no
expiry behind.
- `deleteIfEquals(string $key, mixed $value): bool` — deletes only while the key still holds your
value, so an owner whose TTL quietly lapsed cannot destroy the entry someone else has taken over.
- `expireIfEquals(string $key, mixed $value, DateInterval|int|null $ttl): bool` — extends the TTL
under the same guard.

**Implemented by:** `RedisCacheEngine` (`SET NX EX` plus Lua for the guarded operations) and
`MemcachedEngine` (native `add()` plus `cas()`).

`FileSystemCacheEngine` deliberately does **not** implement it. `flock()` attaches to an inode
rather than a path, and this engine unlinks the file on delete, so two processes can hold "the
lock" on two different inodes at the same path. The file system is not a reliable substrate for
mutual exclusion and the engine does not pretend otherwise.

This is a separate interface rather than an addition to `AtomicOperationInterface`, so nothing that
already implements the latter breaks. Probe with `instanceof` and degrade explicitly:

```php
if (!$cache instanceof \ByJG\Cache\CompareAndSwapInterface) {
throw new RuntimeException('This engine cannot guarantee mutual exclusion');
}
```

### Documentation

- New [Compare and Swap](docs/compare-and-swap.md) guide, including a worked lock example and an
explicit section on what the interface does *not* give you (no fairness, no reentrancy, the TTL
is still a guess).
- [Atomic Operations](docs/atomic-operations.md) now documents the TTL semantics and what happens
when `add()` is called on a key previously written by `set()`.

## Bug Fixes

### Memcached: non-atomic seeding in every atomic operation

`increment()`, `decrement()` and `add()` all initialised a missing key with `get() === false`
followed by `set()`. Memcached's own `increment()` is atomic, but those two preparatory calls are
not, and the interleaving loses updates:

```
P1: get() === false P2: get() === false
P1: set(0)
P1: increment() -> 1
P2: set(0) <- resets the counter
P2: increment() -> 1 <- the same value issued twice
```

All three now seed with `Memcached::add()`, which the server resolves in a single step: exactly one
caller creates the key and the rest move on.

`add()` was additionally hardened — it no longer dereferences the CAS token when the key expired
mid-loop, and it claims an absent key with `add()` instead of `set()`.

### Redis: `add()` corrupted the list when converting from a plain value

The first `add()` to a key written by `set()` has to turn a string into a list. That was done as
`GET` → `DEL` → re-push → `RPUSH` with no protection, so concurrent callers each deleted a list the
others were mid-way through rebuilding. The conversion is now a compare-guarded script that only
rewrites the key while it still holds the value that was read; losing that compare means another
process already converted it.

### Redis: TTL applied as a separate command

`increment()` and `decrement()` set the expiry with a follow-up `EXPIRE`. A crash between the two
left a counter with no expiry at all. Both now apply it inside the script.

### `psr/simple-cache` 3.0 is now allowed

The engines already declare the PSR-16 3.0 signatures, but the constraint stopped at `^2.0`, so the
package could not be installed alongside anything requiring `psr/simple-cache ^3.0`. The constraint
is now `^2.0|^3.0`.

### Redis: `has()` and `clear()` never opened the connection

Both read `$this->redis` without calling `lazyLoadRedisServer()` first. Every other public method
established the connection; these two were missed. Calling either as the first operation on a new
instance died with:

```
Error: Call to a member function exists() on null
```

It went unnoticed because in practice something else — `isAvailable()`, `get()`, `set()` — almost
always ran first and left the connection open. A regression test now exercises each entry point on
an instance that has never been touched.

### FileSystem: expiry dropped outside the lock

`putContents()` deleted the `.ttl` file before acquiring the lock, leaving the value briefly
immortal — a reader arriving in that window saw an entry that should already have expired. The
delete now happens under the lock.

## Testing

- `tests/CompareAndSwapTest.php` — 18 tests across both engines covering expiry, non-owner
rejection and the stale-owner-versus-new-owner case.
- `tests/ConcurrencyTest.php` — forks twenty real processes at a synchronised barrier. Sequential
tests cannot prove atomicity; these fail against 6.x and pass on 7.0.
- A TTL regression test added to `tests/CachePSR16Test.php` covering every engine that implements
`AtomicOperationInterface`.

Note for anyone writing similar tests: forked children inherit the parent's Memcached socket and
close it as they exit, killing the parent's connection. Reconnect in the parent before asserting.

## Migration Path from 6.x to 7.0

### Step 1: Confirm Lua scripting is available on your Redis server

`increment()`, `decrement()` and `add()` now use `EVAL`. This is enabled by default in Redis, but
some managed and proxied deployments restrict it. Verify with:

```bash
redis-cli EVAL "return 1" 0 # should print (integer) 1
```

If `EVAL` is unavailable in your environment, stay on 6.x or open an issue.

### Step 2: Update the dependency

```bash
composer require byjg/cache-engine:^7.0
composer update
```

### Step 3: Review any use of `add()` with a TTL on Redis

The TTL was previously ignored, so lists built this way never expired. They now do. If you were
relying on the old behaviour, drop the TTL argument:

```php
$cache->add('my-key', 'value', 3600); // now expires after an hour
$cache->add('my-key', 'value'); // never expires, as 6.x behaved
```

### Step 4: Review any use of atomic operations with a TTL on FileSystem

These were broken and returned already-expired entries, so working code is unlikely to depend on
them. If you worked around the bug by omitting the TTL, you can now pass it.

### Step 5: Adopt compare-and-swap where you were hand-rolling a lock

If you have code shaped like this, replace it — it has a race between the two calls:

```php
// Before - two operations with a gap between them
if (!$cache->has('lock')) {
$cache->set('lock', $token, 30);
}

// After - one indivisible operation
if ($cache->setIfAbsent('lock', $token, 30)) {
try {
// ...
} finally {
$cache->deleteIfEquals('lock', $token);
}
}
```

### Common Migration Issues

**Issue**: `NOSCRIPT` or "unknown command EVAL" errors from Redis
**Solution**: Lua scripting is disabled or proxied away in your deployment. See Step 1.

**Issue**: Cached lists built with `add()` now disappear
**Solution**: You are passing a TTL that was previously ignored. See Step 3.

**Issue**: `$cache->setIfAbsent()` does not exist
**Solution**: The engine does not implement `CompareAndSwapInterface`. Only `RedisCacheEngine` and
`MemcachedEngine` do. Probe with `instanceof` before calling.

## Notes

- `AtomicOperationInterface` is unchanged; existing implementations continue to work untouched.
- No changes to the PSR-6 or PSR-16 interface implementations.
- Existing cache data remains compatible across versions.
- PHP requirement is unchanged at `>=8.3 <8.6`.

## Links

- [Full Commit History](https://github.com/byjg/php-cache-engine/compare/6.0.1...7.0.0)
- [Documentation](https://github.com/byjg/php-cache-engine/tree/master/docs)
- [Report Issues](https://github.com/byjg/php-cache-engine/issues)
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A powerful, versatile cache implementation providing both PSR-6 and PSR-16 inter
- **PSR-6 Cache Pool interface** - More verbose caching with fine-grained control
- **Multiple storage backends** - Choose from memory, file system, Redis, Memcached and more
- **Atomic operations** - Support for increment, decrement and add operations in compatible engines
- **Compare and swap** - Conditional writes for correct distributed locking on Redis and Memcached
- **Garbage collection** - Automatic cleanup of expired items
- **PSR-11 container support** - Retrieve cache keys via dependency container
- **Logging capabilities** - PSR-3 compatible logging of cache operations
Expand Down Expand Up @@ -68,6 +69,7 @@ $value = $item->get();

### Advanced Features
- [Atomic Operations](docs/atomic-operations.md)
- [Compare and Swap](docs/compare-and-swap.md)
- [Garbage Collection](docs/garbage-collection.md)
- [Logging](docs/setup-log-handler.md)
- [PSR-11 Container Usage](docs/psr11-usage.md)
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"php": ">=8.3 <8.6",
"psr/cache": "^1.0|^2.0|^3.0",
"psr/log": "^1.0|^2.0|^3.0",
"psr/simple-cache": "^1.0|^2.0",
"psr/simple-cache": "^2.0|^3.0",
"psr/container": "^1.0|^1.1|^2.0"
},
"require-dev": {
Expand All @@ -29,7 +29,7 @@
},
"provide": {
"psr/cache-implementation": "1.0",
"psr/simple-cache-implementation": "1.0"
"psr/simple-cache-implementation": "2.0|3.0"
},
"scripts": {
"test": "vendor/bin/phpunit",
Expand Down
Loading
Loading