#2 [Illuminate2] Model Guards Attributes on Create

Helium Services
4 min readNov 30, 2020

--

In my first article a few weeks ago, I covered the benefits of a Model-centric approach to Laravel. If you haven’t already, check out that article here.

This week is the second in a long line of tutorials for the Model-centric components I’ve built over the past several months. If you read my first piece, you’ll know that all of my components were originally developed as the result of mish-mash experimentation and hacking something together until it did what I wanted. As I publish this tutorial series, I’ll also be cleaning things up by re-developing each of the components into my new package, helium/illuminate2.

Guards Attributes On Create

One short and convenient modification to the built-in Eloquent Logic that I’ve developed is the ability to conditionally apply $fillable and $guarded based on whether the model instance is being created or updated. In my experience, it's a fairly common requirement that certain model attributes be fillable when the instance is created, but guarded from changes forever afterwards. For example, consider a class to represent Comments on a Post. The Comment object has a user_id and a post_id which must be set when the object is created, but should never be modified thereafter.

Installation

To get started, install the package into your Laravel project:

composer require helium/illuminate2

Please note that this package is only compatible with Laravel 8.14 and later, so you should make sure your project is up-to-date with the most recent release.

Fillable On Create

Include the GuardsAttributesOnCreate trait on your Model.

use Helium\Illuminate2\Database\Eloquent\Concerns\GuardsAttributesOnCreate;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
use GuardsAttributesOnCreate;
}

Great! Now all that’s left is to declare your fillable and fillableOnCreate properties.

protected $fillableOnCreate = [
'user_id',
'post_id'
];
protected $fillable = [
'comment'
];

In the given example, the $fillableOnCreate property declares a list of attributes which are only mass-assignable if the object is being created (i.e. not updated). And, as always, the $fillable property declares a list of attributes which are always mass-assignable.

In Practice

Consider how this affects the usage of your model class in practice.

$comment = Comment::create([
'user_id' => 1,
'post_id' => 1,
'comment' => 'Hello, World!'
]);
$comment->update([
'comment' => 'Hello, Laravel!'
]);

In the given example, you are able to mass-assign the user_id and post_id while creating the Comment instance, but once they're set, any attempt to mass-assign those attributes on a call to update will result in the changes being ignored.

Guarded On Create

Although I can imagine the use case for this is much less likely, for the sake of completeness, I have included the option to set a list of attributes which are only guarded on create. Perhaps you have a Post model which must be created as a draft before it can be published. In this case, you may wish to guard the published_at attribute on create, but unguard it for updates. As you might expect, you can set the guardedOnCreate property to define properties which are only guarded from mass-assignment on create, while the guarded property continues to guard specified attributes from mass-assignment at all times.

use Helium\Illuminate2\Database\Eloquent\Concerns\GuardsAttributesOnCreate;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use GuardsAttributesOnCreate;
protected $guardedOnCreate = [
'published_at'
];
}

Customization

As with all of the Model-centric components I’ve developed, you can customize the fillableOnCreate and guardedOnCreate attributes by overriding the requisite getter functions. This would be required if, for example, you wanted to conditionally add attributes to fillableOnCreate or guardedOnCreate based on some external state, such as circumstances defined by different Controller endpoints.

Please note that the $creationState property shown here is not part of the GuardsAttributesOnCreate trait. Instead, it is intended to demonstrate one the many ways in which you could customize the logic of these getter functions to meet your circumstantial needs.

class Comment extends Model
{
use GuardsAttributesOnCreate;
public static $creationState = 'parent'; public function getFillableOnCreate()
{
if (static::creationState == 'child') {
return ['parent_id'];
}
}
public function getGuardedOnCreate()
{
if (static::creationState == 'parent') {
return ['parent_id'];
}
}
}
class CommentController extends Controller
{
public function create(Request $request)
{
return Comment::create($request->all());
}
public function reply(Request $request)
{
Comment::$creationState = 'child';
return Comment::create($request->all());
}
}

A Word On Philosophy

Model-centric logic isn’t for everybody, and for your own purposes, you may find it more effective to simply use the mergeFillable and mergeGuarded functions provided by Eloquent out of the box. However, for most straightforward use cases, this approach has worked quite well for me and my team, as the logic of determining mass-assignable attributes almost never changes circumstantially (except between create and update, which is the entire purpose of this tutorial). This tutorial is intended for developers who can benefit from this alternate approach based on their own philosophy and style, and if this approach isn't for you, I hope this at least sparks a lively and respectful conversation in the comments!

To understand my particular approach on using Laravel, I encourage you to read my opinion piece The Case For Model-Centric Laravel.

Conclusion

Again, this is the second in a series of tutorials covering the various Model-centric components I’ve developed over the past several months. Check back in in two weeks to see the next tutorial on generating Model primary keys.

--

--