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 Null Carbon Laravel Package

hnhdigital-os/laravel-null-carbon

Null Carbon for Laravel provides a null-object implementation of Carbon, letting models, views, and date helpers handle missing dates gracefully. Instead of scattering null checks or hitting errors when a date is absent, you get a safe, predictable Carbon instance that behaves sensibly.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Inspect before you install. This is a one-star package with no dependents and a 2023 release, so treat the source as the documentation. From a scratch branch:

composer show -a hnhdigital-os/laravel-null-carbon   # versions, php/illuminate constraints

Then open vendor/hnhdigital-os/laravel-null-carbon/ and read composer.json plus src/. You are specifically looking for four things before writing any code:

  • The FQCN and whether a facade/root alias is registered via extra.laravel.providers / extra.laravel.aliases.
  • What format() returns on a null input — an empty string '' or null. This decides your Blade output and your JSON contract, so confirm it rather than assume.
  • Whether parse(null) returns a null-object (a real instance whose methods are no-ops) or a literal null. The whole ergonomics story depends on which.
  • Whether the class implements Carbon\CarbonInterface / Illuminate\Contracts\Database\Eloquent\CastsAttributes / Castable — this determines whether you can use it as an Eloquent cast or only as a wrapper.

2. Install and confirm discovery.

composer require hnhdigital-os/laravel-null-carbon
php artisan package:discover

If no provider is auto-discovered, the package is likely a plain class you use directly, which is fine — it means less lifecycle surface to worry about.

3. First use case: an optional date on a model, formatted in a view. The canonical scenario is a nullable published_at, last_login_at, or deleted_at that you want to render without ?-> noise:

use HnhDigital\NullCarbon\NullCarbon; // confirm namespace from src/

// Null input: no exception, no null-check
NullCarbon::parse($post->published_at)->format('M j, Y');

// Non-null input: identical to Carbon
NullCarbon::parse($post->published_at)->diffForHumans();

Then in Blade, replace the null-safe chain with a plain one and give yourself a fallback in a single expression:

{{ NullCarbon::parse($post->published_at)->format('M j, Y') ?: 'Draft' }}

That ?: is the pattern to adopt on day one — the wrapper removes the crash, but you still own the "what do we display when it is empty" decision.

Implementation Patterns

Wrap at the boundary, not in the core. Use the package in presenters, Blade views, API resources, mail templates, and console output. Keep domain logic, services, and repositories on native Carbon/CarbonImmutable and ?Carbon. Null-object patterns are an ergonomics layer; when they leak into business logic they start making "missing" and "intentionally empty" indistinguishable.

A single helper to keep the dependency swappable. One function or macro means replacing the package later is a one-line change:

// app/Support/DatePresenter.php or a helpers file
function optional_date(null|string|DateTimeInterface $value, ?string $tz = null): NullCarbon
{
    return NullCarbon::parse($value, $tz);
}

Call optional_date($model->starts_at)->format('D, M j') everywhere. If the package turns out to be unmaintained, you reimplement that function in ten lines.

Eloquent accessors over casts. If the class is not a proper Eloquent cast, wrap in an accessor instead of forcing a cast:

protected function publishedAtDisplay(): Attribute
{
    return Attribute::get(
        fn () => optional_date($this->published_at)->format('M j, Y') ?: 'Draft'
    );
}

If it does implement Castable, casting works but changes the return type of $model->published_at for every consumer — including third-party packages that expect a Carbon instance. Only cast on models you fully control.

Chaining is the point; use it for display pipelines. The value over ?-> is that a whole chain survives:

optional_date($event->ends_at)
    ->addMinutes($event->grace_minutes)
    ->setTimezone($user->timezone)
    ->format('g:i A');   // renders empty on a null start

This is legitimate for output. It is not legitimate for computing eligibility ("is registration still open?"), because a null silently propagates through the chain and you get a wrong-but-non-crashing answer instead of an exception.

Fallbacks belong at the render edge. Prefer ->format(...) ?: 'Never' (works whether the package returns '' or null) over ?? 'Never' (which only catches null) and over ->isNull() checks sprinkled through templates. Standardise on one idiom and put it in your coding standards so the team writes the same thing.

Test the contract you depend on. Because the package ships little or no documented behaviour, write three or four assertions of your own around the semantics you actually rely on:

it('renders an empty string for a null date', function () {
    expect(optional_date(null)->format('Y-m-d'))->toBe('');
});

it('still parses real dates', function () {
    expect(optional_date('2024-03-01')->format('Y-m-d'))->toBe('2024-03-01');
});

These tests are your defence on the next composer update and the first thing you run when you evaluate dropping the dependency.

Make the output contract explicit in APIs. Decide once whether a missing date serialises as null (correct for most JSON APIs) or "" (what the wrapper may give you). In an API Resource, do the conversion deliberately:

'published_at' => $this->published_at
    ? optional_date($this->published_at)->toIso8601String()
    : null,

Centralise the "hand back to real Carbon" step. The moment you need a genuine date — a query, a comparison, an export — convert at that seam. A short toCarbonOrNull() helper keeps the escape hatch in one place:

function to_carbon_or_null($date): ?Carbon
{
    return optional_date($date)->isNull() ? null : $date;
}

Gotchas and Tips

Carbon::parse(null) is not null — it is now. If the wrapper's parse() delegates to Carbon's parser before applying its null check (common in small wrappers), you can get "today" rendered where you expected an empty cell. Verify with a one-line dd(optional_date(null)) before trusting it anywhere.

Silent failure is the design trade-off. A null-object suppresses "call to a member function format() on null" — which is exactly what you want in a view and exactly what you don't want three layers deep in a service where a missing date is a genuine data bug. Keep the wrapper out of domain code, and keep one path in your app where a missing required date still throws.

Passing a null-Carbon into the query builder or validation is where it bites. whereDate, whereBetween, after:, before_or_equal:, ->greaterThan(), and comparisons against real Carbon instances all expect a genuine DateTimeInterface. An empty wrapper may be coerced to '', to '0000-00-00', or to now depending on the casting path. Convert to null at the boundary and let the query handle the null case explicitly.

format() return type is your API contract. If it returns '', then ->format('Y-m-d') === null is false, ?? $fallback never fires, and ->toIso8601String() on a null object may return an empty string that lands in your JSON. Pick ?: for fallbacks rather than ??, and normalise in API resources.

Empty string vs. null input are different problems. A null-object handles null; it does not necessarily handle '', 'not a date', or 0. If your data has dirty values (imported CSVs, form fields that submit blank strings), you still need validation upstream — the package prevents the class of null bugs, not the class of bad input bugs.

Carbon version drift. A 2023 package predates Carbon 3, which changed several behaviours on the CarbonInterface (notably around diffForHumans output and diff sign conventions). If your app has moved to Laravel 11/12 + Carbon 3, run your own contract tests against the wrapper before assuming formatting is unchanged. Likewise check the declared illuminate/support constraint — a cap below your Laravel version means Composer may pin you or refuse to install.

== is not isNull(). Two null-objects, and a null-object versus null, will not compare the way you expect with loose equality. Use ->isNull() / ->isEmpty() if the package exposes them; otherwise compare on the formatted string. Never == null a wrapper.

Model attribute side effects. If you cast an Eloquent attribute to the wrapper, $model->toArray(), serializeDate(), $model->toJson(), and anything calling $model->published_at instanceof Carbon will now see your wrapper. That can quietly change API payloads and break packages that assume Carbon. Prefer accessors for display and cast only when you have checked both serialisation paths.

Blade may not need the package at all. On PHP 8+ the native ?-> operator and ?? cover much of this, and Laravel's optional() covers the rest. Be honest about the value: the package buys you unbroken chains and shorter expressions, which is real in heavy view/presenter code and marginal elsewhere. Adopt it where the null-check boilerplate is genuinely repetitive, not as a default for every date.

Extension points. If the class is Carbon-derived, register your own formatting macros on it (NullCarbon::macro('shortDate', fn () => $this->format('d/m/Y') ?: '—')) so the fallback lives in one place rather than in every template. If it is not Carbon-derived, use a plain helper trait instead of fighting the class.

Plan your exit before you need it. One-star, zero-dependent, single-maintainer packages are fine as thin dependencies but should never be load-bearing. The optional_date() helper above, plus a composer.lock pin and your contract tests, means that if the package is abandoned you replace one function body — not a codebase.

Debugging checklist. When output looks wrong: get_class($date) (is it really the wrapper, or did Carbon's static methods return a real instance?), var_dump($date) to see whether the inner Carbon is set, ->isNull(), then ->format('c') to confirm the underlying value and timezone. Timezone handling in particular is easy to lose in a wrapper — explicitly pass or ->setTimezone() rather than relying on the wrapper to inherit it.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
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