dyrynda/laravel-nullable-fields
Keeps nullable database columns genuinely null. Add the NullableFields trait to an Eloquent model and list your nullable fields; empty strings are converted to NULL on save, keeping your Laravel data consistent across queries, filters, and relationships.
Install
composer require dyrynda/laravel-nullable-fields
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.
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:
Model::create()'', ' '-adjacent junk, 'null' strings)web middleware groupLean 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]);
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:
'active' => 'boolean' cast + false → null is a silent data-loss bug)0 is meaningful (counts, sort_order, balance)0 becoming NULL can violate an FK constraint or, worse, trigger nullOnDeleteThe 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.
Debugging checklist when a field isn't nulled:
$nullable / returned by getNullableColumns()? (Spelling, and is it a column name, not a form field name.)setAttribute, or through the query builder?' ', "\t", 'null', and '0' are not.TrimStrings-less path leave whitespace behind?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.
How can I help you explore Laravel packages today?