php 105 lines · 3 tabs

Cast a JSON Column to an Immutable Value Object with a Custom Eloquent Cast in Laravel

Shared by codesnips Aug 2026
3 tabs
<?php

namespace App\ValueObjects;

final readonly class NotificationSettings
{
    public function __construct(
        public bool $emailEnabled,
        public bool $smsEnabled,
        public string $digestFrequency,
        public array $mutedChannels,
    ) {
    }

    public static function fromArray(array $data): self
    {
        return new self(
            emailEnabled: (bool) ($data['email_enabled'] ?? true),
            smsEnabled: (bool) ($data['sms_enabled'] ?? false),
            digestFrequency: $data['digest_frequency'] ?? 'daily',
            mutedChannels: array_values($data['muted_channels'] ?? []),
        );
    }

    public function toArray(): array
    {
        return [
            'email_enabled' => $this->emailEnabled,
            'sms_enabled' => $this->smsEnabled,
            'digest_frequency' => $this->digestFrequency,
            'muted_channels' => $this->mutedChannels,
        ];
    }

    public function with(array $changes): self
    {
        return self::fromArray(array_merge($this->toArray(), $changes));
    }
}
3 files · php Explain with highlit

This snippet shows how a JSON settings column on an Eloquent model can be surfaced as a typed, immutable value object instead of a loosely-typed associative array. The problem it solves is that raw JSON columns leak untyped array data everywhere: callers guess key names, forget defaults, and mutate nested arrays without any guarantee the shape stays valid. A CastsAttributes implementation moves that structure into one place.

The NotificationSettings tab defines the value object. It is a readonly class with typed properties, so once constructed it cannot be mutated in place — any change produces a new instance via with(), which mirrors how immutable value objects behave elsewhere. fromArray() centralises defaulting and coercion so missing keys never blow up, while toArray() provides the canonical serialisation. Keeping both directions here means the JSON shape has exactly one source of truth.

The NotificationSettingsCast tab implements Laravel's CastsAttributes interface. Its get() receives the raw string pulled from the database and rehydrates it into a NotificationSettings object, tolerating null by falling back to defaults. set() accepts either an existing value object or a plain array, validates the type, and returns the JSON string that Eloquent actually persists. Returning an encoded string from set() is important: the cast owns serialisation, so the model column stays a real JSON string and the object never bleeds into the query builder.

The User model tab wires the cast in via the $casts array using the class-string form. Because Eloquent memoises cast results per attribute, repeated reads of $user->notification_settings return the same object without re-parsing JSON. The muteEmails() helper demonstrates the immutable update dance: read the value object, derive a new one with with(), and assign it back so set() re-serialises on save.

The trade-off is a little ceremony compared to a bare array, and callers must remember that mutating the returned object does nothing until reassigned. In return the model gains a validated, self-documenting API, IDE autocompletion on settings, and a single choke point for migrating the JSON schema. This pattern fits any column holding structured configuration — feature flags, preferences, or embedded metadata.


Related snips

ruby
class Money
  include Comparable

  attr_reader :amount, :currency

  def initialize(amount, currency = 'USD')

Value objects for domain modeling

ruby value-objects domain-driven-design
by Sarah Mitchell 2 tabs
python
class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)

Django model signals vs overriding save

django python models
by Priya Sharma 2 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
ruby
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]

sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first

SQL injection prevention with unsafe and safe query patterns

sql-injection owasp database
by Kai Nakamura 3 tabs
php
<?php

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

Laravel service container and dependency injection

laravel dependency-injection service-container
by Carlos Mendez 2 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab

Share this code

Here's the card — post it anywhere.

Cast a JSON Column to an Immutable Value Object with a Custom Eloquent Cast in Laravel — share card
Link copied