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

Streak Laravel Package

streak/streak

Streak is a Laravel package for adding consecutive-activity tracking to Eloquent models. Record daily check-ins, calculate current and longest streaks, and query streak history through a simple, configurable API—useful for habit trackers, gamification, and engagement features.

View on GitHub
Deep Wiki
Context7

Getting Started

Install via Composer and let Laravel’s package discovery wire it up:

composer require streak/streak
php artisan vendor:publish --provider="Streak\StreakServiceProvider"
php artisan migrate

The migrations create the persistence layer (an event store table and, typically, a snapshots table). If nothing appears after publishing, run php artisan vendor:publish without arguments and pick the streak tag manually — with a package this young, the provider class name may not match the convention above.

Where to look first, in this order:

  1. vendor/streak/streak/src/ — the base AggregateRoot (or equivalent) and the event base class. Read these before writing anything; the package is small enough that the source is the documentation.
  2. The service provider’s bindings — this tells you what the event store, serializer, and dispatcher are bound to, and therefore what you can swap.
  3. config/streak.php after publishing — serializer, store driver, queue connection, table names.

First use case: model one aggregate with exactly two events and one projector. A BankAccount with AccountOpened and MoneyDeposited is the standard starting point because the invariants are trivial and the replay story is obvious.

final class Account extends AggregateRoot
{
    private int $balance = 0;

    public static function open(string $id, string $owner): self
    {
        $account = new self($id);
        $account->record(new AccountOpened($id, $owner));
        return $account;
    }

    public function deposit(int $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Deposit must be positive.');
        }
        $this->record(new MoneyDeposited($this->id(), $amount));
    }

    protected function applyAccountOpened(AccountOpened $event): void
    {
        $this->balance = 0;
    }

    protected function applyMoneyDeposited(MoneyDeposited $event): void
    {
        $this->balance += $event->amount;
    }
}

Then persist the aggregate and confirm the rows land in the store. If that round-trips, every other pattern in this package is a variation on it.

Implementation Patterns

Named constructors, never public constructors. Every aggregate should be born through a static factory that records a “created” event. This keeps replay symmetrical: the same apply path reconstructs the object from the store as from a live call. Avoid __construct logic that isn’t driven by an event — it will silently diverge on replay.

Keep apply* methods dumb. They replay events; they must not validate, dispatch, hit the database, or read the clock. All guards belong in the command method before record(). A useful rule: if you deleted every apply method body and rehydrated from the store, the result must be identical.

Events are immutable and self-describing. Use readonly properties or public promoted constructor args, and include everything a projector needs — no lazy-loading an Eloquent model from inside a projector to fill in a gap. Payloads are serialized, so scalar types and simple value objects with a serializer hook are safest.

Projectors for read models, reactors for side effects. Split them. A projector should be a pure upsert into a query table, safe to run twice and safe to replay from event 1. A reactor (send email, call a payment API) should be queued, idempotent, and keyed on the event’s identity so a re-delivery is a no-op.

final class AccountBalanceProjector
{
    public function onMoneyDeposited(MoneyDeposited $event): void
    {
        DB::table('account_balances')->upsert(
            [['account_id' => $event->accountId, 'balance' => $event->amount]],
            ['account_id'],
            // use raw SQL increment rather than read-modify-write
        );
    }
}

Prefer an increment-style update over a read-then-write; concurrent projections of the same aggregate will otherwise lose writes.

Wrap command handling in a transaction with the event append. If you write to your read model synchronously, do it inside the same database transaction as the store append. If you queue projections, adopt an outbox: append events and outbox rows together, then drain the outbox. Without this you get events that exist but were never projected, which is invisible until someone reconciles a report.

Snapshots are an optimisation, not a design. Add a snapshot policy once a stream exceeds a few hundred events. Until then, replaying a 40-event stream is faster than the code you’d write to avoid it. Snapshot on a count threshold, not on time, so behaviour is deterministic. Keep the snapshot’s schema version stamped so you can invalidate it wholesale.

Testing: given/when/then over an in-memory store. The package’s value in tests is that aggregates are pure. Structure specs as:

$account = Account::open('a-1', 'Ada');
$this->when(fn () => $account->deposit(500));
$this->then([new MoneyDeposited('a-1', 500)]);

If the package provides a test harness or an in-memory event store binding, bind it in phpunit.xml rather than mocking the store — mocks of an event store tend to assert on internals and break on every refactor. If it doesn’t, a FakeEventStore implementing the same interface is a 30-line class worth writing on day one.

Laravel integration tips. Bind aggregates through the container only when they need collaborators (a clock, an ID generator, a policy service); otherwise construct them plainly. Register projectors in a service provider rather than relying on auto-discovery — with a small ecosystem you want the wiring visible. Run projectors on a dedicated queue connection so a backlog of side effects never starves your web queue.

Gotchas and Tips

Event schema changes are the hard part. Once events are in the store, you can’t change them — you can only add new ones or write an upcaster that transforms old payloads on read. Decide your upcaster strategy before the first production deploy. A single version key in each payload, plus a upcast(array $payload): array hook in the serializer, buys you a lot of future flexibility for almost no cost today.

Missing apply methods fail silently. If applyMoneyDeposited doesn’t exist or is misspelled, replay may skip the event rather than throw, leaving you with a wrong aggregate state that only surfaces in production. Add a test that rehydrates each aggregate from a fixture stream and asserts the resulting state — it catches this class of bug immediately.

Optimistic concurrency is not automatic. Two requests loading the same aggregate, both appending, produce a stream with interleaved events and no error. Store and check the expected aggregate version on append. If the package doesn’t expose this, wrap your store in a decorator that does — it’s the single highest-value addition you can make.

Idempotency is your responsibility. Delivery is at-least-once in any real setup. Every projector and reactor needs a natural key (event ID, or aggregate ID + version) with a uniqueness constraint. “It ran twice” should be a database error you catch, not a duplicate row you discover in a report.

Never dispatch side effects from a projector. The temptation is to send the welcome email from the projector that creates the user row. It works until you replay, and then every customer gets a welcome email a second time. Projectors write rows; reactors do things.

Serialization of value objects. Money, UUIDs, and enums need a serializer path. Enums in particular round-trip badly if stored as objects — persist ->value explicitly. Test a full write-then-read cycle for every event class; a broken serializer for one field is invisible until you replay.

Debugging: the event store is the ground truth, so learn to query it directly. SELECT * FROM <event_store> WHERE aggregate_id = ? ORDER BY version answers most “why is the state wrong” questions faster than any debugger. Build a small php artisan streak:replay {projector} command early — you will use it weekly.

Replay cost and ordering. Replaying everything is fine at thousands of events and painful at millions. Replay into a shadow table, then swap, so your read model stays live during the rebuild. Preserve per-aggregate ordering; global ordering across aggregates usually doesn’t matter for projections but does for reactors that correlate streams.

Config quirks. Table names and the serializer are the settings you’ll actually touch; everything else is best left alone. If you switch serializers after there’s data in the store, old rows become unreadable — decide once, at the start, and write it down.

Ecosystem risk. With 17 stars and zero dependents, treat this as a library you may end up maintaining. Pin the version exactly ("streak/streak": "1.0.*"), keep your aggregates and projectors free of package-specific types at the boundaries where you can, and read the source before upgrading. The upside is that it’s small and hackable — the extension points (store, serializer, dispatcher) are the interfaces to code against, and if you find yourself fighting one, forking is a realistic option rather than a drastic one.

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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings