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

Laravel Daily Activity Laravel Package

vergil-lai/laravel-daily-activity

Laravel package for tracking daily user activity and streaks. Log check-ins, query active days, and calculate consecutive-day streaks through a simple Eloquent-friendly API. Lightweight and easy to drop into existing models.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require vergil-lai/laravel-daily-activity:^1.0
php artisan vendor:publish --tag="laravel-daily-activity"
php artisan migrate

Requirements to confirm before you commit: PHP 8.3+, Laravel 12/13, and a supported driver (SQLite, MySQL, PostgreSQL). If any of your apps are still on Laravel 11 or PHP 8.2, this package is a non-starter until you upgrade.

Where to look first

  1. The published config file — it holds the activity model class, cache key prefix, and timezone behaviour. Read it before writing any code; almost every quirk you'll hit is configured here.
  2. The published migration — it shows the actual table shape (one row per subject per year, with a 46-byte bitmap covering 365/366 days). That shape explains the package's performance profile and its limits.
  3. The trait/contract you add to your subject models, and the default activity model you can extend.

First use case

The 15-minute win is a streak + calendar widget on a dashboard:

use Illuminate\Database\Eloquent\Model;
use VergilLai\DailyActivity\Concerns\HasDailyActivity; // verify namespace in README

class User extends Model
{
    use HasDailyActivity;
}
// Idempotent: calling it twice in a day is a no-op
$user->recordActivity();

$user->currentStreak();   // e.g. 12
$user->longestStreak();   // e.g. 41
$user->activeDaysThisWeek();
$user->activityCalendar(now()->year);

Method names above are illustrative — the release notes describe the capabilities (record, calendar, range/week/month counts, current streak, longest streak) but not exact signatures. Copy the real names from the README before wiring UI to them; that's the single most common source of wasted time with a v1.0.0 package.

Since writes are de-duplicated per day, the safest integration is to call recordActivity() liberally at the points where the action means something (login, lesson completed, workout logged) rather than building elaborate "have I already recorded this today?" guards in your own code.

Implementation Patterns

Use the polymorphic subject, not a user-only column. The main subject is a morph, so User, Team, Project, or Device all get the same streak/calendar API. For multi-tenant apps this is the natural place to hang per-workspace engagement rather than per-account.

class Team extends Model { use HasDailyActivity; }

$team->recordActivity();
$team->activeDaysInRange($start, $end);

Record from domain events, not controllers. Because the write is cheap and idempotent, a listener is the cleanest seam:

class RecordDailyActivity
{
    public function handle(LessonCompleted $event): void
    {
        $event->user->recordActivity();
    }
}

This keeps the "what counts as activity" rule in one auditable place, and makes it trivial to add a new trigger later without touching controllers.

Backfill deliberately, and keep it auditable. The package supports recording for a specific date, which is what you want when importing legacy logins/orders tables:

foreach ($legacyLogins as $login) {
    $user->recordActivity($login->logged_in_at);
}

Do this in a queued job with chunking, and log the date range imported. Backfill directly inflates longestStreak(), so treat it as a data-migration decision, not a convenience call.

Calendar rendering. Fetch the calendar for one year, then merge it with your own per-day payloads (workouts, minutes, scores) keyed by date string. Don't try to render the calendar from multiple packages' date structures — normalise into a ['Y-m-d' => bool] map first.

Streak UI needs a "today not yet counted" state. A user who was active yesterday but not yet today has a current streak that is one keystroke away from breaking. Display it as "12 days — keep it going" rather than a bare number, and compute the "at risk" flag yourself from recordActivity() state for today.

Cross-subject analytics need a side channel. The bitmap is optimised for "give me one subject's history." If a product requirement is "list the 50 users active yesterday" or "order users by streak," you will not get that out of a 46-byte blob efficiently. Derive a normalised last_active_on column or a daily_activity_rollup table in the same listener that calls recordActivity(), and query that.

Custom activity model. The release supports swapping in your own model — a good hook for adding team_id, soft deletes, or a connection override. Extend the package model, change one config value, and keep the migration in sync. Do this early; retrofitting a connection or added columns after data exists is more painful.

Caching. The package caches after transaction commit and ships a configurable key prefix. Trust it rather than layering your own cache over currentStreak(); if you must cache higher-level aggregates, invalidate from the same event listener that records activity. Set the key prefix per tenant/environment if you share a Redis instance.

Testing. Dates are the whole surface area, so freeze time:

$this->travelTo('2025-03-01 09:00:00');
$user->recordActivity();
$this->travelTo('2025-03-02 09:00:00');
$user->recordActivity();
$this->assertSame(2, $user->currentStreak());

Include a case that crosses 31 December and one that lands on 29 February — those are the paths the release notes explicitly advertise as handled, so your tests should prove it in your config.

Gotchas and Tips

Timezone is a config decision, not an afterthought. Activity is bucketed per day in the application timezone, not UTC. Two consequences: a user active at 23:30 local is credited to the local day, which is usually what you want; and if you later change the timezone config, historical bits are reinterpreted against a different boundary. Pick the timezone on day one, document it, and don't change it without accepting a discontinuity in streaks.

Streaks span year rows. A streak running 28 Dec → 3 Jan touches two rows. The package handles it (that's the "current streak reads years on demand" optimisation), but your code must not assume one row per subject: any raw query, export, or cache you build needs to account for the year dimension.

Leap years change the day index. With a 365/366-bit bitmap, 29 February has no stable slot across years. Verify your calendar/grid UI against a leap year before shipping, and don't hand-roll bit arithmetic to answer "was there activity on 29 Feb 2024" — use the package's calendar accessor.

Do not write to the bitmap yourself. Concurrency protection lives inside the package's write path. Any UPDATE ... SET bitmap = ... of your own is a read-modify-write race and will silently drop days under load. Same for importing: go through the API (chunked, queued), not raw SQL.

Reads inside an open transaction can look stale. Writes are cached after commit, which is correct — but it also means code that records activity and immediately reads a cached streak inside the same DB::transaction() may not see the new value. If you need the post-write state in the same request, do the read outside the transaction.

Changing your morph map is a data migration. The subject type string is stored. Switching from FQCNs to a morph map (or renaming an alias) orphans every existing row unless you write the migration to rewrite subject_type. Adopt a morph map before you have production data.

Don't benchmark with the bundled script and draw conclusions. The included performance comparison script is a development aid against synthetic data. Real cost is dominated by row count (subjects × years), so measure with your actual subject count before deciding whether streaks need to be precomputed for a leaderboard.

Bitwise SQL is dialect-specific. SQLite, MySQL, and PostgreSQL are all supported, but if you ever write reporting SQL against the bitmap, you'll get three different behaviours. Keep all bitmap interpretation in PHP via the package API, and export to a normalised table if BI tools need access.

Debugging a missing day: fetch the subject's activity row for that year and confirm the bit, then render the calendar for the same year and compare. A mismatch almost always means a timezone boundary issue or a write that happened outside the package's API — not a bug in the bitmap itself.

Version pinning. This is 1.0.0. Pin ^1.0 and read the changelog on minors, since the public API surface (method names, config keys, model class) has had exactly one release to settle.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle