initial commit
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
name: fortify-development
|
||||||
|
description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.'
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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
|
||||||
|
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
|
||||||
|
- [ ] 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 for session-based SPA authentication
|
||||||
|
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
|
||||||
|
- [ ] Set up CSRF token handling
|
||||||
|
- [ ] Test XHR authentication flows
|
||||||
|
```
|
||||||
|
|
||||||
|
> Use `search-docs` for integration and SPA authentication patterns.
|
||||||
|
|
||||||
|
#### Two-Factor Authentication in SPA Mode
|
||||||
|
|
||||||
|
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
|
||||||
|
|
||||||
|
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"two_factor": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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` |
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
---
|
||||||
|
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."
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Page Components Location
|
||||||
|
|
||||||
|
Vue page components should be placed in the `resources/js/pages` directory.
|
||||||
|
|
||||||
|
### Page Component Structure
|
||||||
|
|
||||||
|
<!-- Basic Vue Page Component -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
users: Array
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Users</h1>
|
||||||
|
<ul>
|
||||||
|
<li v-for="user in users" :key="user.id">
|
||||||
|
{{ user.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client-Side Navigation
|
||||||
|
|
||||||
|
### Basic Link Component
|
||||||
|
|
||||||
|
Use `<Link>` for client-side navigation instead of traditional `<a>` tags:
|
||||||
|
|
||||||
|
<!-- Inertia Vue Navigation -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { Link } from '@inertiajs/vue3'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Link href="/">Home</Link>
|
||||||
|
<Link href="/users">Users</Link>
|
||||||
|
<Link :href="`/users/${user.id}`">View User</Link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link with Method
|
||||||
|
|
||||||
|
<!-- Link with POST Method -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { Link } from '@inertiajs/vue3'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Link href="/logout" method="post" as="button">
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prefetching
|
||||||
|
|
||||||
|
Prefetch pages to improve perceived performance:
|
||||||
|
|
||||||
|
<!-- Prefetch on Hover -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { Link } from '@inertiajs/vue3'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Link href="/users" prefetch>
|
||||||
|
Users
|
||||||
|
</Link>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Navigation
|
||||||
|
|
||||||
|
<!-- Router Visit -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { router } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
router.visit('/users')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or with options
|
||||||
|
function createUser() {
|
||||||
|
router.visit('/users', {
|
||||||
|
method: 'post',
|
||||||
|
data: { name: 'John' },
|
||||||
|
onSuccess: () => console.log('Done'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Link href="/users">Users</Link>
|
||||||
|
<Link href="/logout" method="post" as="button">Logout</Link>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Handling
|
||||||
|
|
||||||
|
### Form Component (Recommended)
|
||||||
|
|
||||||
|
The recommended way to build forms is with the `<Form>` component:
|
||||||
|
|
||||||
|
<!-- Form Component Example -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { Form } from '@inertiajs/vue3'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form action="/users" method="post" #default="{ errors, processing, wasSuccessful }">
|
||||||
|
<input type="text" name="name" />
|
||||||
|
<div v-if="errors.name">{{ errors.name }}</div>
|
||||||
|
|
||||||
|
<input type="email" name="email" />
|
||||||
|
<div v-if="errors.email">{{ errors.email }}</div>
|
||||||
|
|
||||||
|
<button type="submit" :disabled="processing">
|
||||||
|
{{ processing ? 'Creating...' : 'Create User' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="wasSuccessful">User created!</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Component With All Props
|
||||||
|
|
||||||
|
<!-- Form Component Full Example -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { Form } from '@inertiajs/vue3'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
action="/users"
|
||||||
|
method="post"
|
||||||
|
#default="{
|
||||||
|
errors,
|
||||||
|
hasErrors,
|
||||||
|
processing,
|
||||||
|
progress,
|
||||||
|
wasSuccessful,
|
||||||
|
recentlySuccessful,
|
||||||
|
setError,
|
||||||
|
clearErrors,
|
||||||
|
resetAndClearErrors,
|
||||||
|
defaults,
|
||||||
|
isDirty,
|
||||||
|
reset,
|
||||||
|
submit
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<input type="text" name="name" :value="defaults.name" />
|
||||||
|
<div v-if="errors.name">{{ errors.name }}</div>
|
||||||
|
|
||||||
|
<button type="submit" :disabled="processing">
|
||||||
|
{{ processing ? 'Saving...' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<progress v-if="progress" :value="progress.percentage" max="100">
|
||||||
|
{{ progress.percentage }}%
|
||||||
|
</progress>
|
||||||
|
|
||||||
|
<div v-if="wasSuccessful">Saved!</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Component Reset Props
|
||||||
|
|
||||||
|
The `<Form>` component supports automatic resetting:
|
||||||
|
|
||||||
|
- `resetOnError` - Reset form data when the request fails
|
||||||
|
- `resetOnSuccess` - Reset form data when the request succeeds
|
||||||
|
- `setDefaultsOnSuccess` - Update default values on success
|
||||||
|
|
||||||
|
Use the `search-docs` tool with a query of `form component resetting` for detailed guidance.
|
||||||
|
|
||||||
|
<!-- Form with Reset Props -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { Form } from '@inertiajs/vue3'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
action="/users"
|
||||||
|
method="post"
|
||||||
|
reset-on-success
|
||||||
|
set-defaults-on-success
|
||||||
|
#default="{ errors, processing, wasSuccessful }"
|
||||||
|
>
|
||||||
|
<input type="text" name="name" />
|
||||||
|
<div v-if="errors.name">{{ errors.name }}</div>
|
||||||
|
|
||||||
|
<button type="submit" :disabled="processing">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Forms can also be built using the `useForm` composable for more programmatic control. Use the `search-docs` tool with a query of `useForm helper` for guidance.
|
||||||
|
|
||||||
|
### `useForm` Composable
|
||||||
|
|
||||||
|
For more programmatic control or to follow existing conventions, use the `useForm` composable:
|
||||||
|
|
||||||
|
<!-- useForm Composable Example -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { useForm } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
form.post('/users', {
|
||||||
|
onSuccess: () => form.reset('password'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<input type="text" v-model="form.name" />
|
||||||
|
<div v-if="form.errors.name">{{ form.errors.name }}</div>
|
||||||
|
|
||||||
|
<input type="email" v-model="form.email" />
|
||||||
|
<div v-if="form.errors.email">{{ form.errors.email }}</div>
|
||||||
|
|
||||||
|
<input type="password" v-model="form.password" />
|
||||||
|
<div v-if="form.errors.password">{{ form.errors.password }}</div>
|
||||||
|
|
||||||
|
<button type="submit" :disabled="form.processing">
|
||||||
|
Create User
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deferred Props
|
||||||
|
|
||||||
|
Use deferred props to load data after initial page render:
|
||||||
|
|
||||||
|
<!-- Deferred Props with Empty State -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
users: Array
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Users</h1>
|
||||||
|
<div v-if="!users" class="animate-pulse">
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
<ul v-else>
|
||||||
|
<li v-for="user in users" :key="user.id">
|
||||||
|
{{ user.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Polling
|
||||||
|
|
||||||
|
Use the `usePoll` composable to automatically refresh data at intervals. It handles cleanup on unmount and throttles polling when the tab is inactive.
|
||||||
|
|
||||||
|
<!-- Basic Polling -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { usePoll } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
stats: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
usePoll(5000)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<div>Active Users: {{ stats.activeUsers }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Polling With Request Options and Manual Control -->
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { usePoll } from '@inertiajs/vue3'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
stats: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const { start, stop } = usePoll(5000, {
|
||||||
|
only: ['stats'],
|
||||||
|
onStart() {
|
||||||
|
console.log('Polling request started')
|
||||||
|
},
|
||||||
|
onFinish() {
|
||||||
|
console.log('Polling request finished')
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
autoStart: false,
|
||||||
|
keepAlive: true,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<div>Active Users: {{ stats.activeUsers }}</div>
|
||||||
|
<button @click="start">Start Polling</button>
|
||||||
|
<button @click="stop">Stop Polling</button>
|
||||||
|
</div>
|
||||||
|
</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
|
||||||
|
|
||||||
|
### WhenVisible
|
||||||
|
|
||||||
|
Lazy-load a prop when an element scrolls into view. Useful for deferring expensive data that sits below the fold:
|
||||||
|
|
||||||
|
<!-- WhenVisible Example -->
|
||||||
|
```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 v-for="user in users.data" :key="user.id">
|
||||||
|
{{ user.name }}
|
||||||
|
</div>
|
||||||
|
</InfiniteScroll>
|
||||||
|
</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.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Using traditional `<a>` links instead of Inertia's `<Link>` component (breaks SPA behavior)
|
||||||
|
- Forgetting that Vue components must have a single root element
|
||||||
|
- 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
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
---
|
||||||
|
name: laravel-best-practices
|
||||||
|
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
|
||||||
|
# Laravel Best Practices
|
||||||
|
|
||||||
|
Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
|
||||||
|
|
||||||
|
## Consistency First
|
||||||
|
|
||||||
|
Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
|
||||||
|
|
||||||
|
Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### 1. Database Performance → `rules/db-performance.md`
|
||||||
|
|
||||||
|
- Eager load with `with()` to prevent N+1 queries
|
||||||
|
- Enable `Model::preventLazyLoading()` in development
|
||||||
|
- Select only needed columns, avoid `SELECT *`
|
||||||
|
- `chunk()` / `chunkById()` for large datasets
|
||||||
|
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
|
||||||
|
- `withCount()` instead of loading relations to count
|
||||||
|
- `cursor()` for memory-efficient read-only iteration
|
||||||
|
- Never query in Blade templates
|
||||||
|
|
||||||
|
### 2. Advanced Query Patterns → `rules/advanced-queries.md`
|
||||||
|
|
||||||
|
- `addSelect()` subqueries over eager-loading entire has-many for a single value
|
||||||
|
- Dynamic relationships via subquery FK + `belongsTo`
|
||||||
|
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
|
||||||
|
- `setRelation()` to prevent circular N+1 queries
|
||||||
|
- `whereIn` + `pluck()` over `whereHas` for better index usage
|
||||||
|
- Two simple queries can beat one complex query
|
||||||
|
- Compound indexes matching `orderBy` column order
|
||||||
|
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
|
||||||
|
|
||||||
|
### 3. Security → `rules/security.md`
|
||||||
|
|
||||||
|
- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
|
||||||
|
- No raw SQL with user input — use Eloquent or query builder
|
||||||
|
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
|
||||||
|
- Validate MIME type, extension, and size for file uploads
|
||||||
|
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
|
||||||
|
|
||||||
|
### 4. Caching → `rules/caching.md`
|
||||||
|
|
||||||
|
- `Cache::remember()` over manual get/put
|
||||||
|
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
|
||||||
|
- `Cache::memo()` to avoid redundant cache hits within a request
|
||||||
|
- Cache tags to invalidate related groups
|
||||||
|
- `Cache::add()` for atomic conditional writes
|
||||||
|
- `once()` to memoize per-request or per-object lifetime
|
||||||
|
- `Cache::lock()` / `lockForUpdate()` for race conditions
|
||||||
|
- Failover cache stores in production
|
||||||
|
|
||||||
|
### 5. Eloquent Patterns → `rules/eloquent.md`
|
||||||
|
|
||||||
|
- Correct relationship types with return type hints
|
||||||
|
- Local scopes for reusable query constraints
|
||||||
|
- Global scopes sparingly — document their existence
|
||||||
|
- Attribute casts in the `casts()` method
|
||||||
|
- Cast date columns, use Carbon instances in templates
|
||||||
|
- `whereBelongsTo($model)` for cleaner queries
|
||||||
|
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
|
||||||
|
|
||||||
|
### 6. Validation & Forms → `rules/validation.md`
|
||||||
|
|
||||||
|
- Form Request classes, not inline validation
|
||||||
|
- Array notation `['required', 'email']` for new code; follow existing convention
|
||||||
|
- `$request->validated()` only — never `$request->all()`
|
||||||
|
- `Rule::when()` for conditional validation
|
||||||
|
- `after()` instead of `withValidator()`
|
||||||
|
|
||||||
|
### 7. Configuration → `rules/config.md`
|
||||||
|
|
||||||
|
- `env()` only inside config files
|
||||||
|
- `App::environment()` or `app()->isProduction()`
|
||||||
|
- Config, lang files, and constants over hardcoded text
|
||||||
|
|
||||||
|
### 8. Testing Patterns → `rules/testing.md`
|
||||||
|
|
||||||
|
- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
|
||||||
|
- `assertModelExists()` over raw `assertDatabaseHas()`
|
||||||
|
- Factory states and sequences over manual overrides
|
||||||
|
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
|
||||||
|
- `recycle()` to share relationship instances across factories
|
||||||
|
|
||||||
|
### 9. Queue & Job Patterns → `rules/queue-jobs.md`
|
||||||
|
|
||||||
|
- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
|
||||||
|
- `ShouldBeUnique` to prevent duplicates; `WithoutOverlapping::untilProcessing()` for concurrency
|
||||||
|
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
|
||||||
|
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
|
||||||
|
- Horizon for complex multi-queue scenarios
|
||||||
|
|
||||||
|
### 10. Routing & Controllers → `rules/routing.md`
|
||||||
|
|
||||||
|
- Implicit route model binding
|
||||||
|
- Scoped bindings for nested resources
|
||||||
|
- `Route::resource()` or `apiResource()`
|
||||||
|
- Methods under 10 lines — extract to actions/services
|
||||||
|
- Type-hint Form Requests for auto-validation
|
||||||
|
|
||||||
|
### 11. HTTP Client → `rules/http-client.md`
|
||||||
|
|
||||||
|
- Explicit `timeout` and `connectTimeout` on every request
|
||||||
|
- `retry()` with exponential backoff for external APIs
|
||||||
|
- Check response status or use `throw()`
|
||||||
|
- `Http::pool()` for concurrent independent requests
|
||||||
|
- `Http::fake()` and `preventStrayRequests()` in tests
|
||||||
|
|
||||||
|
### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
|
||||||
|
|
||||||
|
- Event discovery over manual registration; `event:cache` in production
|
||||||
|
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
|
||||||
|
- Queue notifications and mailables with `ShouldQueue`
|
||||||
|
- On-demand notifications for non-user recipients
|
||||||
|
- `HasLocalePreference` on notifiable models
|
||||||
|
- `assertQueued()` not `assertSent()` for queued mailables
|
||||||
|
- Markdown mailables for transactional emails
|
||||||
|
|
||||||
|
### 13. Error Handling → `rules/error-handling.md`
|
||||||
|
|
||||||
|
- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
|
||||||
|
- `ShouldntReport` for exceptions that should never log
|
||||||
|
- Throttle high-volume exceptions to protect log sinks
|
||||||
|
- `dontReportDuplicates()` for multi-catch scenarios
|
||||||
|
- Force JSON rendering for API routes
|
||||||
|
- Structured context via `context()` on exception classes
|
||||||
|
|
||||||
|
### 14. Task Scheduling → `rules/scheduling.md`
|
||||||
|
|
||||||
|
- `withoutOverlapping()` on variable-duration tasks
|
||||||
|
- `onOneServer()` on multi-server deployments
|
||||||
|
- `runInBackground()` for concurrent long tasks
|
||||||
|
- `environments()` to restrict to appropriate environments
|
||||||
|
- `takeUntilTimeout()` for time-bounded processing
|
||||||
|
- Schedule groups for shared configuration
|
||||||
|
|
||||||
|
### 15. Architecture → `rules/architecture.md`
|
||||||
|
|
||||||
|
- Single-purpose Action classes; dependency injection over `app()` helper
|
||||||
|
- Prefer official Laravel packages and follow conventions, don't override defaults
|
||||||
|
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
|
||||||
|
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
|
||||||
|
|
||||||
|
### 16. Migrations → `rules/migrations.md`
|
||||||
|
|
||||||
|
- Generate migrations with `php artisan make:migration`
|
||||||
|
- `constrained()` for foreign keys
|
||||||
|
- Never modify migrations that have run in production
|
||||||
|
- Add indexes in the migration, not as an afterthought
|
||||||
|
- Mirror column defaults in model `$attributes`
|
||||||
|
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
|
||||||
|
- One concern per migration — never mix DDL and DML
|
||||||
|
|
||||||
|
### 17. Collections → `rules/collections.md`
|
||||||
|
|
||||||
|
- Higher-order messages for simple collection operations
|
||||||
|
- `cursor()` vs. `lazy()` — choose based on relationship needs
|
||||||
|
- `lazyById()` when updating records while iterating
|
||||||
|
- `toQuery()` for bulk operations on collections
|
||||||
|
|
||||||
|
### 18. Blade & Views → `rules/blade-views.md`
|
||||||
|
|
||||||
|
- `$attributes->merge()` in component templates
|
||||||
|
- Blade components over `@include`; `@pushOnce` for per-component scripts
|
||||||
|
- View Composers for shared view data
|
||||||
|
- `@aware` for deeply nested component props
|
||||||
|
|
||||||
|
### 19. Conventions & Style → `rules/style.md`
|
||||||
|
|
||||||
|
- Follow Laravel naming conventions for all entities
|
||||||
|
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
|
||||||
|
- No JS/CSS in Blade, no HTML in PHP classes
|
||||||
|
- Code should be readable; comments only for config files
|
||||||
|
|
||||||
|
## How to Apply
|
||||||
|
|
||||||
|
Always use a sub-agent to read rule files and explore this skill's content.
|
||||||
|
|
||||||
|
1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
|
||||||
|
2. Check sibling files for existing patterns — follow those first per Consistency First
|
||||||
|
3. Verify API syntax with `search-docs` for the installed Laravel version
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Advanced Query Patterns
|
||||||
|
|
||||||
|
## Use `addSelect()` Subqueries for Single Values from Has-Many
|
||||||
|
|
||||||
|
Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function scopeWithLastLoginAt($query): void
|
||||||
|
{
|
||||||
|
$query->addSelect([
|
||||||
|
'last_login_at' => Login::select('created_at')
|
||||||
|
->whereColumn('user_id', 'users.id')
|
||||||
|
->latest()
|
||||||
|
->take(1),
|
||||||
|
])->withCasts(['last_login_at' => 'datetime']);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create Dynamic Relationships via Subquery FK
|
||||||
|
|
||||||
|
Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function lastLogin(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Login::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeWithLastLogin($query): void
|
||||||
|
{
|
||||||
|
$query->addSelect([
|
||||||
|
'last_login_id' => Login::select('id')
|
||||||
|
->whereColumn('user_id', 'users.id')
|
||||||
|
->latest()
|
||||||
|
->take(1),
|
||||||
|
])->with('lastLogin');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Conditional Aggregates Instead of Multiple Count Queries
|
||||||
|
|
||||||
|
Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$statuses = Feature::toBase()
|
||||||
|
->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
|
||||||
|
->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
|
||||||
|
->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
|
||||||
|
->first();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `setRelation()` to Prevent Circular N+1
|
||||||
|
|
||||||
|
When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$feature->load('comments.user');
|
||||||
|
$feature->comments->each->setRelation('feature', $feature);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prefer `whereIn` + Subquery Over `whereHas`
|
||||||
|
|
||||||
|
`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
|
||||||
|
|
||||||
|
Incorrect (correlated EXISTS re-executes per row):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (index-friendly subquery, no PHP memory overhead):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sometimes Two Simple Queries Beat One Complex Query
|
||||||
|
|
||||||
|
Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
|
||||||
|
|
||||||
|
## Use Compound Indexes Matching `orderBy` Column Order
|
||||||
|
|
||||||
|
When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Migration
|
||||||
|
$table->index(['last_name', 'first_name']);
|
||||||
|
|
||||||
|
// Query — column order must match the index
|
||||||
|
User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Correlated Subqueries for Has-Many Ordering
|
||||||
|
|
||||||
|
When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function scopeOrderByLastLogin($query): void
|
||||||
|
{
|
||||||
|
$query->orderByDesc(Login::select('created_at')
|
||||||
|
->whereColumn('user_id', 'users.id')
|
||||||
|
->latest()
|
||||||
|
->take(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
# Architecture Best Practices
|
||||||
|
|
||||||
|
## Single-Purpose Action Classes
|
||||||
|
|
||||||
|
Extract discrete business operations into invokable Action classes.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class CreateOrderAction
|
||||||
|
{
|
||||||
|
public function __construct(private InventoryService $inventory) {}
|
||||||
|
|
||||||
|
public function execute(array $data): Order
|
||||||
|
{
|
||||||
|
$order = Order::create($data);
|
||||||
|
$this->inventory->reserve($order);
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Dependency Injection
|
||||||
|
|
||||||
|
Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
class OrderController extends Controller
|
||||||
|
{
|
||||||
|
public function store(StoreOrderRequest $request)
|
||||||
|
{
|
||||||
|
$service = app(OrderService::class);
|
||||||
|
|
||||||
|
return $service->create($request->validated());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
class OrderController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private OrderService $service) {}
|
||||||
|
|
||||||
|
public function store(StoreOrderRequest $request)
|
||||||
|
{
|
||||||
|
return $this->service->create($request->validated());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code to Interfaces
|
||||||
|
|
||||||
|
Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
|
||||||
|
|
||||||
|
Incorrect (concrete dependency):
|
||||||
|
```php
|
||||||
|
class OrderService
|
||||||
|
{
|
||||||
|
public function __construct(private StripeGateway $gateway) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (interface dependency):
|
||||||
|
```php
|
||||||
|
interface PaymentGateway
|
||||||
|
{
|
||||||
|
public function charge(int $amount, string $customerId): PaymentResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderService
|
||||||
|
{
|
||||||
|
public function __construct(private PaymentGateway $gateway) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bind in a service provider:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$this->app->bind(PaymentGateway::class, StripeGateway::class);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Sort by Descending
|
||||||
|
|
||||||
|
When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$posts = Post::paginate();
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
$posts = Post::latest()->paginate();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Atomic Locks for Race Conditions
|
||||||
|
|
||||||
|
Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
|
||||||
|
$order->process();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or at query level
|
||||||
|
$product = Product::where('id', $id)->lockForUpdate()->first();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `mb_*` String Functions
|
||||||
|
|
||||||
|
When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
strlen('José'); // 5 (bytes, not characters)
|
||||||
|
strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
mb_strlen('José'); // 4 (characters)
|
||||||
|
mb_strtolower('MÜNCHEN'); // 'münchen'
|
||||||
|
|
||||||
|
// Prefer Laravel's Str helpers when available
|
||||||
|
Str::length('José'); // 4
|
||||||
|
Str::lower('MÜNCHEN'); // 'münchen'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `defer()` for Post-Response Work
|
||||||
|
|
||||||
|
For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
|
||||||
|
|
||||||
|
Incorrect (job overhead for trivial work):
|
||||||
|
```php
|
||||||
|
dispatch(new LogPageView($page));
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (runs after response, same process):
|
||||||
|
```php
|
||||||
|
defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
|
||||||
|
```
|
||||||
|
|
||||||
|
Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
|
||||||
|
|
||||||
|
## Use `Context` for Request-Scoped Data
|
||||||
|
|
||||||
|
The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In middleware
|
||||||
|
Context::add('tenant_id', $request->header('X-Tenant-ID'));
|
||||||
|
|
||||||
|
// Anywhere later — controllers, jobs, log context
|
||||||
|
$tenantId = Context::get('tenant_id');
|
||||||
|
```
|
||||||
|
|
||||||
|
Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
|
||||||
|
|
||||||
|
## Use `Concurrency::run()` for Parallel Execution
|
||||||
|
|
||||||
|
Run independent operations in parallel using child processes — no async libraries needed.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Illuminate\Support\Facades\Concurrency;
|
||||||
|
|
||||||
|
[$users, $orders] = Concurrency::run([
|
||||||
|
fn () => User::count(),
|
||||||
|
fn () => Order::where('status', 'pending')->count(),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
|
||||||
|
|
||||||
|
## Convention Over Configuration
|
||||||
|
|
||||||
|
Follow Laravel conventions. Don't override defaults unnecessarily.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
class Customer extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'Customer';
|
||||||
|
protected $primaryKey = 'customer_id';
|
||||||
|
|
||||||
|
public function roles(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
class Customer extends Model
|
||||||
|
{
|
||||||
|
public function roles(): BelongsToMany
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Role::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Blade & Views Best Practices
|
||||||
|
|
||||||
|
## Use `$attributes->merge()` in Component Templates
|
||||||
|
|
||||||
|
Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
|
||||||
|
|
||||||
|
```blade
|
||||||
|
<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
|
||||||
|
{{ $message }}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `@pushOnce` for Per-Component Scripts
|
||||||
|
|
||||||
|
If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
|
||||||
|
|
||||||
|
## Prefer Blade Components Over `@include`
|
||||||
|
|
||||||
|
`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
|
||||||
|
|
||||||
|
## Use View Composers for Shared View Data
|
||||||
|
|
||||||
|
If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
|
||||||
|
|
||||||
|
## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
|
||||||
|
|
||||||
|
A single view can return either the full page or just a fragment, keeping routing clean.
|
||||||
|
|
||||||
|
```php
|
||||||
|
return view('dashboard', compact('users'))
|
||||||
|
->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `@aware` for Deeply Nested Component Props
|
||||||
|
|
||||||
|
Avoids re-passing parent props through every level of nested components.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Caching Best Practices
|
||||||
|
|
||||||
|
## Use `Cache::remember()` Instead of Manual Get/Put
|
||||||
|
|
||||||
|
Atomic pattern prevents race conditions and removes boilerplate.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$val = Cache::get('stats');
|
||||||
|
if (! $val) {
|
||||||
|
$val = $this->computeStats();
|
||||||
|
Cache::put('stats', $val, 60);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
$val = Cache::remember('stats', 60, fn () => $this->computeStats());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `Cache::flexible()` for Stale-While-Revalidate
|
||||||
|
|
||||||
|
On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
|
||||||
|
|
||||||
|
Incorrect: `Cache::remember('users', 300, fn () => User::all());`
|
||||||
|
|
||||||
|
Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
|
||||||
|
|
||||||
|
## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
|
||||||
|
|
||||||
|
If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
|
||||||
|
|
||||||
|
`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
|
||||||
|
|
||||||
|
## Use Cache Tags to Invalidate Related Groups
|
||||||
|
|
||||||
|
Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Cache::tags(['user-1'])->flush();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `Cache::add()` for Atomic Conditional Writes
|
||||||
|
|
||||||
|
`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
|
||||||
|
|
||||||
|
Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
|
||||||
|
|
||||||
|
Correct: `Cache::add('lock', true, 10);`
|
||||||
|
|
||||||
|
## Use `once()` for Per-Request Memoization
|
||||||
|
|
||||||
|
`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function roles(): Collection
|
||||||
|
{
|
||||||
|
return once(fn () => $this->loadRoles());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
|
||||||
|
|
||||||
|
## Configure Failover Cache Stores in Production
|
||||||
|
|
||||||
|
If Redis goes down, the app falls back to a secondary store automatically.
|
||||||
|
|
||||||
|
```php
|
||||||
|
'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
|
||||||
|
```
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Collection Best Practices
|
||||||
|
|
||||||
|
## Use Higher-Order Messages for Simple Operations
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$users->each(function (User $user) {
|
||||||
|
$user->markAsVip();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct: `$users->each->markAsVip();`
|
||||||
|
|
||||||
|
Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
|
||||||
|
|
||||||
|
## Choose `cursor()` vs. `lazy()` Correctly
|
||||||
|
|
||||||
|
- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
|
||||||
|
- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
|
||||||
|
|
||||||
|
Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
|
||||||
|
|
||||||
|
Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
|
||||||
|
|
||||||
|
## Use `lazyById()` When Updating Records While Iterating
|
||||||
|
|
||||||
|
`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
|
||||||
|
|
||||||
|
## Use `toQuery()` for Bulk Operations on Collections
|
||||||
|
|
||||||
|
Avoids manual `whereIn` construction.
|
||||||
|
|
||||||
|
Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
|
||||||
|
|
||||||
|
Correct: `$users->toQuery()->update([...]);`
|
||||||
|
|
||||||
|
## Use `#[CollectedBy]` for Custom Collection Classes
|
||||||
|
|
||||||
|
More declarative than overriding `newCollection()`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[CollectedBy(UserCollection::class)]
|
||||||
|
class User extends Model {}
|
||||||
|
```
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Configuration Best Practices
|
||||||
|
|
||||||
|
## `env()` Only in Config Files
|
||||||
|
|
||||||
|
Direct `env()` calls return `null` when config is cached.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$key = env('API_KEY');
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
// config/services.php
|
||||||
|
'key' => env('API_KEY'),
|
||||||
|
|
||||||
|
// Application code
|
||||||
|
$key = config('services.key');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Encrypted Env or External Secrets
|
||||||
|
|
||||||
|
Never store production secrets in plain `.env` files in version control.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```bash
|
||||||
|
|
||||||
|
# .env committed to repo or shared in Slack
|
||||||
|
|
||||||
|
STRIPE_SECRET=sk_live_abc123
|
||||||
|
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```bash
|
||||||
|
php artisan env:encrypt --env=production --readable
|
||||||
|
php artisan env:decrypt --env=production
|
||||||
|
```
|
||||||
|
|
||||||
|
For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
|
||||||
|
|
||||||
|
## Use `App::environment()` for Environment Checks
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
if (env('APP_ENV') === 'production') {
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
if (app()->isProduction()) {
|
||||||
|
// or
|
||||||
|
if (App::environment('production')) {
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Constants and Language Files
|
||||||
|
|
||||||
|
Use class constants instead of hardcoded magic strings for model states, types, and statuses.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Incorrect
|
||||||
|
return $this->type === 'normal';
|
||||||
|
|
||||||
|
// Correct
|
||||||
|
return $this->type === self::TYPE_NORMAL;
|
||||||
|
```
|
||||||
|
|
||||||
|
If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Only when lang files already exist in the project
|
||||||
|
return back()->with('message', __('app.article_added'));
|
||||||
|
```
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Database Performance Best Practices
|
||||||
|
|
||||||
|
## Always Eager Load Relationships
|
||||||
|
|
||||||
|
Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
|
||||||
|
|
||||||
|
Incorrect (N+1 — executes 1 + N queries):
|
||||||
|
```php
|
||||||
|
$posts = Post::all();
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
echo $post->author->name;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (2 queries total):
|
||||||
|
```php
|
||||||
|
$posts = Post::with('author')->get();
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
echo $post->author->name;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Constrain eager loads to select only needed columns (always include the foreign key):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$users = User::with(['posts' => function ($query) {
|
||||||
|
$query->select('id', 'user_id', 'title')
|
||||||
|
->where('published', true)
|
||||||
|
->latest()
|
||||||
|
->limit(10);
|
||||||
|
}])->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prevent Lazy Loading in Development
|
||||||
|
|
||||||
|
Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Model::preventLazyLoading(! app()->isProduction());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
|
||||||
|
|
||||||
|
## Select Only Needed Columns
|
||||||
|
|
||||||
|
Avoid `SELECT *` — especially when tables have large text or JSON columns.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$posts = Post::with('author')->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
$posts = Post::select('id', 'title', 'user_id', 'created_at')
|
||||||
|
->with(['author:id,name,avatar'])
|
||||||
|
->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
|
||||||
|
|
||||||
|
## Chunk Large Datasets
|
||||||
|
|
||||||
|
Never load thousands of records at once. Use chunking for batch processing.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$users = User::all();
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$user->notify(new WeeklyDigest);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
User::where('subscribed', true)->chunk(200, function ($users) {
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$user->notify(new WeeklyDigest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
|
||||||
|
|
||||||
|
```php
|
||||||
|
User::where('active', false)->chunkById(200, function ($users) {
|
||||||
|
$users->each->delete();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add Database Indexes
|
||||||
|
|
||||||
|
Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
Schema::create('orders', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained();
|
||||||
|
$table->string('status');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
Schema::create('orders', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->index()->constrained();
|
||||||
|
$table->string('status')->index();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['status', 'created_at']);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
|
||||||
|
|
||||||
|
## Use `withCount()` for Counting Relations
|
||||||
|
|
||||||
|
Never load entire collections just to count them.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$posts = Post::all();
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
echo $post->comments->count();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
$posts = Post::withCount('comments')->get();
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
echo $post->comments_count;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Conditional counting:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$posts = Post::withCount([
|
||||||
|
'comments',
|
||||||
|
'comments as approved_comments_count' => function ($query) {
|
||||||
|
$query->where('approved', true);
|
||||||
|
},
|
||||||
|
])->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `cursor()` for Memory-Efficient Iteration
|
||||||
|
|
||||||
|
For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$users = User::where('active', true)->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
foreach (User::where('active', true)->cursor() as $user) {
|
||||||
|
ProcessUser::dispatch($user->id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
|
||||||
|
|
||||||
|
## No Queries in Blade Templates
|
||||||
|
|
||||||
|
Never execute queries in Blade templates. Pass data from controllers.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```blade
|
||||||
|
@foreach (User::all() as $user)
|
||||||
|
{{ $user->profile->name }}
|
||||||
|
@endforeach
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
// Controller
|
||||||
|
$users = User::with('profile')->get();
|
||||||
|
return view('users.index', compact('users'));
|
||||||
|
```
|
||||||
|
|
||||||
|
```blade
|
||||||
|
@foreach ($users as $user)
|
||||||
|
{{ $user->profile->name }}
|
||||||
|
@endforeach
|
||||||
|
```
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Eloquent Best Practices
|
||||||
|
|
||||||
|
## Use Correct Relationship Types
|
||||||
|
|
||||||
|
Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function comments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Comment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Local Scopes for Reusable Queries
|
||||||
|
|
||||||
|
Extract reusable query constraints into local scopes to avoid duplication.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$active = User::where('verified', true)->whereNotNull('activated_at')->get();
|
||||||
|
$articles = Article::whereHas('user', function ($q) {
|
||||||
|
$q->where('verified', true)->whereNotNull('activated_at');
|
||||||
|
})->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
public function scopeActive(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('verified', true)->whereNotNull('activated_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
$active = User::active()->get();
|
||||||
|
$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Apply Global Scopes Sparingly
|
||||||
|
|
||||||
|
Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
|
||||||
|
|
||||||
|
Incorrect (global scope for a conditional filter):
|
||||||
|
```php
|
||||||
|
class PublishedScope implements Scope
|
||||||
|
{
|
||||||
|
public function apply(Builder $builder, Model $model): void
|
||||||
|
{
|
||||||
|
$builder->where('published', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now admin panels, reports, and background jobs all silently skip drafts
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (local scope you opt into):
|
||||||
|
```php
|
||||||
|
public function scopePublished(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('published', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Post::published()->paginate(); // Explicit
|
||||||
|
Post::paginate(); // Admin sees all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Define Attribute Casts
|
||||||
|
|
||||||
|
Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'metadata' => 'array',
|
||||||
|
'total' => 'decimal:2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cast Date Columns Properly
|
||||||
|
|
||||||
|
Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```blade
|
||||||
|
{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ordered_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```blade
|
||||||
|
{{ $order->ordered_at->toDateString() }}
|
||||||
|
{{ $order->ordered_at->format('m-d') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `whereBelongsTo()` for Relationship Queries
|
||||||
|
|
||||||
|
Cleaner than manually specifying foreign keys.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
Post::where('user_id', $user->id)->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
Post::whereBelongsTo($user)->get();
|
||||||
|
Post::whereBelongsTo($user, 'author')->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avoid Hardcoded Table Names in Queries
|
||||||
|
|
||||||
|
Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
DB::table('users')->where('active', true)->get();
|
||||||
|
|
||||||
|
$query->join('companies', 'companies.id', '=', 'users.company_id');
|
||||||
|
|
||||||
|
DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct — reference the model's table:
|
||||||
|
```php
|
||||||
|
DB::table((new User)->getTable())->where('active', true)->get();
|
||||||
|
|
||||||
|
// Even better — use Eloquent or the query builder instead of raw SQL
|
||||||
|
User::where('active', true)->get();
|
||||||
|
Order::where('status', 'pending')->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
|
||||||
|
|
||||||
|
**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Error Handling Best Practices
|
||||||
|
|
||||||
|
## Exception Reporting and Rendering
|
||||||
|
|
||||||
|
There are two valid approaches — choose one and apply it consistently across the project.
|
||||||
|
|
||||||
|
**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
|
||||||
|
|
||||||
|
```php
|
||||||
|
class InvalidOrderException extends Exception
|
||||||
|
{
|
||||||
|
public function report(): void { /* custom reporting */ }
|
||||||
|
|
||||||
|
public function render(Request $request): Response
|
||||||
|
{
|
||||||
|
return response()->view('errors.invalid-order', status: 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
|
||||||
|
|
||||||
|
```php
|
||||||
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
|
$exceptions->report(function (InvalidOrderException $e) { /* ... */ });
|
||||||
|
$exceptions->render(function (InvalidOrderException $e, Request $request) {
|
||||||
|
return response()->view('errors.invalid-order', status: 422);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the existing codebase and follow whichever pattern is already established.
|
||||||
|
|
||||||
|
## Use `ShouldntReport` for Exceptions That Should Never Log
|
||||||
|
|
||||||
|
More discoverable than listing classes in `dontReport()`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class PodcastProcessingException extends Exception implements ShouldntReport {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Throttle High-Volume Exceptions
|
||||||
|
|
||||||
|
A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
|
||||||
|
|
||||||
|
## Enable `dontReportDuplicates()`
|
||||||
|
|
||||||
|
Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
|
||||||
|
|
||||||
|
## Force JSON Error Rendering for API Routes
|
||||||
|
|
||||||
|
Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
|
||||||
|
return $request->is('api/*') || $request->expectsJson();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add Context to Exception Classes
|
||||||
|
|
||||||
|
Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class InvalidOrderException extends Exception
|
||||||
|
{
|
||||||
|
public function context(): array
|
||||||
|
{
|
||||||
|
return ['order_id' => $this->orderId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Events & Notifications Best Practices
|
||||||
|
|
||||||
|
## Rely on Event Discovery
|
||||||
|
|
||||||
|
Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
|
||||||
|
|
||||||
|
## Run `event:cache` in Production Deploy
|
||||||
|
|
||||||
|
Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
|
||||||
|
|
||||||
|
## Use `ShouldDispatchAfterCommit` Inside Transactions
|
||||||
|
|
||||||
|
Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class OrderShipped implements ShouldDispatchAfterCommit {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Always Queue Notifications
|
||||||
|
|
||||||
|
Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class InvoicePaid extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `afterCommit()` on Notifications in Transactions
|
||||||
|
|
||||||
|
Same race condition as events — the queued notification job may run before the transaction commits.
|
||||||
|
|
||||||
|
## Route Notification Channels to Dedicated Queues
|
||||||
|
|
||||||
|
Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
|
||||||
|
|
||||||
|
## Use On-Demand Notifications for Non-User Recipients
|
||||||
|
|
||||||
|
Avoid creating dummy models to send notifications to arbitrary addresses.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implement `HasLocalePreference` on Notifiable Models
|
||||||
|
|
||||||
|
Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# HTTP Client Best Practices
|
||||||
|
|
||||||
|
## Always Set Explicit Timeouts
|
||||||
|
|
||||||
|
The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$response = Http::get('https://api.example.com/users');
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
$response = Http::timeout(5)
|
||||||
|
->connectTimeout(3)
|
||||||
|
->get('https://api.example.com/users');
|
||||||
|
```
|
||||||
|
|
||||||
|
For service-specific clients, define timeouts in a macro:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Http::macro('github', function () {
|
||||||
|
return Http::baseUrl('https://api.github.com')
|
||||||
|
->timeout(10)
|
||||||
|
->connectTimeout(3)
|
||||||
|
->withToken(config('services.github.token'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = Http::github()->get('/repos/laravel/framework');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Retry with Backoff for External APIs
|
||||||
|
|
||||||
|
External APIs have transient failures. Use `retry()` with increasing delays.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$response = Http::post('https://api.stripe.com/v1/charges', $data);
|
||||||
|
|
||||||
|
if ($response->failed()) {
|
||||||
|
throw new PaymentFailedException('Charge failed');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
$response = Http::retry([100, 500, 1000])
|
||||||
|
->timeout(10)
|
||||||
|
->post('https://api.stripe.com/v1/charges', $data);
|
||||||
|
```
|
||||||
|
|
||||||
|
Only retry on specific errors:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$response = Http::retry(3, 100, function (Exception $exception, PendingRequest $request) {
|
||||||
|
return $exception instanceof ConnectionException
|
||||||
|
|| ($exception instanceof RequestException && $exception->response->serverError());
|
||||||
|
})->post('https://api.example.com/data');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handle Errors Explicitly
|
||||||
|
|
||||||
|
The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$response = Http::get('https://api.example.com/users/1');
|
||||||
|
$user = $response->json(); // Could be an error body
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
$response = Http::timeout(5)
|
||||||
|
->get('https://api.example.com/users/1')
|
||||||
|
->throw();
|
||||||
|
|
||||||
|
$user = $response->json();
|
||||||
|
```
|
||||||
|
|
||||||
|
For graceful degradation:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$response = Http::get('https://api.example.com/users/1');
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
return $response->json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->notFound()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response->throw();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Request Pooling for Concurrent Requests
|
||||||
|
|
||||||
|
When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$users = Http::get('https://api.example.com/users')->json();
|
||||||
|
$posts = Http::get('https://api.example.com/posts')->json();
|
||||||
|
$comments = Http::get('https://api.example.com/comments')->json();
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
use Illuminate\Http\Client\Pool;
|
||||||
|
|
||||||
|
$responses = Http::pool(fn (Pool $pool) => [
|
||||||
|
$pool->as('users')->get('https://api.example.com/users'),
|
||||||
|
$pool->as('posts')->get('https://api.example.com/posts'),
|
||||||
|
$pool->as('comments')->get('https://api.example.com/comments'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$users = $responses['users']->json();
|
||||||
|
$posts = $responses['posts']->json();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fake HTTP Calls in Tests
|
||||||
|
|
||||||
|
Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
it('syncs user from API', function () {
|
||||||
|
$service = new UserSyncService;
|
||||||
|
$service->sync(1); // Hits the real API
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
it('syncs user from API', function () {
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'api.example.com/users/1' => Http::response([
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = new UserSyncService;
|
||||||
|
$service->sync(1);
|
||||||
|
|
||||||
|
Http::assertSent(function (Request $request) {
|
||||||
|
return $request->url() === 'https://api.example.com/users/1';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Test failure scenarios too:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Http::fake([
|
||||||
|
'api.example.com/*' => Http::failedConnection(),
|
||||||
|
]);
|
||||||
|
```
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Mail Best Practices
|
||||||
|
|
||||||
|
## Implement `ShouldQueue` on the Mailable Class
|
||||||
|
|
||||||
|
Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
|
||||||
|
|
||||||
|
## Use `afterCommit()` on Mailables Inside Transactions
|
||||||
|
|
||||||
|
A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
|
||||||
|
|
||||||
|
## Use `assertQueued()` Not `assertSent()` for Queued Mailables
|
||||||
|
|
||||||
|
`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence.
|
||||||
|
|
||||||
|
Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
|
||||||
|
|
||||||
|
Correct: `Mail::assertQueued(OrderShipped::class);`
|
||||||
|
|
||||||
|
## Use Markdown Mailables for Transactional Emails
|
||||||
|
|
||||||
|
Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
|
||||||
|
|
||||||
|
## Separate Content Tests from Sending Tests
|
||||||
|
|
||||||
|
Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
|
||||||
|
Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
|
||||||
|
Don't mix them — it conflates concerns and makes tests brittle.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Migration Best Practices
|
||||||
|
|
||||||
|
## Generate Migrations with Artisan
|
||||||
|
|
||||||
|
Always use `php artisan make:migration` for consistent naming and timestamps.
|
||||||
|
|
||||||
|
Incorrect (manually created file):
|
||||||
|
```php
|
||||||
|
// database/migrations/posts_migration.php ← wrong naming, no timestamp
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (Artisan-generated):
|
||||||
|
```bash
|
||||||
|
php artisan make:migration create_posts_table
|
||||||
|
php artisan make:migration add_slug_to_posts_table
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `constrained()` for Foreign Keys
|
||||||
|
|
||||||
|
Automatic naming and referential integrity.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
|
||||||
|
// Non-standard names
|
||||||
|
$table->foreignId('author_id')->constrained('users');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Never Modify Deployed Migrations
|
||||||
|
|
||||||
|
Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
|
||||||
|
|
||||||
|
Incorrect (editing a deployed migration):
|
||||||
|
```php
|
||||||
|
// 2024_01_01_create_posts_table.php — already in production
|
||||||
|
$table->string('slug')->unique(); // ← added after deployment
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (new migration to alter):
|
||||||
|
```php
|
||||||
|
// 2024_03_15_add_slug_to_posts_table.php
|
||||||
|
Schema::table('posts', function (Blueprint $table) {
|
||||||
|
$table->string('slug')->unique()->after('title');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add Indexes in the Migration
|
||||||
|
|
||||||
|
Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
Schema::create('orders', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained();
|
||||||
|
$table->string('status');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
Schema::create('orders', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->index();
|
||||||
|
$table->string('status')->index();
|
||||||
|
$table->timestamp('shipped_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mirror Defaults in Model `$attributes`
|
||||||
|
|
||||||
|
When a column has a database default, mirror it in the model so new instances have correct values before saving.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Migration
|
||||||
|
$table->string('status')->default('pending');
|
||||||
|
|
||||||
|
// Model
|
||||||
|
protected $attributes = [
|
||||||
|
'status' => 'pending',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Write Reversible `down()` Methods by Default
|
||||||
|
|
||||||
|
Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('posts', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('slug');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
|
||||||
|
|
||||||
|
## Keep Migrations Focused
|
||||||
|
|
||||||
|
One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
|
||||||
|
|
||||||
|
Incorrect (partial failure creates unrecoverable state):
|
||||||
|
```php
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('settings', function (Blueprint $table) { ... });
|
||||||
|
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (separate migrations):
|
||||||
|
```php
|
||||||
|
// Migration 1: create_settings_table
|
||||||
|
Schema::create('settings', function (Blueprint $table) { ... });
|
||||||
|
|
||||||
|
// Migration 2: seed_default_settings
|
||||||
|
DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
|
||||||
|
```
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Queue & Job Best Practices
|
||||||
|
|
||||||
|
## Set `retry_after` Greater Than `timeout`
|
||||||
|
|
||||||
|
If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
|
||||||
|
|
||||||
|
Incorrect (`retry_after` ≤ `timeout`):
|
||||||
|
```php
|
||||||
|
class ProcessReport implements ShouldQueue
|
||||||
|
{
|
||||||
|
public $timeout = 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
// config/queue.php — retry_after: 90 ← job retried while still running!
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (`retry_after` > `timeout`):
|
||||||
|
```php
|
||||||
|
class ProcessReport implements ShouldQueue
|
||||||
|
{
|
||||||
|
public $timeout = 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
// config/queue.php — retry_after: 180 ← safely longer than any job timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Exponential Backoff
|
||||||
|
|
||||||
|
Use progressively longer delays between retries to avoid hammering failing services.
|
||||||
|
|
||||||
|
Incorrect (fixed retry interval):
|
||||||
|
```php
|
||||||
|
class SyncWithStripe implements ShouldQueue
|
||||||
|
{
|
||||||
|
public $tries = 3;
|
||||||
|
// Default: retries immediately, overwhelming the API
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct (exponential backoff):
|
||||||
|
```php
|
||||||
|
class SyncWithStripe implements ShouldQueue
|
||||||
|
{
|
||||||
|
public $tries = 3;
|
||||||
|
public $backoff = [1, 5, 10];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implement `ShouldBeUnique`
|
||||||
|
|
||||||
|
Prevent duplicate job processing.
|
||||||
|
|
||||||
|
```php
|
||||||
|
class GenerateInvoice implements ShouldQueue, ShouldBeUnique
|
||||||
|
{
|
||||||
|
public function uniqueId(): string
|
||||||
|
{
|
||||||
|
return $this->order->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public $uniqueFor = 3600;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Always Implement `failed()`
|
||||||
|
|
||||||
|
Handle errors explicitly — don't rely on silent failure.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function failed(?Throwable $exception): void
|
||||||
|
{
|
||||||
|
$this->podcast->update(['status' => 'failed']);
|
||||||
|
Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limit External API Calls in Jobs
|
||||||
|
|
||||||
|
Use `RateLimited` middleware to throttle jobs calling third-party APIs.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function middleware(): array
|
||||||
|
{
|
||||||
|
return [new RateLimited('external-api')];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Related Jobs
|
||||||
|
|
||||||
|
Use `Bus::batch()` when jobs should succeed or fail together.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Bus::batch([
|
||||||
|
new ImportCsvChunk($chunk1),
|
||||||
|
new ImportCsvChunk($chunk2),
|
||||||
|
])
|
||||||
|
->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
|
||||||
|
->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
|
||||||
|
->dispatch();
|
||||||
|
```
|
||||||
|
|
||||||
|
## `retryUntil()` Needs `$tries = 0`
|
||||||
|
|
||||||
|
When using time-based retry limits, set `$tries = 0` to avoid premature failure.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public $tries = 0;
|
||||||
|
|
||||||
|
public function retryUntil(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return now()->addHours(4);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `WithoutOverlapping::untilProcessing()`
|
||||||
|
|
||||||
|
Prevents concurrent execution while allowing new instances to queue.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function middleware(): array
|
||||||
|
{
|
||||||
|
return [new WithoutOverlapping($this->product->id)->untilProcessing()];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `untilProcessing()`, the lock extends through queue wait time. With it, the lock releases when processing starts.
|
||||||
|
|
||||||
|
## Use Horizon for Complex Queue Scenarios
|
||||||
|
|
||||||
|
Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// config/horizon.php
|
||||||
|
'environments' => [
|
||||||
|
'production' => [
|
||||||
|
'supervisor-1' => [
|
||||||
|
'connection' => 'redis',
|
||||||
|
'queue' => ['high', 'default', 'low'],
|
||||||
|
'balance' => 'auto',
|
||||||
|
'minProcesses' => 1,
|
||||||
|
'maxProcesses' => 10,
|
||||||
|
'tries' => 3,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Routing & Controllers Best Practices
|
||||||
|
|
||||||
|
## Use Implicit Route Model Binding
|
||||||
|
|
||||||
|
Let Laravel resolve models automatically from route parameters.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
$post = Post::findOrFail($id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
public function show(Post $post)
|
||||||
|
{
|
||||||
|
return view('posts.show', ['post' => $post]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Scoped Bindings for Nested Resources
|
||||||
|
|
||||||
|
Enforce parent-child relationships automatically.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
|
||||||
|
// $post is automatically scoped to $user
|
||||||
|
})->scopeBindings();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Resource Controllers
|
||||||
|
|
||||||
|
Use `Route::resource()` or `apiResource()` for RESTful endpoints.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route::resource('posts', PostController::class);
|
||||||
|
Route::apiResource('api/posts', Api\PostController::class);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keep Controllers Thin
|
||||||
|
|
||||||
|
Aim for under 10 lines per method. Extract business logic to action or service classes.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([...]);
|
||||||
|
if ($request->hasFile('image')) {
|
||||||
|
$request->file('image')->move(public_path('images'));
|
||||||
|
}
|
||||||
|
$post = Post::create($validated);
|
||||||
|
$post->tags()->sync($validated['tags']);
|
||||||
|
event(new PostCreated($post));
|
||||||
|
return redirect()->route('posts.show', $post);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
public function store(StorePostRequest $request, CreatePostAction $create)
|
||||||
|
{
|
||||||
|
$post = $create->execute($request->validated());
|
||||||
|
|
||||||
|
return redirect()->route('posts.show', $post);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type-Hint Form Requests
|
||||||
|
|
||||||
|
Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
public function store(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => ['required', 'max:255'],
|
||||||
|
'body' => ['required'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Post::create($validated);
|
||||||
|
|
||||||
|
return redirect()->route('posts.index');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
public function store(StorePostRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
Post::create($request->validated());
|
||||||
|
|
||||||
|
return redirect()->route('posts.index');
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Task Scheduling Best Practices
|
||||||
|
|
||||||
|
## Use `withoutOverlapping()` on Variable-Duration Tasks
|
||||||
|
|
||||||
|
Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
|
||||||
|
|
||||||
|
## Use `onOneServer()` on Multi-Server Deployments
|
||||||
|
|
||||||
|
Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
|
||||||
|
|
||||||
|
## Use `runInBackground()` for Concurrent Long Tasks
|
||||||
|
|
||||||
|
By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
|
||||||
|
|
||||||
|
## Use `environments()` to Restrict Tasks
|
||||||
|
|
||||||
|
Prevent accidental execution of production-only tasks (billing, reporting) on staging.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Schedule::command('billing:charge')->monthly()->environments(['production']);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `takeUntilTimeout()` for Time-Bounded Processing
|
||||||
|
|
||||||
|
A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
|
||||||
|
|
||||||
|
## Use Schedule Groups for Shared Configuration
|
||||||
|
|
||||||
|
Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Schedule::daily()
|
||||||
|
->onOneServer()
|
||||||
|
->timezone('America/New_York')
|
||||||
|
->group(function () {
|
||||||
|
Schedule::command('emails:send --force');
|
||||||
|
Schedule::command('emails:prune');
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# Security Best Practices
|
||||||
|
|
||||||
|
## Mass Assignment Protection
|
||||||
|
|
||||||
|
Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
class User extends Model
|
||||||
|
{
|
||||||
|
protected $guarded = []; // All fields are mass assignable
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
class User extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Never use `$guarded = []` on models that accept user input.
|
||||||
|
|
||||||
|
## Authorize Every Action
|
||||||
|
|
||||||
|
Use policies or gates in controllers. Never skip authorization.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
public function update(Request $request, Post $post)
|
||||||
|
{
|
||||||
|
$post->update($request->validated());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
public function update(UpdatePostRequest $request, Post $post)
|
||||||
|
{
|
||||||
|
Gate::authorize('update', $post);
|
||||||
|
|
||||||
|
$post->update($request->validated());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via Form Request:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()->can('update', $this->route('post'));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prevent SQL Injection
|
||||||
|
|
||||||
|
Always use parameter binding. Never interpolate user input into queries.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
User::where('name', $request->name)->get();
|
||||||
|
|
||||||
|
// Raw expressions with bindings
|
||||||
|
User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Escape Output to Prevent XSS
|
||||||
|
|
||||||
|
Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```blade
|
||||||
|
{!! $user->bio !!}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```blade
|
||||||
|
{{ $user->bio }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSRF Protection
|
||||||
|
|
||||||
|
Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```blade
|
||||||
|
<form method="POST" action="/posts">
|
||||||
|
<input type="text" name="title">
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```blade
|
||||||
|
<form method="POST" action="/posts">
|
||||||
|
@csrf
|
||||||
|
<input type="text" name="title">
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limit Auth and API Routes
|
||||||
|
|
||||||
|
Apply `throttle` middleware to authentication and API routes.
|
||||||
|
|
||||||
|
```php
|
||||||
|
RateLimiter::for('login', function (Request $request) {
|
||||||
|
return Limit::perMinute(5)->by($request->ip());
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::post('/login', LoginController::class)->middleware('throttle:login');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validate File Uploads
|
||||||
|
|
||||||
|
Validate MIME type, extension, and size. Never trust client-provided filenames.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Store with generated filenames:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$path = $request->file('avatar')->store('avatars', 'public');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keep Secrets Out of Code
|
||||||
|
|
||||||
|
Never commit `.env`. Access secrets via `config()` only.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
$key = env('API_KEY');
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
// config/services.php
|
||||||
|
'api_key' => env('API_KEY'),
|
||||||
|
|
||||||
|
// In application code
|
||||||
|
$key = config('services.api_key');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Dependencies
|
||||||
|
|
||||||
|
Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer audit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Encrypt Sensitive Database Fields
|
||||||
|
|
||||||
|
Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
class Integration extends Model
|
||||||
|
{
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'api_key' => 'string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
class Integration extends Model
|
||||||
|
{
|
||||||
|
protected $hidden = ['api_key', 'api_secret'];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'api_key' => 'encrypted',
|
||||||
|
'api_secret' => 'encrypted',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Conventions & Style
|
||||||
|
|
||||||
|
## Follow Laravel Naming Conventions
|
||||||
|
|
||||||
|
| What | Convention | Good | Bad |
|
||||||
|
|------|-----------|------|-----|
|
||||||
|
| Controller | singular | `ArticleController` | `ArticlesController` |
|
||||||
|
| Model | singular | `User` | `Users` |
|
||||||
|
| Table | plural, snake_case | `article_comments` | `articleComments` |
|
||||||
|
| Pivot table | singular alphabetical | `article_user` | `user_article` |
|
||||||
|
| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
|
||||||
|
| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
|
||||||
|
| Route | plural | `articles/1` | `article/1` |
|
||||||
|
| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
|
||||||
|
| Method | camelCase | `getAll` | `get_all` |
|
||||||
|
| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
|
||||||
|
| Collection | descriptive, plural | `$activeUsers` | `$data` |
|
||||||
|
| Object | descriptive, singular | `$activeUser` | `$users` |
|
||||||
|
| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
|
||||||
|
| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
|
||||||
|
| Enum | singular | `UserType` | `UserTypes` |
|
||||||
|
|
||||||
|
## Prefer Shorter Readable Syntax
|
||||||
|
|
||||||
|
| Verbose | Shorter |
|
||||||
|
|---------|---------|
|
||||||
|
| `Session::get('cart')` | `session('cart')` |
|
||||||
|
| `$request->session()->get('cart')` | `session('cart')` |
|
||||||
|
| `$request->input('name')` | `$request->name` |
|
||||||
|
| `return Redirect::back()` | `return back()` |
|
||||||
|
| `Carbon::now()` | `now()` |
|
||||||
|
| `App::make('Class')` | `app('Class')` |
|
||||||
|
| `->where('column', '=', 1)` | `->where('column', 1)` |
|
||||||
|
| `->orderBy('created_at', 'desc')` | `->latest()` |
|
||||||
|
| `->orderBy('created_at', 'asc')` | `->oldest()` |
|
||||||
|
| `->first()->name` | `->value('name')` |
|
||||||
|
|
||||||
|
## Use Laravel String & Array Helpers
|
||||||
|
|
||||||
|
Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
|
||||||
|
|
||||||
|
Strings — use `Str` and fluent `Str::of()` over raw PHP:
|
||||||
|
```php
|
||||||
|
// Incorrect
|
||||||
|
$slug = strtolower(str_replace(' ', '-', $title));
|
||||||
|
$short = substr($text, 0, 100) . '...';
|
||||||
|
$class = substr(strrchr('App\Models\User', '\'), 1);
|
||||||
|
|
||||||
|
// Correct
|
||||||
|
$slug = Str::slug($title);
|
||||||
|
$short = Str::limit($text, 100);
|
||||||
|
$class = class_basename('App\Models\User');
|
||||||
|
```
|
||||||
|
|
||||||
|
Fluent strings — chain operations for complex transformations:
|
||||||
|
```php
|
||||||
|
// Incorrect
|
||||||
|
$result = strtolower(trim(str_replace('_', '-', $input)));
|
||||||
|
|
||||||
|
// Correct
|
||||||
|
$result = Str::of($input)->trim()->replace('_', '-')->lower();
|
||||||
|
```
|
||||||
|
|
||||||
|
Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
|
||||||
|
|
||||||
|
Arrays — use `Arr` over raw PHP:
|
||||||
|
```php
|
||||||
|
// Incorrect
|
||||||
|
$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
|
||||||
|
|
||||||
|
// Correct
|
||||||
|
$name = Arr::get($array, 'user.name', 'default');
|
||||||
|
```
|
||||||
|
|
||||||
|
Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
|
||||||
|
|
||||||
|
Numbers — use `Number` for display formatting:
|
||||||
|
```php
|
||||||
|
Number::format(1000000); // "1,000,000"
|
||||||
|
Number::currency(1500, 'USD'); // "$1,500.00"
|
||||||
|
Number::abbreviate(1000000); // "1M"
|
||||||
|
Number::fileSize(1024 * 1024); // "1 MB"
|
||||||
|
Number::percentage(75.5); // "75.5%"
|
||||||
|
```
|
||||||
|
|
||||||
|
URIs — use `Uri` for URL manipulation:
|
||||||
|
```php
|
||||||
|
$uri = Uri::of('https://example.com/search')
|
||||||
|
->withQuery(['q' => 'laravel', 'page' => 1]);
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
|
||||||
|
|
||||||
|
Use `search-docs` for the full list of available methods — these helpers are extensive.
|
||||||
|
|
||||||
|
## No Inline JS/CSS in Blade
|
||||||
|
|
||||||
|
Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```blade
|
||||||
|
let article = `{{ json_encode($article) }}`;
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```blade
|
||||||
|
<button class="js-fav-article" data-article='@json($article)'>{{ $article->name }}</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
|
||||||
|
|
||||||
|
## No Unnecessary Comments
|
||||||
|
|
||||||
|
Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
// Check if there are any joins
|
||||||
|
if (count((array) $builder->getQuery()->joins) > 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
if ($this->hasJoins())
|
||||||
|
```
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Testing Best Practices
|
||||||
|
|
||||||
|
## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
|
||||||
|
|
||||||
|
`RefreshDatabase` runs all migrations every test run even when the schema hasn't changed. `LazilyRefreshDatabase` only migrates when needed, significantly speeding up large suites.
|
||||||
|
|
||||||
|
## Use Model Assertions Over Raw Database Assertions
|
||||||
|
|
||||||
|
Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
|
||||||
|
|
||||||
|
Correct: `$this->assertModelExists($user);`
|
||||||
|
|
||||||
|
More expressive, type-safe, and fails with clearer messages.
|
||||||
|
|
||||||
|
## Use Factory States and Sequences
|
||||||
|
|
||||||
|
Named states make tests self-documenting. Sequences eliminate repetitive setup.
|
||||||
|
|
||||||
|
Incorrect: `User::factory()->create(['email_verified_at' => null]);`
|
||||||
|
|
||||||
|
Correct: `User::factory()->unverified()->create();`
|
||||||
|
|
||||||
|
## Use `Exceptions::fake()` to Assert Exception Reporting
|
||||||
|
|
||||||
|
Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
|
||||||
|
|
||||||
|
## Call `Event::fake()` After Factory Setup
|
||||||
|
|
||||||
|
Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
|
||||||
|
|
||||||
|
Incorrect: `Event::fake(); $user = User::factory()->create();`
|
||||||
|
|
||||||
|
Correct: `$user = User::factory()->create(); Event::fake();`
|
||||||
|
|
||||||
|
## Use `recycle()` to Share Relationship Instances Across Factories
|
||||||
|
|
||||||
|
Without `recycle()`, nested factories create separate instances of the same conceptual entity.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Ticket::factory()
|
||||||
|
->recycle(Airline::factory()->create())
|
||||||
|
->create();
|
||||||
|
```
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Validation & Forms Best Practices
|
||||||
|
|
||||||
|
## Use Form Request Classes
|
||||||
|
|
||||||
|
Extract validation from controllers into dedicated Form Request classes.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'title' => 'required|max:255',
|
||||||
|
'body' => 'required',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
public function store(StorePostRequest $request)
|
||||||
|
{
|
||||||
|
Post::create($request->validated());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Array vs. String Notation for Rules
|
||||||
|
|
||||||
|
Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Preferred for new code
|
||||||
|
'email' => ['required', 'email', Rule::unique('users')],
|
||||||
|
|
||||||
|
// Follow existing convention if the project uses string notation
|
||||||
|
'email' => 'required|email|unique:users',
|
||||||
|
```
|
||||||
|
|
||||||
|
## Always Use `validated()`
|
||||||
|
|
||||||
|
Get only validated data. Never use `$request->all()` for mass operations.
|
||||||
|
|
||||||
|
Incorrect:
|
||||||
|
```php
|
||||||
|
Post::create($request->all());
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
```php
|
||||||
|
Post::create($request->validated());
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `Rule::when()` for Conditional Validation
|
||||||
|
|
||||||
|
```php
|
||||||
|
'company_name' => [
|
||||||
|
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use the `after()` Method for Custom Validation
|
||||||
|
|
||||||
|
Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function after(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
function (Validator $validator) {
|
||||||
|
if ($this->quantity > Product::find($this->product_id)?->stock) {
|
||||||
|
$validator->errors()->add('quantity', 'Not enough stock.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
name: pest-testing
|
||||||
|
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pest Testing 4
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Creating Tests
|
||||||
|
|
||||||
|
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||||
|
|
||||||
|
### Test Organization
|
||||||
|
|
||||||
|
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||||
|
- Browser tests: `tests/Browser/` directory.
|
||||||
|
- Do NOT remove tests without approval - these are core application code.
|
||||||
|
|
||||||
|
### Basic Test Structure
|
||||||
|
|
||||||
|
Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
|
||||||
|
|
||||||
|
<!-- Basic Pest Test Example -->
|
||||||
|
```php
|
||||||
|
it('is true', function () {
|
||||||
|
expect(true)->toBeTrue();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||||
|
|
||||||
|
<!-- Pest Response Assertion -->
|
||||||
|
```php
|
||||||
|
it('returns all', function () {
|
||||||
|
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
| Use | Instead of |
|
||||||
|
|-----|------------|
|
||||||
|
| `assertSuccessful()` | `assertStatus(200)` |
|
||||||
|
| `assertNotFound()` | `assertStatus(404)` |
|
||||||
|
| `assertForbidden()` | `assertStatus(403)` |
|
||||||
|
|
||||||
|
## Mocking
|
||||||
|
|
||||||
|
Import mock function before use: `use function Pest\Laravel\mock;`
|
||||||
|
|
||||||
|
## Datasets
|
||||||
|
|
||||||
|
Use datasets for repetitive tests (validation rules, etc.):
|
||||||
|
|
||||||
|
<!-- Pest Dataset Example -->
|
||||||
|
```php
|
||||||
|
it('has emails', function (string $email) {
|
||||||
|
expect($email)->not->toBeEmpty();
|
||||||
|
})->with([
|
||||||
|
'james' => 'james@laravel.com',
|
||||||
|
'taylor' => 'taylor@laravel.com',
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pest 4 Features
|
||||||
|
|
||||||
|
| Feature | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| Browser Testing | Full integration tests in real browsers |
|
||||||
|
| Smoke Testing | Validate multiple pages quickly |
|
||||||
|
| Visual Regression | Compare screenshots for visual changes |
|
||||||
|
| Test Sharding | Parallel CI runs |
|
||||||
|
| Architecture Testing | Enforce code conventions |
|
||||||
|
|
||||||
|
### Browser Test Example
|
||||||
|
|
||||||
|
Browser tests run in real browsers for full integration testing:
|
||||||
|
|
||||||
|
- Browser tests live in `tests/Browser/`.
|
||||||
|
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||||
|
- Use `RefreshDatabase` for clean state per test.
|
||||||
|
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||||
|
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||||
|
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||||
|
- Switch color schemes (light/dark mode) when appropriate.
|
||||||
|
- Take screenshots or pause tests for debugging.
|
||||||
|
|
||||||
|
<!-- Pest Browser Test Example -->
|
||||||
|
```php
|
||||||
|
it('may reset the password', function () {
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$page = visit('/sign-in');
|
||||||
|
|
||||||
|
$page->assertSee('Sign In')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->click('Forgot Password?')
|
||||||
|
->fill('email', 'nuno@laravel.com')
|
||||||
|
->click('Send Reset Link')
|
||||||
|
->assertSee('We have emailed your password reset link!');
|
||||||
|
|
||||||
|
Notification::assertSent(ResetPassword::class);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Smoke Testing
|
||||||
|
|
||||||
|
Quickly validate multiple pages have no JavaScript errors:
|
||||||
|
|
||||||
|
<!-- Pest Smoke Testing Example -->
|
||||||
|
```php
|
||||||
|
$pages = visit(['/', '/about', '/contact']);
|
||||||
|
|
||||||
|
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Regression Testing
|
||||||
|
|
||||||
|
Capture and compare screenshots to detect visual changes.
|
||||||
|
|
||||||
|
### Test Sharding
|
||||||
|
|
||||||
|
Split tests across parallel processes for faster CI runs.
|
||||||
|
|
||||||
|
### Architecture Testing
|
||||||
|
|
||||||
|
Pest 4 includes architecture testing (from Pest 3):
|
||||||
|
|
||||||
|
<!-- Architecture Test Example -->
|
||||||
|
```php
|
||||||
|
arch('controllers')
|
||||||
|
->expect('App\Http\Controllers')
|
||||||
|
->toExtendNothing()
|
||||||
|
->toHaveSuffix('Controller');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Not importing `use function Pest\Laravel\mock;` before using mock
|
||||||
|
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||||
|
- Forgetting datasets for repetitive validation tests
|
||||||
|
- Deleting tests without approval
|
||||||
|
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
name: tailwindcss-development
|
||||||
|
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tailwind CSS Development
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
|
||||||
|
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
|
||||||
|
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.
|
||||||
|
|
||||||
|
## Tailwind CSS v4 Specifics
|
||||||
|
|
||||||
|
- Always use Tailwind CSS v4 and avoid deprecated utilities.
|
||||||
|
- `corePlugins` is not supported in Tailwind v4.
|
||||||
|
|
||||||
|
### CSS-First Configuration
|
||||||
|
|
||||||
|
In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:
|
||||||
|
|
||||||
|
<!-- CSS-First Config -->
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
--color-brand: oklch(0.72 0.11 178);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Syntax
|
||||||
|
|
||||||
|
In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:
|
||||||
|
|
||||||
|
<!-- v4 Import Syntax -->
|
||||||
|
```diff
|
||||||
|
- @tailwind base;
|
||||||
|
- @tailwind components;
|
||||||
|
- @tailwind utilities;
|
||||||
|
+ @import "tailwindcss";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replaced Utilities
|
||||||
|
|
||||||
|
Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.
|
||||||
|
|
||||||
|
| Deprecated | Replacement |
|
||||||
|
|------------|-------------|
|
||||||
|
| bg-opacity-* | bg-black/* |
|
||||||
|
| text-opacity-* | text-black/* |
|
||||||
|
| border-opacity-* | border-black/* |
|
||||||
|
| divide-opacity-* | divide-black/* |
|
||||||
|
| ring-opacity-* | ring-black/* |
|
||||||
|
| placeholder-opacity-* | placeholder-black/* |
|
||||||
|
| flex-shrink-* | shrink-* |
|
||||||
|
| flex-grow-* | grow-* |
|
||||||
|
| overflow-ellipsis | text-ellipsis |
|
||||||
|
| decoration-slice | box-decoration-slice |
|
||||||
|
| decoration-clone | box-decoration-clone |
|
||||||
|
|
||||||
|
## Spacing
|
||||||
|
|
||||||
|
Use `gap` utilities instead of margins for spacing between siblings:
|
||||||
|
|
||||||
|
<!-- Gap Utilities -->
|
||||||
|
```html
|
||||||
|
<div class="flex gap-8">
|
||||||
|
<div>Item 1</div>
|
||||||
|
<div>Item 2</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dark Mode
|
||||||
|
|
||||||
|
If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:
|
||||||
|
|
||||||
|
<!-- Dark Mode -->
|
||||||
|
```html
|
||||||
|
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||||
|
Content adapts to color scheme
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Flexbox Layout
|
||||||
|
|
||||||
|
<!-- Flexbox Layout -->
|
||||||
|
```html
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div>Left content</div>
|
||||||
|
<div>Right content</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grid Layout
|
||||||
|
|
||||||
|
<!-- Grid Layout -->
|
||||||
|
```html
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div>Card 1</div>
|
||||||
|
<div>Card 2</div>
|
||||||
|
<div>Card 3</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
|
||||||
|
- Using `@tailwind` directives instead of `@import "tailwindcss"`
|
||||||
|
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
|
||||||
|
- Using margins for spacing between siblings instead of gap utilities
|
||||||
|
- Forgetting to add dark mode variants when the project uses dark mode
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: wayfinder-development
|
||||||
|
description: "Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task"
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
author: laravel
|
||||||
|
---
|
||||||
|
|
||||||
|
# Wayfinder Development
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Use `search-docs` for detailed Wayfinder patterns and documentation.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Generate Routes
|
||||||
|
|
||||||
|
Run after route changes if Vite plugin isn't installed:
|
||||||
|
```bash
|
||||||
|
php artisan wayfinder:generate --no-interaction
|
||||||
|
```
|
||||||
|
For form helpers, use `--with-form` flag:
|
||||||
|
```bash
|
||||||
|
php artisan wayfinder:generate --with-form --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
### Import Patterns
|
||||||
|
|
||||||
|
<!-- Controller Action Imports -->
|
||||||
|
```typescript
|
||||||
|
// Named imports for tree-shaking (preferred)...
|
||||||
|
import { show, store, update } from '@/actions/App/Http/Controllers/PostController'
|
||||||
|
|
||||||
|
// Named route imports...
|
||||||
|
import { show as postShow } from '@/routes/post'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Methods
|
||||||
|
|
||||||
|
<!-- Wayfinder Methods -->
|
||||||
|
```typescript
|
||||||
|
// Get route object...
|
||||||
|
show(1) // { url: "/posts/1", method: "get" }
|
||||||
|
|
||||||
|
// Get URL string...
|
||||||
|
show.url(1) // "/posts/1"
|
||||||
|
|
||||||
|
// Specific HTTP methods...
|
||||||
|
show.get(1)
|
||||||
|
store.post()
|
||||||
|
update.patch(1)
|
||||||
|
destroy.delete(1)
|
||||||
|
|
||||||
|
// Form attributes for HTML forms...
|
||||||
|
store.form() // { action: "/posts", method: "post" }
|
||||||
|
|
||||||
|
// Query parameters...
|
||||||
|
show(1, { query: { page: 1 } }) // "/posts/1?page=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wayfinder + Inertia
|
||||||
|
|
||||||
|
Use Wayfinder with the `<Form>` component:
|
||||||
|
<!-- Wayfinder Form (Vue) -->
|
||||||
|
```vue
|
||||||
|
<Form v-bind="store.form()"><input name="title" /></Form>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
- Using default imports instead of named imports (breaks tree-shaking)
|
||||||
|
- Forgetting to regenerate after route changes
|
||||||
|
- Not using type-safe parameter objects for route model binding
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[compose.yaml]
|
||||||
|
indent_size = 4
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
APP_MAINTENANCE_DRIVER=file
|
||||||
|
# APP_MAINTENANCE_STORE=database
|
||||||
|
|
||||||
|
# PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_STACK=single
|
||||||
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
DB_CONNECTION=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# DB_USERNAME=root
|
||||||
|
# DB_PASSWORD=
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_LIFETIME=120
|
||||||
|
SESSION_ENCRYPT=false
|
||||||
|
SESSION_PATH=/
|
||||||
|
SESSION_DOMAIN=null
|
||||||
|
|
||||||
|
BROADCAST_CONNECTION=log
|
||||||
|
FILESYSTEM_DISK=local
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
CACHE_STORE=database
|
||||||
|
# CACHE_PREFIX=
|
||||||
|
|
||||||
|
MEMCACHED_HOST=127.0.0.1
|
||||||
|
|
||||||
|
REDIS_CLIENT=phpredis
|
||||||
|
REDIS_HOST=127.0.0.1
|
||||||
|
REDIS_PASSWORD=null
|
||||||
|
REDIS_PORT=6379
|
||||||
|
|
||||||
|
MAIL_MAILER=log
|
||||||
|
MAIL_SCHEME=null
|
||||||
|
MAIL_HOST=127.0.0.1
|
||||||
|
MAIL_PORT=2525
|
||||||
|
MAIL_USERNAME=null
|
||||||
|
MAIL_PASSWORD=null
|
||||||
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_BUCKET=
|
||||||
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
*.blade.php diff=html
|
||||||
|
*.css diff=css
|
||||||
|
*.html diff=html
|
||||||
|
*.md diff=markdown
|
||||||
|
*.php diff=php
|
||||||
|
|
||||||
|
CHANGELOG.md export-ignore
|
||||||
|
README.md export-ignore
|
||||||
|
.github/workflows/browser-tests.yml export-ignore
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: linter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.4'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||||
|
npm install
|
||||||
|
|
||||||
|
- name: Run Pint
|
||||||
|
run: composer lint
|
||||||
|
|
||||||
|
- name: Format Frontend
|
||||||
|
run: npm run format
|
||||||
|
|
||||||
|
- name: Lint Frontend
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
# - name: Commit Changes
|
||||||
|
# uses: stefanzweifel/git-auto-commit-action@v7
|
||||||
|
# with:
|
||||||
|
# commit_message: fix code style
|
||||||
|
# commit_options: '--no-verify'
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
- workos
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
php-version: ['8.3', '8.4', '8.5']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: ${{ matrix.php-version }}
|
||||||
|
tools: composer:v2
|
||||||
|
coverage: xdebug
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Install Node Dependencies
|
||||||
|
run: npm i
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
|
|
||||||
|
- name: Copy Environment File
|
||||||
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Generate Application Key
|
||||||
|
run: php artisan key:generate
|
||||||
|
|
||||||
|
- name: Build Assets
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: ./vendor/bin/pest
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/.phpunit.cache
|
||||||
|
/bootstrap/ssr
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/resources/js/actions
|
||||||
|
/resources/js/routes
|
||||||
|
/resources/js/wayfinder
|
||||||
|
/vendor
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
/auth.json
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"laravel-boost": {
|
||||||
|
"command": "php",
|
||||||
|
"args": [
|
||||||
|
"artisan",
|
||||||
|
"boost:mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
resources/js/components/ui/*
|
||||||
|
resources/views/mail/*
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"singleAttributePerLine": false,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"printWidth": 80,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-tailwindcss"
|
||||||
|
],
|
||||||
|
"tailwindFunctions": [
|
||||||
|
"clsx",
|
||||||
|
"cn",
|
||||||
|
"cva"
|
||||||
|
],
|
||||||
|
"tailwindStylesheet": "resources/css/app.css",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "**/*.yml",
|
||||||
|
"options": {
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
<laravel-boost-guidelines>
|
||||||
|
=== foundation rules ===
|
||||||
|
|
||||||
|
# Laravel Boost Guidelines
|
||||||
|
|
||||||
|
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||||
|
|
||||||
|
## Foundational Context
|
||||||
|
|
||||||
|
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||||
|
|
||||||
|
- php - 8.4
|
||||||
|
- inertiajs/inertia-laravel (INERTIA_LARAVEL) - v3
|
||||||
|
- laravel/fortify (FORTIFY) - v1
|
||||||
|
- laravel/framework (LARAVEL) - v13
|
||||||
|
- laravel/prompts (PROMPTS) - v0
|
||||||
|
- laravel/sanctum (SANCTUM) - v4
|
||||||
|
- laravel/wayfinder (WAYFINDER) - v0
|
||||||
|
- laravel/boost (BOOST) - v2
|
||||||
|
- laravel/mcp (MCP) - v0
|
||||||
|
- laravel/pail (PAIL) - v1
|
||||||
|
- laravel/pint (PINT) - v1
|
||||||
|
- laravel/sail (SAIL) - v1
|
||||||
|
- pestphp/pest (PEST) - v4
|
||||||
|
- phpunit/phpunit (PHPUNIT) - v12
|
||||||
|
- @inertiajs/vue3 (INERTIA_VUE) - v3
|
||||||
|
- tailwindcss (TAILWINDCSS) - v4
|
||||||
|
- vue (VUE) - v3
|
||||||
|
- @laravel/vite-plugin-wayfinder (WAYFINDER_VITE) - v0
|
||||||
|
- eslint (ESLINT) - v9
|
||||||
|
- prettier (PRETTIER) - v3
|
||||||
|
|
||||||
|
## Skills Activation
|
||||||
|
|
||||||
|
This project has domain-specific skills available. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||||
|
|
||||||
|
- `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.
|
||||||
|
- `laravel-best-practices` — Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns.
|
||||||
|
- `wayfinder-development` — Use this skill for Laravel Wayfinder which auto-generates typed functions for Laravel controllers and routes. ALWAYS use this skill when frontend code needs to call backend routes or controller actions. Trigger when: connecting any React/Vue/Svelte/Inertia frontend to Laravel controllers, routes, building end-to-end features with both frontend and backend, wiring up forms or links to backend endpoints, fixing route-related TypeScript errors, importing from @/actions or @/routes, or running wayfinder:generate. Use Wayfinder route functions instead of hardcoded URLs. Covers: wayfinder() vite plugin, .url()/.get()/.post()/.form(), query params, route model binding, tree-shaking. Do not use for backend-only task
|
||||||
|
- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
|
||||||
|
- `inertia-vue-development` — 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.
|
||||||
|
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||||
|
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||||
|
- Check for existing components to reuse before writing a new one.
|
||||||
|
|
||||||
|
## Verification Scripts
|
||||||
|
|
||||||
|
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||||
|
|
||||||
|
## Application Structure & Architecture
|
||||||
|
|
||||||
|
- Stick to existing directory structure; don't create new base folders without approval.
|
||||||
|
- Do not change the application's dependencies without approval.
|
||||||
|
|
||||||
|
## Frontend Bundling
|
||||||
|
|
||||||
|
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||||
|
|
||||||
|
## Documentation Files
|
||||||
|
|
||||||
|
- You must only create documentation files if explicitly requested by the user.
|
||||||
|
|
||||||
|
## Replies
|
||||||
|
|
||||||
|
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||||
|
|
||||||
|
=== boost rules ===
|
||||||
|
|
||||||
|
# Laravel Boost
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
|
||||||
|
- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker.
|
||||||
|
- Use `database-schema` to inspect table structure before writing migrations or models.
|
||||||
|
- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
|
||||||
|
- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
|
||||||
|
|
||||||
|
## Searching Documentation (IMPORTANT)
|
||||||
|
|
||||||
|
- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
|
||||||
|
- Pass a `packages` array to scope results when you know which packages are relevant.
|
||||||
|
- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first.
|
||||||
|
- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`.
|
||||||
|
|
||||||
|
### Search Syntax
|
||||||
|
|
||||||
|
1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit".
|
||||||
|
2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order.
|
||||||
|
3. Combine words and phrases for mixed queries: `middleware "rate limit"`.
|
||||||
|
4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`.
|
||||||
|
|
||||||
|
## Artisan
|
||||||
|
|
||||||
|
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||||
|
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
||||||
|
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
||||||
|
- To check environment variables, read the `.env` file directly.
|
||||||
|
|
||||||
|
## Tinker
|
||||||
|
|
||||||
|
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
|
||||||
|
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
|
||||||
|
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
|
||||||
|
|
||||||
|
=== php rules ===
|
||||||
|
|
||||||
|
# PHP
|
||||||
|
|
||||||
|
- Always use curly braces for control structures, even for single-line bodies.
|
||||||
|
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
|
||||||
|
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
|
||||||
|
- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||||
|
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
||||||
|
- Use array shape type definitions in PHPDoc blocks.
|
||||||
|
|
||||||
|
=== tests rules ===
|
||||||
|
|
||||||
|
# Test Enforcement
|
||||||
|
|
||||||
|
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||||
|
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||||
|
|
||||||
|
=== inertia-laravel/core rules ===
|
||||||
|
|
||||||
|
# Inertia
|
||||||
|
|
||||||
|
- Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
|
||||||
|
- Components live in `resources/js/pages` (unless specified in `vite.config.js`). Use `Inertia::render()` for server-side routing instead of Blade views.
|
||||||
|
- ALWAYS use `search-docs` tool for version-specific Inertia documentation and updated code examples.
|
||||||
|
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
|
||||||
|
|
||||||
|
# Inertia v3
|
||||||
|
|
||||||
|
- Use all Inertia features from v1, v2, and v3. Check the documentation before making changes to ensure the correct approach.
|
||||||
|
- New v3 features: standalone HTTP requests (`useHttp` hook), optimistic updates with automatic rollback, layout props (`useLayoutProps` hook), instant visits, simplified SSR via `@inertiajs/vite` plugin, custom exception handling for error pages.
|
||||||
|
- Carried over from v2: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data.
|
||||||
|
- When using deferred props, add an empty state with a pulsing or animated skeleton.
|
||||||
|
- Axios has been removed. Use the built-in XHR client with interceptors, or install Axios separately if needed.
|
||||||
|
- `Inertia::lazy()` / `LazyProp` has been removed. Use `Inertia::optional()` instead.
|
||||||
|
- Prop types (`Inertia::optional()`, `Inertia::defer()`, `Inertia::merge()`) work inside nested arrays with dot-notation paths.
|
||||||
|
- SSR works automatically in Vite dev mode with `@inertiajs/vite` - no separate Node.js server needed during development.
|
||||||
|
- Event renames: `invalid` is now `httpException`, `exception` is now `networkError`.
|
||||||
|
- `router.cancel()` replaced by `router.cancelAll()`.
|
||||||
|
- The `future` configuration namespace has been removed - all v2 future options are now always enabled.
|
||||||
|
|
||||||
|
=== laravel/core rules ===
|
||||||
|
|
||||||
|
# Do Things the Laravel Way
|
||||||
|
|
||||||
|
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||||
|
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||||
|
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||||
|
|
||||||
|
### Model Creation
|
||||||
|
|
||||||
|
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||||
|
|
||||||
|
## APIs & Eloquent Resources
|
||||||
|
|
||||||
|
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||||
|
|
||||||
|
## URL Generation
|
||||||
|
|
||||||
|
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||||
|
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||||
|
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||||
|
|
||||||
|
## Vite Error
|
||||||
|
|
||||||
|
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
|
||||||
|
|
||||||
|
=== wayfinder/core rules ===
|
||||||
|
|
||||||
|
# Laravel Wayfinder
|
||||||
|
|
||||||
|
Use Wayfinder to generate TypeScript functions for Laravel routes. Import from `@/actions/` (controllers) or `@/routes/` (named routes).
|
||||||
|
|
||||||
|
=== pint/core rules ===
|
||||||
|
|
||||||
|
# Laravel Pint Code Formatter
|
||||||
|
|
||||||
|
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||||
|
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||||
|
|
||||||
|
=== pest/core rules ===
|
||||||
|
|
||||||
|
## Pest
|
||||||
|
|
||||||
|
- This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.
|
||||||
|
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
|
||||||
|
- Do NOT delete tests without approval.
|
||||||
|
|
||||||
|
=== inertia-vue/core rules ===
|
||||||
|
|
||||||
|
# Inertia + Vue
|
||||||
|
|
||||||
|
Vue components must have a single root element.
|
||||||
|
- IMPORTANT: Activate `inertia-vue-development` when working with Inertia Vue client-side patterns.
|
||||||
|
|
||||||
|
</laravel-boost-guidelines>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Concerns\PasswordValidationRules;
|
||||||
|
use App\Concerns\ProfileValidationRules;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||||
|
|
||||||
|
class CreateNewUser implements CreatesNewUsers
|
||||||
|
{
|
||||||
|
use PasswordValidationRules, ProfileValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and create a newly registered user.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function create(array $input): User
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
...$this->profileRules(),
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
return User::create([
|
||||||
|
'name' => $input['name'],
|
||||||
|
'email' => $input['email'],
|
||||||
|
'password' => $input['password'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Actions\Fortify;
|
||||||
|
|
||||||
|
use App\Concerns\PasswordValidationRules;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||||
|
|
||||||
|
class ResetUserPassword implements ResetsUserPasswords
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and reset the user's forgotten password.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $input
|
||||||
|
*/
|
||||||
|
public function reset(User $user, array $input): void
|
||||||
|
{
|
||||||
|
Validator::make($input, [
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$user->forceFill([
|
||||||
|
'password' => $input['password'],
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
trait PasswordValidationRules
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate passwords.
|
||||||
|
*
|
||||||
|
* @return array<int, Rule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
protected function passwordRules(): array
|
||||||
|
{
|
||||||
|
return ['required', 'string', Password::default(), 'confirmed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate the current password.
|
||||||
|
*
|
||||||
|
* @return array<int, Rule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
protected function currentPasswordRules(): array
|
||||||
|
{
|
||||||
|
return ['required', 'string', 'current_password'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Concerns;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
trait ProfileValidationRules
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate user profiles.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>>
|
||||||
|
*/
|
||||||
|
protected function profileRules(?int $userId = null): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $this->nameRules(),
|
||||||
|
'email' => $this->emailRules($userId),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate user names.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
protected function nameRules(): array
|
||||||
|
{
|
||||||
|
return ['required', 'string', 'max:255'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules used to validate user emails.
|
||||||
|
*
|
||||||
|
* @return array<int, \Illuminate\Contracts\Validation\Rule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
protected function emailRules(?int $userId = null): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
$userId === null
|
||||||
|
? Rule::unique(User::class)
|
||||||
|
: Rule::unique(User::class)->ignore($userId),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Settings\ProfileDeleteRequest;
|
||||||
|
use App\Http\Requests\Settings\ProfileUpdateRequest;
|
||||||
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Show the user's profile settings page.
|
||||||
|
*/
|
||||||
|
public function edit(Request $request): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('settings/Profile', [
|
||||||
|
'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail,
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*/
|
||||||
|
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()->fill($request->validated());
|
||||||
|
|
||||||
|
if ($request->user()->isDirty('email')) {
|
||||||
|
$request->user()->email_verified_at = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->user()->save();
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]);
|
||||||
|
|
||||||
|
return to_route('profile.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the user's profile.
|
||||||
|
*/
|
||||||
|
public function destroy(ProfileDeleteRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Settings\PasswordUpdateRequest;
|
||||||
|
use App\Http\Requests\Settings\TwoFactorAuthenticationRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Routing\Controllers\HasMiddleware;
|
||||||
|
use Illuminate\Routing\Controllers\Middleware;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
|
||||||
|
class SecurityController extends Controller implements HasMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the middleware that should be assigned to the controller.
|
||||||
|
*/
|
||||||
|
public static function middleware(): array
|
||||||
|
{
|
||||||
|
return Features::canManageTwoFactorAuthentication()
|
||||||
|
&& Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')
|
||||||
|
? [new Middleware('password.confirm', only: ['edit'])]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the user's security settings page.
|
||||||
|
*/
|
||||||
|
public function edit(TwoFactorAuthenticationRequest $request): Response
|
||||||
|
{
|
||||||
|
$props = [
|
||||||
|
'canManageTwoFactor' => Features::canManageTwoFactorAuthentication(),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (Features::canManageTwoFactorAuthentication()) {
|
||||||
|
$request->ensureStateIsValid();
|
||||||
|
|
||||||
|
$props['twoFactorEnabled'] = $request->user()->hasEnabledTwoFactorAuthentication();
|
||||||
|
$props['requiresConfirmation'] = Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Inertia::render('settings/Security', $props);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's password.
|
||||||
|
*/
|
||||||
|
public function update(PasswordUpdateRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => $request->password,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => __('Password updated.')]);
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class HandleAppearance
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param Closure(Request): (Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Middleware;
|
||||||
|
|
||||||
|
class HandleInertiaRequests extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The root template that's loaded on the first page visit.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/server-side-setup#root-template
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $rootView = 'app';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the current asset version.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/asset-versioning
|
||||||
|
*/
|
||||||
|
public function version(Request $request): ?string
|
||||||
|
{
|
||||||
|
return parent::version($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the props that are shared by default.
|
||||||
|
*
|
||||||
|
* @see https://inertiajs.com/shared-data
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function share(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
...parent::share($request),
|
||||||
|
'name' => config('app.name'),
|
||||||
|
'auth' => [
|
||||||
|
'user' => $request->user(),
|
||||||
|
],
|
||||||
|
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Settings;
|
||||||
|
|
||||||
|
use App\Concerns\PasswordValidationRules;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class PasswordUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'current_password' => $this->currentPasswordRules(),
|
||||||
|
'password' => $this->passwordRules(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Settings;
|
||||||
|
|
||||||
|
use App\Concerns\PasswordValidationRules;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ProfileDeleteRequest extends FormRequest
|
||||||
|
{
|
||||||
|
use PasswordValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'password' => $this->currentPasswordRules(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Settings;
|
||||||
|
|
||||||
|
use App\Concerns\ProfileValidationRules;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ProfileUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
use ProfileValidationRules;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return $this->profileRules($this->user()->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Settings;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Laravel\Fortify\InteractsWithTwoFactorState;
|
||||||
|
|
||||||
|
class TwoFactorAuthenticationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
use InteractsWithTwoFactorState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||||
|
|
||||||
|
#[Fillable(['name', 'email', 'password'])]
|
||||||
|
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<UserFactory> */
|
||||||
|
use HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
'two_factor_confirmed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Support\Facades\Date;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->configureDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure default behaviors for production-ready applications.
|
||||||
|
*/
|
||||||
|
protected function configureDefaults(): void
|
||||||
|
{
|
||||||
|
Date::use(CarbonImmutable::class);
|
||||||
|
|
||||||
|
DB::prohibitDestructiveCommands(
|
||||||
|
app()->isProduction(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Password::defaults(fn (): ?Password => app()->isProduction()
|
||||||
|
? Password::min(12)
|
||||||
|
->mixedCase()
|
||||||
|
->letters()
|
||||||
|
->numbers()
|
||||||
|
->symbols()
|
||||||
|
->uncompromised()
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Actions\Fortify\CreateNewUser;
|
||||||
|
use App\Actions\Fortify\ResetUserPassword;
|
||||||
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
use Laravel\Fortify\Fortify;
|
||||||
|
|
||||||
|
class FortifyServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->configureActions();
|
||||||
|
$this->configureViews();
|
||||||
|
$this->configureRateLimiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Fortify actions.
|
||||||
|
*/
|
||||||
|
private function configureActions(): void
|
||||||
|
{
|
||||||
|
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||||
|
Fortify::createUsersUsing(CreateNewUser::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Fortify views.
|
||||||
|
*/
|
||||||
|
private function configureViews(): void
|
||||||
|
{
|
||||||
|
Fortify::loginView(fn (Request $request) => Inertia::render('auth/Login', [
|
||||||
|
'canResetPassword' => Features::enabled(Features::resetPasswords()),
|
||||||
|
'canRegister' => Features::enabled(Features::registration()),
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::resetPasswordView(fn (Request $request) => Inertia::render('auth/ResetPassword', [
|
||||||
|
'email' => $request->email,
|
||||||
|
'token' => $request->route('token'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::requestPasswordResetLinkView(fn (Request $request) => Inertia::render('auth/ForgotPassword', [
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::verifyEmailView(fn (Request $request) => Inertia::render('auth/VerifyEmail', [
|
||||||
|
'status' => $request->session()->get('status'),
|
||||||
|
]));
|
||||||
|
|
||||||
|
Fortify::registerView(fn () => Inertia::render('auth/Register'));
|
||||||
|
|
||||||
|
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/TwoFactorChallenge'));
|
||||||
|
|
||||||
|
Fortify::confirmPasswordView(fn () => Inertia::render('auth/ConfirmPassword'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure rate limiting.
|
||||||
|
*/
|
||||||
|
private function configureRateLimiting(): void
|
||||||
|
{
|
||||||
|
RateLimiter::for('two-factor', function (Request $request) {
|
||||||
|
return Limit::perMinute(5)->by($request->session()->get('login.id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
RateLimiter::for('login', function (Request $request) {
|
||||||
|
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
|
||||||
|
|
||||||
|
return Limit::perMinute(5)->by($throttleKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"agents": [
|
||||||
|
"claude_code"
|
||||||
|
],
|
||||||
|
"guidelines": true,
|
||||||
|
"mcp": true,
|
||||||
|
"nightwatch_mcp": false,
|
||||||
|
"sail": false,
|
||||||
|
"skills": [
|
||||||
|
"fortify-development",
|
||||||
|
"laravel-best-practices",
|
||||||
|
"wayfinder-development",
|
||||||
|
"pest-testing",
|
||||||
|
"inertia-vue-development",
|
||||||
|
"tailwindcss-development"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\HandleAppearance;
|
||||||
|
use App\Http\Middleware\HandleInertiaRequests;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->encryptCookies(except: ['appearance', 'sidebar_state']);
|
||||||
|
|
||||||
|
$middleware->web(append: [
|
||||||
|
HandleAppearance::class,
|
||||||
|
HandleInertiaRequests::class,
|
||||||
|
AddLinkHeadersForPreloadedAssets::class,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
use App\Providers\FortifyServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
FortifyServiceProvider::class,
|
||||||
|
];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
|
"style": "new-york-v4",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "resources/css/app.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"composables": "@/composables",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/vue-starter-kit",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"framework"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"inertiajs/inertia-laravel": "^3.0",
|
||||||
|
"laravel/fortify": "^1.34",
|
||||||
|
"laravel/framework": "^13.0",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
|
"laravel/tinker": "^3.0",
|
||||||
|
"laravel/wayfinder": "^0.1.14"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.24",
|
||||||
|
"laravel/boost": "^2.2",
|
||||||
|
"laravel/pail": "^1.2.5",
|
||||||
|
"laravel/pint": "^1.27",
|
||||||
|
"laravel/sail": "^1.53",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.9",
|
||||||
|
"pestphp/pest": "^4.4",
|
||||||
|
"pestphp/pest-plugin-laravel": "^4.1"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"lint": [
|
||||||
|
"pint --parallel"
|
||||||
|
],
|
||||||
|
"lint:check": [
|
||||||
|
"pint --parallel --test"
|
||||||
|
],
|
||||||
|
"ci:check": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npm run lint:check",
|
||||||
|
"npm run format:check",
|
||||||
|
"npm run types:check",
|
||||||
|
"@test"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi",
|
||||||
|
"@lint:check",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
|
||||||
|
"@php artisan boost:update --ansi"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable"
|
||||||
|
}
|
||||||
Generated
+9920
File diff suppressed because it is too large
Load Diff
+126
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Environment
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the "environment" your application is currently
|
||||||
|
| running in. This may determine how you prefer to configure various
|
||||||
|
| services the application utilizes. Set this in your ".env" file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Debug Mode
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When your application is in debug mode, detailed error messages with
|
||||||
|
| stack traces will be shown on every error that occurs within your
|
||||||
|
| application. If disabled, a simple generic error page is shown.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application URL
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This URL is used by the console to properly generate URLs when using
|
||||||
|
| the Artisan command line tool. You should set this to the root of
|
||||||
|
| the application so that it's available within Artisan commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Timezone
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default timezone for your application, which
|
||||||
|
| will be used by the PHP date and date-time functions. The timezone
|
||||||
|
| is set to "UTC" by default as it is suitable for most use cases.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Locale Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The application locale determines the default locale that will be used
|
||||||
|
| by Laravel's translation / localization methods. This option can be
|
||||||
|
| set to any locale for which you plan to have translation strings.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Encryption Key
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This key is utilized by Laravel's encryption services and should be set
|
||||||
|
| to a random, 32 character string to ensure that all encrypted values
|
||||||
|
| are secure. You should do this prior to deploying the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
|
'key' => env('APP_KEY'),
|
||||||
|
|
||||||
|
'previous_keys' => [
|
||||||
|
...array_filter(
|
||||||
|
explode(',', (string) env('APP_PREVIOUS_KEYS', '')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Maintenance Mode Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options determine the driver used to determine and
|
||||||
|
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||||
|
| allow maintenance mode to be controlled across multiple machines.
|
||||||
|
|
|
||||||
|
| Supported drivers: "file", "cache"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'maintenance' => [
|
||||||
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| which utilizes session storage plus the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| User Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| All authentication guards have a user provider, which defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| system used by the application. Typically, Eloquent is utilized.
|
||||||
|
|
|
||||||
|
| If you have multiple user tables or models you may configure multiple
|
||||||
|
| providers to represent the model / table. These providers may then
|
||||||
|
| be assigned to any extra authentication guards you have defined.
|
||||||
|
|
|
||||||
|
| Supported: "database", "eloquent"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => env('AUTH_MODEL', User::class),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 'users' => [
|
||||||
|
// 'driver' => 'database',
|
||||||
|
// 'table' => 'users',
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These configuration options specify the behavior of Laravel's password
|
||||||
|
| reset functionality, including the table utilized for token storage
|
||||||
|
| and the user provider that is invoked to actually retrieve users.
|
||||||
|
|
|
||||||
|
| The expiry time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the number of seconds before a password confirmation
|
||||||
|
| window expires and users are asked to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Stores
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define all of the cache "stores" for your application as
|
||||||
|
| well as their drivers. You may even define multiple stores for the
|
||||||
|
| same cache driver to group types of items stored in your caches.
|
||||||
|
|
|
||||||
|
| Supported drivers: "array", "database", "file", "memcached",
|
||||||
|
| "redis", "dynamodb", "octane",
|
||||||
|
| "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stores' => [
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'driver' => 'array',
|
||||||
|
'serialize' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_CACHE_CONNECTION'),
|
||||||
|
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||||
|
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||||
|
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'file' => [
|
||||||
|
'driver' => 'file',
|
||||||
|
'path' => storage_path('framework/cache/data'),
|
||||||
|
'lock_path' => storage_path('framework/cache/data'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'memcached' => [
|
||||||
|
'driver' => 'memcached',
|
||||||
|
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||||
|
'sasl' => [
|
||||||
|
env('MEMCACHED_USERNAME'),
|
||||||
|
env('MEMCACHED_PASSWORD'),
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||||
|
],
|
||||||
|
'servers' => [
|
||||||
|
[
|
||||||
|
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MEMCACHED_PORT', 11211),
|
||||||
|
'weight' => 100,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||||
|
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'dynamodb' => [
|
||||||
|
'driver' => 'dynamodb',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||||
|
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'octane' => [
|
||||||
|
'driver' => 'octane',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'stores' => [
|
||||||
|
'database',
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Cache Key Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||||
|
| stores, there might be other applications using the same cache. For
|
||||||
|
| that reason, you may prefix every cache key to avoid collisions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'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,
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Database Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below are all of the database connections defined for your application.
|
||||||
|
| An example configuration is provided for each database system which
|
||||||
|
| is supported by Laravel. You're free to add / remove connections.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
'busy_timeout' => null,
|
||||||
|
'journal_mode' => null,
|
||||||
|
'synchronous' => null,
|
||||||
|
'transaction_mode' => 'DEFERRED',
|
||||||
|
],
|
||||||
|
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'mariadb' => [
|
||||||
|
'driver' => 'mariadb',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||||
|
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
|
||||||
|
'pgsql' => [
|
||||||
|
'driver' => 'pgsql',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '5432'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'search_path' => 'public',
|
||||||
|
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqlsrv' => [
|
||||||
|
'driver' => 'sqlsrv',
|
||||||
|
'url' => env('DB_URL'),
|
||||||
|
'host' => env('DB_HOST', 'localhost'),
|
||||||
|
'port' => env('DB_PORT', '1433'),
|
||||||
|
'database' => env('DB_DATABASE', 'laravel'),
|
||||||
|
'username' => env('DB_USERNAME', 'root'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||||
|
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Migration Repository Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This table keeps track of all the migrations that have already run for
|
||||||
|
| your application. Using this information, we can determine which of
|
||||||
|
| the migrations on disk haven't actually been run on the database.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'migrations' => [
|
||||||
|
'table' => 'migrations',
|
||||||
|
'update_date_on_publish' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Redis Databases
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Redis is an open source, fast, and advanced key-value store that also
|
||||||
|
| provides a richer body of commands than a typical key-value system
|
||||||
|
| such as Memcached. You may define your connection settings here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
|
||||||
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|
||||||
|
'options' => [
|
||||||
|
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||||
|
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||||
|
'persistent' => env('REDIS_PERSISTENT', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
'default' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_DB', '0'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
'url' => env('REDIS_URL'),
|
||||||
|
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||||
|
'username' => env('REDIS_USERNAME'),
|
||||||
|
'password' => env('REDIS_PASSWORD'),
|
||||||
|
'port' => env('REDIS_PORT', '6379'),
|
||||||
|
'database' => env('REDIS_CACHE_DB', '1'),
|
||||||
|
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||||
|
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||||
|
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||||
|
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filesystem Disks
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Below you may configure as many filesystem disks as necessary, and you
|
||||||
|
| may even configure multiple disks for the same driver. Examples for
|
||||||
|
| most supported storage drivers are configured here for reference.
|
||||||
|
|
|
||||||
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disks' => [
|
||||||
|
|
||||||
|
'local' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/private'),
|
||||||
|
'serve' => true,
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'public' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/public'),
|
||||||
|
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||||
|
'visibility' => 'public',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
's3' => [
|
||||||
|
'driver' => 's3',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION'),
|
||||||
|
'bucket' => env('AWS_BUCKET'),
|
||||||
|
'url' => env('AWS_URL'),
|
||||||
|
'endpoint' => env('AWS_ENDPOINT'),
|
||||||
|
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Symbolic Links
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the symbolic links that will be created when the
|
||||||
|
| `storage:link` Artisan command is executed. The array keys should be
|
||||||
|
| the locations of the links and the values should be their targets.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'links' => [
|
||||||
|
public_path('storage') => storage_path('app/public'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Fortify\Features;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Guard
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which authentication guard Fortify will use while
|
||||||
|
| authenticating users. This value should correspond with one of your
|
||||||
|
| guards that is already present in your "auth" configuration file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => 'web',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Password Broker
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which password broker Fortify can use when a user
|
||||||
|
| is resetting their password. This configured value should match one
|
||||||
|
| of your password brokers setup in your "auth" configuration file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passwords' => 'users',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Username / Email
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value defines which model attribute should be considered as your
|
||||||
|
| application's "username" field. Typically, this might be the email
|
||||||
|
| address of the users but you are free to change this value here.
|
||||||
|
|
|
||||||
|
| Out of the box, Fortify expects forgot password and reset password
|
||||||
|
| requests to have a field named 'email'. If the application uses
|
||||||
|
| another name for the field you may define it below as needed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'username' => 'email',
|
||||||
|
|
||||||
|
'email' => 'email',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Lowercase Usernames
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value defines whether usernames should be lowercased before saving
|
||||||
|
| them in the database, as some database system string fields are case
|
||||||
|
| sensitive. You may disable this for your application if necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lowercase_usernames' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Home Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the path where users will get redirected during
|
||||||
|
| authentication or password reset when the operations are successful
|
||||||
|
| and the user is authenticated. You are free to change this value.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'home' => '/dashboard',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Routes Prefix / Subdomain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which prefix Fortify will assign to all the routes
|
||||||
|
| that it registers with the application. If necessary, you may change
|
||||||
|
| subdomain under which all of the Fortify routes will be available.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'prefix' => '',
|
||||||
|
|
||||||
|
'domain' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fortify Routes Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which middleware Fortify will assign to the routes
|
||||||
|
| that it registers with the application. If necessary, you may change
|
||||||
|
| these middleware but typically this provided default is preferred.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Rate Limiting
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default, Fortify will throttle logins to five requests per minute for
|
||||||
|
| every email and IP address combination. However, if you would like to
|
||||||
|
| specify a custom rate limiter to call then you may specify it here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'limiters' => [
|
||||||
|
'login' => 'login',
|
||||||
|
'two-factor' => 'two-factor',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register View Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify if the routes returning views should be disabled as
|
||||||
|
| you may not need them when building your own application. This may be
|
||||||
|
| especially true if you're writing a custom single-page application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'views' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Features
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some of the Fortify features are optional. You may disable the features
|
||||||
|
| by removing them from this array. You're free to only remove some of
|
||||||
|
| these features, or you can even remove all of these if you need to.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'features' => [
|
||||||
|
Features::registration(),
|
||||||
|
Features::resetPasswords(),
|
||||||
|
Features::emailVerification(),
|
||||||
|
Features::twoFactorAuthentication([
|
||||||
|
'confirm' => true,
|
||||||
|
'confirmPassword' => true,
|
||||||
|
// 'window' => 0
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Server Side Rendering
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configures if and how Inertia uses Server Side Rendering
|
||||||
|
| to pre-render each initial request made to your application's pages
|
||||||
|
| so that server rendered HTML is delivered for the user's browser.
|
||||||
|
|
|
||||||
|
| See: https://inertiajs.com/server-side-rendering
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'ssr' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'url' => 'http://127.0.0.1:13714',
|
||||||
|
// 'bundle' => base_path('bootstrap/ssr/ssr.mjs'),
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Pages
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure how Inertia discovers page components on the
|
||||||
|
| filesystem. The paths and extensions are used to locate components
|
||||||
|
| when rendering responses and during testing assertions.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'pages' => [
|
||||||
|
|
||||||
|
'paths' => [
|
||||||
|
resource_path('js/pages'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'extensions' => [
|
||||||
|
'js',
|
||||||
|
'jsx',
|
||||||
|
'svelte',
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'vue',
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Testing
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The values described here are used to locate Inertia components on the
|
||||||
|
| filesystem. For instance, when using `assertInertia`, the assertion
|
||||||
|
| attempts to locate the component as a file relative to the paths.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'testing' => [
|
||||||
|
|
||||||
|
'ensure_pages_exist' => true,
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deprecations Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the log channel that should be used to log warnings
|
||||||
|
| regarding deprecated PHP and library features. This allows you to get
|
||||||
|
| your application ready for upcoming major versions of dependencies.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'deprecations' => [
|
||||||
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Log Channels
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the log channels for your application. Laravel
|
||||||
|
| utilizes the Monolog PHP logging library, which includes a variety
|
||||||
|
| of powerful log handlers and formatters that you're free to use.
|
||||||
|
|
|
||||||
|
| Available drivers: "single", "daily", "slack", "syslog",
|
||||||
|
| "errorlog", "monolog", "custom", "stack"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'channels' => [
|
||||||
|
|
||||||
|
'stack' => [
|
||||||
|
'driver' => 'stack',
|
||||||
|
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||||
|
'ignore_exceptions' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'single' => [
|
||||||
|
'driver' => 'single',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'daily' => [
|
||||||
|
'driver' => 'daily',
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'days' => env('LOG_DAILY_DAYS', 14),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'driver' => 'slack',
|
||||||
|
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||||
|
'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
|
||||||
|
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||||
|
'level' => env('LOG_LEVEL', 'critical'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'papertrail' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||||
|
'handler_with' => [
|
||||||
|
'host' => env('PAPERTRAIL_URL'),
|
||||||
|
'port' => env('PAPERTRAIL_PORT'),
|
||||||
|
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||||
|
],
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'stderr' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'handler' => StreamHandler::class,
|
||||||
|
'handler_with' => [
|
||||||
|
'stream' => 'php://stderr',
|
||||||
|
],
|
||||||
|
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||||
|
'processors' => [PsrLogMessageProcessor::class],
|
||||||
|
],
|
||||||
|
|
||||||
|
'syslog' => [
|
||||||
|
'driver' => 'syslog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'errorlog' => [
|
||||||
|
'driver' => 'errorlog',
|
||||||
|
'level' => env('LOG_LEVEL', 'debug'),
|
||||||
|
'replace_placeholders' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
'null' => [
|
||||||
|
'driver' => 'monolog',
|
||||||
|
'handler' => NullHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'emergency' => [
|
||||||
|
'path' => storage_path('logs/laravel.log'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Mailer Configurations
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure all of the mailers used by your application plus
|
||||||
|
| their respective settings. Several examples have been configured for
|
||||||
|
| you and you are free to add your own as your application requires.
|
||||||
|
|
|
||||||
|
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||||
|
| when delivering an email. You may specify which one you're using for
|
||||||
|
| your mailers below. You may also add additional mailers if needed.
|
||||||
|
|
|
||||||
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||||
|
| "postmark", "resend", "log", "array",
|
||||||
|
| "failover", "roundrobin"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'mailers' => [
|
||||||
|
|
||||||
|
'smtp' => [
|
||||||
|
'transport' => 'smtp',
|
||||||
|
'scheme' => env('MAIL_SCHEME'),
|
||||||
|
'url' => env('MAIL_URL'),
|
||||||
|
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('MAIL_PORT', 2525),
|
||||||
|
'username' => env('MAIL_USERNAME'),
|
||||||
|
'password' => env('MAIL_PASSWORD'),
|
||||||
|
'timeout' => null,
|
||||||
|
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'transport' => 'ses',
|
||||||
|
],
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'transport' => 'postmark',
|
||||||
|
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||||
|
// 'client' => [
|
||||||
|
// 'timeout' => 5,
|
||||||
|
// ],
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'transport' => 'resend',
|
||||||
|
],
|
||||||
|
|
||||||
|
'sendmail' => [
|
||||||
|
'transport' => 'sendmail',
|
||||||
|
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'log' => [
|
||||||
|
'transport' => 'log',
|
||||||
|
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'array' => [
|
||||||
|
'transport' => 'array',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'transport' => 'failover',
|
||||||
|
'mailers' => [
|
||||||
|
'smtp',
|
||||||
|
'log',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
'roundrobin' => [
|
||||||
|
'transport' => 'roundrobin',
|
||||||
|
'mailers' => [
|
||||||
|
'ses',
|
||||||
|
'postmark',
|
||||||
|
],
|
||||||
|
'retry_after' => 60,
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global "From" Address
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may wish for all emails sent by your application to be sent from
|
||||||
|
| the same address. Here you may specify a name and address that is
|
||||||
|
| used globally for all emails that are sent by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'from' => [
|
||||||
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
|
'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queue Connections
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may configure the connection options for every queue backend
|
||||||
|
| used by your application. An example configuration is provided for
|
||||||
|
| each backend supported by Laravel. You're also free to add more.
|
||||||
|
|
|
||||||
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||||
|
| "deferred", "background", "failover", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connections' => [
|
||||||
|
|
||||||
|
'sync' => [
|
||||||
|
'driver' => 'sync',
|
||||||
|
],
|
||||||
|
|
||||||
|
'database' => [
|
||||||
|
'driver' => 'database',
|
||||||
|
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||||
|
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||||
|
'queue' => env('DB_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'beanstalkd' => [
|
||||||
|
'driver' => 'beanstalkd',
|
||||||
|
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||||
|
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => 0,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'sqs' => [
|
||||||
|
'driver' => 'sqs',
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||||
|
'queue' => env('SQS_QUEUE', 'default'),
|
||||||
|
'suffix' => env('SQS_SUFFIX'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'driver' => 'redis',
|
||||||
|
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||||
|
'queue' => env('REDIS_QUEUE', 'default'),
|
||||||
|
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||||
|
'block_for' => null,
|
||||||
|
'after_commit' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'deferred' => [
|
||||||
|
'driver' => 'deferred',
|
||||||
|
],
|
||||||
|
|
||||||
|
'background' => [
|
||||||
|
'driver' => 'background',
|
||||||
|
],
|
||||||
|
|
||||||
|
'failover' => [
|
||||||
|
'driver' => 'failover',
|
||||||
|
'connections' => [
|
||||||
|
'database',
|
||||||
|
'deferred',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Job Batching
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following options configure the database and table that store job
|
||||||
|
| batching information. These options can be updated to any database
|
||||||
|
| connection and table which has been defined by your application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'batching' => [
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'job_batches',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Failed Queue Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These options configure the behavior of failed queue job logging so you
|
||||||
|
| can control how and where failed jobs are stored. Laravel ships with
|
||||||
|
| support for storing failed jobs in a simple file or in a database.
|
||||||
|
|
|
||||||
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'failed' => [
|
||||||
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
|
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'table' => 'failed_jobs',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
|
'%s%s',
|
||||||
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||||
|
Sanctum::currentApplicationUrlWithPort(),
|
||||||
|
// Sanctum::currentRequestHost(),
|
||||||
|
))),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This array contains the authentication guards that will be checked when
|
||||||
|
| Sanctum is trying to authenticate a request. If none of these guards
|
||||||
|
| are able to authenticate the request, Sanctum will use the bearer
|
||||||
|
| token that's present on an incoming request for authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Expiration Minutes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the number of minutes until an issued token will be
|
||||||
|
| considered expired. This will override any values set in the token's
|
||||||
|
| "expires_at" attribute, but first-party sessions are not affected.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Token Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||||
|
| security scanning initiatives maintained by open source platforms
|
||||||
|
| that notify developers if they commit tokens into repositories.
|
||||||
|
|
|
||||||
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When authenticating your first-party SPA with Sanctum you may need to
|
||||||
|
| customize some of the middleware Sanctum uses while processing the
|
||||||
|
| request. You may change the middleware listed below as required.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||||
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'resend' => [
|
||||||
|
'key' => env('RESEND_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'ses' => [
|
||||||
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||||
|
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'slack' => [
|
||||||
|
'notifications' => [
|
||||||
|
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||||
|
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Lifetime
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the number of minutes that you wish the session
|
||||||
|
| to be allowed to remain idle before it expires. If you want them
|
||||||
|
| to expire immediately when the browser is closed then you may
|
||||||
|
| indicate that via the expire_on_close configuration option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Encryption
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option allows you to easily specify that all of your session data
|
||||||
|
| should be encrypted before it's stored. All encryption is performed
|
||||||
|
| automatically by Laravel and you may use the session like normal.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session File Location
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When utilizing the "file" session driver, the session files are placed
|
||||||
|
| on disk. The default storage location is defined here; however, you
|
||||||
|
| are free to provide another location where they should be stored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Connection
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" or "redis" session drivers, you may specify a
|
||||||
|
| connection that should be used to manage these sessions. This should
|
||||||
|
| correspond to a connection in your database configuration options.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'connection' => env('SESSION_CONNECTION'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Database Table
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the "database" session driver, you may specify the table to
|
||||||
|
| be used to store sessions. Of course, a sensible default is defined
|
||||||
|
| for you; however, you're welcome to change this to another table.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using one of the framework's cache driven session backends, you may
|
||||||
|
| define the cache store which should be used to store the session data
|
||||||
|
| between requests. This must match one of your defined cache stores.
|
||||||
|
|
|
||||||
|
| Affects: "dynamodb", "memcached", "redis"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Sweeping Lottery
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Some session drivers must manually sweep their storage location to get
|
||||||
|
| rid of old sessions from storage. Here are the chances that it will
|
||||||
|
| happen on a given request. By default, the odds are 2 out of 100.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may change the name of the session cookie that is created by
|
||||||
|
| the framework. Typically, you should not need to change this value
|
||||||
|
| since doing so does not grant a meaningful security improvement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'cookie' => env(
|
||||||
|
'SESSION_COOKIE',
|
||||||
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session',
|
||||||
|
),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The session cookie path determines the path for which the cookie will
|
||||||
|
| be regarded as available. Typically, this will be the root path of
|
||||||
|
| your application, but you're free to change this when necessary.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Session Cookie Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines the domain and subdomains the session cookie is
|
||||||
|
| available to. By default, the cookie will be available to the root
|
||||||
|
| domain without subdomains. Typically, this shouldn't be changed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTPS Only Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By setting this option to true, session cookies will only be sent back
|
||||||
|
| to the server if the browser has a HTTPS connection. This will keep
|
||||||
|
| the cookie from being sent to you when it can't be done securely.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| HTTP Access Only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will prevent JavaScript from accessing the
|
||||||
|
| value of the cookie and the cookie will only be accessible through
|
||||||
|
| the HTTP protocol. It's unlikely you should disable this option.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Same-Site Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines how your cookies behave when cross-site requests
|
||||||
|
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||||
|
| will set this value to "lax" to permit secure cross-site requests.
|
||||||
|
|
|
||||||
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||||
|
|
|
||||||
|
| Supported: "lax", "strict", "none", null
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Partitioned Cookies
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Setting this value to true will tie the cookie to the top-level site for
|
||||||
|
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||||
|
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'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',
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<User>
|
||||||
|
*/
|
||||||
|
class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->name(),
|
||||||
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
'remember_token' => Str::random(10),
|
||||||
|
'two_factor_secret' => null,
|
||||||
|
'two_factor_recovery_codes' => null,
|
||||||
|
'two_factor_confirmed_at' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model's email address should be unverified.
|
||||||
|
*/
|
||||||
|
public function unverified(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'email_verified_at' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that the model has two-factor authentication configured.
|
||||||
|
*/
|
||||||
|
public function withTwoFactor(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (array $attributes) => [
|
||||||
|
'two_factor_secret' => encrypt('secret'),
|
||||||
|
'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])),
|
||||||
|
'two_factor_confirmed_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('password');
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->mediumText('value');
|
||||||
|
$table->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('cache_locks', function (Blueprint $table) {
|
||||||
|
$table->string('key')->primary();
|
||||||
|
$table->string('owner');
|
||||||
|
$table->integer('expiration')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('cache');
|
||||||
|
Schema::dropIfExists('cache_locks');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('queue')->index();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->unsignedTinyInteger('attempts');
|
||||||
|
$table->unsignedInteger('reserved_at')->nullable();
|
||||||
|
$table->unsignedInteger('available_at');
|
||||||
|
$table->unsignedInteger('created_at');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('job_batches', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->string('name');
|
||||||
|
$table->integer('total_jobs');
|
||||||
|
$table->integer('pending_jobs');
|
||||||
|
$table->integer('failed_jobs');
|
||||||
|
$table->longText('failed_job_ids');
|
||||||
|
$table->mediumText('options')->nullable();
|
||||||
|
$table->integer('cancelled_at')->nullable();
|
||||||
|
$table->integer('created_at');
|
||||||
|
$table->integer('finished_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('uuid')->unique();
|
||||||
|
$table->text('connection');
|
||||||
|
$table->text('queue');
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->longText('exception');
|
||||||
|
$table->timestamp('failed_at')->useCurrent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('jobs');
|
||||||
|
Schema::dropIfExists('job_batches');
|
||||||
|
Schema::dropIfExists('failed_jobs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->text('two_factor_secret')->after('password')->nullable();
|
||||||
|
$table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable();
|
||||||
|
$table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'two_factor_secret',
|
||||||
|
'two_factor_recovery_codes',
|
||||||
|
'two_factor_confirmed_at',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->text('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class DatabaseSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// User::factory(10)->create();
|
||||||
|
|
||||||
|
User::factory()->create([
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import stylistic from '@stylistic/eslint-plugin';
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
|
||||||
|
import prettier from 'eslint-config-prettier/flat';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import vue from 'eslint-plugin-vue';
|
||||||
|
|
||||||
|
const controlStatements = [
|
||||||
|
'if',
|
||||||
|
'return',
|
||||||
|
'for',
|
||||||
|
'while',
|
||||||
|
'do',
|
||||||
|
'switch',
|
||||||
|
'try',
|
||||||
|
'throw',
|
||||||
|
];
|
||||||
|
const paddingAroundControl = [
|
||||||
|
...controlStatements.flatMap((stmt) => [
|
||||||
|
{ blankLine: 'always', prev: '*', next: stmt },
|
||||||
|
{ blankLine: 'always', prev: stmt, next: '*' },
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
vue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
import: importPlugin,
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/consistent-type-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
prefer: 'type-imports',
|
||||||
|
fixStyle: 'separate-type-imports',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/consistent-type-specifier-style': [
|
||||||
|
'error',
|
||||||
|
'prefer-top-level',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'@stylistic': stylistic,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
||||||
|
'@stylistic/padding-line-between-statements': [
|
||||||
|
'error',
|
||||||
|
...paddingAroundControl,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'vendor',
|
||||||
|
'node_modules',
|
||||||
|
'public',
|
||||||
|
'bootstrap/ssr',
|
||||||
|
'tailwind.config.js',
|
||||||
|
'vite.config.ts',
|
||||||
|
'resources/js/actions/**',
|
||||||
|
'resources/js/components/ui/*',
|
||||||
|
'resources/js/routes/**',
|
||||||
|
'resources/js/wayfinder/**',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
prettier, // Turn off all rules that might conflict with Prettier
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'@stylistic': stylistic,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
curly: ['error', 'all'],
|
||||||
|
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
Generated
+6904
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"build:ssr": "vite build && vite build --ssr",
|
||||||
|
"dev": "vite",
|
||||||
|
"format": "prettier --write resources/",
|
||||||
|
"format:check": "prettier --check resources/",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"lint:check": "eslint .",
|
||||||
|
"types:check": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.19.0",
|
||||||
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
|
"@laravel/vite-plugin-wayfinder": "^0.1.3",
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@types/node": "^22.13.5",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.3.0",
|
||||||
|
"concurrently": "^9.0.1",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"typescript-eslint": "^8.23.0",
|
||||||
|
"vite": "^8.0.0",
|
||||||
|
"vue-tsc": "^2.2.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@inertiajs/vite": "^3.0.0",
|
||||||
|
"@inertiajs/vue3": "^3.0.0",
|
||||||
|
"@vueuse/core": "^12.8.2",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"laravel-vite-plugin": "^3.0.0",
|
||||||
|
"lucide-vue-next": "^0.468.0",
|
||||||
|
"reka-ui": "^2.6.1",
|
||||||
|
"tailwind-merge": "^3.2.0",
|
||||||
|
"tailwindcss": "^4.1.1",
|
||||||
|
"tw-animate-css": "^1.2.5",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-sonner": "^2.0.0",
|
||||||
|
"vue-input-otp": "^0.3.2"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.9.5",
|
||||||
|
"@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",
|
||||||
|
"@tailwindcss/oxide-win32-x64-msvc": "^4.0.1",
|
||||||
|
"lightningcss-linux-x64-gnu": "^1.29.1",
|
||||||
|
"lightningcss-win32-x64-msvc": "^1.29.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests/Unit</directory>
|
||||||
|
</testsuite>
|
||||||
|
<testsuite name="Feature">
|
||||||
|
<directory>tests/Feature</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory>app</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<env name="DB_CONNECTION" value="sqlite"/>
|
||||||
|
<env name="DB_DATABASE" value=":memory:"/>
|
||||||
|
<env name="DB_URL" value=""/>
|
||||||
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
<env name="SESSION_DRIVER" value="array"/>
|
||||||
|
<env name="PULSE_ENABLED" value="false"/>
|
||||||
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
|
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||||
|
</php>
|
||||||
|
</phpunit>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
publicHoistPattern:
|
||||||
|
- '@inertiajs/core'
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
<IfModule mod_negotiation.c>
|
||||||
|
Options -MultiViews -Indexes
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Handle Authorization Header
|
||||||
|
RewriteCond %{HTTP:Authorization} .
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Handle X-XSRF-Token Header
|
||||||
|
RewriteCond %{HTTP:x-xsrf-token} .
|
||||||
|
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||||
|
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
</IfModule>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M162.041 38.7592C162.099 38.9767 162.129 39.201 162.13 39.4264V74.4524C162.13 74.9019 162.011 75.3435 161.786 75.7325C161.561 76.1216 161.237 76.4442 160.847 76.6678L131.462 93.5935V127.141C131.462 128.054 130.977 128.897 130.186 129.357L68.8474 164.683C68.707 164.763 68.5538 164.814 68.4007 164.868C68.3432 164.887 68.289 164.922 68.2284 164.938C67.7996 165.051 67.3489 165.051 66.9201 164.938C66.8499 164.919 66.7861 164.881 66.7191 164.855C66.5787 164.804 66.4319 164.76 66.2979 164.683L4.97219 129.357C4.58261 129.133 4.2589 128.81 4.0337 128.421C3.8085 128.032 3.68976 127.591 3.68945 127.141L3.68945 22.0634C3.68945 21.8336 3.72136 21.6101 3.7788 21.393C3.79794 21.3196 3.84262 21.2526 3.86814 21.1791C3.91601 21.0451 3.96068 20.9078 4.03088 20.7833C4.07874 20.7003 4.14894 20.6333 4.20638 20.5566C4.27977 20.4545 4.34678 20.3491 4.43293 20.2598C4.50632 20.1863 4.60205 20.1321 4.68501 20.0682C4.77755 19.9916 4.86051 19.9086 4.96581 19.848L35.6334 2.18492C36.0217 1.96139 36.4618 1.84375 36.9098 1.84375C37.3578 1.84375 37.7979 1.96139 38.1862 2.18492L68.8506 19.848H68.857C68.9591 19.9118 69.0452 19.9916 69.1378 20.065C69.2207 20.1289 69.3133 20.1863 69.3867 20.2566C69.476 20.3491 69.5398 20.4545 69.6164 20.5566C69.6707 20.6333 69.7441 20.7003 69.7887 20.7833C69.8621 20.911 69.9036 21.0451 69.9546 21.1791C69.9802 21.2526 70.0248 21.3196 70.044 21.3962C70.1027 21.6138 70.1328 21.8381 70.1333 22.0634V87.6941L95.686 72.9743V39.4232C95.686 39.1997 95.7179 38.9731 95.7753 38.7592C95.7977 38.6826 95.8391 38.6155 95.8647 38.5421C95.9157 38.408 95.9604 38.2708 96.0306 38.1463C96.0785 38.0633 96.1487 37.9962 96.2029 37.9196C96.2795 37.8175 96.3433 37.7121 96.4326 37.6227C96.506 37.5493 96.5986 37.495 96.6815 37.4312C96.7773 37.3546 96.8602 37.2716 96.9623 37.2109L127.633 19.5479C128.021 19.324 128.461 19.2062 128.91 19.2062C129.358 19.2062 129.798 19.324 130.186 19.5479L160.85 37.2109C160.959 37.2748 161.042 37.3546 161.137 37.428C161.217 37.4918 161.31 37.5493 161.383 37.6195C161.473 37.7121 161.536 37.8175 161.613 37.9196C161.67 37.9962 161.741 38.0633 161.785 38.1463C161.859 38.2708 161.9 38.408 161.951 38.5421C161.98 38.6155 162.021 38.6826 162.041 38.7592ZM157.018 72.9743V43.8477L146.287 50.028L131.462 58.5675V87.6941L157.021 72.9743H157.018ZM126.354 125.663V96.5176L111.771 104.85L70.1301 128.626V158.046L126.354 125.663ZM8.80126 26.4848V125.663L65.0183 158.043V128.629L35.6494 112L35.6398 111.994L35.6271 111.988C35.5281 111.93 35.4452 111.847 35.3526 111.777C35.2729 111.713 35.1803 111.662 35.1101 111.592L35.1038 111.582C35.0208 111.502 34.9634 111.403 34.8932 111.314C34.8293 111.228 34.7528 111.154 34.7017 111.065L34.6985 111.055C34.6411 110.96 34.606 110.845 34.5645 110.736C34.523 110.64 34.4688 110.551 34.4432 110.449C34.4113 110.328 34.4049 110.197 34.3922 110.072C34.3794 109.976 34.3539 109.881 34.3539 109.785V109.778V41.2045L19.5322 32.6619L8.80126 26.4848ZM36.913 7.35007L11.3635 22.0634L36.9066 36.7768L62.4529 22.0602L36.9066 7.35007H36.913ZM50.1999 99.1736L65.0215 90.6374V26.4848L54.2906 32.6651L39.4657 41.2045V105.357L50.1999 99.1736ZM128.91 24.713L103.363 39.4264L128.91 54.1397L154.453 39.4232L128.91 24.713ZM126.354 58.5675L111.529 50.028L100.798 43.8477V72.9743L115.619 81.5106L126.354 87.6941V58.5675ZM67.5711 124.205L105.042 102.803L123.772 92.109L98.2451 77.4053L68.8538 94.3341L42.0663 109.762L67.5711 124.205Z" fill="#FF2D20"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Determine if the application is in maintenance mode...
|
||||||
|
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||||
|
require $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the request...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->handleRequest(Request::capture());
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||||
|
@source '../../storage/framework/views/*.php';
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-sans:
|
||||||
|
Instrument Sans, ui-sans-serif, system-ui, sans-serif,
|
||||||
|
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
|
||||||
|
--color-sidebar: var(--sidebar-background);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||||
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
looks the same as it did with Tailwind CSS v3.
|
||||||
|
|
||||||
|
If we ever want to remove these styles, we need to add an explicit border
|
||||||
|
color utility to any element that depends on these defaults.
|
||||||
|
*/
|
||||||
|
@layer base {
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-gray-200, currentColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
--font-sans:
|
||||||
|
'Instrument Sans', ui-sans-serif, system-ui, sans-serif,
|
||||||
|
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: hsl(0 0% 100%);
|
||||||
|
--foreground: hsl(0 0% 3.9%);
|
||||||
|
--card: hsl(0 0% 100%);
|
||||||
|
--card-foreground: hsl(0 0% 3.9%);
|
||||||
|
--popover: hsl(0 0% 100%);
|
||||||
|
--popover-foreground: hsl(0 0% 3.9%);
|
||||||
|
--primary: hsl(0 0% 9%);
|
||||||
|
--primary-foreground: hsl(0 0% 98%);
|
||||||
|
--secondary: hsl(0 0% 92.1%);
|
||||||
|
--secondary-foreground: hsl(0 0% 9%);
|
||||||
|
--muted: hsl(0 0% 96.1%);
|
||||||
|
--muted-foreground: hsl(0 0% 45.1%);
|
||||||
|
--accent: hsl(0 0% 96.1%);
|
||||||
|
--accent-foreground: hsl(0 0% 9%);
|
||||||
|
--destructive: hsl(0 84.2% 60.2%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--border: hsl(0 0% 92.8%);
|
||||||
|
--input: hsl(0 0% 89.8%);
|
||||||
|
--ring: hsl(0 0% 3.9%);
|
||||||
|
--chart-1: hsl(12 76% 61%);
|
||||||
|
--chart-2: hsl(173 58% 39%);
|
||||||
|
--chart-3: hsl(197 37% 24%);
|
||||||
|
--chart-4: hsl(43 74% 66%);
|
||||||
|
--chart-5: hsl(27 87% 67%);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--sidebar-background: hsl(0 0% 98%);
|
||||||
|
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
||||||
|
--sidebar-primary: hsl(0 0% 10%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||||
|
--sidebar-accent: hsl(0 0% 94%);
|
||||||
|
--sidebar-accent-foreground: hsl(0 0% 30%);
|
||||||
|
--sidebar-border: hsl(0 0% 91%);
|
||||||
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
--sidebar: hsl(0 0% 98%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: hsl(0 0% 3.9%);
|
||||||
|
--foreground: hsl(0 0% 98%);
|
||||||
|
--card: hsl(0 0% 3.9%);
|
||||||
|
--card-foreground: hsl(0 0% 98%);
|
||||||
|
--popover: hsl(0 0% 3.9%);
|
||||||
|
--popover-foreground: hsl(0 0% 98%);
|
||||||
|
--primary: hsl(0 0% 98%);
|
||||||
|
--primary-foreground: hsl(0 0% 9%);
|
||||||
|
--secondary: hsl(0 0% 14.9%);
|
||||||
|
--secondary-foreground: hsl(0 0% 98%);
|
||||||
|
--muted: hsl(0 0% 16.08%);
|
||||||
|
--muted-foreground: hsl(0 0% 63.9%);
|
||||||
|
--accent: hsl(0 0% 14.9%);
|
||||||
|
--accent-foreground: hsl(0 0% 98%);
|
||||||
|
--destructive: hsl(0 84% 60%);
|
||||||
|
--destructive-foreground: hsl(0 0% 98%);
|
||||||
|
--border: hsl(0 0% 14.9%);
|
||||||
|
--input: hsl(0 0% 14.9%);
|
||||||
|
--ring: hsl(0 0% 83.1%);
|
||||||
|
--chart-1: hsl(220 70% 50%);
|
||||||
|
--chart-2: hsl(160 60% 45%);
|
||||||
|
--chart-3: hsl(30 80% 55%);
|
||||||
|
--chart-4: hsl(280 65% 60%);
|
||||||
|
--chart-5: hsl(340 75% 55%);
|
||||||
|
--sidebar-background: hsl(0 0% 7%);
|
||||||
|
--sidebar-foreground: hsl(0 0% 95.9%);
|
||||||
|
--sidebar-primary: hsl(360, 100%, 100%);
|
||||||
|
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||||
|
--sidebar-accent: hsl(0 0% 15.9%);
|
||||||
|
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||||
|
--sidebar-border: hsl(0 0% 15.9%);
|
||||||
|
--sidebar-ring: hsl(217.2 91.2% 59.8%);
|
||||||
|
--sidebar: hsl(240 5.9% 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { createInertiaApp } from '@inertiajs/vue3';
|
||||||
|
import { initializeTheme } from '@/composables/useAppearance';
|
||||||
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
|
import AuthLayout from '@/layouts/AuthLayout.vue';
|
||||||
|
import SettingsLayout from '@/layouts/settings/Layout.vue';
|
||||||
|
import { initializeFlashToast } from '@/lib/flashToast';
|
||||||
|
|
||||||
|
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||||
|
|
||||||
|
createInertiaApp({
|
||||||
|
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||||
|
layout: (name) => {
|
||||||
|
switch (true) {
|
||||||
|
case name === 'Welcome':
|
||||||
|
return null;
|
||||||
|
case name.startsWith('auth/'):
|
||||||
|
return AuthLayout;
|
||||||
|
case name.startsWith('settings/'):
|
||||||
|
return [AppLayout, SettingsLayout];
|
||||||
|
default:
|
||||||
|
return AppLayout;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
color: '#4B5563',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// This will set light / dark mode on page load...
|
||||||
|
initializeTheme();
|
||||||
|
|
||||||
|
// This will listen for flash toast data from the server...
|
||||||
|
initializeFlashToast();
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AlertCircle } from 'lucide-vue-next';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
errors: string[];
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
|
||||||
|
const uniqueErrors = computed(() => Array.from(new Set(props.errors)));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertCircle class="size-4" />
|
||||||
|
<AlertTitle>{{ title }}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<ul class="list-inside list-disc text-sm">
|
||||||
|
<li v-for="(error, index) in uniqueErrors" :key="index">
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { SidebarInset } from '@/components/ui/sidebar';
|
||||||
|
import type { AppVariant } from '@/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
variant?: AppVariant;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
variant: 'sidebar',
|
||||||
|
});
|
||||||
|
const className = computed(() => props.class);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SidebarInset v-if="props.variant === 'sidebar'" :class="className">
|
||||||
|
<slot />
|
||||||
|
</SidebarInset>
|
||||||
|
<main
|
||||||
|
v-else
|
||||||
|
class="mx-auto flex h-full w-full max-w-7xl flex-1 flex-col gap-4 rounded-xl"
|
||||||
|
:class="className"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
|
import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-vue-next';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import AppLogo from '@/components/AppLogo.vue';
|
||||||
|
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||||
|
import Breadcrumbs from '@/components/Breadcrumbs.vue';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuList,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from '@/components/ui/navigation-menu';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from '@/components/ui/sheet';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||||
|
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
||||||
|
import { getInitials } from '@/composables/useInitials';
|
||||||
|
import { toUrl } from '@/lib/utils';
|
||||||
|
import { dashboard } from '@/routes';
|
||||||
|
import type { BreadcrumbItem, NavItem } from '@/types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
breadcrumbs: () => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = usePage();
|
||||||
|
const auth = computed(() => page.props.auth);
|
||||||
|
const { isCurrentUrl, whenCurrentUrl } = useCurrentUrl();
|
||||||
|
|
||||||
|
const activeItemStyles =
|
||||||
|
'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100';
|
||||||
|
|
||||||
|
const mainNavItems: NavItem[] = [
|
||||||
|
{
|
||||||
|
title: 'Dashboard',
|
||||||
|
href: dashboard(),
|
||||||
|
icon: LayoutGrid,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const rightNavItems: NavItem[] = [
|
||||||
|
{
|
||||||
|
title: 'Repository',
|
||||||
|
href: 'https://github.com/laravel/vue-starter-kit',
|
||||||
|
icon: Folder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Documentation',
|
||||||
|
href: 'https://laravel.com/docs/starter-kits#vue',
|
||||||
|
icon: BookOpen,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="border-b border-sidebar-border/80">
|
||||||
|
<div class="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
|
||||||
|
<!-- Mobile Menu -->
|
||||||
|
<div class="lg:hidden">
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger :as-child="true">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="mr-2 h-9 w-9"
|
||||||
|
>
|
||||||
|
<Menu class="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" class="w-[300px] p-6">
|
||||||
|
<SheetTitle class="sr-only"
|
||||||
|
>Navigation menu</SheetTitle
|
||||||
|
>
|
||||||
|
<SheetHeader class="flex justify-start text-left">
|
||||||
|
<AppLogoIcon
|
||||||
|
class="size-6 fill-current text-black dark:text-white"
|
||||||
|
/>
|
||||||
|
</SheetHeader>
|
||||||
|
<div
|
||||||
|
class="flex h-full flex-1 flex-col justify-between space-y-4 py-6"
|
||||||
|
>
|
||||||
|
<nav class="-mx-3 space-y-1">
|
||||||
|
<Link
|
||||||
|
v-for="item in mainNavItems"
|
||||||
|
:key="item.title"
|
||||||
|
:href="item.href"
|
||||||
|
class="flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent"
|
||||||
|
:class="
|
||||||
|
whenCurrentUrl(
|
||||||
|
item.href,
|
||||||
|
activeItemStyles,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
|
{{ item.title }}
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<a
|
||||||
|
v-for="item in rightNavItems"
|
||||||
|
:key="item.title"
|
||||||
|
:href="toUrl(item.href)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="flex items-center space-x-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
class="h-5 w-5"
|
||||||
|
/>
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link :href="dashboard()" class="flex items-center gap-x-2">
|
||||||
|
<AppLogo />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<!-- Desktop Menu -->
|
||||||
|
<div class="hidden h-full lg:flex lg:flex-1">
|
||||||
|
<NavigationMenu class="ml-10 flex h-full items-stretch">
|
||||||
|
<NavigationMenuList
|
||||||
|
class="flex h-full items-stretch space-x-2"
|
||||||
|
>
|
||||||
|
<NavigationMenuItem
|
||||||
|
v-for="(item, index) in mainNavItems"
|
||||||
|
:key="index"
|
||||||
|
class="relative flex h-full items-center"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
:class="[
|
||||||
|
navigationMenuTriggerStyle(),
|
||||||
|
whenCurrentUrl(
|
||||||
|
item.href,
|
||||||
|
activeItemStyles,
|
||||||
|
),
|
||||||
|
'h-9 cursor-pointer px-3',
|
||||||
|
]"
|
||||||
|
:href="item.href"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-if="item.icon"
|
||||||
|
:is="item.icon"
|
||||||
|
class="mr-2 h-4 w-4"
|
||||||
|
/>
|
||||||
|
{{ item.title }}
|
||||||
|
</Link>
|
||||||
|
<div
|
||||||
|
v-if="isCurrentUrl(item.href)"
|
||||||
|
class="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"
|
||||||
|
></div>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</NavigationMenuList>
|
||||||
|
</NavigationMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-auto flex items-center space-x-2">
|
||||||
|
<div class="relative flex items-center space-x-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="group h-9 w-9 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Search
|
||||||
|
class="size-5 opacity-80 group-hover:opacity-100"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div class="hidden space-x-1 lg:flex">
|
||||||
|
<template
|
||||||
|
v-for="item in rightNavItems"
|
||||||
|
:key="item.title"
|
||||||
|
>
|
||||||
|
<TooltipProvider :delay-duration="0">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
as-child
|
||||||
|
class="group h-9 w-9 cursor-pointer"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
:href="toUrl(item.href)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span class="sr-only">{{
|
||||||
|
item.title
|
||||||
|
}}</span>
|
||||||
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
class="size-5 opacity-80 group-hover:opacity-100"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{{ item.title }}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger :as-child="true">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="relative size-10 w-auto rounded-full p-1 focus-within:ring-2 focus-within:ring-primary"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
class="size-8 overflow-hidden rounded-full"
|
||||||
|
>
|
||||||
|
<AvatarImage
|
||||||
|
v-if="auth.user.avatar"
|
||||||
|
:src="auth.user.avatar"
|
||||||
|
:alt="auth.user.name"
|
||||||
|
/>
|
||||||
|
<AvatarFallback
|
||||||
|
class="rounded-lg bg-neutral-200 font-semibold text-black dark:bg-neutral-700 dark:text-white"
|
||||||
|
>
|
||||||
|
{{ getInitials(auth.user?.name) }}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" class="w-56">
|
||||||
|
<UserMenuContent :user="auth.user" />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.breadcrumbs.length > 1"
|
||||||
|
class="flex w-full border-b border-sidebar-border/70"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl"
|
||||||
|
>
|
||||||
|
<Breadcrumbs :breadcrumbs="breadcrumbs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import AppLogoIcon from '@/components/AppLogoIcon.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex aspect-square size-8 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground"
|
||||||
|
>
|
||||||
|
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-1 grid flex-1 text-left text-sm">
|
||||||
|
<span class="mb-0.5 truncate leading-tight font-semibold"
|
||||||
|
>Laravel Starter Kit</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user