PHPackages                             jessegall/concurrent - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Caching](/categories/caching)
4. /
5. jessegall/concurrent

ActiveLibrary[Caching](/categories/caching)

jessegall/concurrent
====================

A thread-safe wrapper for cached values with locking, validation, and read-only method support

v3.2.2(2mo ago)0113MITPHPPHP ^8.4

Since Apr 2Pushed 2mo agoCompare

[ Source](https://github.com/jessegall/concurrent)[ Packagist](https://packagist.org/packages/jessegall/concurrent)[ RSS](/packages/jessegall-concurrent/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (6)Versions (52)Used By (0)

Concurrent
==========

[](#concurrent)

Thread-safe shared state for PHP. Wrap any value (object, array, scalar) in a proxy that handles locking, caching, and persistence across processes. Works with Laravel out of the box; pluggable cache and lock drivers otherwise.

Why?
----

[](#why)

When multiple processes (web requests, queue workers, cron jobs) share state, you scatter cache calls across the codebase: duplicated keys, no locking, race conditions on read-modify-write, business logic tangled with cache plumbing.

Concurrent wraps the value in a thread-safe proxy. You interact with it normally; it handles locking and persistence. Writes are atomic.

Installation
------------

[](#installation)

```
composer require jessegall/concurrent
```

Wrapping Any Value
------------------

[](#wrapping-any-value)

Wrap any value by passing it (or a factory) as the `default`. The wrapper looks and acts like the value itself: methods, properties, array access all proxy through. Reads hit the cache directly; writes lock, mutate, write back.

```
use JesseGall\Concurrent\Concurrent;

/** @var Concurrent $cart */
$cart = new Concurrent(
    key: "cart:{$userId}",
    default: fn () => new ShoppingCart(),
    ttl: 1800,
);

$cart->addItem('T-Shirt', 2);  // method call: locks, writes back
$cart->itemCount();            // method call: locks (see Read-only methods to skip)
$cart->items;                  // property read: no lock
$cart->couponCode = 'SAVE10';  // property write: locks, writes back
$cart();                       // get the value
$cart(null);                   // forget
```

Every write persists to the cache automatically. No `save()` or `flush()` step. Each write is atomic on its own.

Grouping Writes Into One Atomic Update
--------------------------------------

[](#grouping-writes-into-one-atomic-update)

Use a callback when you need several writes (or a read-then-write) to land as one atomic step, so nothing else can interleave.

```
// Two separate atomic writes. Another worker can read or write
// between them and see a half-updated cart.
$cart->discount = 10;
$cart->total = $cart->subtotal - 10;

// One atomic update. The lock is held across both lines.
$cart(function (Cart $data) {
    $data->discount = 10;
    $data->total = $data->subtotal - 10;
    return $data;
});
```

Three ways to express the grouped update. Pick whichever fits.

### 1. Methods on the wrapped class

[](#1-methods-on-the-wrapped-class)

The cleanest option when you own the source: put the mutation logic in a method on the wrapped class.

```
class Cart {
    public array $items = [];

    public function addItem(string $sku): void {
        $this->items[] = $sku;
        $this->lastSku = $sku;
    }
}

/** @var Concurrent $cart */
$cart = new Concurrent(key: 'cart', default: fn () => new Cart);
$cart->addItem('shirt');           // atomic: Concurrent locks, runs the method, writes back
```

### 2. Callbacks

[](#2-callbacks)

#### Bound Callback

[](#bound-callback)

Pass a zero-param closure to `$concurrent(...)`:

```
$cart(function () {
    $this->items[] = $newItem;
    $this->totals['subtotal'] = 100;
    $this->status = 'pending';
});
```

Inside the callback, `$this` behaves like the Concurrent wrapper merged with the wrapped value: the wrapped value's properties and methods take precedence, anything missing falls through to the wrapper. `self::`, `parent::`, and `static::` still resolve to the wrapper class, so constants and static methods on it work as you'd expect.

Arrow functions work too:

```
$counter(fn () => $this->count++);
$cart(fn () => $this->items[] = $newItem);
```

#### Transform Callback

[](#transform-callback)

Receive the value, return the new one. Best for replacing the whole value, especially scalars:

```
// Arrow functions return the expression's value implicitly.
$counter(fn (int $n) => $n + 1);
$concurrent(fn (array $value) => [...$value, 'new entry']);

// Non-arrow functions need an explicit return.
$cart(function (Cart $data) {
    $data->discount = 10;
    return $data;
});
```

#### By-reference Callback

[](#by-reference-callback)

Take the wrapped value as a `&`-marked parameter and mutate it directly. Concurrent sees the mutated value and writes it back; no return needed.

```
$cart(fn (Cart &$data) => $data->items[] = $newItem);

$cart(function (Cart &$data) {
    $data->items[] = $newItem;
    $data->totals['count']++;
});
```

Use a By-reference Callback when:

- **The wrapped value is an array.** `$this[]` doesn't work on the bound proxy; `$data[]` does.
- **You want better static analysis.** PHPStan and Psalm read a typed `Cart &$data` parameter directly. With bound `$this` they see `BoundProxy`.
- **You want the outer `$this`.** Any callback with a parameter keeps `$this` as the surrounding class, so you can still call its methods or read its properties.

Without the `&`, the closure falls back to a Transform Callback (above). A block with no `return` writes `null` to the cache. An arrow writes the expression value, so `fn ($d) => $d->items[] = $x` writes `$x`, not the cart. Use `&`, or return the value yourself.

### 3. A wrapper subclass that owns the domain API

[](#3-a-wrapper-subclass-that-owns-the-domain-api)

When you control neither the source nor want ad-hoc callbacks all over your codebase, define your own `Concurrent` subclass with domain methods that internally use callbacks. See [Subclassing](#subclassing).

Subclassing
-----------

[](#subclassing)

Encapsulate the key, default, TTL, and domain methods. Add `@extends Concurrent` so the IDE picks up the wrapped class's methods on the subclass too. If your IDE doesn't resolve the generic and apply the `@mixin` through it, fall back to `/** @mixin T */` on the subclass:

```
/** @extends Concurrent */
class ProcessingSession extends Concurrent
{
    public function __construct(string $id)
    {
        parent::__construct(
            key: "processing:{$id}",
            default: fn () => new SessionData(),
            ttl: 3600,
            validator: fn ($v) => $v instanceof SessionData,
        );
    }

    public function start(int $total): void
    {
        $this(function () use ($total) {
            $this->total = $total;
            $this->status = 'processing';
        });
    }

    public function advance(): void
    {
        $this(fn () => $this->processed++);
    }

    public function addError(string $message): void
    {
        $this(fn () => $this->errors[] = $message);
    }
}
```

Auto-generated Keys
-------------------

[](#auto-generated-keys)

When a `Concurrent` is constructed inside another class's `__construct` and stored on a property of that class, you can omit `key:`. The wrapper figures out the key on first use by reflecting on the owning class and finding the property it's assigned to. The result is `{FullyQualifiedClassName}:{propertyName}`.

```
class RateLimiter
{
    /** @var Concurrent */
    private Concurrent $attempts; // attempts = new Concurrent(default: fn () => []);
    }
}
```

Two `RateLimiter` instances share the same auto-key, so they see the same cached state.

### WithAccessors

[](#withaccessors)

Concurrent's public surface is deliberately small. Every method on `Concurrent` is one that can't appear on the wrapped value or a subclass, so wrapping a raw value or extending `Concurrent` with your own domain methods doesn't collide with the proxy's API.

`WithAccessors` is opt-in for that reason: it adds helpers (`get`, `set`, `has`, `update`, `clear`) on subclasses that want them, without baking them into the base class where they'd shadow methods on whatever you wrap.

```
/** @extends Concurrent */
class UserActivity extends Concurrent
{
    use WithAccessors;

    public function __construct(int $userId) {
        parent::__construct(
            key: "activity:{$userId}",
            default: fn () => new ActivityData,
            ttl: 86400,
        );
    }

    public function recordLogin(): void
    {
        $this->update(function () {
            $this->loginCount++;
            $this->lastLoginAt = time();
        });
    }

    public function loginCount(): int   { return $this->get('loginCount', 0); }
    public function lastLoginAt(): ?int { return $this->get('lastLoginAt'); }
    public function reset(): void       { $this->clear(); }
}
```

Private by default. Expose any of them via PHP's trait conflict resolution:

```
class Settings extends Concurrent
{
    use WithAccessors {
        get as public;
        set as public;
    }
}

$settings->set('theme', 'dark');
$settings->get('theme');
```

### WithPointer

[](#withpointer)

Tracks "the current" instance of a Concurrent class. Implement `fromPointerId()` (constructor shapes vary), get `start()` / `current()` / `release()` for free.

```
final class CurrentImport extends Concurrent
{
    use WithPointer;

    public function __construct(public readonly string $runId) { /* ... */ }

    protected static function fromPointerId(string $id, mixed ...$args): static
    {
        return new static($id);
    }
}

CurrentImport::start();      // mint a new run, claim the pointer
CurrentImport::current();    // resolve the pointed-to instance, or null
CurrentImport::release();    // clear the pointer
```

Override `pointerKey()` for a stable key, `generateId()` for UUIDs/ULIDs/etc. For ad-hoc usage, `ConcurrentPointer` is the underlying primitive.

Read-only Methods
-----------------

[](#read-only-methods)

Mark pure accessors as read-only to skip locking. Either `#[ReadonlyMethod]` on the wrapped value's method, or list method names on a Concurrent subclass via `DeclaresReadOnlyMethods`. Mutating from a read-only method throws `ReadonlyViolationException` so silent write loss is caught early.

Built-in Data Structures
------------------------

[](#built-in-data-structures)

Thread-safe data structures built on top of `Concurrent`:

- `ConcurrentMap`: key-value map.
- `ConcurrentSet`: collection of unique values.
- `ConcurrentCounter`: atomic counter, optional `min`/`max`/`wrap`.
- `ConcurrentQueue`: FIFO queue.
- `ConcurrentList`: ordered list with chainable map/filter/each.

Each has its own focused API; see the source for the full method list.

Using Without Laravel
---------------------

[](#using-without-laravel)

Implement `CacheDriver` and `LockDriver` against your backend (Redis, etc.) and register them globally:

```
Concurrent::useCache(new RedisCache());
Concurrent::useLock(new RedisLock());
```

Or pass them to a single instance via the constructor's `cache:` and `lock:` arguments. For tests, the package ships `InMemoryCache` and `InMemoryLock`.

With Laravel, no setup needed: the service provider auto-registers everything.

How It Works
------------

[](#how-it-works)

Writes lock, reads don't. A mutating operation acquires the lock, reads from cache, runs the operation, writes back, releases. Reads (`$concurrent()`, property reads, isset, read-only methods) hit the cache directly and never block.

Locks are re-entrant: nested writes inside a callback (e.g. multiple `$this->prop = X` inside a bound closure) reuse the outer lock. The whole callback is one atomic operation, one acquire/release.

Caveats
-------

[](#caveats)

A plain overwrite like `$concurrent->value = 10` is its own atomic write, no callback needed. Two shapes look like single writes but aren't:

- `$concurrent->count++` is *not* atomic. `++` is really three steps: read the value, add one, write it back. Each step locks, but nothing holds a lock across all three. If two workers both run `count++` on a value of `5`, both read `5` before either writes, both compute `6`, both write `6`. One increment is lost. Wrap it in a callback so the read and write share one lock.
- `$concurrent->items[] = $x` *silently does nothing*. PHP fetches `items` by value (a copy), appends to the copy, throws the copy away. The cache never sees the change. Wrap it in a callback to mutate the real array.

For read-modify-write or nested mutations, use a callback.

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

[](#requirements)

- PHP 8.4+
- A cache backend (Redis recommended for production)
- Optional: Laravel 10–13 for zero-config integration

License
-------

[](#license)

MIT

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance86

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity65

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

51

Last Release

66d ago

Major Versions

v1.4.6 → v2.0.02026-04-03

v2.5.6 → v3.0.02026-04-25

PHP version history (2 changes)v1.0.0PHP ^8.2

v2.1.1PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/0a13b52a17e71644445ec3597dc1c8faf54ebd8b6b087e219c672aae06ac5332?d=identicon)[JesseGall](/maintainers/JesseGall)

---

Top Contributors

[![jessegall](https://avatars.githubusercontent.com/u/11755417?v=4)](https://github.com/jessegall "jessegall (73 commits)")

---

Tags

laravelconcurrentcachelockingthread safe

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/jessegall-concurrent/health.svg)

```
[![Health](https://phpackages.com/badges/jessegall-concurrent/health.svg)](https://phpackages.com/packages/jessegall-concurrent)
```

###  Alternatives

[spatie/laravel-responsecache

Speed up a Laravel application by caching the entire response

2.8k9.0M68](/packages/spatie-laravel-responsecache)[propaganistas/laravel-disposable-email

Disposable email validator

6023.0M7](/packages/propaganistas-laravel-disposable-email)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

44855.7k](/packages/harris21-laravel-fuse)[iazaran/smart-cache

Smart Cache is a caching optimization package designed to enhance the way your Laravel application handles data caching. It intelligently manages large data sets by compressing, chunking, or applying other optimization strategies to keep your application performant and efficient.

21111.6k](/packages/iazaran-smart-cache)[omaralalwi/lexi-translate

Laravel translation package with morph relationships and caching.

7413.7k3](/packages/omaralalwi-lexi-translate)[nexxai/laravel-cfcache

A handful of Cloudflare cache helpers for Laravel

13314.7k](/packages/nexxai-laravel-cfcache)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
