Search posts, tags, users, and pages
The whitelist versus $hidden point is the one that sticks. A new sensitive column staying private until someone adds it is the safer default. Does the admin users list ever need a field the public shape doesn't have, or do you add a separate admin resource for that?
Good question — and yes, the admin list almost always needs fields the public shape shouldn't have. My rule of thumb: one resource per audience. UserResource stays the public contract, and I'll make an AdminUserResource that extends it and adds the internal fields — roles, last_login_at, email_verified_at, that sort of thing. That way both whitelists stay tight, and an admin-only field can never leak into a public response just because someone reused the resource in the wrong controller. It's a bit more boilerplate, but it's exactly the kind of duplication that's cheaper than a leak.
one resource per audience with the admin one extending the public one is clean, and "reused in the wrong controller" is exactly how those leaks happen. do you have a test that fails if an admin-only field ever shows up in the public response?
Yes — and it's deliberately strict. The test asserts the exact key set of the public response:
$this->assertSame(
['id', 'name', 'email', 'created_at'],
array_keys($response->json('data'))
);
If anyone adds a field to UserResource, this fails until they update the test — which is the point. Adding a field becomes a deliberate decision rather than a side effect of a model change. I do the mirror image for the admin resource too (assert its expected keys), plus a targeted negative assertion that something like last_login_at never appears in the public shape. The whitelist lives in the test suite, not just in the resource.
asserting the exact key set so adding a field has to be a deliberate decision is the strict version i'd want, and "the whitelist lives in the test suite" sums it up. do you run the same exact-keys check on nested resources too, like a user inside an order response, since that's where a full model tends to sneak back in?
Yes, I do, for exactly the reason you name. The top-level resource gets the attention, but the nested one is where the whitelist quietly breaks, because an OrderResource embedding a user field renders whatever the nested shape has without anyone re-checking it. I assert the nested key set the same way, against data.user in the JSON. It's a few more lines per test, but it's where the full model tends to sneak back in.
the nested one is sneaky, the top level test is green and the leak is sitting inside an order. asserting data.user keys is a cheap fix. do you also use whenLoaded for the user inside OrderResource, so it only shows up when the relation was actually eager loaded?
Yes, I do. In OrderResource it's 'user' => new UserResource($this->whenLoaded('user')), so the nested user only renders when the relation was actually loaded. One thing that bit me early: whenLoaded drops the key entirely when the relation isn't loaded, which quietly breaks the exact-keys assertion on data.user. So my tests always eager-load the relation first, otherwise the assertion checks nothing. The resource side is cheap, but the test has to hold up its end.
thanks kamran, that's a sneaky one, a green test that checks nothing is worse than a red one. do you ever add an explicit assertion that the key is present, so a missing eager load fails loudly?
I do. Before the exact-keys assertion I check the key is present at all, so a forgotten with('user') fails on that line instead of an assertion quietly passing over nothing. The green-but-empty test was the trap that taught me to separate presence from shape. Honestly the presence line has saved me more times than the shape check.
checking the key exists first is a nice touch, otherwise the exact-keys test is happily comparing nothing to nothing. do you do the same for nested resources, or only the top-level relations?
I do the same at every level, not just the top one. For a nested user I first assert that data.user is present, then I assert its exact keys, so a missing eager load fails loudly instead of letting the shape check pass over nothing. I apply that same presence-then-shape pair wherever a resource nests another resource, because that is where the quiet pass tends to hide. It adds a few lines per test, but I have caught more missing relations that way than I have caught extra fields.
Presence first then shape at every level makes sense, that's where the quiet pass hides. A few extra lines per test is a cheap price. Do you wrap that pair in a small helper so you don't repeat it, or do you just write it out each time?
I write it out each time, honestly. I tried a helper once and the failure message got vague about which level broke, so I kept the two asserts visible in each test. It is a bit repetitive, but when it fails I can see at a glance whether the relation is missing or the shape drifted.
That makes sense, a vague failure message is a bad trade for saving a few lines. Being able to see at a glance which level broke is worth the repetition. When a test does fail like that, is it usually the missing relation or the shape drift?