Skip to content
Merged
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
19 changes: 9 additions & 10 deletions docs/adapters/apcu.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ APCu Adapter
The **ApcuCacheAdapter** uses PHP’s in-process APCu extension. Ideal for:

* **Single-server web applications** (shared memory across FPM workers)
* **CLI scripts** (requires `apc.enable_cli=1`)
* **CLI scripts** (requires ``apc.enable_cli=1``)

Key Points
----------

* **Namespace prefix**: every key is stored under `<namespace>:<key>`.
* **multiFetch()** uses one `apcu_fetch(array $keys)` call (one round-trip).
* **Namespace prefix**: every key is stored under ``<namespace>:<key>``.
* **multiFetch()** uses one ``apcu_fetch(array $keys)`` call (one round-trip).
* **TTL** is honored natively by APCu; expired keys are purged lazily.
* **Atomic-save** is best-effort via `apcu_cas`, but storing a serialized blob is generally atomic.
* **Atomic-save** is best-effort via ``apcu_cas``, but storing a serialized blob is generally atomic.

Requirements
------------

* `ext-apcu` must be installed and enabled.
* For CLI testing, `apc.enable_cli=1` must be set.
* ``ext-apcu`` must be installed and enabled.
* For CLI testing, ``apc.enable_cli=1`` must be set.

Example:

Expand All @@ -48,8 +48,7 @@ Note on ValueSerializer

APCu stores only strings. We wrap/unserialize via:

1. `ValueSerializer::serialize($item)` → store as string blob
2. On fetch, `ValueSerializer::unserialize($blob)` → reconstruct the `ApcuCacheItem` object

In other words, each APCu entry is a serialized `ApcuCacheItem` containing key, value, hit, and expiration.
1. ``ValueSerializer::serialize($item)`` → store as string blob
2. On fetch, ``ValueSerializer::unserialize($blob)`` → reconstruct the ``ApcuCacheItem`` object

In other words, each APCu entry is a serialized ``ApcuCacheItem`` containing key, value, hit, and expiration.
12 changes: 6 additions & 6 deletions docs/adapters/file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ Directory Layout

By default:

* Base directory: `sys_get_temp_dir()` (e.g. `/tmp` on Linux)
* Per-namespace subdirectory: `cache_<namespace>`
* Each key → `hash('xxh128', $key) . '.cache'`
* Base directory: ``sys_get_temp_dir()`` (e.g. ``/tmp`` on Linux)
* Per-namespace subdirectory: ``cache_<namespace>``
* Each key → ``hash('xxh128', $key) . '.cache'``

Example:

Expand All @@ -38,10 +38,10 @@ Example:
Concurrency & Locks
-------------------

* When you call `save()`, the adapter uses `file_put_contents(..., LOCK_EX)`
* When you call ``save()``, the adapter uses ``file_put_contents(..., LOCK_EX)``
to avoid partial writes.
* Reading does **not** use locks (there is a small race-condition if a write is in progress).
* Bulk `getItems()` scans the directory once, checks each file’s content, and unserializes hits.
* Bulk ``getItems()`` scans the directory once, checks each file’s content, and unserializes hits.

Hot-Reload / Namespace Change
-----------------------------
Expand All @@ -54,7 +54,7 @@ You can call:

This will:

1. Create (if needed) `/path/to/custom-dir/cache_newns/`
1. Create (if needed) ``/path/to/custom-dir/cache_newns/``
2. Discard any existing deferred queue or iterator snapshot
3. Subsequent calls use the new directory/namespace

Expand Down
16 changes: 8 additions & 8 deletions docs/adapters/memcached.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Memcached Adapter
========================

The **MemCacheAdapter** connects to one or more Memcached servers via the `\Memcached` extension.
The **MemCacheAdapter** connects to one or more Memcached servers via the ``\Memcached`` extension.
Suitable for:

* **Distributed caching** across multiple web servers
Expand All @@ -16,8 +16,8 @@ Connection

You must supply either:

1. A pre-configured `\Memcached` instance
2. A list of server triples (`[host, port, weight]`)
1. A pre-configured ``\Memcached`` instance
2. A list of server triples (``[host, port, weight]``)

