Output:
Chunk ID: 2e5d14
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1256
Output:
---
name: developing-with-fortify
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
---

# Laravel Fortify Development

Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.

## Documentation

Use `search-docs` for detailed Laravel Fortify patterns and documentation.

## Usage

- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.

## Available Features

Enable in `config/fortify.php` features array:

- `Features::registration()` - User registration
- `Features::resetPasswords()` - Password reset via email
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
- `Features::updateProfileInformation()` - Profile updates
- `Features::updatePasswords()` - Password changes
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes

> Use `search-docs` for feature configuration options and customization patterns.

## Setup Workflows

### Two-Factor Authentication Setup

```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] Run migrations for 2FA columns
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
```

> Use `search-docs` for TOTP implementation and recovery code handling patterns.

### Email Verification Setup

```
- [ ] Enable emailVerification feature in config
- [ ] Implement MustVerifyEmail interface on User model
- [ ] Set up verifyEmailView callback
- [ ] Add verified middleware to protected routes
- [ ] Test verification email flow
```

> Use `search-docs` for MustVerifyEmail implementation patterns.

### Password Reset Setup

```
- [ ] Enable resetPasswords feature in config
- [ ] Set up requestPasswordResetLinkView callback
- [ ] Set up resetPasswordView callback
- [ ] Define password.reset named route (if views disabled)
- [ ] Test reset email and link flow
```

> Use `search-docs` for custom password reset flow patterns.

### SPA Authentication Setup

```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum
- [ ] Use 'web' guard in fortify config
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```

> Use `search-docs` for integration and SPA authentication patterns.

## Best Practices

### Custom Authentication Logic

Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.

### Registration Customization

Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.

### Rate Limiting

Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.

## Key Endpoints

| Feature                | Method   | Endpoint                                    |
|------------------------|----------|---------------------------------------------|
| Login                  | POST     | `/login`                                    |
| Logout                 | POST     | `/logout`                                   |
| Register               | POST     | `/register`                                 |
| Password Reset Request | POST     | `/forgot-password`                          |
| Password Reset         | POST     | `/reset-password`                           |
| Email Verify Notice    | GET      | `/email/verify`                             |
| Resend Verification    | POST     | `/email/verification-notification`          |
| Password Confirm       | POST     | `/user/confirm-password`                    |
| Enable 2FA             | POST     | `/user/two-factor-authentication`           |
| Confirm 2FA            | POST     | `/user/confirmed-two-factor-authentication` |
| 2FA Challenge          | POST     | `/two-factor-challenge`                     |
| Get QR Code            | GET      | `/user/two-factor-qr-code`                  |
| Recovery Codes         | GET/POST | `/user/two-factor-recovery-codes`           |


laravel/framework livewire/livewire laravel/fortify pestphp/pest

custom request guard bearer token policy authorization

form request authorize route model binding

database transactions lockForUpdate race condition

Livewire authorize action policy testing


# Search Results

## Passport Device Authorization Grant Requesting Tokens
Passport | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/passport.md

### Requesting Tokens

---

## Broadcasting Authorizing Channels Defining Authorization Callbacks Authorization Callback Model Binding
Broadcasting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/broadcasting.md

#### Authorization Callback Model Binding

