262 lines
21 KiB
Markdown
262 lines
21 KiB
Markdown
<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 running on PHP 8.4. You are an expert with the Laravel ecosystem. Always use the APIs that match the installed major version of each package — do not assume a version.
|
|
|
|
Before relying on a package's API, confirm its installed version:
|
|
- PHP packages: run `composer show --direct` to list direct dependencies with versions, or `composer show <vendor/package>` for a single package.
|
|
- JS packages: check `package.json` for the installed versions.
|
|
|
|
## Skills Activation
|
|
|
|
This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
|
|
|
## 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"]`.
|
|
|
|
## Project Rules
|
|
|
|
- This project contains committed, area-grouped rules in `.ai/rules` when that directory exists (settled decisions, non-obvious traps, standing constraints). Framework and package guidelines that only apply to specific paths (testing, frontend, components) also live there, under `.ai/rules/boost` — this is not just recorded decisions, it is load-bearing guidance you have not seen inline. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every rule file whose globs cover the path(s) in scope, and run `grep -rin 'keyword' .ai/rules` to catch what a path match alone misses. Do not write code until you have read and are following every matching rule. If `.ai/rules` does not exist, continue without it.
|
|
- Record durable rules with `record-rule` so the next agent or teammate inherits them instead of working them out again. Pass a `glob` (e.g. `app/Http/Controllers/**`), a short `title`, and a few-line `note`. Always use `record-rule`, never your native memory or notes tool — native memory is personal and session-scoped; only `.ai/rules` is shared with the team and persists in the repo.
|
|
|
|
## 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.
|
|
|
|
## 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`
|
|
- Follow existing application Enum naming conventions.
|
|
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
|
- Use array shape type definitions in PHPDoc blocks.
|
|
|
|
=== deployments rules ===
|
|
|
|
# Deployment
|
|
|
|
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
|
|
|
|
=== herd rules ===
|
|
|
|
# Laravel Herd
|
|
|
|
- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available.
|
|
- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start <service>`, `herd php:list`). Run `herd list` to discover all available commands.
|
|
|
|
=== 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`.
|
|
|
|
=== 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}`.
|
|
- The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`.
|
|
- 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>
|
|
|
|
# Vanguard
|
|
|
|
## Frontend
|
|
|
|
- The UI is Inertia + Vue 3 with TypeScript. There is no Blade UI: the only Blade left is `resources/views/app.blade.php` (the Inertia root), the installation wizard, and the mail templates.
|
|
- Components live in `resources/js/components/ui/` and are ours. They wrap Reka UI primitives, which supply focus management, keyboard interaction and ARIA; everything visual is plain Tailwind in those files. Reach for one before writing a new component.
|
|
- Server data reaches pages as Inertia props. Shared props are defined in `App\Http\Middleware\HandleInertiaRequests`; per-page shapes live in `App\Support\*Payload` classes rather than being assembled inline in controllers.
|
|
- `route()` comes from Ziggy, whose route list is emitted once per full page load by `@routes`. Do not re-add it as a shared prop.
|
|
- `__()` in Vue comes from `useTranslations()`. It falls back to the key on an empty string, matching Laravel's `$line ?: $key`.
|
|
|
|
## Plugins
|
|
|
|
- Plugins are separate composer packages that ship their own compiled Vue bundle to `public/vendor/plugins/{slug}`. Installing one never requires rebuilding the host.
|
|
- A bundle registers its pages, slots and widgets against `window.Vanguard` and externalises Vue, Inertia and the UI kit onto that runtime, so it shares the host's single Vue instance.
|
|
- `resources/js/plugin-api.ts` is what plugins import as `@vanguard/ui`. Removing an export from it is a breaking change for third party plugins.
|
|
- Plugins contribute UI through slots (`Vanguard::slot()`), never by reaching into host views.
|
|
|
|
## Demo mode
|
|
|
|
- Turned on with `DEMO_MODE` in `.env`, read once by `config/demo.php`. Never move it into the settings store: blocking settings writes is demo mode's main job, so a toggle living there would be either unreachable to the maintainer or reachable to any visitor on the shared account.
|
|
- The whole policy is data in `config/demo.php`. `App\Http\Middleware\DemoMode` is the only enforcement point and no controller knows demo mode exists — the opposite of the `demo_v8_1` branch, which pasted a guard into fifteen actions.
|
|
- `App\Support\Demo::isProtectedUser()` is the single definition of "the demo account". Never compare a username to `admin` at a call site.
|
|
- Rules match the route **name** and the request **path**, because most of `routes/api.php` is unnamed and a name-only matcher leaves the API half guarded. Dots are names, slashes are paths.
|
|
- `blocked` is consulted after the safe-verb pass, so `settings.*` refuses saving without hiding the settings pages. GET routes that mutate (`install.installation.start`) go in `unsafe_reads`, which is checked before it.
|
|
- Impersonation is deliberately allowed in the demo — it is a feature the demo exists to show off, and it grants nothing, because the rules follow the impersonated identity. Taking over the demo account still cannot modify it.
|
|
- The middleware is appended to the `web` and `api` groups, both of which run `SubstituteBindings` first — the rules read the route-bound `{user}`, `{role}` and `{permission}` models, so it must stay after it. Its position relative to `HandleInertiaRequests` does not matter: Inertia's service provider pushes a *global* `EnsureGetOnRedirect` that rewrites the refusal's 302 into a 303 for PUT/PATCH/DELETE, which is what stops the browser repeating the verb. Do not "fix" that by moving the middleware; the 303 does not come from the group.
|
|
- Refusals use `back()->with('error', ...)`, not `withErrors()`. Only `AuthLayout` renders an `errors[0]` banner, so on an authenticated page `withErrors()` shows nothing.
|
|
- `phpunit.xml` pins `DEMO_MODE` off, so the suite keeps its meaning when run on the demo box, whose `.env` turns it on. Spelled `(false)` and not `false`: PHPUnit parses a bare `false` into a boolean and then puts the *empty string* in the environment, which `env()` returns verbatim.
|
|
- Being a group middleware, it runs before `auth` and `permission:`. A visitor without the permission sees the demo message rather than a 403. That is deliberate — `abort(403)` renders the stock error page, which an Inertia visit cannot parse.
|
|
|
|
## Testing
|
|
|
|
- Feature tests assert against the Inertia payload, not rendered markup. `$response->viewData('page')` gives the whole page object; `assertInertia()` covers the common cases.
|
|
- Plugin suites live in their own repositories and run from there against a Vanguard checkout. See each plugin's readme.
|
|
- Every test in this project is Pest, in Vanguard and in both official plugins. Only the base classes (`tests/TestCase.php`, `tests/Feature/ApiTestCase.php`) are still PHP classes, bound to their directories by `tests/Pest.php`. This overrides the "convert Pest to PHPUnit" rule in the Boost block above, which is generated and cannot be edited.
|
|
- Assertions stayed as `$this->assert*()` rather than becoming `expect()`. Match that in existing files; new files may use either.
|
|
- Browser tests are the ones in the `browser` group, under `tests/Browser`. They are excluded from `php artisan test` because they need Playwright and a built frontend. Run them with `composer test:browser`, which builds first.
|
|
|
|
### Browser test gotchas
|
|
|
|
- `assertNoJavaScriptErrors()` does not catch a Vue render failure — deleting `@routes` leaves `route()` undefined and renders an empty page while that assertion still passes. Always assert on something the page must actually render.
|
|
- `click()` on an element that does not exist **hangs forever** rather than failing. Verify every label against the component source before using it.
|
|
- Text locators match any element, not just controls: `click('Register')` finds the `<h5>` heading. Use `button[type="submit"]` where a heading shares the button's label.
|
|
- Repeated controls (a table's row actions) are a strict-mode violation. Scope them: `tr:has-text("doomed") [aria-label="Delete User"]`.
|
|
- A selector with no CSS special character is treated as **text**, not CSS (`GuessLocator` falls back to `getByText`). `assertMissing('nav ol')` therefore passes on any page, because the text never matches. Give the element an `id`, a `data-testid` or an `aria-label` and select on that.
|
|
- `assertPathIs` does not wait for navigation but `assertSee` does, so put a content assertion first after any click that navigates.
|
|
- After a navigation, `$page->url()`, `script()` and `content()` still report the pre-navigation page. The `assert*` methods do see the current page; use them rather than those readers.
|
|
- `browserLogin()` must be called inside the test, never from `beforeEach` — Pest connects the browser client as the test starts.
|
|
- The Reka-based `Checkbox` and `Switch` render a button with `aria-checked`, not a native input, so assert with `assertAttribute(...)` and toggle with `click()`.
|
|
- A stale `vendor/pestphp/pest-plugin-browser/.temp/playwright-server.json` wedges the next run with "WebSocket client is not connected". `composer test:browser` deletes it first; if you run `pest --group=browser` directly, delete it yourself.
|
|
|
|
### Visual tests
|
|
|
|
- `tests/Visual/` holds two things, both in the `visual` group and both excluded from `php artisan test` and `composer test:browser`. Run them with `composer test:visual`, which builds the frontend and drives Chrome, Firefox and Safari in turn.
|
|
- `GalleryTest.php` captures every page at three viewports and files the images under `tests/Browser/Gallery/{browser}`, with a browsable `index.html`. It asserts nothing about appearance, but every capture still asserts on content first — a silent screenshot will happily photograph a blank page.
|
|
- `RegressionTest.php` pixel diffs a handful of static pages, **Chrome desktop only**. Pest keys a snapshot on test file, test name and dataset with nothing for the browser or device, so a second engine would compare against Chrome's baseline and always fail.
|
|
- `assertScreenshotMatches()` is byte exact. Its `maxDiffPixels`/`threshold` options only decide whether you get a diff image; they never make the assertion pass. Treat any rendering jitter as a failure and keep diffed pages free of dates, faker data and canvas.
|
|
- Baselines (`tests/.pest`) and captures (`tests/Browser/Gallery`) are gitignored. Font rasterisation differs between operating systems, so a baseline captured here would fail on a buyer's Linux or Windows machine. Regenerate with `--update-snapshots`, inspect a failure with `--diff`.
|
|
- The gallery and the diffing run as separate `pest` invocations because global state leaks between them: the settings store and `session.driver` are process wide, and the gallery changes both.
|
|
- Selection is by file path, not by group. A `--group=gallery` on the command line does not override the `<exclude><group>visual</group></exclude>` in `phpunit.xml`, so extra sub-groups are silently filtered out.
|
|
- Log in with `visualLogin()`, not `browserLogin()`. The latter mints a faker user whose random name lands in the navbar and the users table.
|
|
- A test is marked as a browser test by finding `visit()` **in the test itself**. Wrapping every `visit()` in a helper leaves the test unmarked and it fails with "WebSocket client is not connected" — the same error as calling `visit()` from `beforeEach`.
|
|
- The navbar avatar is hidden before each pixel diff. It is a raster image scaled into a 44px box and Chrome picks its scaling path by decode timing, so the same 149 pixels flip between two renders on whichever authenticated page loses the race. `networkidle` and a settle delay both failed to pin it.
|