Example:

Expand All @@ -42,16 +42,16 @@ ensure your namespace + key fit within this limit.
multiFetch()
------------

Internally, `getItems()` calls `multiFetch(array $keys)`, which:
Internally, ``getItems()`` calls ``multiFetch(array $keys)``, which:

1. Maps each key to `<ns>:<key>`
2. Calls `$memcached->getMulti([...])`
3. Returns an array of `MemCacheItem` instances, preserving order
1. Maps each key to ``<ns>:<key>``
2. Calls ``$memcached->getMulti([...])``
3. Returns an array of ``MemCacheItem`` instances, preserving order

TTL & Eviction
--------------

* TTL is stored per-item via `$memcached->set($ns . ':' . $key, $blob, $ttl)`.
* TTL is stored per-item via ``$memcached->set($ns . ':' . $key, $blob, $ttl)``.
* Memcached’s LRU + slab allocator evicts entries when RAM is full.
* No persistence across server restarts.

Expand Down
16 changes: 8 additions & 8 deletions docs/adapters/redis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
Redis Adapter
==================

The **RedisCacheAdapter** uses the `Redis` (phpredis) extension. It supports:
The **RedisCacheAdapter** uses the ``Redis`` (phpredis) extension. It supports:

* **Single or clustered Redis** (including Sentinel or Cluster modes)
* **ACL/username/password** via DSN (e.g. `redis://:pass@host:6379/0`)
* **UNIX socket** connections (`redis:///path/to/socket`)
* **ACL/username/password** via DSN (e.g. ``redis://:pass@host:6379/0``)
* **UNIX socket** connections (``redis:///path/to/socket``)

Highlights
----------

* **multiFetch()** via `MGET` → one round-trip for any number of keys
* **Native TTL** → uses `SETEX` or `EXPIRE`
* **multiFetch()** via ``MGET`` → one round-trip for any number of keys
* **Native TTL** → uses ``SETEX`` or ``EXPIRE``
* **Atomic** operations guaranteed by Redis
* **Persistence** depends on your Redis configuration (RDB/AOF)
* Values are stored as plain strings (the result of `ValueSerializer::serialize($item)`)
* Values are stored as plain strings (the result of ``ValueSerializer::serialize($item)``)

Quick Setup
-----------
Expand All @@ -39,7 +39,7 @@ Quick Setup
Saving with TTL
---------------

Internally, on `save(CacheItemInterface $item)`, the adapter does:
Internally, on ``save(CacheItemInterface $item)``, the adapter does:

.. code-block:: php

Expand All @@ -64,7 +64,7 @@ Bulk Fetch Example
Clearing a Namespace
--------------------

`clear(): bool` uses `SCAN` to iterate over all keys in the `“ns:*”` pattern and deletes them in batches,
``clear(): bool`` uses ``SCAN`` to iterate over all keys in the ``“ns:*”`` pattern and deletes them in batches,
avoiding large blocking operations.

Example:
Expand Down
12 changes: 6 additions & 6 deletions docs/adapters/serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,24 @@
Serialization Internals
=====================

All cache adapters rely on **`ValueSerializer`**
All cache adapters rely on **``ValueSerializer``**
to handle arbitrary PHP values—scalars, arrays, closures and resources.

That component:

1. **Wraps** native PHP resources using user-registered handlers
2. Uses Opis Closure v4 (`serialize` / `unserialize`) to serialize closures
3. Produces a string blob via PHP’s native `serialize()` internally if no closures are involved
2. Uses Opis Closure v4 (``serialize`` / ``unserialize``) to serialize closures
3. Produces a string blob via PHP’s native ``serialize()`` internally if no closures are involved
4. On fetch, **unwraps** resources via your handler and restores closures

Why is this necessary? PSR-6 requires that any stored value must be safely serializable if the adapter
stores it as a string or BLOB (Redis, Memcached, SQLite, File). If you want to cache:

* A **closure** (`fn(int $x) => $x + 5`)
* A **stream** (e.g. `fopen('php://memory','r+')`)
* A **closure** (``fn(int $x) => $x + 5``)
* A **stream** (e.g. ``fopen('php://memory','r+')``)
* A **cURL resource**, **XML parser**, or **GD image resource**, etc.

Then you must teach `ValueSerializer` how to handle that resource type by registering a handler:
Then you must teach ``ValueSerializer`` how to handle that resource type by registering a handler:

.. code-block:: php

Expand Down
14 changes: 7 additions & 7 deletions docs/adapters/sqlite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ Ideal for:
* **CLI tools** or single-host PHP processes
* **Small apps** needing persistence without a full Redis/Memcached setup

Bulk Fetch (`multiFetch`)
Bulk Fetch (``multiFetch``)
-------------------------

Instead of N separate `SELECT` calls, `getItems()` does:
Instead of N separate ``SELECT`` calls, ``getItems()`` does:

- If found **and** not expired: `ValueSerializer::unserialize(value)` → return `SqliteCacheItem` (hit)
- If expired: `DELETE FROM cache WHERE key = ?` → return new `SqliteCacheItem` (miss)
- If not found: return new `SqliteCacheItem` (miss)
- If found **and** not expired: ``ValueSerializer::unserialize(value)`` → return ``SqliteCacheItem`` (hit)
- If expired: ``DELETE FROM cache WHERE key = ?`` → return new ``SqliteCacheItem`` (miss)
- If not found: return new ``SqliteCacheItem`` (miss)

Example:

Expand Down Expand Up @@ -45,11 +45,11 @@ Example:
Clearing All Entries
--------------------

`clear()` simply runs `DELETE FROM cache` on the table and resets the deferred queue.
``clear()`` simply runs ``DELETE FROM cache`` on the table and resets the deferred queue.

Performance Notes
-----------------

* A single SQLite file can handle thousands of keys, but write throughput is limited by
SQLite’s journaling overhead.
* Consider using `memory:` or `file::memory:?cache=shared` DSNs for in-memory databases in tests.
* Consider using ``memory:`` or ``file::memory:?cache=shared`` DSNs for in-memory databases in tests.
21 changes: 21 additions & 0 deletions docs/backstory.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.. _backstory:

======================
Why I Built InterMix
======================

I did not build InterMix as a large framework from the beginning. I built it because I kept running into a smaller, more practical problem: I wanted dependency injection in PHP to feel lightweight, flexible, and actually pleasant to use.

Back in October 2020, I started with a small project called DI-Container. At that time, the goal was straightforward. I wanted a compact utility that could resolve constructors and callables cleanly, without unnecessary heaviness. It began as an experiment, but as I kept working on it, I found myself solving more than just container-related problems. Each improvement opened the door to another need, better invocation flow, more flexible resolution, cleaner abstractions, and utilities that naturally belonged around the core idea.

That was the point where I realized I was no longer building just a DI container.

In May 2021, that earlier work evolved into InterMix. I wanted a better foundation, something that could grow without losing the original simplicity that made the first project useful. The new direction was not about making the project bigger for the sake of being bigger. It was about giving it the structure and freedom to become a toolkit I could genuinely rely on across real applications.

I built InterMix incrementally. There was no single moment where everything was fully designed in advance. The project grew through repeated iteration: building, using, refining, rethinking, and improving. Over time, dependency injection remained the center, but the ecosystem around it expanded naturally into caching, macro-style extensibility, memoization, helper utilities, and safety-focused tools. Those additions were not random. They came from practical needs that kept appearing during development.

That is really the reason InterMix exists.

I wanted a toolkit that stayed lightweight but could still be powerful. I wanted something modular, reusable, and grounded in real usage rather than unnecessary complexity. I wanted tools that worked well together, but could also stand on their own. Most of all, I wanted to build something that could mature over time without losing its original purpose.

InterMix is the result of that journey. What started as a small experiment in 2020 gradually became a broader and more stable PHP toolkit. I did not build it to chase size or trends. I built it to solve real problems in a way that felt clean, practical, and sustainable, and I have continued refining it with that same mindset ever since.
104 changes: 52 additions & 52 deletions docs/cache.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@
Cache – Unified Facade
===========================

The **`Cache`** class is a PSR-6 and PSR-16–compliant cache façade with an adapter-agnostic interface.
The **``Cache``** class is a PSR-6 and PSR-16–compliant cache façade with an adapter-agnostic interface.
It provides:

* **Static factories** for common back-ends:
- `Cache::file()`
- `Cache::apcu()`
- `Cache::memcache()`
- `Cache::redis()`
- `Cache::sqlite()`
- ``Cache::file()``
- ``Cache::apcu()``
- ``Cache::memcache()``
- ``Cache::redis()``
- ``Cache::sqlite()``
* A **convenience API** layering on top of PSR-6 and PSR-16:
- **PSR-6 extras**:
- `set(string $key, mixed $value, int|DateInterval|null $ttl = null): bool`
- `get(string $key, mixed $default = null): mixed`
- If `$default` is a callable, it will be invoked on a cache miss with the `CacheItemInterface` as argument, the returned value will be saved (with any TTL set inside the callback), and then returned.
- **Magic properties** (`$cache->foo`, `$cache->foo = 'bar'`)
- **ArrayAccess** (`$cache['id']`)
- **Countable** (`count($cache)`)
* **Bulk fetch** (`getItems([...])`) that defers to adapter for single-round-trip performance
* **Iteration** (`foreach ($cache->getItemsIterator() as $k => $v)`)
- ``set(string $key, mixed $value, int|DateInterval|null $ttl = null): bool``
- ``get(string $key, mixed $default = null): mixed``
- If ``$default`` is a callable, it will be invoked on a cache miss with the ``CacheItemInterface`` as argument, the returned value will be saved (with any TTL set inside the callback), and then returned.
- **Magic properties** (``$cache->foo``, ``$cache->foo = 'bar'``)
- **ArrayAccess** (``$cache['id']``)
- **Countable** (``count($cache)``)
* **Bulk fetch** (``getItems([...])``) that defers to adapter for single-round-trip performance
* **Iteration** (``foreach ($cache->getItemsIterator() as $k => $v)``)
* Automatic **serialization** of closures, resources, and arbitrary PHP values

-------------------------------
Expand Down Expand Up @@ -71,65 +71,65 @@ Public API

PSR-6 Methods (delegated to the underlying adapter):

* **`getItem(string $key): CacheItemInterface`**
* **`getItems(array $keys = []): iterable`**
(internally calls adapter’s `multiFetch()` if available)
* **`hasItem(string $key): bool`**
* **`clear(): bool`**
* **`deleteItem(string $key): bool`**
* **`deleteItems(array $keys): bool`**
* **`save(CacheItemInterface $item): bool`**
* **`saveDeferred(CacheItemInterface $item): bool`**
* **`commit(): bool`**
* **``getItem(string $key): CacheItemInterface``**
* **``getItems(array $keys = []): iterable``**
(internally calls adapter’s ``multiFetch()`` if available)
* **``hasItem(string $key): bool``**
* **``clear(): bool``**
* **``deleteItem(string $key): bool``**
* **``deleteItems(array $keys): bool``**
* **``save(CacheItemInterface $item): bool``**
* **``saveDeferred(CacheItemInterface $item): bool``**
* **``commit(): bool``**

PSR-16 Methods (implemented on top of PSR-6):

* **`get(string $key, mixed $default = null): mixed`**
- Returns the cached value or `$default`.
- If `$default` is a **callable**, then on a cache miss the callable is invoked with the `CacheItemInterface` argument; its return value is saved (respecting any TTL set inside the callback) and then returned.
* **`set(string $key, mixed $value, int|DateInterval|null $ttl = null): bool`**
* **``get(string $key, mixed $default = null): mixed``**
- Returns the cached value or ``$default``.
- If ``$default`` is a **callable**, then on a cache miss the callable is invoked with the ``CacheItemInterface`` argument; its return value is saved (respecting any TTL set inside the callback) and then returned.
* **``set(string $key, mixed $value, int|DateInterval|null $ttl = null): bool``**
- Shortcut for “create a new CacheItem → set($value) → expiresAfter($ttl) → save()”.
* **`remember(string $key, callable $resolver, int|DateInterval|null $ttl = null, array $tags = []): mixed`**
* **``remember(string $key, callable $resolver, int|DateInterval|null $ttl = null, array $tags = []): mixed``**
- Stampede-protected compute-on-miss with host-local lock + jittered TTL.
* **`setTagged(string $key, mixed $value, array $tags, int|DateInterval|null $ttl = null): bool`**
* **``setTagged(string $key, mixed $value, array $tags, int|DateInterval|null $ttl = null): bool``**
- Store a value and associate it with one or more tags.
* **`invalidateTag(string $tag): bool`**
* **``invalidateTag(string $tag): bool``**
- Delete all keys associated with a tag.
* **`invalidateTags(array $tags): bool`**
* **``invalidateTags(array $tags): bool``**
- Batch invalidation for multiple tags.
* **`has(string $key): bool`**
- Equivalent to `hasItem(string $key)`.
* **`delete(string $key): bool`**
- Equivalent to `deleteItem(string $key)`.
* **`clear(): bool`**
- Equivalent to `clear()`.
* **`getMultiple(iterable $keys, mixed $default = null): iterable`**
* **``has(string $key): bool``**
- Equivalent to ``hasItem(string $key)``.
* **``delete(string $key): bool``**
- Equivalent to ``deleteItem(string $key)``.
* **``clear(): bool``**
- Equivalent to ``clear()``.
* **``getMultiple(iterable $keys, mixed $default = null): iterable``**
- Fetches multiple keys.
- If a `$default` is provided (scalar or callable), it applies per-key on miss.
* **`setMultiple(iterable $values, int|DateInterval|null $ttl = null): bool`**
- Accepts an associative array or Traversable of `$key => $value`, sets each with optional TTL.
* **`deleteMultiple(iterable $keys): bool`**
- Alias for `deleteItems(array $keys)`.
- If a ``$default`` is provided (scalar or callable), it applies per-key on miss.
* **``setMultiple(iterable $values, int|DateInterval|null $ttl = null): bool``**
- Accepts an associative array or Traversable of ``$key => $value``, sets each with optional TTL.
* **``deleteMultiple(iterable $keys): bool``**
- Alias for ``deleteItems(array $keys)``.
* **Cache Stampede Protection**:
- `remember()` method provides compute-once with host-local file locking
- ``remember()`` method provides compute-once with host-local file locking
- Automatic jittered TTL to prevent thundering herd effects
- Lock timeout and retry mechanisms for high concurrency scenarios

Magic props & ArrayAccess & Countable:

* **Magic props:**
- `$cache->foo` ↔ `$cache->get('foo')`
- `$cache->foo = 'bar'` ↔ `$cache->set('foo','bar')`
- `isset($cache->foo)` ↔ `$cache->hasItem('foo')`
- `unset($cache->foo)` ↔ `$cache->deleteItem('foo')`
- ``$cache->foo`` ↔ ``$cache->get('foo')``
- ``$cache->foo = 'bar'`` ↔ ``$cache->set('foo','bar')``
- ``isset($cache->foo)`` ↔ ``$cache->hasItem('foo')``
- ``unset($cache->foo)`` ↔ ``$cache->deleteItem('foo')``
* **ArrayAccess:**
- `$cache['id'] = 123` ; `$val = $cache['id']` ; `isset($cache['id'])` ; `unset($cache['id'])`
- ``$cache['id'] = 123`` ; ``$val = $cache['id']`` ; ``isset($cache['id'])`` ; ``unset($cache['id'])``
* **Countable:**
- `count($cache)` delegates to either adapter’s `count()` or does a manual scan if the adapter is not `Countable`.
- ``count($cache)`` delegates to either adapter’s ``count()`` or does a manual scan if the adapter is not ``Countable``.

Optional:

- `setNamespaceAndDirectory(string $namespace, string|null $dir)`
- ``setNamespaceAndDirectory(string $namespace, string|null $dir)``
- Only supported if the adapter implements it (FileCacheAdapter is the primary one).
- Allows changing namespace and/or storage location at runtime.

Expand Down
Loading
Loading