imranali/smart-timezone

Automatic timezone detection and casting for Laravel - auto-detect user timezone from JS, header, cookie, user profile, or IP, and seamlessly cast all dates to local time.

Maintainers

Package info

github.com/grim-reapper/smart-timezone

pkg:composer/imranali/smart-timezone

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-27 06:47 UTC

This package is auto-updated.

Last update: 2026-08-27 06:51:53 UTC


README

Latest Version on Packagist Total Downloads

Stop timezone bugs forever. SmartTimezone auto-detects your users' timezone and makes every datetime "just work" in their local time.

No more ->setTimezone('UTC') spaghetti. No more "why does this show 5 hours off?"

What It Does

  1. Auto-detects timezone — from browser JS → cookie → user profile → IP → fallback
  2. Auto-casts dates$post->created_at shows in user timezone, saves as UTC
  3. Blade ready@usertime($datetime) prints "25 Jun 2026, 6:45 PM PKT"
  4. API friendly — Reads X-Timezone header for mobile/SPA apps
  5. Zero config — works for 80% of apps. Supports Laravel 10, 11, 12, 13.

Installation

composer require imranali/smart-timezone
php artisan smart-timezone:install
php artisan migrate

Make sure your layout <head> has a CSRF token, then add the script before </body>:

<meta name="csrf-token" content="{{ csrf_token() }}">
...
<script src="{{ asset('vendor/smart-timezone/timezone-detect.js') }}"></script>

The script sets a user_timezone cookie and (when the CSRF token is present) pushes the detected zone to the current session immediately. Without the meta tag it still works — the server just picks the zone up on the next request from the cookie.

That's it. All dates now respect user timezone.

Usage

1. Cast any datetime automatically

use ImranAli\SmartTimezone\Casts\TimezoneDateTime;

class Post extends Model
{
    protected $casts = [
        'published_at' => TimezoneDateTime::class,
        'created_at'   => TimezoneDateTime::class,
    ];
}
$post = Post::find(1);
echo $post->published_at; // "2026-06-25 18:45:00" for Lahore user
                          // DB still stores "2026-06-25 13:45:00" UTC

$post->published_at = now(); // Saves as UTC automatically

// For models loaded BEFORE login, use getDateInTimezone() for fresh conversion
$user = User::find(1);
// Auth::login($user);  // login happens after find
echo $user->getDateInTimezone('created_at'); // always uses latest timezone

2. Blade directive

{{-- Returns Carbon instance --}}
@usertime($post->published_at)

{{-- Returns formatted string --}}
@usertime($post->published_at, 'd M Y, g:i A T')
{{-- Output: 25 Jun 2026, 6:45 PM PKT --}}

3. Manual conversion

use Carbon\Carbon;

$utcDate = Carbon::parse('2026-06-25 10:00:00', 'UTC');
$local = $utcDate->inUserTimezone(); // Macro added by package

echo $local->format('g:i A T'); // "3:00 PM PKT"

4. Add to User model

use ImranAli\SmartTimezone\Traits\HasTimezone;

class User extends Authenticatable
{
    use HasTimezone; // auto-syncs users.timezone column
}

auth()->user()->timezone; // "Asia/Karachi"
User::inTimezone('Asia/Karachi')->get(); // all Lahore users

5. API / Mobile App Usage

Send the timezone header from your frontend. SmartTimezone picks it up automatically.

Axios / React
axios.defaults.headers.common['X-Timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone;
Flutter / Dart
dio.options.headers['X-Timezone'] = await FlutterNativeTimezone.getLocalTimezone();

Laravel API response:

Route::get('/posts', function () {
    return Post::all(); // published_at auto-converted to user TZ
});

Response for a Lahore user:

{
  "id": 1,
  "title": "Hello World",
  "published_at": "2026-06-25T18:45:00+05:00"
}

How Detection Works

When rendering a date, SmartTimezone resolves the timezone in this order:

Priority Source When it's used
1 X-Timezone header API / mobile / SPA requests — overrides everything for that request
2 users.timezone DB Authenticated users — the source of truth once logged in
3 Session Guests — populated by the middleware from the cookie or IP lookup
4 Model's own timezone User-owned rows (e.g. a Post belonging to a user)
5 config('smart-timezone.default') (APP_TIMEZONE) Nothing else matched

The DetectTimezone middleware fills the session once per session for guests, trying detect_from sources in order (header, cookie, user, ip). The user_timezone cookie is set client-side by timezone-detect.js.

Key behaviors

  • Header wins for API/SPA requests and is never persisted to the session.
  • DB column wins for logged-in users — no "one request is wrong after login" race, because resolution reads the user's column directly.
  • Every value is validated against timezone_identifiers_list(). Offset-only values like +05:00 are not IANA identifiers and are ignored — send Asia/Karachi, not an offset.
  • Eloquent caches cast values. If a model was read before Auth::login() and you need the post-login zone in the same request, use $model->getDateInTimezone('created_at') for a fresh conversion.

Queues, jobs, commands, and notifications

There is no request, session, or authenticated user in a queued job, an Artisan command, or a queued notification/mail. In those contexts the cast falls back to config('smart-timezone.default'). When you need a specific user's zone outside a request, convert explicitly:

$localCreatedAt = $post->created_at
    ->clone()
    ->setTimezone($user->timezone ?? config('smart-timezone.default'));

Configuration

Published to config/smart-timezone.php:

return [
    'default'            => env('APP_TIMEZONE', 'UTC'),
    'detect_from'        => ['header', 'cookie', 'user', 'ip'],
    'api_header'         => env('SMART_TIMEZONE_HEADER', 'X-Timezone'),

    'user_column'        => 'timezone',   // null = never touch the DB
    'auto_save_timezone' => true,         // persist a detected zone to the user

    'cookie_name'        => 'user_timezone',
    'cookie_lifetime'    => 31536000,
    'cookie_same_site'   => 'Lax',
    'cookie_secure'      => null,         // null = auto (Secure on HTTPS)

    'session_key'        => 'user_timezone',

    'register_middleware' => true,        // false = wire the middleware yourself
                                          // (aliases: timezone.detect / timezone.header)

    'enable_js_route'   => true,
    'route_prefix'      => '_smart-timezone',
    'route_middleware'  => ['web'],
    'route_throttle'    => '10,1',        // null to disable rate limiting
];

Opting out of automatic middleware

By default the package pushes DetectTimezone onto the web group and SetTimezoneFromHeader onto the api group. Set register_middleware to false and add them where you want:

// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [\ImranAli\SmartTimezone\Middleware\DetectTimezone::class]);
    $middleware->api(append: [\ImranAli\SmartTimezone\Middleware\SetTimezoneFromHeader::class]);
})

Custom route prefix + JS

If you change route_prefix, tell the detector script where to POST:

<meta name="smart-timezone-endpoint" content="{{ url('my-prefix/set') }}">

Events

ImranAli\SmartTimezone\Events\TimezoneDetected fires whenever a timezone is resolved and written to the session (web middleware, API header middleware, or the JS route). It carries $timezone, $source (header / cookie / user / ip / route / default), and $request.

A common use — copy a guest's detected zone onto their account at registration:

Event::listen(function (TimezoneDetected $event) {
    if (auth()->check() && blank(auth()->user()->timezone)) {
        auth()->user()->forceFill(['timezone' => $event->timezone])->saveQuietly();
    }
});

Commands

php artisan smart-timezone:install          # publish config + assets (+ migration prompt)
php artisan smart-timezone:install --force  # overwrite existing published files

Requirements

  • PHP 8.1+
  • Laravel 10, 11, 12, or 13
  • torann/geoipoptional, only needed if you keep 'ip' in detect_from. Install it yourself with composer require torann/geoip. Without it, IP detection is skipped silently.

Package Comparison

Package Auto JS Detect API Header Eloquent Cast DB Saves UTC SPA Ready
SmartTimezone ✅ Yes ✅ Yes ✅ Yes ✅ Yes ✅ Yes
jamesmills/laravel-timezone ❌ No ❌ No ✅ Yes ✅ Yes ❌ No
spatie/laravel-timezone ⚠️ Manual ❌ No ❌ No ❌ No ❌ No

Testing

# Run all tests
composer test

# Run with output
vendor/bin/phpunit --no-coverage

# Run a specific test file
vendor/bin/phpunit tests/TimezoneHelperTest.php

# Run a specific test method
vendor/bin/phpunit --filter test_converts_utc_date_to_user_timezone

# Lint (Laravel Pint)
composer lint

Contributing

PRs welcome! Please ensure tests pass before submitting:

composer test
composer lint

Security

  • All timezone input (header, cookie, request body) is validated against timezone_identifiers_list() before use.
  • The JS detection route (POST {route_prefix}/set) runs behind the web middleware (CSRF) and a throttle limiter (route_throttle).
  • The detection cookie is SameSite=Lax and Secure on HTTPS. It is not HttpOnly by design — the browser script needs to read it.
  • IP detection sends the visitor's IP to whatever torann/geoip driver you configure. Drop 'ip' from detect_from if that is a concern.

If you discover a security vulnerability, please email imran.wtwm@gmail.com.

License

MIT. Use it in any project, commercial or personal.

Built by Imran Ali in Lahore, PK 🇵🇰

If this saved you from timezone hell, ⭐ star the repo!