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 Nullable Fields Laravel Package

dyrynda/laravel-nullable-fields

A Laravel trait for storing nullable model attributes as empty strings instead of NULL. Define your nullable fields, and the package transparently converts values on save and retrieval, making it ideal for legacy schemas or database columns that don't accept NULL.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require dyrynda/laravel-nullable-fields

Check the version constraint first. As of 4.6.0 the package requires PHP 8.3+ (PHP 8.2 support was dropped) and adds Laravel 13 support alongside the existing 11/12 range. If you're still on PHP 8.2 or an older Laravel, Composer will resolve you to 4.5.x — that's fine, the trait's API is unchanged, but pin the constraint ("^4.5") explicitly so composer update doesn't fail a deploy on a platform mismatch. There is nothing to publish and no service provider to register — the package is a single trait. Read vendor/dyrynda/laravel-nullable-fields/src/NullableFields.php first (it's ~40 lines); understanding exactly which values it considers "empty" is the whole game.

Wire it up

use Dyrynda\Database\Support\NullableFields;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use NullableFields;

    protected $nullable = ['bio', 'twitter_handle', 'phone'];
}

Make the columns actually nullable — this is a PHP-side helper only:

$table->string('twitter_handle')->nullable();

First use case: an optional profile form. Without the trait, an empty text input lands in the database as '', which breaks whereNotNull('phone'), makes COALESCE/fallback logic lie, and produces inconsistent rows depending on whether the client omitted the key or sent an empty string. With the trait, '' (and null) collapse to a single canonical NULL regardless of whether the data came from a form request, an import CSV, a console command, or a Nova/Filament form — none of which share the ConvertEmptyStringsToNull web middleware.

Implementation Patterns

Declare once on a base model, configure per model. Most projects have an App\Models\Model or a BaseModel; put the trait there and only the $nullable array in each concrete model, so you never think about it again.

Dynamic columns via the extension point. The trait exposes getNullableColumns(), which by default returns $this->nullable. Override it when nullability depends on context (tenancy, feature flags, a $fillable-style computed list):

protected function getNullableColumns(): array
{
    return array_merge($this->nullable, ['company_name']);
}

Keep it aligned with validation. If a field is in $nullable, the FormRequest rule should be ['nullable', 'string'], not ['sometimes'] — otherwise the two layers disagree about what "no value" means and you get flapping behaviour between create and update.

Use it for the paths middleware can't reach. ConvertEmptyStringsToNull only runs on HTTP requests in the web group. The trait earns its keep in:

  • Artisan commands and scheduled imports writing arrays straight into Model::create()
  • Queue jobs hydrating payloads from external APIs ('', ' '-adjacent junk, 'null' strings)
  • Seeders and factories where you deliberately want a null baseline
  • API routes registered without the web middleware group
  • Admin panel resources (Nova/Filament) that don't always route through your HTTP kernel

Lean on it in upserts. updateOrCreate, firstOrCreate, fill, and forceFill all funnel through setAttribute, so the normalisation applies uniformly:

UserProfile::updateOrCreate(
    ['user_id' => $user->id],
    $request->validated(), // empty strings become NULL here
);

Trim before it matters. The trait treats '' as empty but not ' '. Either keep Laravel's TrimStrings middleware in front of it, or normalise in a mutator before calling the parent.

Test at the attribute level, not just the DB level. Both are worth asserting, because they can diverge if you later add a cast:

$user = User::create(['name' => 'Ada', 'bio' => '']);
$this->assertNull($user->bio);
$this->assertDatabaseHas('users', ['id' => $user->id, 'bio' => null]);

Gotchas and Tips

The trait overrides setAttribute(). That single fact drives most surprises:

  • It fires at assignment time, not save time. $user->bio = ''; already reads back as null, and isDirty('bio') reflects null. Anything comparing against the raw input (observers, audit logs, model events) sees the normalised value, not what the caller passed.

  • If another trait in the same class also overrides setAttribute, PHP will fatal on a collision. Resolve with insteadof/as or compose carefully:

    use NullableFields, OtherSetterTrait {
        NullableFields::setAttribute insteadof OtherSetterTrait;
        OtherSetterTrait::setAttribute as setAttributeFromOther;
    }
    
  • Because it calls parent::setAttribute(), use it on a class whose ancestry actually provides that method — i.e. a model (or base model) extending Illuminate\Database\Eloquent\Model. Putting it on a random DTO will not do what you want. If you place it on a base model and a child model also uses it, you'll get the trait applied twice through the chain; don't.

empty() semantics are broader than "empty string". Verify against the installed source, but treat 0, '0', false, and [] as at-risk. Never put these in $nullable:

  • Booleans ('active' => 'boolean' cast + false → null is a silent data-loss bug)
  • Integer columns where 0 is meaningful (counts, sort_order, balance)
  • Foreign keys — a 0 becoming NULL can violate an FK constraint or, worse, trigger nullOnDelete
  • Primary keys, obviously

The database column still has to be nullable. Forgetting ->nullable() in the migration produces a NOT NULL constraint violation (or a silent '' under non-strict MySQL) — the trait cannot help you there.

Query builder writes bypass it entirely.

User::where('id', $id)->update(['bio' => '']); // '' hits the database
DB::table('users')->insert(['bio' => '']);     // ditto

Same for raw inserts, insertOrIgnore, and anything using DB:: rather than the model.

Casts run after the fact. With 'meta' => 'array' or a date cast, an empty input becomes null before the cast; that's usually right, but if a cast's set mutator expects a string you'll now hand it null. Test the combination.

Unique indexes become friendlier. MySQL and Postgres treat NULL as distinct in unique indexes, so multiple rows can have a "blank" handle. This is often the real motivation for the package — but it only works if those rows are genuinely NULL, so don't let a stray ->update() path smuggle '' back in.

Upgrading across the 4.6 boundary is a platform move, not an API move. 4.6.0 only widens the Laravel constraint (adding 13) and raises the PHP floor to 8.3 — NullableFields itself, $nullable, and getNullableColumns() are untouched, so no code changes are needed. What does bite is CI: if a pipeline matrix still runs PHP 8.2, composer update will now silently resolve back to 4.5.x (or fail on --prefer-lowest/locked installs), so tests keep passing against the old code path and you won't notice you never actually upgraded. Bump the matrix and the platform.php config together. If you're adopting Laravel 13, this is the release that unblocks you — re-run your setAttribute-related tests specifically, since the trait overrides a core Eloquent method and that is exactly the seam a major framework bump is most likely to disturb.

Debugging checklist when a field isn't nulled:

  1. Is the key in $nullable / returned by getNullableColumns()? (Spelling, and is it a column name, not a form field name.)
  2. Did the write go through the model's setAttribute, or through the query builder?
  3. Is the value actually empty by the trait's definition? ' ', "\t", 'null', and '0' are not.
  4. Did a TrimStrings-less path leave whitespace behind?
  5. Is a cast or mutator re-injecting a value after normalisation?

Extension points worth knowing: override hasNullableColumns() to short-circuit when $nullable is absent (useful on a shared base model to skip the in_array cost), and isNullableColumn($key) if you need prefix- or suffix-based rules instead of an explicit list.

Keep the trait's scope honest. It's ~40 lines and does one thing: normalise falsy-ish input to NULL on Eloquent attribute assignment. If you find yourself wanting it to handle whitespace-only strings, enum coercion, or per-context rules, write that in a dedicated mutator or Attribute accessor instead — fighting the trait's empty() definition is how people end up accidentally nulling 0 in production.

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.
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
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle