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

Php Time Ago Laravel Package

jimmiw/php-time-ago

A lightweight PHP library that turns timestamps into human-readable "time ago" strings such as "3 hours ago" or "just now". Includes translations for many languages, handles future dates, and drops easily into any PHP or Laravel project.

Deep Wiki
Context7

Getting Started

Install

composer require jimmiw/php-time-ago

There is no service provider, facade, or config file — Composer's autoloader is the entire integration. The first thing to internalise is the naming mismatch: the package is jimmiw/php-time-ago, but the namespace is Westsworld\TimeAgo. IDE autocomplete on "TimeAgo" will find it; searching for "jimmiw" will not.

Minimal usage

use Westsworld\TimeAgo;

$timeAgo = new TimeAgo();

echo $timeAgo->inWords(new DateTime('2023-01-01 10:00:00')); // "3 years ago"
echo $timeAgo->inWords('2023-01-01 10:00:00');               // strings are parsed too

Localisation is done by injecting a translation class at construction time, not via a setter or config:

use Westsworld\TimeAgo;
use Westsworld\TimeAgo\Translations\Danish;

$timeAgo = new TimeAgo(new Danish());
echo $timeAgo->inWords(new DateTime('-3 days')); // "3 dage siden"

Where to look first

  1. vendor/jimmiw/php-time-ago/src/Westsworld/TimeAgo.php — the whole engine is one class; skim it in five minutes to see the unit thresholds.
  2. vendor/jimmiw/php-time-ago/src/Westsworld/TimeAgo/Translations/ — the definitive list of supported locales and the exact strings each produces. Do not trust third-party lists; check the directory.
  3. The package's own tests/ folder — it doubles as a spec for edge cases (exact boundary values, "just now", future dates).

First realistic use case: a "last active" or "posted X ago" column on a listing page. Anywhere you currently print $user->updated_at verbatim to a human, this is a one-line improvement.


Implementation Patterns

1. Bind a singleton, don't instantiate in views

The object is stateless after construction, so reuse it. Register it once and resolve via the container:

// app/Providers/AppServiceProvider.php
use Westsworld\TimeAgo;
use Westsworld\TimeAgo\Translations\Danish;
use Westsworld\TimeAgo\Translations\English;

public function register(): void
{
    $this->app->singleton(TimeAgo::class, function () {
        return new TimeAgo(match (substr(app()->getLocale(), 0, 2)) {
            'da' => new Danish(),
            default => new English(),
        });
    });
}

Caveat: a singleton pins the locale for the whole request. If you serve multiple locales in one process (queue workers, Octane, tests), wrap it instead:

namespace App\Support;

use DateTimeInterface;
use Westsworld\TimeAgo;
use Westsworld\TimeAgo\Translations\Danish;
use Westsworld\TimeAgo\Translations\English;
use Westsworld\TimeAgo\Translations\German;

class TimeAgoFormatter
{
    /** @var array<string, TimeAgo> */
    private array $instances = [];

    public function format(DateTimeInterface $date, ?string $locale = null): string
    {
        return $this->instance($locale ?? app()->getLocale())->inWords($date);
    }

    private function instance(string $locale): TimeAgo
    {
        return $this->instances[$locale] ??= new TimeAgo($this->translation($locale));
    }

    private function translation(string $locale): object
    {
        return match (substr($locale, 0, 2)) {
            'da'      => new Danish(),
            'de'      => new German(),
            default   => new English(),
        };
    }
}

2. Model accessors and API resources

Keep formatting out of controllers. An accessor makes the value available to Blade, JSON, and mail templates at once:

// app/Models/Post.php
use App\Support\TimeAgoFormatter;

public function getPostedAgoAttribute(): string
{
    return app(TimeAgoFormatter::class)->format($this->created_at);
}

Carbon instances extend \DateTime, so Eloquent's created_at / updated_at work directly — no conversion needed.

For APIs, always ship the machine-readable value alongside the human string, so clients can re-render or localise independently:

return [
    'created_at'      => $post->created_at->toIso8601String(),
    'created_at_human'=> $post->posted_ago,
];

3. Blade component with a <time> element

The package renders server-side only, so the string goes stale the moment the page is loaded. Emit semantic markup with the raw ISO timestamp as an attribute — that gives you a hook for progressive enhancement (a small JS re-render, or just a tooltip showing the exact date):

{{-- resources/views/components/time-ago.blade.php --}}
@props(['date'])

<time datetime="{{ $date->toIso8601String() }}"
      title="{{ $date->toDayDateTimeString() }}">
    {{ app(\App\Support\TimeAgoFormatter::class)->format($date) }}
</time>
<x-time-ago :date="$post->created_at" />

A directive is a cheaper alternative if you don't want components:

Blade::directive('timeAgo', fn ($expr) =>
    "<?php echo e(app(\App\Support\TimeAgoFormatter::class)->format({$expr})); ?>");

4. Batch formatting in loops

Because each inWords() call constructs a "now" internally, formatting 200 rows is 200 new DateTime() calls. This is negligible in practice, but resolve the formatter once outside the loop rather than calling app() per iteration, and consider eager-loading the timestamps you need.

5. Mail, notifications, and queues

Queue workers are long-lived and locale-agnostic. If a job renders a "3 days ago" string, pass the recipient's locale explicitly into format($date, $locale) — do not rely on app()->getLocale() being correct in a worker.


Gotchas and Tips

Timezone drift is the number one bug. The reference time is a bare new \DateTime(), resolved in PHP's default timezone. If a stored timestamp is UTC but config/app.php's timezone is Europe/Copenhagen, a post made 10 minutes ago can read "1 hour ago". Keep Laravel's timezone set to UTC, store UTC, and convert for display. If you must handle mixed zones, normalise before calling:

$date = (new DateTime($row->created_at, new DateTimeZone('UTC')))
    ->setTimezone(new DateTimeZone(config('app.timezone')));

Pass DateTime or Carbon, not DateTimeImmutable or an integer. The parameter is effectively typed around \DateTime. Carbon is fine (it extends DateTime); DateTimeImmutable may be rejected — convert with DateTime::createFromImmutable($d) or $carbon->toDateTime(). A raw Unix timestamp should be wrapped: (new DateTime())->setTimestamp($ts), not passed directly.

Carbon::setTestNow() does not affect this library. It reads the real system clock via native DateTime, so time-travel tests silently pass against wall-clock time and then fail in CI. Options: assert with a tolerance window, freeze the real clock (e.g. php-clock mocking or libfaketime), or — better — route all calls through your TimeAgoFormatter wrapper and, in tests, substitute a fake that returns canned strings. Do not scatter new TimeAgo() through the codebase; you will regret it the first time you need deterministic output.

One unit only, truncated not rounded. Output is the largest single unit ("1 year ago"), so a 364-day-old record and a 20-minute-old record both look "recent-ish" at a glance, and 1 hour 59 minutes reads as "1 hour ago". For dashboards where precision matters, Carbon's diffForHumans(['parts' => 2]) is the better tool — and it's already installed.

Future dates change wording by translation. The prefix/suffix handling lives in each translation class, so "in 3 days" vs "3 days from now" vs a locale that prefixes differently is not something you can assume. If your data can contain future timestamps (scheduled posts, expiry dates), write explicit tests per locale rather than trusting the English output.

"Just now" threshold is hard-coded per translation. Very recent timestamps collapse to a single string. If you need "just now" to mean under 5 minutes rather than under 1 minute, you cannot configure it — subclass the translation class and override the threshold/string, or pre-check the delta yourself and bypass the library.

Unsupported locales fail silently as English. There is no fallback negotiation in the package; your match/switch decides. Log a warning when the app locale has no corresponding translation class, otherwise a Japanese user quietly gets English from a translation file nobody remembered to add.

Escape the output in Blade. Translation strings are trusted package code today, but they flow into user-facing HTML. Always {{ }} / e(), never {!! !!} — the strings are effectively configuration you didn't write.

Maintenance posture: quiet since 2022. The package is stable and tiny (a single class plus translations), so the lack of recent releases is not alarming — but pin the version in composer.json and avoid relying on undocumented internals. If you are starting fresh, weigh it against Carbon::diffForHumans(), which ships with Laravel, is fully translatable via Laravel's own locale files, and never needs a wrapper to be testable. The honest reason to choose jimmiw/php-time-ago is that you want its specific phrasing or one of its ready-made translation classes without writing locale strings yourself.

Debugging checklist when the output looks wrong: (1) print date_default_timezone_get() and config('app.timezone'); (2) print the raw timestamp and its DateTimeZone; (3) verify which translation class was actually constructed; (4) confirm you passed a DateTime, not a string that PHP parsed differently than you assumed. Nine times out of ten it is step 1.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums