---
title: "No class names over the wire — Testing for Laravel | Roundly"
description: "toExposeNoClassNames() reads a payload as a client does and fails on any PHP class name in it, listing each leak with its path."
url: https://roundly-consulting.com/open-source/docs/testing-for-laravel/no-class-names
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Testing for Laravel](https://roundly-consulting.com/open-source/docs/testing-for-laravel.md)

# No class names over the wire

Most host apps have no morph map, so a \*\_type column is App\\Models\\User — and shipping it leaks the host’s namespace layout into its API and ties the API to it. Render each related model through a reference resource holding {id, type: class\_basename} instead, and pin it with toExposeNoClassNames() — or Assert::exposesNoClassNames() in plain PHPUnit:

```php
use Illuminate\Database\Eloquent\Relations\Relation;
use RoundlyConsulting\Testing\Assert;

it('renders the author without a class name', function (): void {
    Relation::morphMap([], merge: false); // no morph map, like most hosts

    $resource = new CommentResource($comment->load('author'));

    expect($resource)->toExposeNoClassNames();                   // the response body, wrapping included
    expect($this->getJson('/comments'))->toExposeNoClassNames(); // a TestResponse
    expect($payload)->toExposeNoClassNames();                    // an array or a JSON string

    Assert::exposesNoClassNames($payload);                       // plain PHPUnit
});
```

## What it reads

| Subject | Read as |
| --- | --- |
| JSON string | Decoded; a string that is not JSON throws InvalidArgumentException. |
| Array | The payload as it is. |
| `JsonResource` | toResponse(request()) — the body a client receives, with data wrapping, with() and additional(). |
| `Response / JsonResponse / TestResponse` | The response body, decoded. |

Anything else — an int, a plain object — throws InvalidArgumentException.

## What fails

Every key and every string value is checked, at any depth, and every leak is listed with its path (data.author.type), not only the first:

- A namespaced class name anywhere — App\\Models\\User, \\App\\Models\\User, a JSON-encoded copy inside a string, one inside prose (No query results for model \[App\\Models\\User\]), one used as a key. A StudlyCase Vendor\\Name shape is enough; a lowercase chain counts when the class really exists.
- A \*\_type key holding a class, reported as a morph type column — including a global class with no namespace to give it away ("subject\_type": "ArrayObject").
- Nothing to check. An empty string, {}, \[\], null, an empty resource or a response with no body fails: a payload with no value in it proves nothing rendered.

```php
// Passes — a reference resource's basename, and morph aliases
expect(['id' => 1, 'type' => 'User'])->toExposeNoClassNames();
expect(['subject_type' => 'post', 'payment_type' => 'card'])->toExposeNoClassNames();

// Fails — every leak listed with its path
expect(['data' => ['author' => ['id' => 1, 'type' => 'App\Models\User']]])->toExposeNoClassNames();
// `data.author.type` holds the class name `App\Models\User`

expect(['commentable_type' => 'App\Models\Post', 'commentable_id' => 3])->toExposeNoClassNames();
// the morph type column `commentable_type` holds the class name `App\Models\Post`

expect(['subject_type' => 'ArrayObject'])->toExposeNoClassNames();
// the morph type column `subject_type` holds the class name `ArrayObject`

expect(['message' => 'No query results for model [App\Models\User] 5'])->toExposeNoClassNames();
// `message` holds the class name `App\Models\User`

// Fails — nothing to check
expect([])->toExposeNoClassNames();
// There is nothing to check: the payload is empty, so "it exposes no class names" would pass
// without anything having rendered. Assert on the populated payload.
```

## What passes

- A reference resource’s own {"id": 1, "type": "User"} — type is not a \*\_type column and a basename has no namespace.
- A morph alias in a \*\_type column ("commentable\_type": "post").
- Scalars, null values, URLs and paths with forward slashes.

## Limits

- A Windows path (C:\\Users\\Jane) looks like a class name and fails — a leak of its own.
- It checks what it is given. Internal round-trip formats — exports, ledger reports, queued payloads — are exempt from the rule; don’t point it at them.
- It does not set up the no-morph-map condition for you: clear the morph map in the test, or the payload may hold an alias that hides what a host without one would see.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/testing-for-laravel.md)
