composer require michaelachrisco/readonly
ReadOnlyTrait to any Eloquent model:
use MichaelAChrisco\ReadOnly\ReadOnlyTrait;
class LegacyUser extends Model
{
use ReadOnlyTrait;
}
LegacyUser will immediately throw a ReadOnlyException:
$user = new LegacyUser(['name' => 'John']);
$user->save(); // Throws ReadOnlyException
ReadOnlyException for method-specific errors.boot() to apply the trait dynamically:
protected static function boot()
{
static::addGlobalScope(new ReadOnlyScope());
}
expectException(ReadOnlyException::class) in PHPUnit tests.Legacy System Integration:
ReadOnlyTrait to models representing read-only data sources (e.g., LegacyInvoice, AuditLog).SELECT only) for defense in depth.Dynamic Application:
public function handle($request, Closure $next)
{
if ($request->isLegacyEndpoint()) {
Model::addGlobalScope(new ReadOnlyScope());
}
return $next($request);
}
API Responses:
403 Forbidden for write attempts:
try {
$user->save();
} catch (ReadOnlyException $e) {
return response()->json(['error' => 'Resource is read-only'], 403);
}
DB::table() to enforce read-only behavior:
DB::macro('readonly', function ($query) {
return $query->getReadOnlyQuery();
});
class Order extends Model
{
use ReadOnlyTrait;
public function items()
{
return $this->hasMany(OrderItem::class)->readonly();
}
}
Cache::remember()).Overridden Methods:
save() or update() methods bypass the trait. Use parent::save() to trigger checks.public function save(array $options = [])
{
throw new ReadOnlyException('Custom save blocked');
// parent::save($options); // Would throw the trait's exception
}
Mass Assignment:
fill() and create() may silently fail. Explicitly check for ReadOnlyException:
$user->fill(['name' => 'Alice']); // No exception, but save() will throw.
Soft Deletes:
restore() and forceDelete() are blocked, but deleted_at may still be set via raw queries.Model Events:
saving, updating, or deleting events fire before the trait’s checks. Use saved, updated, etc., for post-validation logic.ReadOnlyException globally in App\Exceptions\Handler:
public function render($request, Throwable $exception)
{
if ($exception instanceof ReadOnlyException) {
return response()->json(['error' => 'Read-only violation'], 403);
}
return parent::render($request, $exception);
}
catch (ReadOnlyException $e) {
\Log::warning("Read-only violation on {$this->class}: {$e->getMessage()}");
}
Custom Exceptions:
ReadOnlyException for granular error messages:
class LegacyReadOnlyException extends ReadOnlyException {}
throwException() method.Whitelisting:
touchOwnedTimestamps) by modifying the trait:
protected $allowedMethods = ['touch'];
Conditional Read-Only:
class ConditionalReadOnlyScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
if ($model->isReadOnly()) {
$builder->getQuery()->setReadOnly(true);
}
}
}
Testing:
$this->partialMock(User::class, ['save'])
->shouldThrow(ReadOnlyException::class)
->during('save');
How can I help you explore Laravel packages today?