Just like HTTP routes, channel routes may also take advantage of implicit and explicit [route model binding](/docs/{{version}}/routing#route-model-binding). For example, instead of receiving a string or numeric order ID, you may request an actual `Order` model instance:

```php
use App\Models\Order;
use App\Models\User;

Broadcast::channel('orders.{order}', function (User $user, Order $order) {
    return $user->id === $order->user_id;
});
```

> [!WARNING]
> Unlike HTTP route model binding, channel model binding does not support automatic [implicit model binding scoping](/docs/{{version}}/routing#implicit-model-binding-scoping). However, this is rarely a problem because most channels can be scoped based on a single model's unique, primary key.

---

## Attribute Authorize Learn more
Attribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize

## Learn more

For more information on defining abilities and policies, see the [Laravel Authorization documentation](https://laravel.com/docs/authorization).

---

## Verification Introduction Database Preparation
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Database Preparation

Next, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.

---

## Attribute Session Dynamic session keys
Attribute Session | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-session

## Dynamic session keys

You can generate keys dynamically using other properties:

```php
<?php // resources/views/components/post/⚡index.blade.php

use Livewire\Attributes\Session;
use Livewire\Component;
use App\Models\Author;

new class extends Component {
    public Author $author;

    #[Session(key: 'search-{author.id}')] // [tl! highlight]
    public $search = '';
};
```

If `$author->id` is `4`, the session key becomes `search-4`. This allows different session values per author.

---

## Routing Route Model Binding
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

## Route Model Binding

When injecting a model ID to a route or controller action, you will often query the database to retrieve the model that corresponds to that ID. Laravel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user's ID, you can inject the entire `User` model instance that matches the given ID.

---

## Attribute Authorize Argument resolution
Attribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize

## Argument resolution

The attribute resolves the object to authorize against in the following order:

* **No argument** — Checks a simple gate that doesn't require a model (e.g., `#[Authorize('view-dashboard')]`).
* **Class string** — Useful for `create` permissions where no instance exists yet (e.g., `#[Authorize('create', Post::class)]`).
* **Method parameter** — Resolves the argument from the method's own parameters.
* **Component property** — Looks for a property on the component matching the argument name (e.g., `public Post $post`).

### Resolving from method parameters

When authorizing based on a method parameter, you must type-hint the parameter so Livewire knows which model to resolve:

```php
<?php // resources/views/components/⚡comment-manager.blade.php

use Livewire\Attributes\Authorize;
use Livewire\Component;
use App\Models\Comment;

new class extends Component {
    #[Authorize('delete', 'comment')] // [tl! highlight]
    public function deleteComment(Comment $comment) // [tl! highlight]
    {
        $comment->delete();
    }
};
```

> [!important]
> If you resolve a model via a method parameter, a type-hint (e.g., `Comment $comment`) is required. Without it, Livewire cannot determine which model to resolve and the authorization check will fail.

---

## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Database Constructor Signature Changes

**Likelihood Of Impact: Very Low**

In Laravel 12, several low-level database classes now require an `Illuminate\Database\Connection` instance to be provided via their constructors.

**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**

`Illuminate\Database\Schema\Blueprint`

The constructor of the `Illuminate\Database\Schema\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.

`Illuminate\Database\Grammar`

The constructor of the `Illuminate\Database\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:

```php
// Laravel <= 11.x
$grammar = new MySqlGrammar;
$grammar->setConnection($connection);

// Laravel >= 12.x
$grammar = new MySqlGrammar($connection);
````

In addition, the following APIs have been removed or deprecated:

- The `Blueprint::getPrefix()` method is deprecated.
- The `Connection::withTablePrefix()` method has been removed.
- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.
- The `Grammar::setConnection()` method has been removed.

When working with table prefixes, you should now retrieve them directly from the database connection:

```php
$prefix = $connection->getTablePrefix();
```

If you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.

---

## Attribute Authorize When NOT to use
Attribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize

## When NOT to use

> [!warning]
> The `#[Authorize]` attribute only protects server-side execution of an action. It does not hide UI elements in your Blade template.

You should still use Blade's `@can` directives to hide buttons the user isn't allowed to use:

```blade
@can('update', $post)
    <button wire:click="save">Save</button>
@endcan
```

---

## Session Properties Setting a custom key
Session Properties | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/session-properties

## Setting a custom key

When using `[#Session]`, Livewire will store the property value in the session using a dynamically generated key that consists of the component name combined with the property name.

This ensures that properties across component instances will use the same session value. It also ensures properties of the same name from different components won't conflict.

If you want full control over what session key Livewire uses for a given property, you can pass the `key:` parameter:

```php
<?php

use Livewire\Attributes\Session;
use Livewire\Component;

class ShowPosts extends Component
{
    #[Session(key: 'search')] // [tl! highlight]
    public $search;

    // ...
}
```

When Livewire stores and retrieves the value of the `$search` property, it will use the given key: "search".

Additionally, if you want to generate the key dynamically from other properties in your component, you can do so using the following curly brace notation:

```php
<?php

use Livewire\Attributes\Session;
use Livewire\Component;
use App\Models\Author;

class ShowPosts extends Component
{
    public Author $author;

    #[Session(key: 'search-{author.id}')] // [tl! highlight]
    public $search;

    // ...
}
```

In the above example, if the `$author` model's id is "4", the session key will become: `search-4`

---

## Wire Transition Customizing animations with CSS
Wire Transition | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-transition

## Customizing animations with CSS

View Transitions are controlled entirely through CSS. You can customize the animation by targeting the view-transition pseudo-elements:

```css
/* Customize the transition for a specific element */
::view-transition-old(sidebar) {
    animation: 300ms ease-out both slide-out;
}

::view-transition-new(sidebar) {
    animation: 300ms ease-in both slide-in;
}

@keyframes slide-out {
    to { transform: translateX(-100%); }
}

@keyframes slide-in {
    from { transform: translateX(100%); }
}
```

The View Transitions API provides three pseudo-elements you can style:
- `::view-transition-old(name)` — The outgoing element's snapshot
- `::view-transition-new(name)` — The incoming element's snapshot
- `::view-transition-group(name)` — Container for both snapshots

---

## Database Database Transactions Handling Deadlocks
Database | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database.md

#### Handling Deadlocks

The `transaction` method accepts an optional second argument which defines the number of times a transaction should be retried when a deadlock occurs. Once these attempts have been exhausted, an exception will be thrown:

```php
use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    DB::update('update users set votes = 1');

    DB::delete('delete from posts');
}, attempts: 5);
```

---

## Authorization Authorizing Actions Using Policies
Authorization | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/authorization.md

## Authorizing Actions Using Policies

---

## Attribute Authorize Basic usage
Attribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize

## Basic usage

Apply the `#[Authorize]` attribute to any action method. Pass the ability name and an optional argument:

```php
<?php // resources/views/components/post/⚡edit.blade.php

use Livewire\Attributes\Authorize;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public Post $post;

    #[Authorize('update', 'post')] // [tl! highlight]
    public function save()
    {
        $this->post->save();
    }
};
```

```blade
<button wire:click="save">
    Update Post
</button>
```

When `save()` is called, Livewire automatically checks if the current user is authorized to `update` the `$post` model stored on the component.

---

## Validation Form objects
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Form objects

As more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.

Below is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:

```php
<?php

namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    #[Validate('required|min:3')]
	public $title = '';

    #[Validate('required|min:3')]
    public $content = '';
}
```

The `PostForm` above can now be defined as a property on the `CreatePost` component:

```php
<?php

namespace App\Livewire;

use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public PostForm $form;

    public function save()
    {
		Post::create(
    		$this->form->all()
    	);

		return redirect()->to('/posts');
    }

    // ...
}
```

As you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.

Also, when referencing the property names in the template, you must prepend `form.` to each instance:

```blade
<form wire:submit="save">
	<input type="text" wire:model="form.title">
    <div>@error('form.title') {{ $message }} @enderror</div>

	<textarea wire:model="form.content"></textarea>
    <div>@error('form.content') {{ $message }} @enderror</div>

	<button type="submit">Save</button>
</form>
```

When using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:

```php
public function save()
{
    Post::create(
        $this->form->validate()
    );

    return redirect()->to('/posts');
}
```

Form objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).

---

## Attribute Authorize Stacking multiple checks
Attribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize

## Stacking multiple checks

The attribute is repeatable, so you can stack multiple authorization checks on a single method:

```php
#[Authorize('create', Post::class)]
#[Authorize('update', 'post')]
public function save()
{
    // Both checks must pass...
}
```

---

## Attribute Locked When to use
Attribute Locked | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-locked

## When to use

Use `#[Locked]` when you need to:

* Store model IDs that should never be changed by users
* Preserve authorization-sensitive data throughout component lifecycle
* Protect any public property that acts as a security boundary

> [!tip] Model properties are secure by default
> If you store an Eloquent model in a public property, Livewire automatically ensures the ID isn't tampered with—no `#[Locked]` attribute needed:
> ```php
> <?php // resources/views/components/post/⚡show.blade.php
>
> use Livewire\Component;
> use App\Models\Post;
>
> new class extends Component {
>     public Post $post; // Already protected [tl! highlight]
>
>     public function mount($id)
>     {
>         $this->post = Post::find($id);
>     }
> };
> ```

---

## Upgrade Upgrading To 12.0 From 11.x Database Multi-Schema Database Inspecting
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Multi-Schema Database Inspecting

**Likelihood Of Impact: Low**

The `Schema::getTables()`, `Schema::getViews()`, and `Schema::getTypes()` methods now include the results from all schemas by default. You may pass the `schema` argument to retrieve the result for the given schema only:

```php
// All tables on all schemas...
$tables = Schema::getTables();

// All tables on the 'main' schema...
$tables = Schema::getTables(schema: 'main');

// All tables on the 'main' and 'blog' schemas...
$tables = Schema::getTables(schema: ['main', 'blog']);
```

The `Schema::getTableListing()` method now returns schema-qualified table names by default. You may pass the `schemaQualified` argument to change the behavior as desired:

```php
$tables = Schema::getTableListing();
// ['main.migrations', 'main.users', 'blog.posts']

$tables = Schema::getTableListing(schema: 'main');
// ['main.migrations', 'main.users']

$tables = Schema::getTableListing(schema: 'main', schemaQualified: false);
// ['migrations', 'users']
```

The `db:table` and `db:show` commands now output the results of all schemas on MySQL, MariaDB, and SQLite, just like PostgreSQL and SQL Server.

---

## Wire Model Customizing update timing
Wire Model | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-model

## Customizing update timing

By default, Livewire will only send a network request when an action is performed (like `wire:click` or `wire:submit`), NOT when a `wire:model` input is updated.

This drastically improves the performance of Livewire by reducing network requests and provides a smoother experience for your users.

However, there are occasions where you may want to update the server more frequently for things like real-time validation.

### Live updating

To send property updates to the server as a user types into an input-field, you can append the `.live` modifier to `wire:model`:

```html
<input type="text" wire:model.live="title">
```

#### Customizing the debounce

By default, when using `wire:model.live`, Livewire adds a 150 millisecond debounce to server updates. This means if a user is continually typing, Livewire will wait until the user stops typing for 150 milliseconds before sending a request.

You can customize this timing by appending `.debounce.Xms` after `.live`. Here is an example of changing the debounce to 250 milliseconds:

```html
<input type="text" wire:model.live.debounce.250ms="title">
```

### Updating on "blur" event

The `.blur` modifier delays syncing until the user clicks away from the input:

```html
<input type="text" wire:model.blur="title">
```

To also send a network request on blur, add `.live`:

```html
<input type="text" wire:model.blur.live="title">
```

### Updating on "change" event

The `.change` modifier triggers on the change event, which is useful for select elements:

```html
<select wire:model.change="state">...</select>

<!-- With network request -->
<select wire:model.change.live="state">...</select>
```

### Updating on "enter" key

The `.enter` modifier syncs when the user presses the Enter key:

```html
<input type="text" wire:model.enter="search">

<!-- With network request -->
<input type="text" wire:model.enter.live="search">
```

---

## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Updated `DatabaseTokenRepository` Constructor Signature

**Likelihood Of Impact: Very Low**

The constructor of the `Illuminate\Auth\Passwords\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.

---

## Understanding Nesting Performance implications
Understanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting

## Performance implications

Livewire's independent component architecture can have both positive and negative implications for your application.

An advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.

However, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.

For example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be "reactive". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.

Livewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).

Armed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.

---

## Nesting Slots
Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/nesting

## Slots

Slots allow you to pass Blade content from a parent component into a child component. This is useful when a child component needs to render its own content while also allowing the parent to inject custom content in specific places.

Below is an example of a parent component that renders a list of comments. Each comment is rendered by a `Comment` child component, but the parent passes in a "Remove" button via a slot:

```php
<?php

use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public Post $post;

    #[Computed]
    public function comments()
    {
        return $this->post->comments;
    }

    public function removeComment($id)
    {
        $this->post->comments()->find($id)->delete();
    }
};
?>

<div>
    @foreach ($this->comments as $comment)
        <livewire:comment :$comment :wire:key="$comment->id">
            <button wire:click="removeComment({{ $comment->id }})">
                Remove
            </button>
        </livewire:comment>
    @endforeach
</div>
```

Now that content has been passed to the `Comment` child component, you can render it using the `$slot` variable:

```php
<?php

use Livewire\Component;
use App\Models\Comment;

new class extends Component {
    public Comment $comment;
};
?>

<div>
    <p>{{ $comment->author }}</p>
    <p>{{ $comment->body }}</p>

    {{ $slot }}
</div>
```

When the `Comment` component renders `$slot`, Livewire will inject the content passed from the parent.

It's important to understand that slots are evaluated in the context of the parent component. This means any properties or methods referenced inside the slot belong to the parent, not the child. In the example above, the `removeComment()` method is called on the parent component, not the `Comment` child.

### Named slots

In addition to the default slot, you may also pass multiple named slots into a child component. This is useful when you want to provide content for multiple areas of a child component.

Below is an example of passing both a default slot and a named `actions` slot to the `Comment` component:

```blade
<div>
    @foreach ($this->comments as $comment)
        <livewire:comment :$comment :wire:key="$comment->id">
            <livewire:slot name="actions">
                <button wire:click="removeComment({{ $comment->id }})">
                    Remove
                </button>
            </livewire:slot>

            <span>Posted on {{ $comment->created_at }}</span>
        </livewire:comment>
    @endforeach
</div>
```

You can access named slots in the child component by passing the slot name to the `$slots` variable:

```blade
<div>
    <p>{{ $comment->author }}</p>
    <p>{{ $comment->body }}</p>

    <div class="actions">
        {{ $slots['actions'] }}
    </div>

    <div class="metadata">
        {{ $slot }}
    </div>
</div>
```

### Checking if a slot was provided

You can check if a slot was provided by the parent using the `has()` method on the `$slots` variable. This is helpful when you want to conditionally render content based on whether or not a slot is present:

```blade
<div>
    <p>{{ $comment->author }}</p>
    <p>{{ $comment->body }}</p>

    @if ($slots->has('actions'))
        <div class="actions">
            {{ $slots['actions'] }}
        </div>
    @endif

    {{ $slot }}
</div>
```

_[Truncated]_


Output:
Wall time: 4.6382 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Passport Device Authorization Grant Requesting Tokens\nPassport | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/passport.md\n\n\n### Requesting Tokens\n\n---\n\n## Broadcasting Authorizing Channels Defining Authorization Callbacks Authorization Callback Model Binding\nBroadcasting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/broadcasting.md\n\n\n#### Authorization Callback Model Binding\n\nJust like HTTP routes, channel routes may also take advantage of implicit and explicit [route model binding](/docs/{{version}}/routing#route-model-binding). For example, instead of receiving a string or numeric order ID, you may request an actual `Order` model instance:\n\n```php\nuse App\\Models\\Order;\nuse App\\Models\\User;\n\nBroadcast::channel('orders.{order}', function (User $user, Order $order) {\n    return $user->id === $order->user_id;\n});\n```\n\n> [!WARNING]\n> Unlike HTTP route model binding, channel model binding does not support automatic [implicit model binding scoping](/docs/{{version}}/routing#implicit-model-binding-scoping). However, this is rarely a problem because most channels can be scoped based on a single model's unique, primary key.\n\n---\n\n## Attribute Authorize Learn more\nAttribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize\n\n## Learn more\n\nFor more information on defining abilities and policies, see the [Laravel Authorization documentation](https://laravel.com/docs/authorization).\n\n---\n\n## Verification Introduction Database Preparation\nVerification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md\n\n\n### Database Preparation\n\nNext, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.\n\n---\n\n## Attribute Session Dynamic session keys\nAttribute Session | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-session\n\n## Dynamic session keys\n\nYou can generate keys dynamically using other properties:\n\n```php\n<?php // resources/views/components/post/⚡index.blade.php\n\nuse Livewire\\Attributes\\Session;\nuse Livewire\\Component;\nuse App\\Models\\Author;\n\nnew class extends Component {\n    public Author $author;\n\n    #[Session(key: 'search-{author.id}')] // [tl! highlight]\n    public $search = '';\n};\n```\n\nIf `$author->id` is `4`, the session key becomes `search-4`. This allows different session values per author.\n\n---\n\n## Routing Route Model Binding\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n## Route Model Binding\n\nWhen injecting a model ID to a route or controller action, you will often query the database to retrieve the model that corresponds to that ID. Laravel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user's ID, you can inject the entire `User` model instance that matches the given ID.\n\n---\n\n## Attribute Authorize Argument resolution\nAttribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize\n\n## Argument resolution\n\nThe attribute resolves the object to authorize against in the following order:\n\n* **No argument** — Checks a simple gate that doesn't require a model (e.g., `#[Authorize('view-dashboard')]`).\n* **Class string** — Useful for `create` permissions where no instance exists yet (e.g., `#[Authorize('create', Post::class)]`).\n* **Method parameter** — Resolves the argument from the method's own parameters.\n* **Component property** — Looks for a property on the component matching the argument name (e.g., `public Post $post`).\n\n### Resolving from method parameters\n\nWhen authorizing based on a method parameter, you must type-hint the parameter so Livewire knows which model to resolve:\n\n```php\n<?php // resources/views/components/⚡comment-manager.blade.php\n\nuse Livewire\\Attributes\\Authorize;\nuse Livewire\\Component;\nuse App\\Models\\Comment;\n\nnew class extends Component {\n    #[Authorize('delete', 'comment')] // [tl! highlight]\n    public function deleteComment(Comment $comment) // [tl! highlight]\n    {\n        $comment->delete();\n    }\n};\n```\n\n> [!important]\n> If you resolve a model via a method parameter, a type-hint (e.g., `Comment $comment`) is required. Without it, Livewire cannot determine which model to resolve and the authorization check will fail.\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Database Constructor Signature Changes\n\n**Likelihood Of Impact: Very Low**\n\nIn Laravel 12, several low-level database classes now require an `Illuminate\\Database\\Connection` instance to be provided via their constructors.\n\n**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**\n\n`Illuminate\\Database\\Schema\\Blueprint`\n\nThe constructor of the `Illuminate\\Database\\Schema\\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.\n\n`Illuminate\\Database\\Grammar`\n\nThe constructor of the `Illuminate\\Database\\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:\n\n```php\n// Laravel <= 11.x\n$grammar = new MySqlGrammar;\n$grammar->setConnection($connection);\n\n// Laravel >= 12.x\n$grammar = new MySqlGrammar($connection);\n````\n\nIn addition, the following APIs have been removed or deprecated:\n\n
\n\n- The `Blueprint::getPrefix()` method is deprecated.\n- The `Connection::withTablePrefix()` method has been removed.\n- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.\n- The `Grammar::setConnection()` method has been removed.\n\n
\n\nWhen working with table prefixes, you should now retrieve them directly from the database connection:\n\n```php\n$prefix = $connection->getTablePrefix();\n```\n\nIf you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.\n\n---\n\n## Attribute Authorize When NOT to use\nAttribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize\n\n## When NOT to use\n\n> [!warning]\n> The `#[Authorize]` attribute only protects server-side execution of an action. It does not hide UI elements in your Blade template.\n\nYou should still use Blade's `@can` directives to hide buttons the user isn't allowed to use:\n\n```blade\n@can('update', $post)\n    <button wire:click=\"save\">Save</button>\n@endcan\n```\n\n---\n\n## Session Properties Setting a custom key\nSession Properties | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/session-properties\n\n## Setting a custom key\n\nWhen using `[#Session]`, Livewire will store the property value in the session using a dynamically generated key that consists of the component name combined with the property name.\n\nThis ensures that properties across component instances will use the same session value. It also ensures properties of the same name from different components won't conflict.\n\nIf you want full control over what session key Livewire uses for a given property, you can pass the `key:` parameter:\n\n```php\n<?php\n\nuse Livewire\\Attributes\\Session;\nuse Livewire\\Component;\n\nclass ShowPosts extends Component\n{\n    #[Session(key: 'search')] // [tl! highlight]\n    public $search;\n\n    // ...\n}\n```\n\nWhen Livewire stores and retrieves the value of the `$search` property, it will use the given key: \"search\".\n\nAdditionally, if you want to generate the key dynamically from other properties in your component, you can do so using the following curly brace notation:\n\n```php\n<?php\n\nuse Livewire\\Attributes\\Session;\nuse Livewire\\Component;\nuse App\\Models\\Author;\n\nclass ShowPosts extends Component\n{\n    public Author $author;\n\n    #[Session(key: 'search-{author.id}')] // [tl! highlight]\n    public $search;\n\n    // ...\n}\n```\n\nIn the above example, if the `$author` model's id is \"4\", the session key will become: `search-4`\n\n---\n\n## Wire Transition Customizing animations with CSS\nWire Transition | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-transition\n\n## Customizing animations with CSS\n\nView Transitions are controlled entirely through CSS. You can customize the animation by targeting the view-transition pseudo-elements:\n\n```css\n/* Customize the transition for a specific element */\n::view-transition-old(sidebar) {\n    animation: 300ms ease-out both slide-out;\n}\n\n::view-transition-new(sidebar) {\n    animation: 300ms ease-in both slide-in;\n}\n\n@keyframes slide-out {\n    to { transform: translateX(-100%); }\n}\n\n@keyframes slide-in {\n    from { transform: translateX(100%); }\n}\n```\n\nThe View Transitions API provides three pseudo-elements you can style:\n- `::view-transition-old(name)` — The outgoing element's snapshot\n- `::view-transition-new(name)` — The incoming element's snapshot\n- `::view-transition-group(name)` — Container for both snapshots\n\n---\n\n## Database Database Transactions Handling Deadlocks\nDatabase | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database.md\n\n\n#### Handling Deadlocks\n\nThe `transaction` method accepts an optional second argument which defines the number of times a transaction should be retried when a deadlock occurs. Once these attempts have been exhausted, an exception will be thrown:\n\n```php\nuse Illuminate\\Support\\Facades\\DB;\n\nDB::transaction(function () {\n    DB::update('update users set votes = 1');\n\n    DB::delete('delete from posts');\n}, attempts: 5);\n```\n\n---\n\n## Authorization Authorizing Actions Using Policies\nAuthorization | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/authorization.md\n\n\n## Authorizing Actions Using Policies\n\n---\n\n## Attribute Authorize Basic usage\nAttribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize\n\n## Basic usage\n\nApply the `#[Authorize]` attribute to any action method. Pass the ability name and an optional argument:\n\n```php\n<?php // resources/views/components/post/⚡edit.blade.php\n\nuse Livewire\\Attributes\\Authorize;\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nnew class extends Component {\n    public Post $post;\n\n    #[Authorize('update', 'post')] // [tl! highlight]\n    public function save()\n    {\n        $this->post->save();\n    }\n};\n```\n\n```blade\n<button wire:click=\"save\">\n    Update Post\n</button>\n```\n\nWhen `save()` is called, Livewire automatically checks if the current user is authorized to `update` the `$post` model stored on the component.\n\n---\n\n## Validation Form objects\nValidation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation\n\n## Form objects\n\nAs more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.\n\nBelow is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:\n\n```php\n<?php\n\nnamespace App\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass PostForm extends Form\n{\n    #[Validate('required|min:3')]\n\tpublic $title = '';\n\n    #[Validate('required|min:3')]\n    public $content = '';\n}\n```\n\nThe `PostForm` above can now be defined as a property on the `CreatePost` component:\n\n```php\n<?php\n\nnamespace App\\Livewire;\n\nuse App\\Livewire\\Forms\\PostForm;\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nclass CreatePost extends Component\n{\n    public PostForm $form;\n\n    public function save()\n    {\n\t\tPost::create(\n    \t\t$this->form->all()\n    \t);\n\n\t\treturn redirect()->to('/posts');\n    }\n\n    // ...\n}\n```\n\nAs you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.\n\nAlso, when referencing the property names in the template, you must prepend `form.` to each instance:\n\n```blade\n<form wire:submit=\"save\">\n\t<input type=\"text\" wire:model=\"form.title\">\n    <div>@error('form.title') {{ $message }} @enderror</div>\n\n\t<textarea wire:model=\"form.content\"></textarea>\n    <div>@error('form.content') {{ $message }} @enderror</div>\n\n\t<button type=\"submit\">Save</button>\n</form>\n```\n\nWhen using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:\n\n```php\npublic function save()\n{\n    Post::create(\n        $this->form->validate()\n    );\n\n    return redirect()->to('/posts');\n}\n```\n\nForm objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).\n\n---\n\n## Attribute Authorize Stacking multiple checks\nAttribute Authorize | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-authorize\n\n## Stacking multiple checks\n\nThe attribute is repeatable, so you can stack multiple authorization checks on a single method:\n\n```php\n#[Authorize('create', Post::class)]\n#[Authorize('update', 'post')]\npublic function save()\n{\n    // Both checks must pass...\n}\n```\n\n---\n\n## Attribute Locked When to use\nAttribute Locked | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-locked\n\n## When to use\n\nUse `#[Locked]` when you need to:\n\n* Store model IDs that should never be changed by users\n* Preserve authorization-sensitive data throughout component lifecycle\n* Protect any public property that acts as a security boundary\n\n> [!tip] Model properties are secure by default\n> If you store an Eloquent model in a public property, Livewire automatically ensures the ID isn't tampered with—no `#[Locked]` attribute needed:\n> ```php\n> <?php // resources/views/components/post/⚡show.blade.php\n>\n> use Livewire\\Component;\n> use App\\Models\\Post;\n>\n> new class extends Component {\n>     public Post $post; // Already protected [tl! highlight]\n>\n>     public function mount($id)\n>     {\n>         $this->post = Post::find($id);\n>     }\n> };\n> ```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Database Multi-Schema Database Inspecting\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Multi-Schema Database Inspecting\n\n**Likelihood Of Impact: Low**\n\nThe `Schema::getTables()`, `Schema::getViews()`, and `Schema::getTypes()` methods now include the results from all schemas by default. You may pass the `schema` argument to retrieve the result for the given schema only:\n\n```php\n// All tables on all schemas...\n$tables = Schema::getTables();\n\n// All tables on the 'main' schema...\n$tables = Schema::getTables(schema: 'main');\n\n// All tables on the 'main' and 'blog' schemas...\n$tables = Schema::getTables(schema: ['main', 'blog']);\n```\n\nThe `Schema::getTableListing()` method now returns schema-qualified table names by default. You may pass the `schemaQualified` argument to change the behavior as desired:\n\n```php\n$tables = Schema::getTableListing();\n// ['main.migrations', 'main.users', 'blog.posts']\n\n$tables = Schema::getTableListing(schema: 'main');\n// ['main.migrations', 'main.users']\n\n$tables = Schema::getTableListing(schema: 'main', schemaQualified: false);\n// ['migrations', 'users']\n```\n\nThe `db:table` and `db:show` commands now output the results of all schemas on MySQL, MariaDB, and SQLite, just like PostgreSQL and SQL Server.\n\n---\n\n## Wire Model Customizing update timing\nWire Model | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-model\n\n## Customizing update timing\n\nBy default, Livewire will only send a network request when an action is performed (like `wire:click` or `wire:submit`), NOT when a `wire:model` input is updated.\n\nThis drastically improves the performance of Livewire by reducing network requests and provides a smoother experience for your users.\n\nHowever, there are occasions where you may want to update the server more frequently for things like real-time validation.\n\n### Live updating\n\nTo send property updates to the server as a user types into an input-field, you can append the `.live` modifier to `wire:model`:\n\n```html\n<input type=\"text\" wire:model.live=\"title\">\n```\n\n#### Customizing the debounce\n\nBy default, when using `wire:model.live`, Livewire adds a 150 millisecond debounce to server updates. This means if a user is continually typing, Livewire will wait until the user stops typing for 150 milliseconds before sending a request.\n\nYou can customize this timing by appending `.debounce.Xms` after `.live`. Here is an example of changing the debounce to 250 milliseconds:\n\n```html\n<input type=\"text\" wire:model.live.debounce.250ms=\"title\">\n```\n\n### Updating on \"blur\" event\n\nThe `.blur` modifier delays syncing until the user clicks away from the input:\n\n```html\n<input type=\"text\" wire:model.blur=\"title\">\n```\n\nTo also send a network request on blur, add `.live`:\n\n```html\n<input type=\"text\" wire:model.blur.live=\"title\">\n```\n\n### Updating on \"change\" event\n\nThe `.change` modifier triggers on the change event, which is useful for select elements:\n\n```html\n<select wire:model.change=\"state\">...</select>\n\n<!-- With network request -->\n<select wire:model.change.live=\"state\">...</select>\n```\n\n### Updating on \"enter\" key\n\nThe `.enter` modifier syncs when the user presses the Enter key:\n\n```html\n<input type=\"text\" wire:model.enter=\"search\">\n\n<!-- With network request -->\n<input type=\"text\" wire:model.enter.live=\"search\">\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Updated `DatabaseTokenRepository` Constructor Signature\n\n**Likelihood Of Impact: Very Low**\n\nThe constructor of the `Illuminate\\Auth\\Passwords\\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.\n\n---\n\n## Understanding Nesting Performance implications\nUnderstanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting\n\n## Performance implications\n\nLivewire's independent component architecture can have both positive and negative implications for your application.\n\nAn advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.\n\nHowever, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.\n\nFor example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be \"reactive\". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.\n\nLivewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).\n\nArmed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.\n\n---\n\n## Nesting Slots\nNesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/nesting\n\n## Slots\n\nSlots allow you to pass Blade content from a parent component into a child component. This is useful when a child component needs to render its own content while also allowing the parent to inject custom content in specific places.\n\nBelow is an example of a parent component that renders a list of comments. Each comment is rendered by a `Comment` child component, but the parent passes in a \"Remove\" button via a slot:\n\n```php\n<?php\n\nuse Livewire\\Attributes\\Computed;\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nnew class extends Component {\n    public Post $post;\n\n    #[Computed]\n    public function comments()\n    {\n        return $this->post->comments;\n    }\n\n    public function removeComment($id)\n    {\n        $this->post->comments()->find($id)->delete();\n    }\n};\n?>\n\n<div>\n    @foreach ($this->comments as $comment)\n        <livewire:comment :$comment :wire:key=\"$comment->id\">\n            <button wire:click=\"removeComment({{ $comment->id }})\">\n                Remove\n            </button>\n        </livewire:comment>\n    @endforeach\n</div>\n```\n\nNow that content has been passed to the `Comment` child component, you can render it using the `$slot` variable:\n\n```php\n<?php\n\nuse Livewire\\Component;\nuse App\\Models\\Comment;\n\nnew class extends Component {\n    public Comment $comment;\n};\n?>\n\n<div>\n    <p>{{ $comment->author }}</p>\n    <p>{{ $comment->body }}</p>\n\n    {{ $slot }}\n</div>\n```\n\nWhen the `Comment` component renders `$slot`, Livewire will inject the content passed from the parent.\n\nIt's important to understand that slots are evaluated in the context of the parent component. This means any properties or methods referenced inside the slot belong to the parent, not the child. In the example above, the `removeComment()` method is called on the parent component, not the `Comment` child.\n\n### Named slots\n\nIn addition to the default slot, you may also pass multiple named slots into a child component. This is useful when you want to provide content for multiple areas of a child component.\n\nBelow is an example of passing both a default slot and a named `actions` slot to the `Comment` component:\n\n```blade\n<div>\n    @foreach ($this->comments as $comment)\n        <livewire:comment :$comment :wire:key=\"$comment->id\">\n            <livewire:slot name=\"actions\">\n                <button wire:click=\"removeComment({{ $comment->id }})\">\n                    Remove\n                </button>\n            </livewire:slot>\n\n            <span>Posted on {{ $comment->created_at }}</span>\n        </livewire:comment>\n    @endforeach\n</div>\n```\n\nYou can access named slots in the child component by passing the slot name to the `$slots` variable:\n\n```blade\n<div>\n    <p>{{ $comment->author }}</p>\n    <p>{{ $comment->body }}</p>\n\n    <div class=\"actions\">\n        {{ $slots['actions'] }}\n    </div>\n\n    <div class=\"metadata\">\n        {{ $slot }}\n    </div>\n</div>\n```\n\n### Checking if a slot was provided\n\nYou can check if a slot was provided by the parent using the `has()` method on the `$slots` variable. This is helpful when you want to conditionally render content based on whether or not a slot is present:\n\n```blade\n<div>\n    <p>{{ $comment->author }}</p>\n    <p>{{ $comment->body }}</p>\n\n    @if ($slots->has('actions'))\n        <div class=\"actions\">\n            {{ $slots['actions'] }}\n        </div>\n    @endif\n\n    {{ $slot }}\n</div>\n```\n\n_[Truncated]_"}]


 M app/Http/Controllers/Api/Admin/V1/PageController.php
 M app/Http/Controllers/Api/Admin/V1/ProductController.php
 M app/Http/Middleware/AuthenticateAdminApi.php
 M app/Http/Middleware/AuthenticatePlatformApi.php
 M app/Listeners/DispatchWebhooks.php
 M app/Livewire/Admin/Developers/Index.php
 M app/Livewire/Admin/Pages/Form.php
 M app/Livewire/Admin/Products/Form.php
 M app/Observers/ProductObserver.php
 M app/Providers/AppServiceProvider.php
 M app/Services/OrderService.php
 M app/Services/WebhookService.php
 M config/auth.php
 M resources/views/livewire/admin/developers/index.blade.php
 M routes/api.php
 M tests/Feature/Webhooks/WebhookDeliveryTest.php
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T13-33-19-511Z.log
?? .playwright-mcp/console-2026-05-04T18-29-51-216Z.log
?? app/Actions/SanitizeHtml.php
?? app/Events/CheckoutCompleted.php
?? app/Events/ProductCreated.php
?? app/Events/ProductDeleted.php
?? app/Events/ProductUpdated.php
?? app/Models/PersonalAccessToken.php
?? tests/Feature/Security/


Output:
Chunk ID: c94af8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 281
Output:
 M app/Http/Controllers/Api/Admin/V1/PageController.php
 M app/Http/Controllers/Api/Admin/V1/ProductController.php
 M app/Http/Middleware/AuthenticateAdminApi.php
 M app/Http/Middleware/AuthenticatePlatformApi.php
 M app/Listeners/DispatchWebhooks.php
 M app/Livewire/Admin/Developers/Index.php
 M app/Livewire/Admin/Pages/Form.php
 M app/Livewire/Admin/Products/Form.php
 M app/Observers/ProductObserver.php
 M app/Providers/AppServiceProvider.php
 M app/Services/OrderService.php
 M app/Services/WebhookService.php
 M config/auth.php
 M resources/views/livewire/admin/developers/index.blade.php
 M routes/api.php
 M tests/Feature/Webhooks/WebhookDeliveryTest.php
?? .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
?? .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
?? .playwright-mcp/console-2026-05-04T13-33-19-511Z.log
?? .playwright-mcp/console-2026-05-04T18-29-51-216Z.log
?? app/Actions/SanitizeHtml.php
?? app/Events/CheckoutCompleted.php
?? app/Events/ProductCreated.php
?? app/Events/ProductDeleted.php
?? app/Events/ProductUpdated.php
?? app/Models/PersonalAccessToken.php
?? tests/Feature/Security/


The docs reinforce the exact fixes already in progress: policy checks inside Livewire actions, route model binding where possible, and transactional locking for concurrency-sensitive checkout/order paths. I’m going to inspect the current dirty files and the sibling tests before editing so the patch lands cleanly on top of the partial worker output.


In /Users/fabianwesner/Herd/shop, inspect the current codebase for the remaining critical/high blockers from the completion audit. Do not edit files. Focus on: (1) admin Livewire mutation components missing policy authorization, especially products, collections, customers, orders; (2) checkout route/service idempotency gaps against specs requiring /checkout/{checkoutId} and /checkout/{checkoutId}/confirmation; (3) tests likely needing update after the new auth:sanctum-compatible bearer token guard. Return concise findings with file paths and line references plus recommended minimal changes. Avoid duplicating unrelated QA findings.


Prompt: In /Users/fabianwesner/Herd/shop, inspect the current codebase for the remaining critical/high blockers from the completion audit. Do not edit files. Focus on: (1) admin Livewire mutation components missing policy authorization, especially products, collections, customers, orders; (2) checkout route/service idempotency gaps against specs requiring /checkout/{checkoutId} and /checkout/{checkoutId}/confirmation; (3) tests likely needing update after the new auth:sanctum-compatible bearer token guard. Return concise findings with file paths and line references plus recommended minimal changes. Avoid duplicating unrelated QA findings.


rg -n "class (Show|Form|Index)|function (save|delete|refund|fulfill|archive|process|create|update|mark|addProduct|sync|mount|checkout|placeOrder)|Route::livewire|checkout" app/Livewire app/Services routes/web.php routes/api.php tests/Feature/Api tests/Feature/Checkout tests/Feature/Security


sed -n '1,260p' app/Livewire/Admin/Orders/Show.php


sed -n '1,320p' app/Livewire/Admin/Products/Form.php


sed -n '1,340p' app/Livewire/Admin/Collections/Form.php


sed -n '1,300p' app/Services/CheckoutService.php


sed -n '1,360p' app/Services/OrderService.php


sed -n '1,260p' app/Livewire/Admin/Orders/Show.php

<?php

namespace App\Livewire\Admin\Orders;

use App\Enums\RefundStatus;
use App\Exceptions\InvalidFulfillmentOperationException;
use App\Exceptions\InvalidOrderOperationException;
use App\Exceptions\InvalidRefundOperationException;
use App\Models\Fulfillment;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Refund;
use App\Models\Store;
use App\Services\FulfillmentService;
use App\Services\OrderService;
use App\Services\RefundService;
use App\Support\Money;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Show extends Component
{
    #[Locked]
    public int $storeId;

    #[Locked]
    public int $orderId;

    public string $refundAmount = '';

    public string $refundReason = '';

    /**
     * @var array<int, int>
     */
    public array $fulfillmentLineQuantities = [];

    public string $trackingCompany = '';

    public string $trackingNumber = '';

    public string $trackingUrl = '';

    public string $actionMessage = '';

    public function mount(Order $order): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $order = Order::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereKey($order->getKey())
            ->first();

        abort_unless($order instanceof Order, 404);

        $this->storeId = $store->getKey();
        $this->orderId = $order->getKey();
        $this->resetFulfillmentLineQuantities($this->order());
    }

    public function confirmBankTransferPayment(OrderService $orders): void
    {
        try {
            $orders->confirmBankTransferPayment($this->order());
            $this->resetFulfillmentLineQuantities($this->order());
            $this->actionMessage = __('Payment confirmed');
            $this->dispatch('toast', type: 'success', message: __('Payment confirmed'));
        } catch (InvalidOrderOperationException $exception) {
            throw ValidationException::withMessages([
                'orderAction' => $exception->getMessage(),
            ]);
        }
    }

    public function processRefund(RefundService $refunds): void
    {
        $this->validate([
            'refundAmount' => ['nullable', 'numeric', 'min:0.01'],
            'refundReason' => ['nullable', 'string', 'max:500'],
        ]);

        $request = [
            'reason' => $this->refundReason !== '' ? $this->refundReason : null,
        ];

        if ($this->refundAmount !== '') {
            $request['amount'] = Money::fromDecimalString($this->refundAmount);
        }

        try {
            $refunds->process($this->order(), $request);
            $this->refundAmount = '';
            $this->refundReason = '';
            $this->actionMessage = __('Refund processed');
            $this->modal('refund-order')->close();
            $this->dispatch('toast', type: 'success', message: __('Refund processed'));
        } catch (InvalidRefundOperationException $exception) {
            throw ValidationException::withMessages([
                'refundAmount' => $exception->getMessage(),
            ]);
        }
    }

    public function createFulfillment(FulfillmentService $fulfillments): void
    {
        $this->validate([
            'fulfillmentLineQuantities' => ['array'],
            'trackingCompany' => ['nullable', 'string', 'max:255'],
            'trackingNumber' => ['nullable', 'string', 'max:255'],
            'trackingUrl' => ['nullable', 'url', 'max:2048'],
        ]);

        $lines = collect($this->fulfillmentLineQuantities)
            ->mapWithKeys(fn (mixed $quantity, int|string $lineId): array => [(int) $lineId => (int) $quantity])
            ->filter(fn (int $quantity): bool => $quantity > 0)
            ->all();

        if ($lines === []) {
            throw ValidationException::withMessages([
                'fulfillment' => __('At least one fulfillment line is required.'),
            ]);
        }

        try {
            $fulfillments->create($this->order(), $lines, [
                'tracking_company' => $this->trackingCompany !== '' ? $this->trackingCompany : null,
                'tracking_number' => $this->trackingNumber !== '' ? $this->trackingNumber : null,
                'tracking_url' => $this->trackingUrl !== '' ? $this->trackingUrl : null,
            ]);

            $this->trackingCompany = '';
            $this->trackingNumber = '';
            $this->trackingUrl = '';
            $this->resetFulfillmentLineQuantities($this->order());
            $this->actionMessage = __('Fulfillment created');
            $this->modal('fulfillment-order')->close();
            $this->dispatch('toast', type: 'success', message: __('Fulfillment created'));
        } catch (InvalidFulfillmentOperationException $exception) {
            throw ValidationException::withMessages([
                'fulfillment' => $exception->getMessage(),
            ]);
        }
    }

    public function markFulfillmentShipped(int $fulfillmentId, FulfillmentService $fulfillments): void
    {
        try {
            $fulfillments->markShipped($this->fulfillment($fulfillmentId));
            $this->actionMessage = __('Fulfillment marked as shipped');
            $this->dispatch('toast', type: 'success', message: __('Fulfillment marked as shipped'));
        } catch (InvalidFulfillmentOperationException $exception) {
            throw ValidationException::withMessages([
                'fulfillment' => $exception->getMessage(),
            ]);
        }
    }

    public function markFulfillmentDelivered(int $fulfillmentId, FulfillmentService $fulfillments): void
    {
        try {
            $fulfillments->markDelivered($this->fulfillment($fulfillmentId));
            $this->actionMessage = __('Fulfillment marked as delivered');
            $this->dispatch('toast', type: 'success', message: __('Fulfillment marked as delivered'));
        } catch (InvalidFulfillmentOperationException $exception) {
            throw ValidationException::withMessages([
                'fulfillment' => $exception->getMessage(),
            ]);
        }
    }

    public function render(): mixed
    {
        $order = $this->order();

        return view('livewire.admin.orders.show', [
            'order' => $order,
            'remainingFulfillmentQuantities' => $this->remainingFulfillmentQuantities($order),
            'refundableAmount' => $this->refundableAmount($order),
        ])->layout('layouts.app', [
            'title' => $order->order_number,
        ]);
    }

    private function order(): Order
    {
        return Order::withoutGlobalScopes()
            ->with([
                'customer',
                'lines.fulfillmentLines',
                'payments',
                'refunds',
                'fulfillments.lines.orderLine',
            ])
            ->where('store_id', $this->storeId)
            ->whereKey($this->orderId)
            ->firstOrFail();
    }

    private function fulfillment(int $fulfillmentId): Fulfillment
    {
        $fulfillment = $this->order()
            ->fulfillments
            ->firstWhere('id', $fulfillmentId);

        abort_unless($fulfillment instanceof Fulfillment, 404);

        return $fulfillment;
    }

    /**
     * @return array<int, int>
     */
    private function remainingFulfillmentQuantities(Order $order): array
    {
        return $order->lines
            ->mapWithKeys(function (OrderLine $line): array {
                $fulfilled = $line->fulfillmentLines->sum('quantity');

                return [$line->getKey() => max(0, $line->quantity - $fulfilled)];
            })
            ->all();
    }

    private function resetFulfillmentLineQuantities(Order $order): void
    {
        $this->fulfillmentLineQuantities = $this->remainingFulfillmentQuantities($order);
    }

    private function refundableAmount(Order $order): int
    {
        $refunded = $order->refunds
            ->reject(fn (Refund $refund): bool => $refund->status === RefundStatus::Failed)
            ->sum('amount');

        return max(0, $order->total_amount - $refunded);
    }
}


rg -n "class (Show|Form|Index)|function (save|delete|refund|fulfill|archive|process|create|update|mark|addProduct|sync|mount|checkout|placeOrder)|Route::livewire|checkout" app/Livewire app/Services routes/web.php routes/api.php tests/Feature/Api tests/Feature/Checkout tests/Feature/Security

rg -n 'class (Show|Form|Index)|function (save|delete|refund|fulfill|archive|process|create|update|mark|addProduct|sync|mount|checkout|placeOrder)|Route::livewire|checkout' app/Livewire app/Services routes/web.php routes/api.php tests/Feature/Api tests/Feature/Checkout tests/Feature/Security

Question: class (Show|Form|Index)|function (save|delete|refund|fulfill|archive|process|create|update|mark|addProduct|sync|mount|checkout|placeOrder)|Route::livewire|checkout

app/Services/VariantMatrixService.php:167:    private function createVariantFromTemplate(Product $product, ?ProductVariant $template, int $position): ProductVariant
tests/Feature/Api/StorefrontCheckoutApiTest.php:64:test('storefront checkout api progresses through address shipping discount removal and payment selection', function (): void {
tests/Feature/Api/StorefrontCheckoutApiTest.php:80:    $checkoutResponse = $api()
tests/Feature/Api/StorefrontCheckoutApiTest.php:81:        ->postJson('/api/storefront/v1/checkouts', [
tests/Feature/Api/StorefrontCheckoutApiTest.php:86:    $checkoutResponse
tests/Feature/Api/StorefrontCheckoutApiTest.php:93:    $checkoutId = $checkoutResponse['data']['id'];
tests/Feature/Api/StorefrontCheckoutApiTest.php:96:        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
tests/Feature/Api/StorefrontCheckoutApiTest.php:109:        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
tests/Feature/Api/StorefrontCheckoutApiTest.php:118:        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'SAVE10'])
tests/Feature/Api/StorefrontCheckoutApiTest.php:124:        ->deleteJson("/api/storefront/v1/checkouts/{$checkoutId}/discount")
tests/Feature/Api/StorefrontCheckoutApiTest.php:130:        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/payment-method", [
tests/Feature/Api/StorefrontCheckoutApiTest.php:137:    $checkout = Checkout::withoutGlobalScopes()->findOrFail($checkoutId);
tests/Feature/Api/StorefrontCheckoutApiTest.php:140:    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
tests/Feature/Api/StorefrontCheckoutApiTest.php:144:test('storefront checkout api rejects invalid addresses shipping methods and discounts', function (): void {
tests/Feature/Api/StorefrontCheckoutApiTest.php:160:    $checkoutId = $api()
tests/Feature/Api/StorefrontCheckoutApiTest.php:161:        ->postJson('/api/storefront/v1/checkouts', [
tests/Feature/Api/StorefrontCheckoutApiTest.php:168:        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
tests/Feature/Api/StorefrontCheckoutApiTest.php:175:        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/address", [
tests/Feature/Api/StorefrontCheckoutApiTest.php:185:        ->putJson("/api/storefront/v1/checkouts/{$checkoutId}/shipping-method", [
tests/Feature/Api/StorefrontCheckoutApiTest.php:191:        ->postJson("/api/storefront/v1/checkouts/{$checkoutId}/apply-discount", ['code' => 'NOTREAL'])
routes/api.php:54:        Route::middleware('throttle:checkout')->group(function (): void {
routes/api.php:55:            Route::post('checkouts', [CheckoutController::class, 'store'])->name('checkouts.store');
routes/api.php:56:            Route::get('checkouts/{checkout}', [CheckoutController::class, 'show'])->name('checkouts.show');
routes/api.php:57:            Route::put('checkouts/{checkout}/address', [CheckoutController::class, 'address'])->name('checkouts.address');
routes/api.php:58:            Route::put('checkouts/{checkout}/shipping-method', [CheckoutController::class, 'shippingMethod'])->name('checkouts.shipping-method');
routes/api.php:59:            Route::post('checkouts/{checkout}/apply-discount', [CheckoutController::class, 'applyDiscount'])->name('checkouts.apply-discount');
routes/api.php:60:            Route::delete('checkouts/{checkout}/discount', [CheckoutController::class, 'destroyDiscount'])->name('checkouts.discount.destroy');
routes/api.php:61:            Route::put('checkouts/{checkout}/payment-method', [CheckoutController::class, 'paymentMethod'])->name('checkouts.payment-method');
routes/api.php:62:            Route::post('checkouts/{checkout}/pay', [CheckoutController::class, 'pay'])->name('checkouts.pay');
app/Services/SearchService.php:167:    public function syncProduct(Product $product): void
app/Livewire/Admin/Developers/Index.php:17:class Index extends Component
app/Livewire/Admin/Developers/Index.php:47:    public function mount(): void
app/Livewire/Admin/Developers/Index.php:99:    public function saveWebhook(WebhookService $webhooks): void
app/Livewire/Admin/Developers/Index.php:130:    public function deleteWebhook(int $webhookId): void
tests/Feature/Checkout/PricingServicesTest.php:107:    $checkout = pricingCheckout($store, $variant);
tests/Feature/Checkout/PricingServicesTest.php:108:    $checkout->forceFill(['shipping_method_id' => $rate->getKey()])->save();
tests/Feature/Checkout/PricingServicesTest.php:110:    $result = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkout/PricingServicesTest.php:116:        ->and($checkout->refresh()->totals_json['total'])->toBe(6544);
tests/Feature/Checkout/PricingServicesTest.php:137:    $checkout = pricingCheckout($store, $variant);
tests/Feature/Checkout/PricingServicesTest.php:138:    $checkout->forceFill([
tests/Feature/Checkout/PricingServicesTest.php:143:    $discounted = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkou…48162 chars truncated…         app(CheckoutService::class)->setShippingMethod($checkout, null);
app/Livewire/Storefront/Checkout/Show.php:141:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:143:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:154:            app(CheckoutService::class)->setShippingMethod($checkout, $this->selectedShippingRateId);
app/Livewire/Storefront/Checkout/Show.php:165:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:167:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:171:        $checkout->forceFill([
app/Livewire/Storefront/Checkout/Show.php:176:            app(PricingEngine::class)->calculate($checkout);
app/Livewire/Storefront/Checkout/Show.php:179:            $checkout->forceFill(['discount_code' => null])->save();
app/Livewire/Storefront/Checkout/Show.php:181:            app(PricingEngine::class)->calculate($checkout);
app/Livewire/Storefront/Checkout/Show.php:195:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:197:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:202:            app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
app/Livewire/Storefront/Checkout/Show.php:211:    public function placeOrder(): void
app/Livewire/Storefront/Checkout/Show.php:229:        $checkout = $this->checkout();
app/Livewire/Storefront/Checkout/Show.php:231:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:236:            if ($checkout->status !== CheckoutStatus::PaymentSelected) {
app/Livewire/Storefront/Checkout/Show.php:237:                $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
app/Livewire/Storefront/Checkout/Show.php:240:            $order = app(CheckoutService::class)->completeCheckout($checkout, [
app/Livewire/Storefront/Checkout/Show.php:252:            $this->redirectRoute('checkout.confirmation', ['order' => $order], navigate: true);
app/Livewire/Storefront/Checkout/Show.php:288:    public function checkout(): ?Checkout
app/Livewire/Storefront/Checkout/Show.php:296:        $checkout = Checkout::withoutGlobalScopes()
app/Livewire/Storefront/Checkout/Show.php:302:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:303:            $checkout = app(CheckoutService::class)->createFromCart($cart, $this->customer());
app/Livewire/Storefront/Checkout/Show.php:306:        $checkout = $this->syncSessionDiscount($checkout);
app/Livewire/Storefront/Checkout/Show.php:308:        return $checkout->load(['cart.lines.variant.product', 'cart.lines.variant.optionValues.option']);
app/Livewire/Storefront/Checkout/Show.php:373:        return $this->checkout()?->totals_json ?? [
app/Livewire/Storefront/Checkout/Show.php:385:        return view('livewire.storefront.checkout.show', [
app/Livewire/Storefront/Checkout/Show.php:387:            'checkout' => $this->checkout(),
app/Livewire/Storefront/Checkout/Show.php:406:    private function fillFromCheckout(?Checkout $checkout): void
app/Livewire/Storefront/Checkout/Show.php:408:        if (! $checkout instanceof Checkout) {
app/Livewire/Storefront/Checkout/Show.php:412:        $this->email = $checkout->email ?: $this->email;
app/Livewire/Storefront/Checkout/Show.php:413:        $this->shippingAddress = array_replace($this->shippingAddress, $checkout->shipping_address_json ?? []);
app/Livewire/Storefront/Checkout/Show.php:414:        $this->billingAddress = array_replace($this->billingAddress, $checkout->billing_address_json ?? []);
app/Livewire/Storefront/Checkout/Show.php:415:        $this->selectedShippingRateId = $checkout->shipping_method_id;
app/Livewire/Storefront/Checkout/Show.php:416:        $this->discountCode = (string) ($checkout->discount_code ?? '');
app/Livewire/Storefront/Checkout/Show.php:417:        $this->paymentMethod = (string) ($checkout->payment_method ?? $this->paymentMethod);
app/Livewire/Storefront/Checkout/Show.php:418:        $this->step = match ($checkout->status) {
app/Livewire/Storefront/Checkout/Show.php:427:    private function syncSessionDiscount(Checkout $checkout): Checkout
app/Livewire/Storefront/Checkout/Show.php:431:        if ($code === '' || $checkout->discount_code !== null) {
app/Livewire/Storefront/Checkout/Show.php:432:            return $checkout;
app/Livewire/Storefront/Checkout/Show.php:435:        $checkout->forceFill(['discount_code' => $code])->save();
app/Livewire/Storefront/Checkout/Show.php:438:            app(PricingEngine::class)->calculate($checkout);
app/Livewire/Storefront/Checkout/Show.php:440:            $checkout->forceFill(['discount_code' => null])->save();
app/Livewire/Storefront/Checkout/Show.php:442:            app(PricingEngine::class)->calculate($checkout);
app/Livewire/Storefront/Checkout/Show.php:445:        return $checkout->refresh();