upgrade: add Laravel 13 config changes and update Boost skills
Add serializable_classes to cache config and serialization to session config for security hardening. Update Boost skills to latest versions.
This commit is contained in:
parent
56ffe2da90
commit
05eedd0761
15 changed files with 1855 additions and 203 deletions
413
.claude/skills/ai-sdk-development/SKILL.md
Normal file
413
.claude/skills/ai-sdk-development/SKILL.md
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
---
|
||||
name: ai-sdk-development
|
||||
description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter).
|
||||
---
|
||||
|
||||
# Developing with the Laravel AI SDK
|
||||
|
||||
The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers.
|
||||
|
||||
## Searching the Documentation
|
||||
|
||||
This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth.
|
||||
|
||||
- Use broad, simple queries that match the documentation section headings below.
|
||||
- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`.
|
||||
- Run multiple queries at once — the most relevant results are returned first.
|
||||
|
||||
### Documentation Sections
|
||||
|
||||
Use these section headings as query terms for accurate results:
|
||||
|
||||
- Introduction, Installation, Configuration, Provider Support
|
||||
- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration
|
||||
- Images
|
||||
- Audio (TTS)
|
||||
- Transcription (STT)
|
||||
- Embeddings: Querying Embeddings, Caching Embeddings
|
||||
- Reranking
|
||||
- Files
|
||||
- Vector Stores: Adding Files to Stores
|
||||
- Failover
|
||||
- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores
|
||||
- Events
|
||||
|
||||
## Decision Workflow
|
||||
|
||||
Determine the right entry point before writing code:
|
||||
|
||||
Text generation or chat? → Agent class with `Promptable` trait
|
||||
Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic)
|
||||
Structured JSON output? → Agent + `HasStructuredOutput` interface
|
||||
Image generation? → `Image::of()->generate()`
|
||||
Audio synthesis? → `Audio::of()->generate()`
|
||||
Transcription? → `Transcription::fromPath()->generate()`
|
||||
Embeddings? → `Embeddings::for()->generate()`
|
||||
Reranking? → `Reranking::of()->rerank()`
|
||||
File storage? → `Document::fromPath()->put()`
|
||||
Vector stores? → `Stores::create()`
|
||||
|
||||
## Basic Usage Examples
|
||||
|
||||
### Agents
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class SalesCoach implements Agent
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function instructions(): string
|
||||
{
|
||||
return 'You are a sales coach.';
|
||||
}
|
||||
}
|
||||
|
||||
// Prompting
|
||||
$response = (new SalesCoach)->prompt('Analyze this transcript...');
|
||||
echo $response->text;
|
||||
|
||||
// Streaming (returns SSE response from a route)
|
||||
return (new SalesCoach)->stream('Analyze this transcript...');
|
||||
|
||||
// Queueing
|
||||
(new SalesCoach)->queue('Analyze this transcript...')
|
||||
->then(fn ($response) => /* ... */);
|
||||
|
||||
// Anonymous agents
|
||||
use function Laravel\Ai\{agent};
|
||||
|
||||
$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello');
|
||||
```
|
||||
|
||||
### Conversation Context
|
||||
|
||||
Manual conversation history via the `Conversational` interface:
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Messages\Message;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class SalesCoach implements Agent, Conversational
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function __construct(public User $user) {}
|
||||
|
||||
public function instructions(): string { return 'You are a sales coach.'; }
|
||||
|
||||
public function messages(): iterable
|
||||
{
|
||||
return History::where('user_id', $this->user->id)
|
||||
->latest()->limit(50)->get()->reverse()
|
||||
->map(fn ($m) => new Message($m->role, $m->content))
|
||||
->all();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Automatic conversation persistence via the `RemembersConversations` trait:
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class SalesCoach implements Agent, Conversational
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
|
||||
public function instructions(): string { return 'You are a sales coach.'; }
|
||||
}
|
||||
|
||||
// Start a new conversation
|
||||
$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
|
||||
$conversationId = $response->conversationId;
|
||||
|
||||
// Continue an existing conversation
|
||||
$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.');
|
||||
```
|
||||
|
||||
### Structured Output
|
||||
|
||||
```php
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\HasStructuredOutput;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
class Reviewer implements Agent, HasStructuredOutput
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function instructions(): string { return 'Review and score content.'; }
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'feedback' => $schema->string()->required(),
|
||||
'score' => $schema->integer()->min(1)->max(10)->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$response = (new Reviewer)->prompt('Review this...');
|
||||
echo $response['score']; // Access like an array
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Image;
|
||||
|
||||
$image = Image::of('A sunset over mountains')
|
||||
->landscape()
|
||||
->quality('high')
|
||||
->generate();
|
||||
|
||||
$path = $image->store(); // Store to default disk
|
||||
```
|
||||
|
||||
### Audio
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Audio;
|
||||
|
||||
$audio = Audio::of('Hello from Laravel.')
|
||||
->female()
|
||||
->instructions('Speak warmly')
|
||||
->generate();
|
||||
|
||||
$path = $audio->store();
|
||||
```
|
||||
|
||||
### Transcription
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Transcription;
|
||||
|
||||
$transcript = Transcription::fromStorage('audio.mp3')
|
||||
->diarize()
|
||||
->generate();
|
||||
|
||||
echo (string) $transcript;
|
||||
```
|
||||
|
||||
### Embeddings
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Embeddings;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
$response = Embeddings::for(['Text one', 'Text two'])
|
||||
->dimensions(1536)
|
||||
->cache()
|
||||
->generate();
|
||||
|
||||
// Single string via Stringable
|
||||
$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings();
|
||||
```
|
||||
|
||||
### Reranking
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Reranking;
|
||||
|
||||
$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.'])
|
||||
->limit(5)
|
||||
->rerank('PHP frameworks');
|
||||
|
||||
$response->first()->document; // "Laravel is PHP."
|
||||
```
|
||||
|
||||
### Files and Vector Stores
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Files\Document;
|
||||
use Laravel\Ai\Stores;
|
||||
|
||||
// Store a file with the provider
|
||||
$file = Document::fromPath('/path/to/doc.pdf')->put();
|
||||
|
||||
// Create a vector store and add files
|
||||
$store = Stores::create('Knowledge Base');
|
||||
$store->add($file->id);
|
||||
$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step
|
||||
```
|
||||
|
||||
## Agent Configuration
|
||||
|
||||
### PHP Attributes
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout};
|
||||
|
||||
#[Provider('anthropic')]
|
||||
#[MaxSteps(10)]
|
||||
#[MaxTokens(4096)]
|
||||
#[Temperature(0.7)]
|
||||
#[Timeout(120)]
|
||||
class MyAgent implements Agent
|
||||
{
|
||||
use Promptable;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection.
|
||||
|
||||
### Tools
|
||||
|
||||
Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`:
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
|
||||
class MyAgent implements Agent, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [new MyCustomTool];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Provider Tools
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch};
|
||||
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [
|
||||
(new WebSearch)->max(5)->allow(['laravel.com']),
|
||||
new WebFetch,
|
||||
new FileSearch(stores: ['store_id']),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Conversation Memory
|
||||
|
||||
```php
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
|
||||
class ChatBot implements Agent, Conversational
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
// ...
|
||||
}
|
||||
|
||||
$response = (new ChatBot)->forUser($user)->prompt('Hello!');
|
||||
$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...');
|
||||
```
|
||||
|
||||
### Failover
|
||||
|
||||
```php
|
||||
$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']);
|
||||
```
|
||||
|
||||
## Testing and Faking
|
||||
|
||||
Each capability supports `fake()` with assertions:
|
||||
|
||||
```php
|
||||
use App\Ai\Agents\SalesCoach;
|
||||
use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores};
|
||||
|
||||
// Agents
|
||||
SalesCoach::fake(['Response 1', 'Response 2']);
|
||||
SalesCoach::assertPrompted('query');
|
||||
SalesCoach::assertNotPrompted('query');
|
||||
SalesCoach::assertNeverPrompted();
|
||||
SalesCoach::fake()->preventStrayPrompts();
|
||||
|
||||
// Images
|
||||
Image::fake();
|
||||
Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset'));
|
||||
Image::assertNothingGenerated();
|
||||
|
||||
// Audio
|
||||
Audio::fake();
|
||||
Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello'));
|
||||
|
||||
// Transcription
|
||||
Transcription::fake(['Transcribed text.']);
|
||||
Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized());
|
||||
|
||||
// Embeddings
|
||||
Embeddings::fake();
|
||||
Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel'));
|
||||
|
||||
// Reranking
|
||||
Reranking::fake();
|
||||
Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP'));
|
||||
|
||||
// Files
|
||||
Files::fake();
|
||||
Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain');
|
||||
|
||||
// Stores
|
||||
Stores::fake();
|
||||
Stores::assertCreated('Knowledge Base');
|
||||
$store = Stores::get('id');
|
||||
$store->assertAdded('file_id');
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
- Namespace: `Laravel\Ai\`
|
||||
- Package: `composer require laravel/ai`
|
||||
- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait
|
||||
- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational`
|
||||
- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores`
|
||||
- Artisan commands: `php artisan make:agent`, `php artisan make:tool`
|
||||
- Global helper: `agent()` for anonymous agents
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Wrong Namespace
|
||||
|
||||
The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`.
|
||||
|
||||
```php
|
||||
// Correct
|
||||
use Laravel\Ai\Image;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Promptable;
|
||||
|
||||
// Wrong — these do not exist
|
||||
use Illuminate\Ai\Image;
|
||||
use Laravel\AI\Agent;
|
||||
```
|
||||
|
||||
### Unsupported Provider Capability
|
||||
|
||||
Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below.
|
||||
|
||||
### Never Use Prism Directly
|
||||
|
||||
Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally.
|
||||
|
||||
## Provider Support
|
||||
|
||||
| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores |
|
||||
| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ |
|
||||
| OpenAI | Y | Y | Y | Y | Y | - | Y | Y |
|
||||
| Anthropic | Y | - | - | - | - | - | Y | - |
|
||||
| Gemini | Y | Y | - | - | Y | - | Y | Y |
|
||||
| xAI | Y | Y | - | - | - | - | - | - |
|
||||
| Groq | Y | - | - | - | - | - | - | - |
|
||||
| OpenRouter | Y | - | - | - | - | - | - | - |
|
||||
| ElevenLabs | - | - | Y | Y | - | - | - | - |
|
||||
| Cohere | - | - | - | - | Y | Y | - | - |
|
||||
| Jina | - | - | - | - | Y | Y | - | - |
|
||||
|
|
@ -33,9 +33,9 @@ ## Basic Usage
|
|||
### Installation
|
||||
|
||||
```bash
|
||||
vendor/bin/sail artisan vendor:publish --tag="cashier-migrations"
|
||||
vendor/bin/sail artisan migrate
|
||||
vendor/bin/sail artisan vendor:publish --tag="cashier-config"
|
||||
php artisan vendor:publish --tag="cashier-migrations"
|
||||
php artisan migrate
|
||||
php artisan vendor:publish --tag="cashier-config"
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ ## Basic Usage
|
|||
### Installation
|
||||
|
||||
```bash
|
||||
vendor/bin/sail artisan horizon:install
|
||||
php artisan horizon:install
|
||||
```
|
||||
|
||||
### Supervisor Configuration
|
||||
|
|
@ -70,7 +70,7 @@ ### Dashboard Authorization
|
|||
|
||||
## Verification
|
||||
|
||||
1. Run `vendor/bin/sail artisan horizon` and visit `/horizon`
|
||||
1. Run `php artisan horizon` and visit `/horizon`
|
||||
2. Confirm dashboard access is restricted as expected
|
||||
3. Check that metrics populate after scheduling `horizon:snapshot`
|
||||
|
||||
|
|
@ -81,5 +81,5 @@ ## Common Pitfalls
|
|||
- Always check `config/horizon.php` before making changes to understand the current supervisor and environment configuration.
|
||||
- The `environments` array overrides only the keys you specify. It merges into `defaults` and does not replace it.
|
||||
- The timeout chain must be ordered: job `timeout` less than supervisor `timeout` less than `retry_after`. The wrong order can cause jobs to be retried before Horizon finishes timing them out.
|
||||
- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `vendor/bin/sail artisan horizon` alone does not populate metrics.
|
||||
- The metrics dashboard stays blank until `horizon:snapshot` is scheduled. Running `php artisan horizon` alone does not populate metrics.
|
||||
- Always use `search-docs` for the latest Horizon documentation rather than relying on this skill alone.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: inertia-vue-development
|
||||
description: "Develops Inertia.js v3 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, useHttp, setLayoutProps, or router; working with deferred props, prefetching, optimistic updates, instant visits, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation."
|
||||
description: "Develops Inertia.js v2 Vue client-side applications. Activates when creating Vue pages, forms, or navigation; using <Link>, <Form>, useForm, or router; working with deferred props, prefetching, or polling; or when user mentions Vue with Inertia, Vue pages, Vue forms, or Vue navigation."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
|
|
@ -8,19 +8,9 @@
|
|||
|
||||
# Inertia Vue Development
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or modifying Vue page components for Inertia
|
||||
- Working with forms in Vue (using `<Form>`, `useForm`, or `useHttp`)
|
||||
- Implementing client-side navigation with `<Link>` or `router`
|
||||
- Using v3 features: deferred props, prefetching, optimistic updates, instant visits, layout props, HTTP requests, WhenVisible, InfiniteScroll, once props, flash data, or polling
|
||||
- Building Vue-specific features with the Inertia protocol
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Inertia v3 Vue patterns and documentation.
|
||||
Use `search-docs` for detailed Inertia v2 Vue patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
|
|
@ -30,6 +20,8 @@ ### Page Components Location
|
|||
|
||||
### Page Component Structure
|
||||
|
||||
Important: Vue components must have a single root element.
|
||||
|
||||
<!-- Basic Vue Page Component -->
|
||||
```vue
|
||||
<script setup>
|
||||
|
|
@ -279,137 +271,7 @@ ### `useForm` Composable
|
|||
</template>
|
||||
```
|
||||
|
||||
## Inertia v3 Features
|
||||
|
||||
### HTTP Requests
|
||||
|
||||
Use the `useHttp` hook for standalone HTTP requests that do not trigger Inertia page visits. It provides the same developer experience as `useForm`, but for plain JSON endpoints.
|
||||
|
||||
<!-- useHttp Example -->
|
||||
```vue
|
||||
<script setup>
|
||||
import { useHttp } from '@inertiajs/vue3'
|
||||
|
||||
const http = useHttp({
|
||||
query: '',
|
||||
})
|
||||
|
||||
function search() {
|
||||
http.get('/api/search', {
|
||||
onSuccess: (response) => {
|
||||
console.log(response)
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-model="http.query" @input="search" />
|
||||
<div v-if="http.processing">Searching...</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Optimistic Updates
|
||||
|
||||
Apply data changes instantly before the server responds, with automatic rollback on failure:
|
||||
|
||||
<!-- Optimistic Update with Router -->
|
||||
```vue
|
||||
<script setup>
|
||||
import { router } from '@inertiajs/vue3'
|
||||
|
||||
function like(post) {
|
||||
router.optimistic((props) => ({
|
||||
post: {
|
||||
...props.post,
|
||||
likes: props.post.likes + 1,
|
||||
},
|
||||
})).post(`/posts/${post.id}/like`)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Optimistic updates also work with `useForm` and the `<Form>` component:
|
||||
|
||||
<!-- Optimistic Update with Form Component -->
|
||||
```vue
|
||||
<template>
|
||||
<Form
|
||||
action="/todos"
|
||||
method="post"
|
||||
:optimistic="(props, data) => ({
|
||||
todos: [...props.todos, { id: Date.now(), name: data.name, done: false }],
|
||||
})"
|
||||
>
|
||||
<input type="text" name="name" />
|
||||
<button type="submit">Add Todo</button>
|
||||
</Form>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Instant Visits
|
||||
|
||||
Navigate to a new page immediately without waiting for the server response. The target component renders right away with shared props, while page-specific props load in the background.
|
||||
|
||||
<!-- Instant Visit with Link -->
|
||||
```vue
|
||||
<script setup>
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link href="/dashboard" component="Dashboard">Dashboard</Link>
|
||||
|
||||
<Link
|
||||
href="/posts/1"
|
||||
component="Posts/Show"
|
||||
:page-props="{ post: { id: 1, title: 'My Post' } }"
|
||||
>
|
||||
View Post
|
||||
</Link>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Layout Props
|
||||
|
||||
Share dynamic data between pages and persistent layouts:
|
||||
|
||||
<!-- Layout Props in Layout -->
|
||||
```vue
|
||||
<script setup>
|
||||
withDefaults(defineProps({
|
||||
title: String,
|
||||
showSidebar: Boolean,
|
||||
}), {
|
||||
title: 'My App',
|
||||
showSidebar: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>{{ title }}</header>
|
||||
<aside v-if="showSidebar">Sidebar</aside>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</template>
|
||||
```
|
||||
|
||||
<!-- Setting Layout Props from Page -->
|
||||
```vue
|
||||
<script setup>
|
||||
import { setLayoutProps } from '@inertiajs/vue3'
|
||||
|
||||
setLayoutProps({
|
||||
title: 'Dashboard',
|
||||
showSidebar: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Dashboard</h1>
|
||||
</template>
|
||||
```
|
||||
## Inertia v2 Features
|
||||
|
||||
### Deferred Props
|
||||
|
||||
|
|
@ -496,69 +358,42 @@ ### Polling
|
|||
</template>
|
||||
```
|
||||
|
||||
- `autoStart` (default `true`) - set to `false` to start polling manually via the returned `start()` function
|
||||
- `keepAlive` (default `false`) - set to `true` to prevent throttling when the browser tab is inactive
|
||||
- `autoStart` (default `true`) — set to `false` to start polling manually via the returned `start()` function
|
||||
- `keepAlive` (default `false`) — set to `true` to prevent throttling when the browser tab is inactive
|
||||
|
||||
### WhenVisible
|
||||
### WhenVisible (Infinite Scroll)
|
||||
|
||||
Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold:
|
||||
Load more data when user scrolls to a specific element:
|
||||
|
||||
<!-- WhenVisible Example -->
|
||||
<!-- Infinite Scroll with WhenVisible -->
|
||||
```vue
|
||||
<script setup>
|
||||
import { WhenVisible } from '@inertiajs/vue3'
|
||||
|
||||
defineProps({
|
||||
stats: Object
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<WhenVisible data="stats" :buffer="200">
|
||||
<template #fallback>
|
||||
<div class="animate-pulse">Loading stats...</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ fetching }">
|
||||
<div>
|
||||
<p>Total Users: {{ stats.total_users }}</p>
|
||||
<p>Revenue: {{ stats.revenue }}</p>
|
||||
<span v-if="fetching">Refreshing...</span>
|
||||
</div>
|
||||
</template>
|
||||
</WhenVisible>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### InfiniteScroll
|
||||
|
||||
Automatically load additional pages of paginated data as users scroll:
|
||||
|
||||
<!-- InfiniteScroll Example -->
|
||||
```vue
|
||||
<script setup>
|
||||
import { InfiniteScroll } from '@inertiajs/vue3'
|
||||
|
||||
defineProps({
|
||||
users: Object
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InfiniteScroll data="users">
|
||||
<div>
|
||||
<div v-for="user in users.data" :key="user.id">
|
||||
{{ user.name }}
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
|
||||
<WhenVisible
|
||||
v-if="users.next_page_url"
|
||||
data="users"
|
||||
:params="{ page: users.current_page + 1 }"
|
||||
>
|
||||
<template #fallback>
|
||||
<div>Loading more...</div>
|
||||
</template>
|
||||
</WhenVisible>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
The server must use `Inertia::scroll()` to configure the paginated data. Use the `search-docs` tool with a query of `infinite scroll` for detailed guidance on buffers, manual loading, reverse mode, and custom trigger elements.
|
||||
|
||||
## Server-Side Patterns
|
||||
|
||||
Server-side patterns (Inertia::render, props, middleware) are covered in inertia-laravel guidelines.
|
||||
|
|
@ -570,6 +405,4 @@ ## Common Pitfalls
|
|||
- Forgetting to add loading states (skeleton screens) when using deferred props
|
||||
- Not handling the `undefined` state of deferred props before data loads
|
||||
- Using `<form>` without preventing default submission (use `<Form>` component or `@submit.prevent`)
|
||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
||||
- Using `router.cancel()` instead of `router.cancelAll()` (v3 breaking change)
|
||||
- Using `router.on('invalid', ...)` or `router.on('exception', ...)` instead of the renamed `httpException` and `networkError` events
|
||||
- Forgetting to check if `<Form>` component is available in your Inertia version
|
||||
155
.claude/skills/mcp-development/SKILL.md
Normal file
155
.claude/skills/mcp-development/SKILL.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
---
|
||||
name: mcp-development
|
||||
description: "Use this skill for Laravel MCP development only. Trigger when creating or editing MCP tools, resources, prompts, or servers in Laravel projects. Covers: artisan make:mcp-* generators, mcp:inspector, routes/ai.php, Tool/Resource/Prompt classes, schema validation, shouldRegister(), OAuth setup, URI templates, read-only attributes, and MCP debugging. Do not use for non-Laravel MCP projects or generic AI features without MCP."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# MCP Development
|
||||
|
||||
## Documentation First
|
||||
|
||||
**CRITICAL**: Always use `search-docs` BEFORE writing MCP code. The documentation is version-specific, comprehensive, and always up-to-date.
|
||||
|
||||
<!-- Search MCP Documentation -->
|
||||
```bash
|
||||
|
||||
# Example searches
|
||||
|
||||
search-docs(['mcp tools', 'mcp resources', 'mcp validation'])
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Artisan Commands
|
||||
|
||||
Create MCP Primitives"
|
||||
```bash
|
||||
php artisan make:mcp-tool ToolName
|
||||
php artisan make:mcp-resource ResourceName
|
||||
php artisan make:mcp-prompt PromptName
|
||||
php artisan make:mcp-server ServerName
|
||||
```
|
||||
|
||||
### Basic Tool Implementation
|
||||
|
||||
<!-- Tool Example -->
|
||||
```php
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
class MyTool extends Tool
|
||||
{
|
||||
protected string $description = 'Tool description for LLM';
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'param' => $schema->string()->required(),
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
return Response::text($request->get('param'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Resource Implementation
|
||||
|
||||
<!-- Resource Example -->
|
||||
```php
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
|
||||
class MyResource extends Resource
|
||||
{
|
||||
protected string $description = 'Resource description';
|
||||
protected string $uri = 'file://path/to/resource';
|
||||
protected string $mimeType = 'text/markdown';
|
||||
|
||||
public function handle(): Response
|
||||
{
|
||||
return Response::text($content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Response Methods
|
||||
|
||||
<!-- Available Responses -->
|
||||
```php
|
||||
Response::text('Text content');
|
||||
Response::error('Error message');
|
||||
Response::structured(['key' => 'value']);
|
||||
```
|
||||
|
||||
## Testing MCP Primitives
|
||||
|
||||
Test tools, resources, and prompts directly on their server:
|
||||
|
||||
<!-- Test MCP Primitives -->
|
||||
```php
|
||||
// Test a tool
|
||||
$response = MyServer::tool(MyTool::class, ['param' => 'value']);
|
||||
$response->assertOk()->assertSee('Expected text');
|
||||
|
||||
// Test as authenticated user
|
||||
$response = MyServer::actingAs($user)->tool(MyTool::class, [...]);
|
||||
|
||||
// Available assertions
|
||||
$response->assertOk();
|
||||
$response->assertSee('text');
|
||||
$response->assertHasErrors();
|
||||
$response->assertHasNoErrors();
|
||||
$response->assertName('tool-name');
|
||||
$response->assertSentNotification('event/type', ['data' => 'value']);
|
||||
```
|
||||
|
||||
### MCP Inspector
|
||||
|
||||
Test interactively using the inspector:
|
||||
|
||||
<!--Launch MCP Inspector-->
|
||||
```bash
|
||||
php artisan mcp:inspector mcp/my-server # Web server
|
||||
|
||||
php artisan mcp:inspector my-server # Local server
|
||||
|
||||
```
|
||||
|
||||
## Available Features
|
||||
|
||||
The following features exist—**use `search-docs` for implementation details**:
|
||||
|
||||
- **Tools**: `schema()`, validation, annotations (`#[IsReadOnly]`, `#[IsDestructive]`, etc.)
|
||||
- **Resources**: URI templates (`HasUriTemplate`), Dynamic resources
|
||||
- **Prompts**: Arguments, multi-message responses
|
||||
- **All primitives**: Dependency injection, `shouldRegister()`, validation
|
||||
- **Responses**: Text, error, structured, streaming, metadata
|
||||
- **Server registration**: Web routes, local routes, OAuth
|
||||
|
||||
## Critical Imports
|
||||
|
||||
<!-- Correct Imports -->
|
||||
```php
|
||||
use Laravel\Mcp\Request; // NOT Laravel\Mcp\Server\Request
|
||||
use Laravel\Mcp\Response; // NOT Laravel\Mcp\Server\Response
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Not using `search-docs` before implementation**
|
||||
- Wrong imports: `Laravel\Mcp\Server\Request` (wrong) vs `Laravel\Mcp\Request` (correct)
|
||||
- Forgetting `schema()` method for tools with parameters
|
||||
- Missing required properties: `$description`, `$uri`, `$mimeType`
|
||||
- Wrong response pattern: `new Response()` instead of `Response::text()`
|
||||
- Running `mcp:start` command locally (hangs waiting for stdin)
|
||||
106
.claude/skills/medialibrary-development/SKILL.md
Normal file
106
.claude/skills/medialibrary-development/SKILL.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
name: medialibrary-development
|
||||
description: Build and work with spatie/laravel-medialibrary features including associating files with Eloquent models, defining media collections and conversions, generating responsive images, and retrieving media URLs and paths.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Spatie
|
||||
---
|
||||
|
||||
# Media Library Development
|
||||
|
||||
## Overview
|
||||
|
||||
Use spatie/laravel-medialibrary to associate files with Eloquent models. Supports image/video conversions, responsive images, multiple collections, and various storage disks.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Activate when working with file uploads, media attachments, or image processing in Laravel.
|
||||
- Activate when code references `HasMedia`, `InteractsWithMedia`, the `Media` model, or media collections/conversions.
|
||||
- Activate when the user wants to add, retrieve, convert, or manage files attached to Eloquent models.
|
||||
|
||||
## Scope
|
||||
|
||||
- In scope: media uploads, collections, conversions, responsive images, custom properties, file retrieval, path/URL generation.
|
||||
- Out of scope: general file storage without Eloquent association, non-Laravel frameworks.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Identify the task (model setup, adding media, defining conversions, retrieving files, etc.).
|
||||
2. Read `references/medialibrary-guide.md` and focus on the relevant section.
|
||||
3. Apply the patterns from the reference, keeping code minimal and Laravel-native.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Model Setup
|
||||
|
||||
Every model that should have media must implement `HasMedia` and use the `InteractsWithMedia` trait:
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
class BlogPost extends Model implements HasMedia
|
||||
{
|
||||
use InteractsWithMedia;
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Media
|
||||
|
||||
```php
|
||||
$blogPost->addMedia($file)->toMediaCollection('images');
|
||||
$blogPost->addMediaFromUrl($url)->toMediaCollection('images');
|
||||
$blogPost->addMediaFromRequest('file')->toMediaCollection('images');
|
||||
```
|
||||
|
||||
### Defining Collections
|
||||
|
||||
```php
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('avatar')->singleFile();
|
||||
$this->addMediaCollection('downloads')->useDisk('s3');
|
||||
}
|
||||
```
|
||||
|
||||
### Defining Conversions
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Spatie\Image\Enums\Fit;
|
||||
|
||||
public function registerMediaConversions(?Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('thumb')
|
||||
->fit(Fit::Contain, 300, 300)
|
||||
->nonQueued();
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieving Media
|
||||
|
||||
```php
|
||||
$url = $model->getFirstMediaUrl('images');
|
||||
$thumbUrl = $model->getFirstMediaUrl('images', 'thumb');
|
||||
$allMedia = $model->getMedia('images');
|
||||
```
|
||||
|
||||
## Do and Don't
|
||||
|
||||
Do:
|
||||
- Always implement the `HasMedia` interface alongside the `InteractsWithMedia` trait.
|
||||
- Use `?Media $media = null` as the parameter for `registerMediaConversions()`.
|
||||
- Call `->toMediaCollection()` to finalize adding media.
|
||||
- Use `->nonQueued()` for conversions that should run synchronously.
|
||||
- Use `->singleFile()` on collections that should only hold one file.
|
||||
- Use `Spatie\Image\Enums\Fit` enum values for fit methods.
|
||||
|
||||
Don't:
|
||||
- Don't forget to run `php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"` before migrating.
|
||||
- Don't use `env()` for disk configuration; use `config()` or set it in `config/media-library.php`.
|
||||
- Don't call `addMedia()` without calling `toMediaCollection()` — the media won't be saved.
|
||||
- Don't reference conversion names that aren't registered in `registerMediaConversions()`.
|
||||
|
||||
## References
|
||||
|
||||
- `references/medialibrary-guide.md`
|
||||
|
|
@ -0,0 +1,577 @@
|
|||
# Laravel Media Library Reference
|
||||
|
||||
Complete reference for `spatie/laravel-medialibrary`. Full documentation: https://spatie.be/docs/laravel-medialibrary
|
||||
|
||||
## Model Setup
|
||||
|
||||
Implement `HasMedia` and use `InteractsWithMedia`:
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
class BlogPost extends Model implements HasMedia
|
||||
{
|
||||
use InteractsWithMedia;
|
||||
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('images');
|
||||
}
|
||||
|
||||
public function registerMediaConversions(?Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('thumb')
|
||||
->fit(Fit::Contain, 300, 300);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding Media
|
||||
|
||||
### From uploaded file
|
||||
|
||||
```php
|
||||
$model->addMedia($request->file('image'))->toMediaCollection('images');
|
||||
```
|
||||
|
||||
### From request (shorthand)
|
||||
|
||||
```php
|
||||
$model->addMediaFromRequest('image')->toMediaCollection('images');
|
||||
```
|
||||
|
||||
### From URL
|
||||
|
||||
```php
|
||||
$model->addMediaFromUrl('https://example.com/image.jpg')->toMediaCollection('images');
|
||||
```
|
||||
|
||||
### From string content
|
||||
|
||||
```php
|
||||
$model->addMediaFromString('raw content')->usingFileName('file.txt')->toMediaCollection('files');
|
||||
```
|
||||
|
||||
### From base64
|
||||
|
||||
```php
|
||||
$model->addMediaFromBase64($base64Data)->usingFileName('photo.jpg')->toMediaCollection('images');
|
||||
```
|
||||
|
||||
### From stream
|
||||
|
||||
```php
|
||||
$model->addMediaFromStream($stream)->usingFileName('file.pdf')->toMediaCollection('files');
|
||||
```
|
||||
|
||||
### From existing disk
|
||||
|
||||
```php
|
||||
$model->addMediaFromDisk('path/to/file.jpg', 's3')->toMediaCollection('images');
|
||||
```
|
||||
|
||||
### Multiple files from request
|
||||
|
||||
```php
|
||||
$model->addMultipleMediaFromRequest(['images'])->each(function ($fileAdder) {
|
||||
$fileAdder->toMediaCollection('images');
|
||||
});
|
||||
|
||||
$model->addAllMediaFromRequest()->each(function ($fileAdder) {
|
||||
$fileAdder->toMediaCollection('images');
|
||||
});
|
||||
```
|
||||
|
||||
### Copy instead of move
|
||||
|
||||
```php
|
||||
$model->copyMedia($pathToFile)->toMediaCollection('images');
|
||||
// or
|
||||
$model->addMedia($pathToFile)->preservingOriginal()->toMediaCollection('images');
|
||||
```
|
||||
|
||||
## FileAdder Options
|
||||
|
||||
All methods are chainable before calling `toMediaCollection()`:
|
||||
|
||||
```php
|
||||
$model->addMedia($file)
|
||||
->usingName('Custom Name') // display name
|
||||
->usingFileName('custom-name.jpg') // filename on disk
|
||||
->setOrder(3) // order within collection
|
||||
->withCustomProperties(['alt' => 'A landscape photo'])
|
||||
->withManipulations(['thumb' => ['filter' => 'greyscale']])
|
||||
->withResponsiveImages() // generate responsive variants
|
||||
->storingConversionsOnDisk('s3') // put conversions on different disk
|
||||
->addCustomHeaders(['CacheControl' => 'max-age=31536000'])
|
||||
->toMediaCollection('images');
|
||||
```
|
||||
|
||||
### Store on cloud disk
|
||||
|
||||
```php
|
||||
$model->addMedia($file)->toMediaCollectionOnCloudDisk('images');
|
||||
```
|
||||
|
||||
## Media Collections
|
||||
|
||||
Define in `registerMediaCollections()`:
|
||||
|
||||
```php
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
// Basic collection
|
||||
$this->addMediaCollection('images');
|
||||
|
||||
// Single file (replacing previous on new upload)
|
||||
$this->addMediaCollection('avatar')
|
||||
->singleFile();
|
||||
|
||||
// Keep only latest N items
|
||||
$this->addMediaCollection('recent_photos')
|
||||
->onlyKeepLatest(5);
|
||||
|
||||
// Specific disk
|
||||
$this->addMediaCollection('downloads')
|
||||
->useDisk('s3');
|
||||
|
||||
// With conversions disk
|
||||
$this->addMediaCollection('photos')
|
||||
->useDisk('s3')
|
||||
->storeConversionsOnDisk('s3-thumbnails');
|
||||
|
||||
// MIME type restriction
|
||||
$this->addMediaCollection('documents')
|
||||
->acceptsMimeTypes(['application/pdf', 'application/zip']);
|
||||
|
||||
// Custom validation
|
||||
$this->addMediaCollection('images')
|
||||
->acceptsFile(function ($file) {
|
||||
return $file->mimeType === 'image/jpeg';
|
||||
});
|
||||
|
||||
// Fallback URL/path when collection is empty
|
||||
$this->addMediaCollection('avatar')
|
||||
->singleFile()
|
||||
->useFallbackUrl('/images/default-avatar.jpg')
|
||||
->useFallbackPath(public_path('/images/default-avatar.jpg'));
|
||||
|
||||
// Enable responsive images for entire collection
|
||||
$this->addMediaCollection('hero_images')
|
||||
->withResponsiveImages();
|
||||
|
||||
// Collection-specific conversions
|
||||
$this->addMediaCollection('photos')
|
||||
->registerMediaConversions(function () {
|
||||
$this->addMediaConversion('card')
|
||||
->fit(Fit::Crop, 400, 400);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Media Conversions
|
||||
|
||||
Define in `registerMediaConversions()`:
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Spatie\Image\Enums\Fit;
|
||||
|
||||
public function registerMediaConversions(?Media $media = null): void
|
||||
{
|
||||
$this->addMediaConversion('thumb')
|
||||
->fit(Fit::Contain, 300, 300)
|
||||
->nonQueued();
|
||||
|
||||
$this->addMediaConversion('preview')
|
||||
->fit(Fit::Crop, 500, 500)
|
||||
->withResponsiveImages()
|
||||
->queued();
|
||||
|
||||
$this->addMediaConversion('banner')
|
||||
->fit(Fit::Max, 1200, 630)
|
||||
->performOnCollections('images', 'headers')
|
||||
->nonQueued()
|
||||
->sharpen(10);
|
||||
|
||||
// Conditional conversion based on media properties
|
||||
if ($media?->mime_type === 'image/png') {
|
||||
$this->addMediaConversion('png-thumb')
|
||||
->fit(Fit::Contain, 150, 150);
|
||||
}
|
||||
|
||||
// Keep original format instead of converting to jpg
|
||||
$this->addMediaConversion('web')
|
||||
->fit(Fit::Max, 800, 800)
|
||||
->keepOriginalImageFormat();
|
||||
|
||||
// PDF page rendering
|
||||
$this->addMediaConversion('pdf-preview')
|
||||
->pdfPageNumber(1)
|
||||
->fit(Fit::Contain, 400, 400);
|
||||
|
||||
// Video frame extraction
|
||||
$this->addMediaConversion('video-thumb')
|
||||
->extractVideoFrameAtSecond(5)
|
||||
->fit(Fit::Crop, 300, 300);
|
||||
}
|
||||
```
|
||||
|
||||
### Image Manipulation Methods (via spatie/image)
|
||||
|
||||
Resizing and fitting:
|
||||
- `width(int)`, `height(int)` — constrain dimensions
|
||||
- `fit(Fit, int, int)` — fit within bounds using `Fit::Contain`, `Fit::Max`, `Fit::Fill`, `Fit::Stretch`, `Fit::Crop`
|
||||
- `crop(int, int)` — crop to exact dimensions
|
||||
|
||||
Effects:
|
||||
- `sharpen(int)`, `blur(int)`, `pixelate(int)`
|
||||
- `greyscale()`, `sepia()`
|
||||
- `brightness(int)`, `contrast(int)`, `colorize(int, int, int)`
|
||||
|
||||
Orientation:
|
||||
- `orientation(int)`, `flip(string)`, `rotate(int)`
|
||||
|
||||
Format:
|
||||
- `format(string)` — `'jpg'`, `'png'`, `'webp'`, `'avif'`
|
||||
- `quality(int)` — 1-100
|
||||
|
||||
Other:
|
||||
- `border(int, string, string)`, `watermark(string)`
|
||||
- `optimize()`, `nonOptimized()`
|
||||
|
||||
### Conversion Configuration
|
||||
|
||||
- `performOnCollections('col1', 'col2')` — limit to specific collections
|
||||
- `queued()` / `nonQueued()` — run async or sync
|
||||
- `withResponsiveImages()` — also generate responsive variants for this conversion
|
||||
- `keepOriginalImageFormat()` — preserve png/webp/gif instead of converting to jpg
|
||||
- `pdfPageNumber(int)` — which PDF page to render
|
||||
- `extractVideoFrameAtSecond(int)` — video thumbnail timing
|
||||
|
||||
## Retrieving Media
|
||||
|
||||
### Getting media items
|
||||
|
||||
```php
|
||||
$media = $model->getMedia('images'); // all in collection
|
||||
$first = $model->getFirstMedia('images'); // first item
|
||||
$last = $model->getLastMedia('images'); // last item
|
||||
$has = $model->hasMedia('images'); // boolean check
|
||||
```
|
||||
|
||||
### Getting URLs
|
||||
|
||||
```php
|
||||
$url = $model->getFirstMediaUrl('images'); // original URL
|
||||
$thumbUrl = $model->getFirstMediaUrl('images', 'thumb'); // conversion URL
|
||||
$lastUrl = $model->getLastMediaUrl('images', 'thumb');
|
||||
```
|
||||
|
||||
### Getting paths
|
||||
|
||||
```php
|
||||
$path = $model->getFirstMediaPath('images');
|
||||
$thumbPath = $model->getFirstMediaPath('images', 'thumb');
|
||||
```
|
||||
|
||||
### Temporary URLs (S3)
|
||||
|
||||
```php
|
||||
$tempUrl = $model->getFirstTemporaryUrl(
|
||||
now()->addMinutes(30),
|
||||
'images',
|
||||
'thumb'
|
||||
);
|
||||
```
|
||||
|
||||
### Fallback URLs
|
||||
|
||||
```php
|
||||
$url = $model->getFallbackMediaUrl('avatar');
|
||||
```
|
||||
|
||||
### From the Media model
|
||||
|
||||
```php
|
||||
$media = $model->getFirstMedia('images');
|
||||
|
||||
$media->getUrl(); // original URL
|
||||
$media->getUrl('thumb'); // conversion URL
|
||||
$media->getPath(); // disk path
|
||||
$media->getFullUrl(); // full URL with domain
|
||||
$media->getTemporaryUrl(now()->addMinutes(30));
|
||||
$media->hasGeneratedConversion('thumb'); // check if conversion exists
|
||||
```
|
||||
|
||||
### Filtering media
|
||||
|
||||
```php
|
||||
$media = $model->getMedia('images', function (Media $media) {
|
||||
return $media->getCustomProperty('featured') === true;
|
||||
});
|
||||
|
||||
$media = $model->getMedia('images', ['mime_type' => 'image/jpeg']);
|
||||
```
|
||||
|
||||
## Custom Properties
|
||||
|
||||
Store arbitrary metadata on media items:
|
||||
|
||||
```php
|
||||
// When adding
|
||||
$model->addMedia($file)
|
||||
->withCustomProperties([
|
||||
'alt' => 'Descriptive text',
|
||||
'credits' => 'Photographer Name',
|
||||
])
|
||||
->toMediaCollection('images');
|
||||
|
||||
// Get/set on existing media
|
||||
$media->setCustomProperty('alt', 'Updated text');
|
||||
$media->save();
|
||||
|
||||
$alt = $media->getCustomProperty('alt');
|
||||
$has = $media->hasCustomProperty('alt');
|
||||
$media->forgetCustomProperty('alt');
|
||||
$media->save();
|
||||
```
|
||||
|
||||
## Responsive Images
|
||||
|
||||
Generate multiple sizes for optimal loading:
|
||||
|
||||
```php
|
||||
// On the FileAdder
|
||||
$model->addMedia($file)
|
||||
->withResponsiveImages()
|
||||
->toMediaCollection('images');
|
||||
|
||||
// On a conversion
|
||||
$this->addMediaConversion('hero')
|
||||
->fit(Fit::Max, 1200, 800)
|
||||
->withResponsiveImages();
|
||||
|
||||
// On a collection
|
||||
$this->addMediaCollection('photos')
|
||||
->withResponsiveImages();
|
||||
```
|
||||
|
||||
### Using in Blade
|
||||
|
||||
```blade
|
||||
{{-- Renders img tag with srcset --}}
|
||||
{{ $media->toHtml() }}
|
||||
|
||||
{{-- With attributes --}}
|
||||
{{ $media->img()->attributes(['class' => 'w-full', 'alt' => 'Photo']) }}
|
||||
|
||||
{{-- Get srcset string --}}
|
||||
<img src="{{ $media->getUrl() }}" srcset="{{ $media->getSrcset() }}" />
|
||||
|
||||
{{-- Responsive conversion --}}
|
||||
<img src="{{ $media->getUrl('hero') }}" srcset="{{ $media->getSrcset('hero') }}" />
|
||||
```
|
||||
|
||||
### Placeholder SVG
|
||||
|
||||
```php
|
||||
$svg = $media->responsiveImages()->getPlaceholderSvg(); // tiny blurred base64 placeholder
|
||||
```
|
||||
|
||||
## Managing Media
|
||||
|
||||
### Clear a collection
|
||||
|
||||
```php
|
||||
$model->clearMediaCollection('images');
|
||||
```
|
||||
|
||||
### Clear except specific items
|
||||
|
||||
```php
|
||||
$model->clearMediaCollectionExcept('images', $mediaToKeep);
|
||||
```
|
||||
|
||||
### Delete specific media
|
||||
|
||||
```php
|
||||
$model->deleteMedia($mediaId);
|
||||
```
|
||||
|
||||
### Delete all media
|
||||
|
||||
```php
|
||||
$model->deleteAllMedia();
|
||||
```
|
||||
|
||||
### Delete model but keep media files
|
||||
|
||||
```php
|
||||
$model->deletePreservingMedia();
|
||||
```
|
||||
|
||||
### Reorder media
|
||||
|
||||
```php
|
||||
Media::setNewOrder([3, 1, 2]); // media IDs in desired order
|
||||
```
|
||||
|
||||
### Move/copy media between models
|
||||
|
||||
```php
|
||||
$media->move($otherModel, 'images');
|
||||
$media->copy($otherModel, 'images');
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\MediaCollections\Events\MediaHasBeenAddedEvent;
|
||||
use Spatie\MediaLibrary\Conversions\Events\ConversionWillStartEvent;
|
||||
use Spatie\MediaLibrary\Conversions\Events\ConversionHasBeenCompletedEvent;
|
||||
use Spatie\MediaLibrary\MediaCollections\Events\CollectionHasBeenClearedEvent;
|
||||
```
|
||||
|
||||
Listen to these events to hook into the media lifecycle:
|
||||
```php
|
||||
Event::listen(MediaHasBeenAddedEvent::class, function ($event) {
|
||||
$event->media; // the added Media model
|
||||
});
|
||||
|
||||
Event::listen(ConversionHasBeenCompletedEvent::class, function ($event) {
|
||||
$event->media;
|
||||
$event->conversion;
|
||||
});
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Key `config/media-library.php` options:
|
||||
|
||||
```php
|
||||
return [
|
||||
'disk_name' => 'public', // default disk
|
||||
'max_file_size' => 1024 * 1024 * 10, // 10MB
|
||||
'queue_connection_name' => '', // queue connection
|
||||
'queue_name' => '', // queue name
|
||||
'queue_conversions_by_default' => true, // queue conversions
|
||||
'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class,
|
||||
'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class,
|
||||
'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class,
|
||||
'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class,
|
||||
'image_driver' => 'gd', // 'gd', 'imagick', or 'vips'
|
||||
'image_optimizers' => [/* optimizer config */],
|
||||
'version_urls' => true, // cache busting
|
||||
'default_loading_attribute_value' => null, // 'lazy' for lazy loading
|
||||
];
|
||||
```
|
||||
|
||||
### Custom Path Generator
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\Support\PathGenerator\PathGenerator;
|
||||
|
||||
class CustomPathGenerator implements PathGenerator
|
||||
{
|
||||
public function getPath(Media $media): string
|
||||
{
|
||||
return md5($media->id) . '/';
|
||||
}
|
||||
|
||||
public function getPathForConversions(Media $media): string
|
||||
{
|
||||
return $this->getPath($media) . 'conversions/';
|
||||
}
|
||||
|
||||
public function getPathForResponsiveImages(Media $media): string
|
||||
{
|
||||
return $this->getPath($media) . 'responsive/';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom File Namer
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\Support\FileNamer\FileNamer;
|
||||
|
||||
class CustomFileNamer extends FileNamer
|
||||
{
|
||||
public function originalFileName(string $fileName): string
|
||||
{
|
||||
return Str::slug(pathinfo($fileName, PATHINFO_FILENAME));
|
||||
}
|
||||
|
||||
public function conversionFileName(string $fileName, Conversion $conversion): string
|
||||
{
|
||||
return $this->originalFileName($fileName) . '-' . $conversion->getName();
|
||||
}
|
||||
|
||||
public function responsiveFileName(string $fileName): string
|
||||
{
|
||||
return pathinfo($fileName, PATHINFO_FILENAME);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Media Model
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media as BaseMedia;
|
||||
|
||||
class Media extends BaseMedia
|
||||
{
|
||||
// Add custom methods, scopes, or override behavior
|
||||
}
|
||||
```
|
||||
|
||||
Register in config: `'media_model' => App\Models\Media::class`
|
||||
|
||||
## Downloading Media
|
||||
|
||||
### Single file
|
||||
|
||||
```php
|
||||
return $media->toResponse($request); // download
|
||||
return $media->toInlineResponse($request); // display inline
|
||||
return $media->stream(); // stream
|
||||
```
|
||||
|
||||
### ZIP download of collection
|
||||
|
||||
```php
|
||||
use Spatie\MediaLibrary\Support\MediaStream;
|
||||
|
||||
return MediaStream::create('photos.zip')
|
||||
->addMedia($model->getMedia('images'));
|
||||
```
|
||||
|
||||
## Using with API Resources
|
||||
|
||||
```php
|
||||
class PostResource extends JsonResource
|
||||
{
|
||||
public function toArray($request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'image' => $this->getFirstMediaUrl('images'),
|
||||
'thumb' => $this->getFirstMediaUrl('images', 'thumb'),
|
||||
'media' => $this->getMedia('images')->map(function ($media) {
|
||||
return [
|
||||
'id' => $media->id,
|
||||
'url' => $media->getUrl(),
|
||||
'thumb' => $media->getUrl('thumb'),
|
||||
'name' => $media->name,
|
||||
'size' => $media->size,
|
||||
'type' => $media->mime_type,
|
||||
];
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
77
.claude/skills/pennant-development/SKILL.md
Normal file
77
.claude/skills/pennant-development/SKILL.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
name: pennant-development
|
||||
description: "Use when working with Laravel Pennant the official Laravel feature flag package. Trigger whenever the query mentions Pennant by name or involves feature flags or feature toggles in a Laravel project. Tasks include defining feature flags checking whether features are active creating class based features in `app/Features` using Blade `@feature` directives scoping flags to users or teams building custom Pennant storage drivers protecting routes with feature flags testing feature flags with Pest or PHPUnit and implementing A B testing or gradual rollouts with feature flags. Do not trigger for generic Laravel configuration authorization policies authentication or non Pennant feature management systems."
|
||||
license: MIT
|
||||
metadata:
|
||||
author: laravel
|
||||
---
|
||||
|
||||
# Pennant Features
|
||||
|
||||
## When to Apply
|
||||
|
||||
Activate this skill when:
|
||||
|
||||
- Creating or checking feature flags
|
||||
- Managing feature rollouts
|
||||
- Implementing A/B testing
|
||||
|
||||
## Documentation
|
||||
|
||||
Use `search-docs` for detailed Pennant patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Defining Features
|
||||
|
||||
<!-- Defining Features -->
|
||||
```php
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
Feature::define('new-dashboard', function (User $user) {
|
||||
return $user->isAdmin();
|
||||
});
|
||||
```
|
||||
|
||||
### Checking Features
|
||||
|
||||
<!-- Checking Features -->
|
||||
```php
|
||||
if (Feature::active('new-dashboard')) {
|
||||
// Feature is active
|
||||
}
|
||||
|
||||
// With scope
|
||||
if (Feature::for($user)->active('new-dashboard')) {
|
||||
// Feature is active for this user
|
||||
}
|
||||
```
|
||||
|
||||
### Blade Directive
|
||||
|
||||
<!-- Blade Directive -->
|
||||
```blade
|
||||
@feature('new-dashboard')
|
||||
<x-new-dashboard />
|
||||
@else
|
||||
<x-old-dashboard />
|
||||
@endfeature
|
||||
```
|
||||
|
||||
### Activating / Deactivating
|
||||
|
||||
<!-- Activating Features -->
|
||||
```php
|
||||
Feature::activate('new-dashboard');
|
||||
Feature::for($user)->activate('new-dashboard');
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
1. Check feature flag is defined
|
||||
2. Test with different scopes/users
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Forgetting to scope features for specific users/entities
|
||||
- Not following existing naming conventions
|
||||
|
|
@ -16,7 +16,7 @@ ## Basic Usage
|
|||
|
||||
### Creating Tests
|
||||
|
||||
All tests must be written using Pest. Use `vendor/bin/sail artisan make:test --pest {name}`.
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
|
||||
### Test Organization
|
||||
|
||||
|
|
@ -35,9 +35,9 @@ ### Basic Test Structure
|
|||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `vendor/bin/sail artisan test --compact --filter=testName`.
|
||||
- Run all tests: `vendor/bin/sail artisan test --compact`.
|
||||
- Run file: `vendor/bin/sail artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
|
|
|
|||
460
.claude/skills/upgrade-laravel-v13/SKILL.md
Normal file
460
.claude/skills/upgrade-laravel-v13/SKILL.md
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
# Laravel 12 to 13 Upgrade Specialist
|
||||
|
||||
You are an expert Laravel upgrade specialist with deep knowledge of both Laravel 12.x and 13.0. Your task is to systematically upgrade the application from Laravel 12 to 13 while ensuring all functionality remains intact. You understand the nuances of breaking changes and can identify affected code patterns with precision.
|
||||
|
||||
## Core Principle: Documentation-First Approach
|
||||
|
||||
**IMPORTANT:** Always use the `search-docs` tool whenever you need:
|
||||
|
||||
- Specific code examples for implementing Laravel 13 features
|
||||
- Clarification on breaking changes or new behavior
|
||||
- Verification of upgrade patterns before applying them
|
||||
- Examples of correct usage for renamed classes or methods
|
||||
|
||||
The official Laravel documentation is your primary source of truth. Consult it before making assumptions or implementing changes.
|
||||
|
||||
## Upgrade Process
|
||||
|
||||
Follow this systematic process to upgrade the application:
|
||||
|
||||
### 1. Assess Current State
|
||||
|
||||
Before making any changes:
|
||||
|
||||
- Check `composer.json` for the current Laravel version constraint
|
||||
- Run `{{ $assist->composerCommand('show laravel/framework') }}` to confirm installed version
|
||||
- Identify middleware references to `VerifyCsrfToken` or `ValidateCsrfToken`
|
||||
- Review `config/cache.php` for serialization settings
|
||||
- Review `config/session.php` for cookie name configuration
|
||||
|
||||
### 2. Create Safety Net
|
||||
|
||||
- Ensure you're working on a dedicated branch
|
||||
- Run the existing test suite to establish baseline
|
||||
- Note any custom cache store implementations or queue driver implementations
|
||||
|
||||
### 3. Analyze Codebase for Breaking Changes
|
||||
|
||||
Search the codebase for patterns affected by v13 changes:
|
||||
|
||||
**High Priority Searches:**
|
||||
|
||||
- `VerifyCsrfToken` or `ValidateCsrfToken` — Must rename to `PreventRequestForgery`
|
||||
- `composer.json` — Dependency version constraints to update
|
||||
- `phpunit.xml` or `pest` config — Test framework version compatibility
|
||||
|
||||
**Medium Priority Searches:**
|
||||
|
||||
- `config/cache.php` — Check for `serializable_classes` configuration
|
||||
- Code that stores PHP objects in cache — May need explicit class allow-lists
|
||||
|
||||
**Low Priority Searches:**
|
||||
|
||||
- `$event->exceptionOccurred` — Renamed to `$event->exception` in `JobAttempted`
|
||||
- `$event->connection` on `QueueBusy` — Renamed to `$connectionName`
|
||||
- `pagination::default` or `pagination::simple-default` — View names changed
|
||||
- `Container::call` with nullable class defaults — Behavior changed
|
||||
- Manager `extend` callbacks using `$this` — Binding changed
|
||||
- Custom `Str` factories in tests — Now reset between tests
|
||||
|
||||
### 4. Apply Changes Systematically
|
||||
|
||||
For each category of changes:
|
||||
|
||||
1. **Search** for affected patterns using grep/search tools
|
||||
2. **Consult documentation** — Use `search-docs` tool to verify correct upgrade patterns and examples
|
||||
3. **List** all files that need modification
|
||||
4. **Apply** the fix consistently across all occurrences
|
||||
5. **Verify** each change doesn't break functionality
|
||||
|
||||
### 5. Update Dependencies
|
||||
|
||||
After code changes are complete:
|
||||
|
||||
```bash
|
||||
{{ $assist->composerCommand('require laravel/framework:^13.0 --with-all-dependencies') }}
|
||||
```
|
||||
|
||||
### 6. Test and Verify
|
||||
|
||||
- Run the full test suite
|
||||
- Verify CSRF protection still works correctly
|
||||
- Check cache read/write operations
|
||||
- Test any queue listeners that reference event properties
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
When upgrading, maximize efficiency by:
|
||||
|
||||
- **Batch similar changes** — Group all CSRF middleware renames, then all config updates, etc.
|
||||
- **Use parallel agents** for independent file modifications
|
||||
- **Prioritize high-impact changes** that could cause immediate failures
|
||||
- **Test incrementally** — Verify after each category of changes
|
||||
|
||||
# Upgrading from Laravel 12.x to 13.0
|
||||
|
||||
> [!NOTE]
|
||||
> We attempt to document every possible breaking change. Since some of these breaking changes are in obscure parts of the framework only a portion of these changes may actually affect your application.
|
||||
|
||||
## Updating Dependencies
|
||||
|
||||
**Likelihood Of Impact: High**
|
||||
|
||||
Update the following dependencies in your application's `composer.json` file:
|
||||
|
||||
@boostsnippet('Dependency Updates', 'json')
|
||||
{
|
||||
"require": {
|
||||
"laravel/framework": "^13.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/tinker": "^3.0",
|
||||
"phpunit/phpunit": "^12.0",
|
||||
"pestphp/pest": "^4.0"
|
||||
}
|
||||
}
|
||||
@endboostsnippet
|
||||
|
||||
Run the update:
|
||||
|
||||
```bash
|
||||
{{ $assist->composerCommand('update') }}
|
||||
```
|
||||
|
||||
## Updating the Laravel Installer
|
||||
|
||||
If you use the Laravel installer CLI tool, update it for Laravel 13.x compatibility:
|
||||
|
||||
@if($usesHerd)
|
||||
|
||||
```bash
|
||||
herd laravel:update
|
||||
```
|
||||
|
||||
@else
|
||||
|
||||
```bash
|
||||
{{ $assist->composerCommand('global update laravel/installer') }}
|
||||
```
|
||||
|
||||
@endif
|
||||
|
||||
## Cache
|
||||
|
||||
### Cache Prefixes and Session Cookie Names
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
Laravel's default cache and Redis key prefixes now use hyphenated suffixes. In addition, the default session cookie name now uses `Str::snake(...)` for the application name.
|
||||
|
||||
In most applications, this change will not apply because application-level configuration files already define these values. This primarily affects applications that rely on framework-level fallback configuration when corresponding application config values are not present.
|
||||
|
||||
If your application relies on these generated defaults, cache keys and session cookie names may change after upgrading:
|
||||
|
||||
@boostsnippet('Cache Prefix Changes', 'php')
|
||||
// Laravel <= 12.x
|
||||
Str::slug((string) env('APP*NAME', 'laravel'), '*').'_cache_';
|
||||
Str::slug((string) env('APP*NAME', 'laravel'), '*').'_database_';
|
||||
Str::slug((string) env('APP*NAME', 'laravel'), '*').'\_session';
|
||||
|
||||
// Laravel >= 13.x
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-cache-';
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-database-';
|
||||
Str::snake((string) env('APP_NAME', 'laravel')).'\_session';
|
||||
@endboostsnippet
|
||||
|
||||
To retain previous behavior, explicitly configure `CACHE_PREFIX`, `REDIS_PREFIX`, and `SESSION_COOKIE` in your environment.
|
||||
|
||||
### `Store` and `Repository` Contracts: `touch`
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
The cache contracts now include a `touch` method for extending item TTLs. If you maintain custom cache store implementations, you should add this method:
|
||||
|
||||
@boostsnippet('Cache Store Touch', 'php')
|
||||
// Illuminate\Contracts\Cache\Store
|
||||
public function touch($key, $seconds);
|
||||
@endboostsnippet
|
||||
|
||||
### Cache `serializable_classes` Configuration
|
||||
|
||||
**Likelihood Of Impact: Medium**
|
||||
|
||||
The default application `cache` configuration now includes a `serializable_classes` option set to `false`. This hardens cache unserialization behavior to help prevent PHP deserialization gadget chain attacks if your application's `APP_KEY` is leaked. If your application intentionally stores PHP objects in cache, you should explicitly list the classes that may be unserialized:
|
||||
|
||||
@boostsnippet('Cache Serializable Classes', 'php')
|
||||
'serializable_classes' => [
|
||||
App\Data\CachedDashboardStats::class,
|
||||
App\Support\CachedPricingSnapshot::class,
|
||||
],
|
||||
@endboostsnippet
|
||||
|
||||
If your application previously relied on unserializing arbitrary cached objects, you will need to migrate that usage to explicit class allow-lists or to non-object cache payloads (such as arrays).
|
||||
|
||||
## Container
|
||||
|
||||
### `Container::call` and Nullable Class Defaults
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
`Container::call` now respects nullable class parameter defaults when no binding exists, matching constructor injection behavior introduced in Laravel 12:
|
||||
|
||||
@boostsnippet('Container Call Nullable', 'php')
|
||||
$container->call(function (?Carbon $date = null) {
|
||||
return $date;
|
||||
});
|
||||
|
||||
// Laravel <= 12.x: Carbon instance
|
||||
// Laravel >= 13.x: null
|
||||
@endboostsnippet
|
||||
|
||||
If your method-call injection logic depended on the previous behavior, you may need to update it.
|
||||
|
||||
## Contracts
|
||||
|
||||
### `Dispatcher` Contract: `dispatchAfterResponse`
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
The `Illuminate\Contracts\Bus\Dispatcher` contract now includes the `dispatchAfterResponse($command, $handler = null)` method.
|
||||
|
||||
If you maintain a custom dispatcher implementation, add this method to your class.
|
||||
|
||||
### `ResponseFactory` Contract: `eventStream`
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
The `Illuminate\Contracts\Routing\ResponseFactory` contract now includes an `eventStream` signature.
|
||||
|
||||
If you maintain a custom implementation of this contract, you should add this method.
|
||||
|
||||
### `MustVerifyEmail` Contract: `markEmailAsUnverified`
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
The `Illuminate\Contracts\Auth\MustVerifyEmail` contract now includes `markEmailAsUnverified()`.
|
||||
|
||||
If you provide a custom implementation of this contract, add this method to remain compatible.
|
||||
|
||||
## Database
|
||||
|
||||
### MySQL `DELETE` Queries With `JOIN`, `ORDER BY`, and `LIMIT`
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
Laravel now compiles full `DELETE ... JOIN` queries including `ORDER BY` and `LIMIT` for MySQL grammar.
|
||||
|
||||
In previous versions, `ORDER BY` / `LIMIT` clauses could be silently ignored on joined deletes. In Laravel 13, these clauses are included in the generated SQL. As a result, database engines that do not support this syntax (such as standard MySQL / MariaDB variants) may now throw a `QueryException` instead of executing an unbounded delete.
|
||||
|
||||
## Eloquent
|
||||
|
||||
### Model Booting and Nested Instantiation
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
Creating a new model instance while that model is still booting is now disallowed and throws a `LogicException`.
|
||||
|
||||
This affects code that instantiates models from inside model `boot` methods or trait `boot*` methods:
|
||||
|
||||
@boostsnippet('Model Booting', 'php')
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// No longer allowed during booting...
|
||||
(new static())->getTable();
|
||||
|
||||
}
|
||||
@endboostsnippet
|
||||
|
||||
Move this logic outside the boot cycle to avoid nested booting.
|
||||
|
||||
### Polymorphic Pivot Table Name Generation
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
When table names are inferred for polymorphic pivot models using custom pivot model classes, Laravel now generates pluralized names.
|
||||
|
||||
If your application depended on the previous singular inferred names for morph pivot tables and used custom pivot classes, you should explicitly define the table name on your pivot model.
|
||||
|
||||
### Collection Model Serialization Restores Eager-Loaded Relations
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
When Eloquent model collections are serialized and restored (such as in queued jobs), eager-loaded relations are now restored for the collection's models.
|
||||
|
||||
If your code depended on relations not being present after deserialization, you may need to adjust that logic.
|
||||
|
||||
## HTTP Client
|
||||
|
||||
### HTTP Client `Response::throw` and `throwIf` Signatures
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
The HTTP client response methods now declare their callback parameters in the method signatures:
|
||||
|
||||
@boostsnippet('HTTP Client Throw Signatures', 'php')
|
||||
public function throw($callback = null);
|
||||
public function throwIf($condition, $callback = null);
|
||||
@endboostsnippet
|
||||
|
||||
If you override these methods in custom response classes, ensure your method signatures are compatible.
|
||||
|
||||
## Notifications
|
||||
|
||||
### Default Password Reset Subject
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
Laravel's default password reset mail subject has changed:
|
||||
|
||||
@boostsnippet('Password Reset Subject', 'text')
|
||||
// Laravel <= 12.x
|
||||
Reset Password Notification
|
||||
|
||||
// Laravel >= 13.x
|
||||
Reset your password
|
||||
@endboostsnippet
|
||||
|
||||
If your tests, assertions, or translation overrides depend on the previous default string, update them accordingly.
|
||||
|
||||
### Queued Notifications and Missing Models
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
Queued notifications now respect the `#[DeleteWhenMissingModels]` attribute and `$deleteWhenMissingModels` property defined on the notification class.
|
||||
|
||||
In previous versions, missing models could still cause queued notification jobs to fail in cases where you expected them to be deleted.
|
||||
|
||||
## Queue
|
||||
|
||||
### `JobAttempted` Event Exception Payload
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
The `Illuminate\Queue\Events\JobAttempted` event now exposes the exception object (or `null`) via `$exception`, replacing the previous boolean `$exceptionOccurred` property:
|
||||
|
||||
@boostsnippet('JobAttempted Event', 'php')
|
||||
// Laravel <= 12.x
|
||||
$event->exceptionOccurred;
|
||||
|
||||
// Laravel >= 13.x
|
||||
$event->exception;
|
||||
@endboostsnippet
|
||||
|
||||
If you listen for this event, update your listener code accordingly.
|
||||
|
||||
### `QueueBusy` Event Property Rename
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
The `Illuminate\Queue\Events\QueueBusy` event property `$connection` has been renamed to `$connectionName` for consistency with other queue events.
|
||||
|
||||
If your listeners reference `$connection`, update them to `$connectionName`.
|
||||
|
||||
### `Queue` Contract Method Additions
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
The `Illuminate\Contracts\Queue\Queue` contract now includes queue size inspection methods that were previously only declared in docblocks.
|
||||
|
||||
If you maintain custom queue driver implementations of this contract, add implementations for:
|
||||
|
||||
- `pendingSize`
|
||||
- `delayedSize`
|
||||
- `reservedSize`
|
||||
- `creationTimeOfOldestPendingJob`
|
||||
|
||||
## Routing
|
||||
|
||||
### Domain Route Registration Precedence
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
Routes with an explicit domain are now prioritized before non-domain routes in route matching.
|
||||
|
||||
This allows catch-all subdomain routes to behave consistently even when non-domain routes are registered earlier. If your application relied on previous registration precedence between domain and non-domain routes, review route matching behavior.
|
||||
|
||||
## Scheduling
|
||||
|
||||
### `withScheduling` Registration Timing
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
Schedules registered via `ApplicationBuilder::withScheduling()` are now deferred until `Schedule` is resolved.
|
||||
|
||||
If your application relied on immediate schedule registration timing during bootstrap, you may need to adjust that logic.
|
||||
|
||||
## Security
|
||||
|
||||
### Request Forgery Protection
|
||||
|
||||
**Likelihood Of Impact: High**
|
||||
|
||||
Laravel's CSRF middleware has been renamed from `VerifyCsrfToken` to `PreventRequestForgery`, and now includes request-origin verification using the `Sec-Fetch-Site` header.
|
||||
|
||||
`VerifyCsrfToken` and `ValidateCsrfToken` remain as deprecated aliases, but direct references should be updated to `PreventRequestForgery`, especially when excluding middleware in tests or route definitions:
|
||||
|
||||
@boostsnippet('CSRF Middleware Rename', 'php')
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
|
||||
// Laravel <= 12.x
|
||||
->withoutMiddleware([VerifyCsrfToken::class]);
|
||||
|
||||
// Laravel >= 13.x
|
||||
->withoutMiddleware([PreventRequestForgery::class]);
|
||||
@endboostsnippet
|
||||
|
||||
The middleware configuration API now also provides `preventRequestForgery(...)`.
|
||||
|
||||
## Support
|
||||
|
||||
### Manager `extend` Callback Binding
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
Custom driver closures registered via manager `extend` methods are now bound to the manager instance.
|
||||
|
||||
If you previously relied on another bound object (such as a service provider instance) as `$this` inside these callbacks, you should move those values into closure captures using `use (...)`.
|
||||
|
||||
### `Str` Factories Reset Between Tests
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
Laravel now resets custom `Str` factories during test teardown.
|
||||
|
||||
If your tests depended on custom UUID / ULID / random string factories persisting between test methods, you should set them in each relevant test or setup hook.
|
||||
|
||||
### `Js::from` Uses Unescaped Unicode By Default
|
||||
|
||||
**Likelihood Of Impact: Very Low**
|
||||
|
||||
`Illuminate\Support\Js::from` now uses `JSON_UNESCAPED_UNICODE` by default.
|
||||
|
||||
If your tests or frontend output comparisons depended on escaped Unicode sequences (for example `\u00e8`), update your expectations.
|
||||
|
||||
## Views
|
||||
|
||||
### Pagination Bootstrap View Names
|
||||
|
||||
**Likelihood Of Impact: Low**
|
||||
|
||||
The internal pagination view names for Bootstrap 3 defaults are now explicit:
|
||||
|
||||
@boostsnippet('Pagination Views', 'text')
|
||||
// Laravel <= 12.x
|
||||
pagination::default
|
||||
pagination::simple-default
|
||||
|
||||
// Laravel >= 13.x
|
||||
pagination::bootstrap-3
|
||||
pagination::simple-bootstrap-3
|
||||
@endboostsnippet
|
||||
|
||||
## Getting help
|
||||
|
||||
If you encounter issues during the upgrade:
|
||||
|
||||
- Check the [upgrade guide](https://laravel.com/docs/13.x/upgrade) for the latest details
|
||||
- Review the [GitHub comparison](https://github.com/laravel/laravel/compare/12.x...13.x) for skeleton changes
|
||||
|
|
@ -18,11 +18,11 @@ ### Generate Routes
|
|||
|
||||
Run after route changes if Vite plugin isn't installed:
|
||||
```bash
|
||||
vendor/bin/sail artisan wayfinder:generate --no-interaction
|
||||
php artisan wayfinder:generate --no-interaction
|
||||
```
|
||||
For form helpers, use `--with-form` flag:
|
||||
```bash
|
||||
vendor/bin/sail artisan wayfinder:generate --with-form --no-interaction
|
||||
php artisan wayfinder:generate --with-form --no-interaction
|
||||
```
|
||||
|
||||
### Import Patterns
|
||||
|
|
@ -69,7 +69,7 @@ ## Wayfinder + Inertia
|
|||
|
||||
## Verification
|
||||
|
||||
1. Run `vendor/bin/sail artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
|
||||
1. Run `php artisan wayfinder:generate` to regenerate routes if Vite plugin isn't installed
|
||||
2. Check TypeScript imports resolve correctly
|
||||
3. Verify route URLs match expected paths
|
||||
|
||||
|
|
|
|||
|
|
@ -114,4 +114,17 @@
|
|||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Serializable Classes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the classes that can be unserialized from cache
|
||||
| storage. By default, no PHP classes will be unserialized from your
|
||||
| cache to prevent gadget chain attacks if your APP_KEY is leaked.
|
||||
|
|
||||
*/
|
||||
|
||||
'serializable_classes' => false,
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -214,4 +214,20 @@
|
|||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Serialization
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the serialization strategy for session data, which
|
||||
| is JSON by default. Setting this to "php" allows the storage of PHP
|
||||
| objects in the session but can make an application vulnerable to
|
||||
| "gadget chain" serialization attacks if the APP_KEY is leaked.
|
||||
|
|
||||
| Supported: "json", "php"
|
||||
|
|
||||
*/
|
||||
|
||||
'serialization' => 'json',
|
||||
|
||||
];
|
||||
|
|
|
|||
1
lang/php_en.json
Normal file
1
lang/php_en.json
Normal file
File diff suppressed because one or more lines are too long
1
lang/php_pt-br.json
Normal file
1
lang/php_pt-br.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue