Great question! You're on the right track with your architecture, but there are a few improvements you can make for better separation of concerns, validation, and type safety. Below are my recommendations and a concrete solution.
1. Validation with LaravelData
You want to leverage Spatie's Laravel Data objects for validation and type safety. The main issue is that your SettingsService::set() method is storing raw arrays (or whatever is passed in), without validating or mapping them to the correct Data class.
Solution:
- Always resolve the correct Data class for the setting.
- Validate and map the input to that Data class.
- Store the serialized Data object.
2. Decouple Model from Data
Your Setting Eloquent model should just be a data store. The actual logic for validation and transformation should live in your Data classes and service layer.
3. How to Validate on Set
Here's how you can implement validation using LaravelData in your SettingsService::set() method:
public function set(SettingName $name, array $value): bool
{
// 1. Resolve the Setting class (e.g., BackgroundColorSetting)
$setting = $this->resolver->resolve($name);
// 2. Get the Data class associated with this setting
$dataClass = $setting->getDataClass();
// 3. Validate and create the Data object
/** @var \Spatie\LaravelData\Data $data */
$data = $dataClass::from($value); // This will throw if validation fails
// 4. Store the serialized data
return SettingsRepository::set($name, $data->toJson());
}
4. How to Map SettingName to Data Class
Add a method to your Setting base class:
abstract class Setting extends Model
{
// ...
abstract public function getDataClass(): string;
}
And in your concrete setting:
class BackgroundColorSetting extends Setting
{
public function getDataClass(): string
{
return BackgroundColorSettingData::class;
}
}
5. Getting the Setting
When retrieving, you can hydrate the Data object:
public function get(SettingName $name, mixed $default = null): Setting
{
try {
$serializedSettingValue = SettingsRepository::get($name);
} catch (SettingValueNotFoundException $exception) {
if ($default === null) {
throw $exception;
}
$serializedSettingValue = value($default);
}
$setting = $this->resolver->resolve($name);
$dataClass = $setting->getDataClass();
$setting->value = $dataClass::from(json_decode($serializedSettingValue, true));
return $setting;
}
6. Optional: Custom Eloquent Cast
If you want to automatically cast the value attribute to the correct Data class, you can create a custom Eloquent cast, but since each setting has a different Data class, it's easier to handle this in your service layer as above.
7. Enum-to-Class Mapping
Your resolver is fine, but you could use a static map for more explicitness and IDE support:
protected array $map = [
SettingName::BackgroundColor->value => BackgroundColorSetting::class,
// ...
];
public function resolve(SettingNameInterface $name): Setting
{
$class = $this->map[$name->value] ?? null;
if (!$class || !class_exists($class)) {
throw new RuntimeException("Setting not found: {$name->value}");
}
return app()->make($class);
}
8. Summary
- Validate input using the correct Data class before storing.
- Store the serialized Data object.
- Hydrate the Data object when retrieving.
- Keep your Eloquent model simple; put logic in Data classes and services.
Example Implementation
Setting Base Class
abstract class Setting extends Model
{
use SoftDeletes;
protected $table = 'settings';
protected $fillable = ['name', 'value'];
protected $casts = [
'name' => SettingName::class,
];
abstract public function getDataClass(): string;
}
BackgroundColorSetting
class BackgroundColorSetting extends Setting
{
public function getDataClass(): string
{
return BackgroundColorSettingData::class;
}
}
SettingsService::set
public function set(SettingName $name, array $value): bool
{
$setting = $this->resolver->resolve($name);
$dataClass = $setting->getDataClass();
// Validate and create Data object
$data = $dataClass::from($value); // Throws if invalid
// Store as JSON
return SettingsRepository::set($name, $data->toJson());
}
SettingsService::get
public function get(SettingName $name, mixed $default = null): Setting
{
try {
$serializedSettingValue = SettingsRepository::get($name);
} catch (SettingValueNotFoundException $exception) {
if ($default === null) {
throw $exception;
}
$serializedSettingValue = value($default);
}
$setting = $this->resolver->resolve($name);
$dataClass = $setting->getDataClass();
$setting->value = $dataClass::from(json_decode($serializedSettingValue, true));
return $setting;
}
Conclusion
- Do not store raw arrays or unvalidated data.
- Always use your Data classes for validation and serialization.
- Keep your Eloquent model simple.
- Let your service layer handle the mapping and validation.
This approach gives you strong typing, validation, and a clean separation of concerns.
If you need a full code example or further clarification, let me know!