Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Activeredis Laravel Package

directorytree/activeredis

ActiveRedis provides a simple, efficient, Eloquent-like active record API for interacting with Redis hashes in Laravel. Define models and create, update, delete, expire, and query records, with support for casts, events, timestamps, chunking, and searching.

View on GitHub
Deep Wiki
Context7

Getting Started

ActiveRedis is a single-purpose package: it lets you treat Redis hashes as Eloquent-ish models. There is no service provider config to publish, no migrations, and no schema — everything is driven by class properties on your model.

Minimal setup

  1. composer require directorytree/activeredis (PHP 8.1+, Laravel 9+, Redis 3+).
  2. Add a dedicated Redis connection in config/database.php (e.g. database 10) so your models don't share a keyspace with cache, sessions and queues.
  3. Create a model that extends DirectoryTree\ActiveRedis\Model, set protected ?string $connection = 'activeredis';, and declare $searchable and $casts before you write a single record.
namespace App\Redis;

use DirectoryTree\ActiveRedis\Model;

class Visit extends Model
{
    protected ?string $connection = 'activeredis';

    protected array $searchable = ['ip'];

    protected array $casts = [
        'user_id' => 'integer',
        'authenticated' => 'boolean',
        'payload' => 'array',
    ];
}

Where to look first

  • The README's Model Identifiers section — it explains the key layout visits:id:{uuid} and the getHashKey() / getBaseHash() / getHashPrefix() helpers, which is exactly what you need when inspecting data in redis-cli.
  • The package source for the query builder (query()) once you outgrow the documented find / chunk / each surface, since missing methods are forwarded dynamically.
  • The Searching section before writing production data, because searchable attributes are effectively schema.

First real use case

Request/visit logging with automatic expiry is the sweet spot:

$visit = Visit::create([
    'ip' => request()->ip(),
    'url' => request()->url(),
    'user_agent' => request()->userAgent(),
]);

$visit->setExpiry(now()->addHours(24));

The record self-destructs via Redis TTL, you get UUID keys for free, and you never touch MySQL for high-volume ephemeral writes.

Implementation Patterns

TTL-first ephemeral records. Treat setExpiry() as the default rather than the exception. Rate-limit traces, one-time tokens, short-lived carts, "who viewed this page" logs, and transient job state all fit: create, set expiry, read by ID, let Redis expire it. getExpiry() returns a Carbon or null, which is handy for displaying "expires in…" in an admin view.

Casts as your schema. Since Redis stores strings, $casts is the only type information in the system. Wrap structured data in a single array/json/collection attribute instead of fanning out into many flat fields — it keeps the hash small and avoids searchable/index churn. Enum casts work well for status fields, and stored values are the backed enum values, so they remain greppable in redis-cli.

Custom keys for natural identifiers. Override protected string $key = 'email'; when you genuinely want O(1) lookup by an external identity (Visit::find('[email protected]')). Reserve this for values you control — you inherit responsibility for case sensitivity, reserved characters, and collision handling.

Lifecycle hooks over scattered logic. Push cross-cutting concerns into booted() or an observer: normalising the IP, stamping a tenant prefix into searchable fields, vetoing writes by returning false from creating/updating/deleting, or logging deletions. This is the closest thing the package has to "model policies", so use it instead of duplicating guards at call sites.

Chunk/each for batch jobs. There is no orderBy, so any "process all records" task should use each() or chunk() and take advantage of returning false from the callback to bail out early. This is the right shape for nightly cleanups, re-indexing after adding a cast, or fanning out to a queue.

Connection and prefixing as multi-tenancy. Give each domain its own model class and its own connection/database where practical. Since queries are SCAN {cursor} MATCH visits:id:*, isolating key spaces directly reduces scan cost — the README explicitly calls this out as the reason for a separate connection.

Bulk deletion by ID. Visit::destroy([...]) returns the count deleted, which makes it convenient for "delete these 40 IDs" admin actions. For "delete everything matching X", lean on TTLs or iterate with each() and call delete() per model.

Testing. The README has a Testing section — read it before writing your suite. In practice, point the model's connection at a throwaway Redis database, flush it in setUp, and assert against getHashKey() rather than internal key strings. Anything relying on SCAN should be tested with more than one record to avoid false confidence.

Gotchas and Tips

  • Everything is a string until cast. Without $casts, $model->user_id is "1", not 1, and getAttributes() always returns raw strings. Comparisons like === 1 will silently fail.
  • Renaming the model class orphans data. The hash prefix is the pluralised model name (Visit → visits). Rename the class and every existing record becomes unreachable by the query builder unless you migrate the keys manually.
  • Keys are case-sensitive. Visit::find('ABC') will not find visits:id:abc. Normalise IDs on write if users can supply them.
  • Never put : or * in keys or searchable values. Both are reserved by the key layout and the SCAN MATCH pattern — a stray colon silently breaks queries.
  • $searchable is immutable schema. Adding, removing, or renaming a searchable attribute while records exist means those records can't be found again (the index keys won't match the model data). Decide the list up front; if you must change it, plan a full reindex by iterating every record with each() and re-saving.
  • force: true deletes, it doesn't merge. create([...], force: true) and save(force: true) remove the existing hash and write fresh — old TTL, old fields, and old timestamps are gone. It's a concurrency escape hatch, not an upsert.
  • SCAN is O(N) over the keyspace. COUNT is a hint, not a guarantee, so chunk(100, ...) may hand you 3 or 300 models. Never assume chunk size, and never assume ordering — there is none.
  • Watch for leaked index keys on expiry. setExpiry() sets a TTL on the model's hash. Verify whether the secondary keys created for searchable attributes expire with it; if they don't, long-TTL-less index entries accumulate and search results can point at missing hashes. Test this explicitly with a short TTL before relying on expiry in production.
  • No relations, no transactions, no eager loading. Two models written in the same request are two independent Redis writes. If a failure between them matters, model it in MySQL or restructure so the second write is disposable.
  • Dynamic forwarding hides typos. Visit::get() works because missing methods forward to the query builder — which also means mistyped static calls may fail in less obvious ways than a PHP error would suggest.
  • Debugging is redis-cli. Use SCAN 0 MATCH visits:id:* COUNT 100, HGETALL <hashkey>, and TTL <hashkey> to confirm what the ORM actually wrote. When a search returns nothing, inspect whether the index key exists at all before blaming your query.
  • Pin the version and read diffs. This is a 73-star, single-maintainer package with no known dependents in the wild — the blast radius of a behaviour change lands entirely on you. Lock the constraint, keep integration tests around your model definitions, and avoid depending on undocumented internals of the query builder.
  • Extension points worth knowing: getNewKey() (custom ID generation), the $key property (identifier field name), $connection, $casts, $searchable, $timestamps, CREATED_AT/UPDATED_AT, and model events/observers. Almost every customisation request maps to one of these — reach for them before subclassing the query builder.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource