From 3d747aeef7bf7c3cf870f476950a2d32d4ec11f4 Mon Sep 17 00:00:00 2001 From: uldisrudzitis Date: Tue, 29 Sep 2026 17:07:25 +0300 Subject: [PATCH 1/2] Upgrade laravel/boost --- .../skills/ai-sdk-development/SKILL.md | 2 +- .../skills/deploying-to-cloud/SKILL.md | 267 +++++++++++++++ .../reference/checklists.md | 55 ++++ .../.agents/skills/infer-conventions/SKILL.md | 105 ++++++ .../infer-conventions/references/checklist.md | 137 ++++++++ .../skills/laravel-best-practices/SKILL.md | 215 +++--------- .../rules/advanced-queries.md | 44 +-- .../rules/architecture.md | 105 +++--- .../rules/blade-views.md | 22 +- .../laravel-best-practices/rules/caching.md | 66 +++- .../rules/collections.md | 56 +++- .../laravel-best-practices/rules/config.md | 52 +-- .../rules/db-performance.md | 93 +++--- .../laravel-best-practices/rules/eloquent.md | 62 ++-- .../rules/error-handling.md | 39 ++- .../rules/events-notifications.md | 26 +- .../rules/http-client.md | 135 ++++---- .../laravel-best-practices/rules/mail.md | 55 +++- .../rules/migrations.md | 102 ++---- .../rules/queue-jobs.md | 131 +++----- .../laravel-best-practices/rules/routing.md | 106 +++--- .../rules/scheduling.md | 50 ++- .../laravel-best-practices/rules/security.md | 128 +++---- .../laravel-best-practices/rules/style.md | 145 ++++---- .../laravel-best-practices/rules/testing.md | 43 --- .../rules/validation.md | 76 +++-- _api_app/.agents/skills/pest-testing/SKILL.md | 110 ------- .../skills/testing-best-practices/SKILL.md | 58 ++++ .../rules/assertions.md | 60 ++++ .../rules/endpoint-tests.md | 47 +++ .../rules/finding-features.md | 34 ++ .../testing-best-practices/rules/isolation.md | 52 +++ .../testing-best-practices/rules/naming.md | 45 +++ .../rules/performance.md | 46 +++ .../testing-best-practices/rules/review.md | 44 +++ .../testing-best-practices/rules/security.md | 27 ++ .../testing-best-practices/rules/test-data.md | 56 ++++ .../skills/ai-sdk-development/SKILL.md | 2 +- .../skills/deploying-to-cloud/SKILL.md | 267 +++++++++++++++ .../reference/checklists.md | 55 ++++ .../.claude/skills/infer-conventions/SKILL.md | 105 ++++++ .../infer-conventions/references/checklist.md | 137 ++++++++ .../skills/laravel-best-practices/SKILL.md | 215 +++--------- .../rules/advanced-queries.md | 44 +-- .../rules/architecture.md | 105 +++--- .../rules/blade-views.md | 22 +- .../laravel-best-practices/rules/caching.md | 66 +++- .../rules/collections.md | 56 +++- .../laravel-best-practices/rules/config.md | 52 +-- .../rules/db-performance.md | 93 +++--- .../laravel-best-practices/rules/eloquent.md | 62 ++-- .../rules/error-handling.md | 39 ++- .../rules/events-notifications.md | 26 +- .../rules/http-client.md | 135 ++++---- .../laravel-best-practices/rules/mail.md | 55 +++- .../rules/migrations.md | 102 ++---- .../rules/queue-jobs.md | 131 +++----- .../laravel-best-practices/rules/routing.md | 106 +++--- .../rules/scheduling.md | 50 ++- .../laravel-best-practices/rules/security.md | 128 +++---- .../laravel-best-practices/rules/style.md | 145 ++++---- .../laravel-best-practices/rules/testing.md | 43 --- .../rules/validation.md | 76 +++-- _api_app/.claude/skills/pest-testing/SKILL.md | 110 ------- .../skills/testing-best-practices/SKILL.md | 58 ++++ .../rules/assertions.md | 60 ++++ .../rules/endpoint-tests.md | 47 +++ .../rules/finding-features.md | 34 ++ .../testing-best-practices/rules/isolation.md | 52 +++ .../testing-best-practices/rules/naming.md | 45 +++ .../rules/performance.md | 46 +++ .../testing-best-practices/rules/review.md | 44 +++ .../testing-best-practices/rules/security.md | 27 ++ .../testing-best-practices/rules/test-data.md | 56 ++++ .../skills/ai-sdk-development/SKILL.md | 2 +- .../skills/deploying-to-cloud/SKILL.md | 267 +++++++++++++++ .../reference/checklists.md | 55 ++++ .../.cursor/skills/infer-conventions/SKILL.md | 105 ++++++ .../infer-conventions/references/checklist.md | 137 ++++++++ .../skills/laravel-best-practices/SKILL.md | 215 +++--------- .../rules/advanced-queries.md | 44 +-- .../rules/architecture.md | 105 +++--- .../rules/blade-views.md | 22 +- .../laravel-best-practices/rules/caching.md | 66 +++- .../rules/collections.md | 56 +++- .../laravel-best-practices/rules/config.md | 52 +-- .../rules/db-performance.md | 93 +++--- .../laravel-best-practices/rules/eloquent.md | 62 ++-- .../rules/error-handling.md | 39 ++- .../rules/events-notifications.md | 26 +- .../rules/http-client.md | 135 ++++---- .../laravel-best-practices/rules/mail.md | 55 +++- .../rules/migrations.md | 102 ++---- .../rules/queue-jobs.md | 131 +++----- .../laravel-best-practices/rules/routing.md | 106 +++--- .../rules/scheduling.md | 50 ++- .../laravel-best-practices/rules/security.md | 128 +++---- .../laravel-best-practices/rules/style.md | 145 ++++---- .../laravel-best-practices/rules/testing.md | 43 --- .../rules/validation.md | 76 +++-- _api_app/.cursor/skills/pest-testing/SKILL.md | 110 ------- .../skills/testing-best-practices/SKILL.md | 58 ++++ .../rules/assertions.md | 60 ++++ .../rules/endpoint-tests.md | 47 +++ .../rules/finding-features.md | 34 ++ .../testing-best-practices/rules/isolation.md | 52 +++ .../testing-best-practices/rules/naming.md | 45 +++ .../rules/performance.md | 46 +++ .../testing-best-practices/rules/review.md | 44 +++ .../testing-best-practices/rules/security.md | 27 ++ .../testing-best-practices/rules/test-data.md | 56 ++++ .../skills/ai-sdk-development/SKILL.md | 2 +- .../skills/deploying-to-cloud/SKILL.md | 267 +++++++++++++++ .../reference/checklists.md | 55 ++++ .../.github/skills/infer-conventions/SKILL.md | 105 ++++++ .../infer-conventions/references/checklist.md | 137 ++++++++ .../skills/laravel-best-practices/SKILL.md | 215 +++--------- .../rules/advanced-queries.md | 44 +-- .../rules/architecture.md | 105 +++--- .../rules/blade-views.md | 22 +- .../laravel-best-practices/rules/caching.md | 66 +++- .../rules/collections.md | 56 +++- .../laravel-best-practices/rules/config.md | 52 +-- .../rules/db-performance.md | 93 +++--- .../laravel-best-practices/rules/eloquent.md | 62 ++-- .../rules/error-handling.md | 39 ++- .../rules/events-notifications.md | 26 +- .../rules/http-client.md | 135 ++++---- .../laravel-best-practices/rules/mail.md | 55 +++- .../rules/migrations.md | 102 ++---- .../rules/queue-jobs.md | 131 +++----- .../laravel-best-practices/rules/routing.md | 106 +++--- .../rules/scheduling.md | 50 ++- .../laravel-best-practices/rules/security.md | 128 +++---- .../laravel-best-practices/rules/style.md | 145 ++++---- .../laravel-best-practices/rules/testing.md | 43 --- .../rules/validation.md | 76 +++-- _api_app/.github/skills/pest-testing/SKILL.md | 110 ------- .../skills/testing-best-practices/SKILL.md | 58 ++++ .../rules/assertions.md | 60 ++++ .../rules/endpoint-tests.md | 47 +++ .../rules/finding-features.md | 34 ++ .../testing-best-practices/rules/isolation.md | 52 +++ .../testing-best-practices/rules/naming.md | 45 +++ .../rules/performance.md | 46 +++ .../testing-best-practices/rules/review.md | 44 +++ .../testing-best-practices/rules/security.md | 27 ++ .../testing-best-practices/rules/test-data.md | 56 ++++ _api_app/AGENTS.md | 69 ++-- _api_app/CLAUDE.md | 198 +---------- _api_app/boost.json | 5 +- _api_app/composer.json | 2 +- _api_app/composer.lock | 311 +++++++++++------- 153 files changed, 7591 insertions(+), 4578 deletions(-) create mode 100644 _api_app/.agents/skills/deploying-to-cloud/SKILL.md create mode 100644 _api_app/.agents/skills/deploying-to-cloud/reference/checklists.md create mode 100644 _api_app/.agents/skills/infer-conventions/SKILL.md create mode 100644 _api_app/.agents/skills/infer-conventions/references/checklist.md delete mode 100644 _api_app/.agents/skills/laravel-best-practices/rules/testing.md delete mode 100644 _api_app/.agents/skills/pest-testing/SKILL.md create mode 100644 _api_app/.agents/skills/testing-best-practices/SKILL.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/assertions.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/endpoint-tests.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/finding-features.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/isolation.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/naming.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/performance.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/review.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/security.md create mode 100644 _api_app/.agents/skills/testing-best-practices/rules/test-data.md create mode 100644 _api_app/.claude/skills/deploying-to-cloud/SKILL.md create mode 100644 _api_app/.claude/skills/deploying-to-cloud/reference/checklists.md create mode 100644 _api_app/.claude/skills/infer-conventions/SKILL.md create mode 100644 _api_app/.claude/skills/infer-conventions/references/checklist.md delete mode 100644 _api_app/.claude/skills/laravel-best-practices/rules/testing.md delete mode 100644 _api_app/.claude/skills/pest-testing/SKILL.md create mode 100644 _api_app/.claude/skills/testing-best-practices/SKILL.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/assertions.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/endpoint-tests.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/finding-features.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/isolation.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/naming.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/performance.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/review.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/security.md create mode 100644 _api_app/.claude/skills/testing-best-practices/rules/test-data.md create mode 100644 _api_app/.cursor/skills/deploying-to-cloud/SKILL.md create mode 100644 _api_app/.cursor/skills/deploying-to-cloud/reference/checklists.md create mode 100644 _api_app/.cursor/skills/infer-conventions/SKILL.md create mode 100644 _api_app/.cursor/skills/infer-conventions/references/checklist.md delete mode 100644 _api_app/.cursor/skills/laravel-best-practices/rules/testing.md delete mode 100644 _api_app/.cursor/skills/pest-testing/SKILL.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/SKILL.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/assertions.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/endpoint-tests.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/finding-features.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/isolation.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/naming.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/performance.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/review.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/security.md create mode 100644 _api_app/.cursor/skills/testing-best-practices/rules/test-data.md create mode 100644 _api_app/.github/skills/deploying-to-cloud/SKILL.md create mode 100644 _api_app/.github/skills/deploying-to-cloud/reference/checklists.md create mode 100644 _api_app/.github/skills/infer-conventions/SKILL.md create mode 100644 _api_app/.github/skills/infer-conventions/references/checklist.md delete mode 100644 _api_app/.github/skills/laravel-best-practices/rules/testing.md delete mode 100644 _api_app/.github/skills/pest-testing/SKILL.md create mode 100644 _api_app/.github/skills/testing-best-practices/SKILL.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/assertions.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/endpoint-tests.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/finding-features.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/isolation.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/naming.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/performance.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/review.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/security.md create mode 100644 _api_app/.github/skills/testing-best-practices/rules/test-data.md diff --git a/_api_app/.agents/skills/ai-sdk-development/SKILL.md b/_api_app/.agents/skills/ai-sdk-development/SKILL.md index d74e342e..ef6d72a2 100644 --- a/_api_app/.agents/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.agents/skills/ai-sdk-development/SKILL.md @@ -410,4 +410,4 @@ Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text | OpenRouter | Y | - | - | - | - | - | - | - | | ElevenLabs | - | - | Y | Y | - | - | - | - | | Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file +| Jina | - | - | - | - | Y | Y | - | - | diff --git a/_api_app/.agents/skills/deploying-to-cloud/SKILL.md b/_api_app/.agents/skills/deploying-to-cloud/SKILL.md new file mode 100644 index 00000000..9584216a --- /dev/null +++ b/_api_app/.agents/skills/deploying-to-cloud/SKILL.md @@ -0,0 +1,267 @@ +--- +name: deploying-to-cloud +description: "Deploys and manages Laravel applications on Laravel Cloud using the `cloud` CLI. Use when the user wants to deploy an app, ship to Cloud, create or manage applications, environments, databases, caches, object storage, queues, domains, instances, background processes, secrets, compute, scheduled tasks, or Laravel Cloud infrastructure; or check billing, usage, or spend. Triggers on deployment, shipping, Cloud management, environment setup, database provisioning, billing or usage queries, the `cloud` CLI, and Laravel Cloud deployment troubleshooting." +license: MIT +metadata: + author: laravel +--- + +# Deploying with Laravel Cloud + +Use the [Laravel Cloud documentation](https://cloud.laravel.com/docs/llms.txt) for detailed, current feature behavior. Use the Cloud CLI for Cloud operations rather than guessing dashboard or API workflows. Install it in the project and invoke it as `./vendor/bin/cloud` by default; use a globally installed `cloud` command only as a fallback. Commands below are written as `cloud` for brevity; run them as `./vendor/bin/cloud` unless falling back to the global installation. + +## Application Setup + +- Laravel Cloud deploys from GitHub, GitLab, or Bitbucket. A new Laravel application requires PHP 8.2 or later, Laravel 9 or later, a connected Git provider, and a deployment region. +- Cloud creates environments for applications. Use separate production, staging, and preview environments; each environment has its own compute, resources, and deployment settings. +- Keep application compute and attached resources in the same region where possible. +- Cloud builds a Docker image using the selected PHP version, runs the configured build and deploy commands, and switches traffic to a successful deployment with zero downtime. Push-to-deploy is enabled by default, and manual deployments and deploy hooks are also available. + +## Build and Deploy + +- A typical Laravel build command is `composer install --no-dev && npm run build`. +- Run optimization and cache-building commands during the build, not during deployment. A typical deploy command is `php artisan migrate --force`. +- Build and deploy commands have a 15-minute timeout. Deploy commands run immediately before the release becomes live, and filesystem changes made by deploy commands are not persisted. +- Do not add `php artisan queue:restart`, `php artisan horizon:terminate`, `php artisan optimize:clear`, or `php artisan storage:link` to deploy commands. Cloud handles worker restarts and process management; deploy filesystem changes are not persistent. +- After changing environment settings, attached resources, or linked secrets, redeploy the environment for the changes to take effect. + +## Configuration and Resources + +- Attached databases, caches, and object storage inject their connection variables automatically. Custom environment variables take precedence over injected values. +- Use Secrets Manager for encrypted organization-level values shared across environments. Secret values cannot be read after creation; redeploy affected environments after creating, updating, linking, unlinking, or deleting a secret. +- Environment filesystems are ephemeral and are not shared between replicas. Use a database or Laravel Valkey for persistent cache and sessions, and Laravel Cloud Object Storage for persistent files. Do not rely on local files surviving a deployment. + +### Object Storage and File Visibility + +- Laravel Cloud Object Storage is backed by Cloudflare R2. R2 applies visibility at the bucket level; a bucket cannot contain a mix of private and public objects. +- For private files, always use `Storage::disk()` with no arguments, such as `Storage::disk()->put(...)`, so Laravel uses the environment's default disk. Do not pass a disk name for private/default storage. Attach a private Cloud bucket as the environment's default disk. +- For public files, use the named public disk: `Storage::disk('public')->put(...)`. Attach a second Cloud bucket configured as public and give it the `public` disk name. +- Applications that use both private and public files therefore need two Cloud Object Storage buckets attached to the environment: a private default bucket and a public bucket named `public`. +- Do not set per-file or Flysystem `visibility: 'public'` configuration for Cloud Object Storage. R2 does not support per-object ACL headers and rejects those requests; select the bucket visibility when creating the bucket. +- Private buckets are not internet-accessible, but Laravel can generate temporary public URLs with `Storage::temporaryUrl(...)`. Public buckets expose all objects through their Cloud-provided public URL. +- Install the S3 Flysystem adapter before using Cloud Object Storage: `composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies`. + +## Queues and Scheduling + +- Managed queues are the recommended queue option. They provision dedicated workers and autoscale based on queued work. +- Managed queues require a supported recent Laravel version and `aws/aws-sdk-php`; check the current Cloud documentation before changing dependencies. +- Enable the scheduler on an App or Worker cluster to run `php artisan schedule:run` every minute. When an environment has multiple replicas, use Laravel's `onOneServer` for tasks that must run once. +- Only Flex compute sizes can scale to zero. Scale-to-zero environments wake for Laravel scheduled tasks and queued jobs, but long-running jobs may be interrupted when the sleep timeout is reached. Use managed queues for workloads that must not be interrupted. + +## Domains and Storage + +- Each environment receives a `laravel.cloud` domain after its first successful deployment. Cloud automatically verifies custom domains and provisions SSL after the required DNS records are configured. +- Do not use the ephemeral filesystem for persistent uploads, generated files, cache, or session data. Use the appropriate Cloud resource instead. + +## Cloud CLI + +### Setup + +```shell +composer require --dev laravel/cloud-cli +./vendor/bin/cloud auth -n +``` + +If a project-local installation is not available, install the CLI globally with `composer global require laravel/cloud-cli` and invoke it as `cloud`. + +`cloud auth` opens a browser. If a browser is unavailable, set `LARAVEL_CLOUD_TOKEN` in the environment. It overrides any saved token and writes nothing to disk. To save a token instead, run `cloud auth:token --add --token= -n` or pipe it with `echo "$TOKEN" | cloud auth:token --add -n`. + +### Commands + +Commands follow a CRUD pattern: `resource:list`, `resource:get`, `resource:create`, `resource:update`, `resource:delete`. + +Available resources: `application`, `environment`, `instance`, `database-cluster`, `database`, `cache`, `bucket`, `domain`, `websocket-cluster`, `background-process`, `secret`, `command`, `deployment`. + +Some resources have additional commands (e.g., `domain:verify`, `database:open`, `instance:sizes`, `cache:types`). Discover these via `cloud -h`. + +Never hardcode command signatures. Always run `cloud -h` to discover options at runtime. + +### CLI Flags + +- Always add `-n` to every command to prevent the CLI from hanging. +- Never use `-q` or `--silent`; they suppress all output. + +Flag combos per operation: +- Read (`:list`, `:get`) → `--json -n` +- Create (`:create`) → `--json -n` +- Update (`:update`) → `--json -n --force` +- Delete (`:delete`) → `-n --force` (no `--json`) +- Environment variables → `-n --force` +- Deploy or ship → `-n` with all options passed explicitly (no `--json`) + +### Deployment Workflow + +Determine the task and follow the matching path: + +- First deployment: inspect `cloud ship -h`, then run `cloud ship -n` with all required values. + +- Existing application: + ```shell + cloud deploy {app_name} {environment} -n --open + cloud deploy:monitor -n + ``` + +- Environment variables: `cloud environment:variables -n --force` + +- Secrets: `echo "$VALUE" | cloud secret:create --name=NAME --json -n`, then `cloud environment-secret:attach` + +- Provision infrastructure: `cloud :create --json -n` + +- Monorepo (application in a subdirectory): add `--root-directory=` to `cloud ship` or `cloud application:create`. + +- Custom domain: `cloud domain:create --json -n`, then `cloud domain:verify -n`. + +- Repository defaults: `cloud repo:config {application} -n` sets repository-local application and organization defaults. Pass `--organization=` when the user has multiple organizations. + +For multi-step operations, see [reference/checklists.md](reference/checklists.md). + +- If the user's needs are unclear, ask before running anything. + +### When a Command Fails + +1. Read the error output. +2. Check the resource status with `:list --json -n` or `:get --json -n`. +3. For authentication errors, run `cloud auth -n`. +4. Fix the issue and rerun the command. +5. If the same error occurs after one fix, stop and ask the user rather than repeating the operation. + +Always run `cloud deploy:monitor -n` after every deploy. If it fails, inspect the deployment status and logs and show the user what went wrong before attempting a fix. + +### Subagent Delegation + +Delegate high-output operations to subagents using the Task tool to keep the main context window small. Only the summary returns; verbose output stays in the subagent's context. + +Delegate these to a subagent: +- `cloud deploy:monitor -n` — deployment logs can be very long +- `cloud deployment:get --json -n` — full deployment details +- `cloud :list --json -n` — listing many resources produces large JSON +- `cloud command:run` — when output may be long +- `cloud usage --detailed --json -n` — the payload includes details for every database, cache, bucket, websocket, and application +- Fetching docs from https://cloud.laravel.com/docs/llms.txt via `WebFetch` + +Keep in the main context: +- Short commands like `:create`, `:delete`, `:update` — output is small +- `cloud deploy -n` — you need the deployment ID immediately +- Any command where you need the result for the next step right away + +### Rules + +Follow these rules: +- Flag selection — always use the documented combos above +- Deploy sequence — deploy then monitor, never skip monitoring +- Destructive commands — always confirm with the user first, show the command and wait for approval. This includes deleting applications, environments, databases, caches, buckets, domains, or secrets. +- Error loop — diagnose, fix once, ask user if it fails again + +Use judgment for: +- Instance sizes, regions, cluster types — ask the user if not specified +- Which resources to provision — based on what the user describes +- Order of provisioning — no strict sequence required +- How to present output — summarize, show raw, or extract fields based on context + +### Secrets + +Secrets are encrypted values shared across the organization and attached to environments. The CLI encrypts values locally, so plaintext never reaches the API. + +```shell +cloud secret:list --json -n +echo "$VALUE" | cloud secret:create --name=STRIPE_KEY --json -n +cloud environment-secret:attach {environment} {secretId} -n +cloud environment-secret:list {environment} --json -n +``` + +Pipe the value in. `--value=` works, but leaves the plaintext in shell history and the process list. + +`secret:update`, `secret:delete`, and `environment-secret:attach` take secret IDs, not names — names are not unique. Read IDs from `cloud secret:list --json -n`. + +There is no `secret:get`, and no way to detach a secret from a single environment. `secret:delete` removes it and detaches it everywhere. + +Secrets need the `sodium` PHP extension. No other command does. + +Redeploy affected environments after creating, updating, attaching, or deleting a secret. + +### Remote Access + +#### Tinker (v0.2.0 or later) + +Run PHP code directly in a Cloud environment: + +```shell +cloud tinker {environment} --code='Your PHP code here' --timeout=60 -n +``` + +- `--code` — PHP code to execute (required in non-interactive mode) +- `--timeout` — maximum number of seconds to wait for output (default: 60) + +The code must explicitly output results using `echo`, `dump`, or similar; expressions alone produce no output. + +Always pass `--code` and `-n` to avoid interactive prompts. + +#### Remote Commands + +Run shell commands on a Cloud environment: + +```shell +cloud command:run {environment} --cmd='your command here' -n +``` + +- `--cmd` — the command to run (required in non-interactive mode) +- `--no-monitor` — skip real-time output streaming +- `--copy-output` — copy output to clipboard + +Review past commands: + +- `cloud command:list {environment} --json -n` — list command history +- `cloud command:get {commandId} --json -n` — get details and output of a specific command + +### Billing and Usage + +View billing and usage for the current organization: + +```shell +cloud usage --json -n +``` + +- `--period=current|previous|1|2|3` — Billing period (default: `current`; `1`, `2`, and `3` are one, two, and three periods back, respectively). Other values return an error. +- `--environment=` — filter usage to a single environment +- `--detailed` — include per-application, per-resource, and per-add-on breakdowns +- `--json` — machine-readable output (always pair with `-n`) + +Common queries: + +- Current spend: `cloud usage --json -n | jq '.currentSpendCents'` +- Last month's bill: `cloud usage --period=previous --json -n` +- One environment, full breakdown: `cloud usage --environment= --detailed --json -n` + +All amounts are in cents. Keys are camelCase at every level (e.g., `currentSpendCents`, `bandwidth.allowanceBytes`, `databases[].totalCents`, `applications[].totalCostCents`). + +### Config + +1. Environment: `LARAVEL_CLOUD_TOKEN` — an API token, taking precedence over any saved one (empty counts as unset) +2. Global: `~/.config/cloud/config.json` — auth tokens and preferences +3. Repo-local: `.cloud/config.json` — app and environment defaults (set by `cloud repo:config {application} -n`) +4. CLI arguments override both config files + +Pass the application to `repo:config`. Without it, the command must prompt and fails under `-n` when the organization has more than one application. Deploy commands do not need these defaults; pass the application and environment to them directly. + +Using multiple organizations requires multiple stored API tokens. Commands read `organization_id` from `.cloud/config.json` to select one and fail if it is not set. Set it with `cloud repo:config {application} --organization= -n`. + +`LARAVEL_CLOUD_TOKEN` holds one token, so it picks the organization on its own. Naming a different one with `--organization` fails rather than falling back to the token's organization. + +## Documentation + +Laravel Cloud documentation: https://cloud.laravel.com/docs/llms.txt + +When the user asks how something works or needs an explanation of a Laravel Cloud feature, fetch the docs from the URL above using `WebFetch` and use it to provide accurate answers. + +## When Stuck + +- Fetch https://cloud.laravel.com/docs/llms.txt for official documentation +- Run `cloud -h` for any command's options +- Run `cloud -h` to discover commands + +## Verification + +1. Confirm the target application and environment before changing or deploying anything. +2. Check staged environment changes and required resources before deployment. +3. Monitor every deployment to completion with `cloud deploy:monitor -n`. +4. For production changes, verify the application URL, logs, queues, scheduled tasks, and relevant resource health. diff --git a/_api_app/.agents/skills/deploying-to-cloud/reference/checklists.md b/_api_app/.agents/skills/deploying-to-cloud/reference/checklists.md new file mode 100644 index 00000000..d3cb54bd --- /dev/null +++ b/_api_app/.agents/skills/deploying-to-cloud/reference/checklists.md @@ -0,0 +1,55 @@ +# Checklists for Multi-Step Operations + +Use these checklists when performing complex setups. Copy the relevant checklist and track progress as you go. + +## Full environment setup (app + database + cache + domain) + +``` +- [ ] Create or select the application +- [ ] Create the environment +- [ ] Provision the database (`database-cluster:create` then `database:create`) +- [ ] Provision the cache (`cache:create`) +- [ ] Set environment variables (`environment:variables`) +- [ ] Create and attach any secrets (`secret:create` then `environment-secret:attach`) +- [ ] Attach custom domain (`domain:create` then `domain:verify`) +- [ ] Deploy (`deploy` then `deploy:monitor`) +- [ ] Verify the deployment is live +``` + +## New app from scratch + +``` +- [ ] Run `cloud ship -n` with required options +- [ ] Monitor deployment status +- [ ] Set environment variables if needed +- [ ] Verify the app is accessible +``` + +## Adding a database to an existing environment + +``` +- [ ] Check existing database clusters (`database-cluster:list --json -n`) +- [ ] Create a new cluster if needed (`database-cluster:create --json -n`) +- [ ] Create the database on the cluster (`database:create --json -n`) +- [ ] Update environment variables with connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Adding a cache to an existing environment + +``` +- [ ] Check existing caches (`cache:list --json -n`) +- [ ] Create the cache (`cache:create --json -n`) +- [ ] Update environment variables with cache connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Custom domain setup + +``` +- [ ] Create the domain (`domain:create --json -n`) +- [ ] Get DNS records to configure (`domain:get --json -n`) +- [ ] Tell the user to add the DNS records at their registrar +- [ ] Verify the domain (`domain:verify -n`) +- [ ] Confirm verification passed +``` diff --git a/_api_app/.agents/skills/infer-conventions/SKILL.md b/_api_app/.agents/skills/infer-conventions/SKILL.md new file mode 100644 index 00000000..eb3fc846 --- /dev/null +++ b/_api_app/.agents/skills/infer-conventions/SKILL.md @@ -0,0 +1,105 @@ +--- +name: infer-conventions +description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Only run this skill when the user explicitly asks for it; never start a sweep as part of another task. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand." +disable-model-invocation: true +license: MIT +metadata: + author: laravel +--- + +# Infer Conventions + +Learn how this application writes Laravel, then record what you learn as durable, path-scoped rules other agents will read. You are documenting reality, not improving it. + +## Ground Rules (read before you start) + +- Consistency first. The codebase's majority style is the convention. Never judge it, never propose a "better" pattern, never record what the code should do. If the app validates inline everywhere, that is the rule, even if Form Requests would be nicer. +- Skip what an active tool produces, keep what a tool would fight. Inspect the project's Pint and Rector configuration first; a Rector transformation is tooling-owned only when its package and relevant rule or set are installed and enabled. Active tools may rewrite code toward one canonical form: `$casts` to `casts()`, `$fillable` to attributes, magic accessors to the `Attribute` class, pipe-string rules to arrays, `$signature` to `#[Signature]`, named migrations to anonymous, and many more. When the app already sits at an active tool's target form, the tool owns it, so record nothing. But when the app deliberately holds a form an active tool would refactor away, such as legacy `getXxxAttribute()` accessors the `Attribute` class would replace, no tool can reproduce that choice and an agent defaults the other way. That against-the-grain hold is exactly what to record. +- Record decisions, not defaults. A consistent pattern earns a rule only when it reflects a choice: the app took one valid option where the framework or common practice offered others, or the pattern would surprise a competent agent. Framework defaults steer nothing, so skip them: anonymous migrations, `$signature` commands, `ShouldQueue` jobs, `casts()` on Laravel 11+, named routes, Rule objects in `app/Rules`, and `Mail::fake()` or `Bus::fake()` to isolate framework services. A real fork is not enough on its own. Weigh the side the app took, and record only the side an agent would not reach for by itself: inline closures everywhere, legacy accessors, a bespoke query layer. Watch for the false fork too. "No Mockery" next to facade fakes is not a choice against Mockery, because they double different things. The test for every candidate: without this rule, would the next agent plausibly write it differently? Only "yes" earns a rule. +- Architecture choices are the gold. Record presence and deliberate absence. The structural pattern the app commits to is the highest-signal convention and the one no tool can decide: Action classes and how they are invoked (`handle` / `execute` / `__invoke`), service objects, dedicated query objects exposing `builder()`, DTOs (spatie/laravel-data vs readonly classes), Form Request validation vs inline, an events and listeners spine vs direct calls, and domain or module folders. Also record a consistent non-pattern, such as "query Eloquent directly in controllers, no repository layer", so the next agent matches the app's altitude instead of over-engineering. +- Never duplicate `.ai/rules`. Read `.ai/rules/index.md` and the area files before the sweep. A dimension already covered there is marked done and skipped. +- Evidence or silence. A convention needs at least 3 consistent examples and no meaningful rival to become a candidate. Every Step 1 verdict applies this bar. +- The recorded rule states the convention, nothing else. One or two imperative lines: this project does X, so do X here. Keep detection evidence out. No counts, ratios, current usage, file lists, or example paths, because that is proof for the confirm step, not part of the rule. One short syntax fragment at most, and point to `search-docs` for API details. + +## Process + +Each step ends on a checkable completion criterion. Do not advance until it holds. + +Fan out when you can. The sweep is embarrassingly parallel. If your environment can spawn subagents (a Task, dispatch, or equivalent tool), do Step 0 yourself, then hand each checklist group (A to J) and the architecture map to its own subagent. Each subagent runs the greps, reads a few representative files, and returns structured verdicts (dimension, verdict, evidence, proposed glob / title / note). You aggregate, dedupe, then run Steps 3 to 5. It is far faster on a real app. No subagents available? Run the steps in sequence, with the same bar and the same output. + +### Step 0: Orient + +Read `composer.json` (installed packages tell you which checklist groups apply), the `pint.json` / PHPStan / Rector config, `.ai/rules/index.md` if present, and most important, map the `app/` tree. List every directory under `app/` (and any `Modules/`, `src/`, `packages/`, or domain root). Every folder beyond Laravel's default skeleton (`Http`, `Models`, `Providers`, `Console`, `Exceptions`) is a structural pattern the app committed to and a high-value rule waiting to be written: `Actions`, `Services`, `Data` or DTOs, `Queries`, `Repositories`, `ViewModels`, `Pipelines`, `Support`, `Enums`, `Contracts`, `Observers`, or `Domain` and module roots. Note each one. You will confirm how it is used in Step 2. + +This app has no Livewire/Inertia/Flux packages installed. Treat the frontend group as likely API-only: confirm from `resources/views` before spending time there, and skip the Livewire/Inertia/Flux dimensions. + +Done when: you have the applicable checklist groups, the dimensions already recorded in `.ai/rules`, and a list of every non-default `app/` directory mapped to the pattern it represents. + +### Step 1: Predefined sweep + +Open `references/checklist.md` and work every applicable dimension using its search hints. Give each exactly one verdict: + +- Pattern. Clears the bar, rival under ~20% of sites, and reflects a real choice (passes the decisions-not-defaults test). A recording candidate. Cite 2 to 3 example files. +- Conflict. Both styles present in meaningful numbers. Report the split with counts and example files. Never record a preferred winner while the code remains mixed, even in yolo, because that would describe an aspiration rather than reality. Record only if the user identifies a stable path or context boundary that explains both styles; otherwise defer until the code is reconciled. +- Default. Consistent, but a framework or common-practice default the agent already writes unprompted. Skip it as a no-op, not a convention. +- No signal. Under the bar: feature unused, or too few examples. Skip silently (one summary line at most). +- Tooling-owned or Already-recorded. Skip per the ground rules. + +Done when: every applicable dimension carries exactly one of those verdicts. + +### Step 2: Open-ended pass + +First, close out the architecture map from Step 0. For every non-default `app/` directory you listed, confirm how the pattern is used and apply the same evidence and decisions-not-defaults tests as Step 1. Generator-standard or sparsely used directories such as `Rules`, `Observers`, `Mail`, and `Notifications` are signals to inspect, not automatic conventions. Make genuine structural patterns candidates: Action classes invoked via `handle` / `execute` / `__invoke`, Services constructor-injected, `Queries` objects exposing `builder(): Builder`, DTOs as readonly classes or spatie/laravel-data, module or domain folders as the unit of organization. Scope each qualifying pattern to its own directory glob. Also record a consistent deliberate absence, such as "no repository layer, controllers query Eloquent directly", so the next agent matches the app's altitude. + +Then find what else makes this codebase itself: base or abstract classes most code extends, traits used everywhere, tenancy or authorization scoping woven through queries, naming schemes, and custom helpers. Same evidence bar, cite files. Record every genuine structural pattern, and cap the other house findings at ~5 so the pass stays high-signal. + +Done when: every non-default `app/` directory from Step 0 has a verdict, and the pass has produced its cited house findings (or concluded there are none). + +### Step 3: Confirm + +Present every candidate in one batch. Per item: dimension, verdict, evidence (counts and files), and the exact proposed `glob` or `globs` / `title` / `note`. Conflicts are presented as questions about an existing context boundary or deferred cleanup, not as a choice of future style. + +Default mode is confirm: record only what the user approves. Switch to yolo only when the invocation said so ("yolo", "don't ask", "just record them"), then record all pattern candidates without asking. Conflicts still go to the user in yolo. + +Done when: every candidate is approved, rejected, or (conflicts) decided. + +### Step 4: Record + +Make one `record-rule` call for each glob an approved convention applies to. Choose the most specific globs that cover the cited evidence from the mapping table below; if a convention spans models and migrations, record it under both domains so agents discover it from either path. The `note` is the bare convention: strip every trace of detection (see the ground rule). If `record-rule` is unavailable (rules disabled), report the full rule text so the user can enable `BOOST_RULES_ENABLED` or add it by hand. + +Record this: + +> Accessors and mutators: use the legacy magic-method style (`getXxxAttribute()` / `setXxxAttribute()`), not the `Attribute` class. Match it in models. + +Not this: + +> Accessors/mutators use the legacy magic-method style; the `Attribute`-class style is not used anywhere (13 legacy, 0 Attribute-class), e.g. `app/Models/Post.php`. Match the legacy style in existing models. + +Done when: every approved item has a successful tool response, and any failure is reported with its rule text. + +### Step 5: Summarize + +List recorded rules (file and title), conflicts the user deferred, notable no-signals, and remind the user to commit `.ai/rules` so their team and agents share the conventions. + +## Glob mapping + +Attach each rule to the most specific path that covers its evidence. Never a lazy `app/**` when a subtree fits. Match the glob to where the code actually lives, which is not the same in a default skeleton and in a modular or DDD layout. Use the Step 0 `app/` map to pick the real path. + +Examples: + +- Models: `app/Models/**` in a default app, or `app/Modules/Blog/Models/**` / `src/Domain/Blog/**` in a modular one. +- Controllers, routing, validation, responses: `app/Http/**`, or `app/Modules/*/Http/**` when each module owns its HTTP layer. +- Actions, Services, DTOs: `app/Actions/**`, `app/Services/**`, `app/Data/**`, or the module path the app actually uses. +- Tests: `tests/**`. +- Migrations and database: `database/migrations/**`. +- Truly app-wide (rare, e.g. auth retrieval): `app/**`. + +`record-rule` takes one glob. When a convention genuinely spans two domains (e.g. UUID keys touch models and migrations), call it once per domain with the same title and note; mentioning another path in the note does not make the rule discoverable there. + +## Edge cases + +- Rules disabled or `record-rule` missing: detection is read-only, so Steps 0 to 3 still run, and recording falls back to the manual path in Step 4. +- Tiny or fresh app: most dimensions land on no-signal. Say so honestly ("not enough code to infer conventions yet") and record nothing. +- Huge app: each dimension is a bounded grep plus a handful of file reads. Sample representative files, do not read everything. +- Re-runs: reading `.ai/rules` in Step 0 makes re-runs incremental, so only new or undecided dimensions surface. +- Non-standard layout (modules, DDD): the open-ended pass catches the layout itself as convention #1. Adapt the globs in the mapping table to the observed paths. diff --git a/_api_app/.agents/skills/infer-conventions/references/checklist.md b/_api_app/.agents/skills/infer-conventions/references/checklist.md new file mode 100644 index 00000000..80cef34b --- /dev/null +++ b/_api_app/.agents/skills/infer-conventions/references/checklist.md @@ -0,0 +1,137 @@ +# Detection Checklist + +Every dimension here is a genuine fork: Laravel offers two or more valid approaches, the app's choice changes what the next agent writes, and no active project tool can pick for you. Left out on purpose: pure formatting (Pint owns it), any form an installed and enabled Rector rule rewrites to one canonical shape (`$casts` to `casts()`, `$fillable` to attributes, pipe-string rules to arrays, named to anonymous migrations, `$signature` to `#[Signature]`), and framework defaults any agent writes unprompted (`ShouldQueue` jobs, relation return types, `HasFactory`). + +Each item gives the fork, then a hint (a grep or dir to spot which side the app takes). Hints are only a start. Read the matched files, never record on a raw count. Apply the ground rules to every verdict: a consistent choice that is a default or a tool's target form is not a pattern. Rows tagged (architecture) are the highest-signal, so record presence and deliberate absence. + +--- + +## A. Validation & HTTP input + +1. Validation entry point: inline `$request->validate()` vs Form Request classes vs `Validator::make()`. + - Hint: `ls app/Http/Requests`; grep `->validate(` / `Validator::make(` in `app/Http/Controllers`. +2. Custom rule location: invokable rule objects in `app/Rules` vs inline closures vs `Validator::extend()` in a provider. Rule objects are the default `make:rule` path, so record only if the app leans on closures or `Validator::extend` instead. "No rule objects" alone is just no-signal. + - Hint: `ls app/Rules`; grep `Validator::extend` in `app/Providers`. +3. Typed input retrieval: typed getters (`$request->string()`, `->integer()`, `->enum()`, `->date()`) vs raw `$request->input()` / dynamic properties. + - Hint: grep `->string(` / `->integer(` / `->enum(` vs `->input(` in `app/Http`. +4. Custom messages/attributes: `lang/*/validation.php` vs Form Request `messages()` / `attributes()` methods. + - Hint: `ls lang`; grep `function messages`, `function attributes` in `app/Http/Requests`. + +## B. Controllers & routing + +5. Controller shape: invokable single-action (`__invoke`) vs resource controllers vs plain multi-method. + - Hint: grep `__invoke` in controllers; `Route::resource` / `apiResource` vs verb routes. +6. Business-logic location (architecture): fat controllers vs delegated to Actions / Services / Jobs. + - Hint: read a few controller methods; `ls app/Actions app/Services`. +7. Route handler style: closures in `routes/*.php` vs controller classes. + - Hint: count `function ()` vs `::class` in `routes/web.php`, `routes/api.php`. +8. Middleware assignment: route/group `->middleware()` vs controller `HasMiddleware::middleware()` vs `#[Middleware]` attribute. + - Hint: grep `implements HasMiddleware`, `#[Middleware(` in controllers vs `->middleware(` in routes. +9. Route model binding: implicit (type-hinted models) vs explicit `Route::bind` vs manual `findOrFail`. + - Hint: typed model params in signatures vs `findOrFail(` in controllers; grep `Route::bind`. +10. Rate limiting: named `RateLimiter::for()` + `throttle:name` vs inline `throttle:60,1`. + - Hint: grep `RateLimiter::for` in providers vs `throttle:` in route files. + +## C. Authorization + +11. Authorization home: Gates (`Gate::define`) vs Policy classes in `app/Policies`. + - Hint: `ls app/Policies`; grep `Gate::define` in `app/Providers`. +12. Authorization call site: `$this->authorize()` / `Gate::authorize()` vs `$user->can()` vs `can` middleware vs `#[Authorize]` vs `@can` in Blade. + - Hint: grep `authorize(`, `->can(`, `middleware('can:`, `#[Authorize(`, `@can(`. + +## D. Eloquent & models + +13. Mass assignment: `$fillable` allow-list vs `$guarded` block-list. + - Hint: grep `protected $fillable` / `protected $guarded` in `app/Models`. +14. Accessors/mutators: modern `Attribute` class vs legacy `getXxxAttribute()` / `setXxxAttribute()`. Record a legacy hold, it goes against the tool's grain. + - Hint: grep `: Attribute` / `Attribute::make` vs `function get[A-Z].*Attribute` in `app/Models`. +15. Primary keys: auto-increment vs `HasUuids` vs `HasUlids`. + - Hint: grep `HasUuids` / `HasUlids` in `app/Models`; migration `id()` vs `uuid('id')`. +16. Custom casts: dedicated `CastsAttributes` classes (`app/Casts`) vs inline `Attribute` vs built-in cast strings. + - Hint: `ls app/Casts`; grep `Cast::class`, `AsStringable::class` in models. +17. Data/query layer (architecture): Eloquent directly in controllers vs repositories vs dedicated query objects (e.g. classes exposing `builder(): Builder`). + - Hint: `ls app/Repositories app/Queries`; see where non-trivial queries are built. +18. Query scopes: local `scope`/`#[Scope]` methods vs dedicated builder classes. + - Hint: grep `function scope` / `#[Scope]` in models; `ls app/*/Builders`. +19. Model events: observers (`app/Observers`, `#[ObservedBy]`) vs `booted()` closures vs event classes. + - Hint: `ls app/Observers`; grep `booted`, `::observe`, `#[ObservedBy]`. +20. Eager-load posture: explicit per-query `->with()` vs model-level `$with` defaults. Treat `preventLazyLoading()` separately as a development guard because it can complement either posture. + - Hint: grep `protected $with`, `->with(`, and separately `preventLazyLoading` in `app/`. + +## E. Architecture & organization + +21. Action/Service structure (architecture): Action classes (invoked via `handle` / `execute` / `__invoke`) vs service objects vs neither. Cross-check the Step 0 `app/` map: any `Actions`/`Services`/`Pipelines`/`Jobs`-as-actions folder is this pattern, so record how it is invoked. + - Hint: `ls app/` (the whole tree, not just `Actions`/`Services`); grep the invocation method in the folder you find. +22. DTOs (architecture): spatie/laravel-data vs plain readonly classes vs arrays everywhere. + - Hint: `ls app/Data`; grep `extends Data`, `readonly class` in `app/`. +23. Dependency acquisition: constructor/method injection vs `app()` / `resolve()` / `App::make()` service location. + - Hint: grep `app(` / `resolve(` / `::make(` in `app/` vs promoted constructor deps. +24. Decoupling: events + listeners vs direct service calls. + - Hint: `ls app/Events app/Listeners`; grep `event(`, `::dispatch(`. +25. Helper vs facade idiom: global helpers (`config()`, `auth()`, `response()`) vs facades (`Config::`, `Auth::`, `Response::`). + - Hint: ratio of `config(` vs `Config::` (etc.) across `app/`. +26. Namespace layout (architecture): default `app/` skeleton vs domain/module folders (`app/Domain/**`, modules). + - Hint: `ls app/`, look for `Domain/`, `Modules/`, bounded-context folders. +27. Enums: backed vs pure; case naming; where they live. + - Hint: `ls app/Enums`; grep `enum .*: string`, `enum .*: int`. + +## F. Frontend & views + +No Livewire/Inertia/Flux package is installed. This app may be API-only. Confirm from `resources/views` before sweeping, and treat the Livewire/Flux dimensions as not applicable. + +28. Frontend stack: Blade+Livewire vs Inertia (Vue/React/Svelte) vs Blade-only / API + separate SPA. + - Hint: `composer.json` + `package.json`; `ls resources/js/pages`, `resources/views`. +29. Blade composition: class `` components vs anonymous components (`@props`) vs `@include` partials. + - Hint: `ls app/View/Components`; grep `constrained()` vs `foreignIdFor(Model::class)` vs manual `foreign()->references()->on()`. + - Hint: grep `foreignId(`, `foreignIdFor(`, `->foreign(` in `database/migrations`. +34. `down()` methods: real reverse logic vs omitted / one-way migrations. + - Hint: grep `function down` vs the migration count. +35. Enum storage: DB `enum()` column vs `string()` + PHP-enum cast on the model. + - Hint: grep `->enum(` in migrations vs string columns cast to enums. +36. Transactions: `DB::transaction(fn ...)` closure vs manual `beginTransaction` / `commit` / `rollBack`. + - Hint: grep `DB::transaction`, `beginTransaction` in `app/`. +37. Idempotent writes: `upsert` / `updateOrCreate` / `firstOrCreate` vs find-then-save. + - Hint: grep `upsert(`, `updateOrCreate(`, `firstOrCreate(` in `app/`. + +## H. Testing + +38. Framework: Pest (`it()` / `test()` / `expect()`) vs PHPUnit classes. + - Hint: `ls tests/Pest.php`; grep `it(` / `test(` vs `extends TestCase`. +39. DB reset: `RefreshDatabase` vs `DatabaseTruncation` vs `DatabaseMigrations`. + - Hint: grep those trait names in `tests/`. +40. Fixtures: compare how equivalent test-owned records are created, such as factories vs manual inserts. Track seeders separately for shared reference data because `$this->seed()` commonly and legitimately coexists with factories. + - Hint: grep `::factory(` and direct inserts in `tests/`; separately inspect `$this->seed(` calls and what those seeders provide. +41. Collaborator isolation: how the app doubles its own classes, Mockery `mock()` / `spy()` vs real integration. Ignore facade fakes like `Mail::fake()` here, they isolate framework services by default and are not a fork against Mockery. + - Hint: grep `->mock(`, `->spy(`, `Mockery::` in `tests/`. +42. Endpoint assertions: array `assertJson([...])` / `assertJsonFragment` vs fluent `AssertableJson`. + - Hint: grep `AssertableJson`, `assertJsonFragment` in `tests/`. + +## I. Responses & API resources + +43. Response shape: API Resource classes vs `response()->json()` vs returning models/arrays directly. + - Hint: `ls app/Http/Resources`; grep `JsonResource`, `->json(` in controllers. +44. Resource relationship inclusion: `whenLoaded()` guards vs unconditional relationship access. Do not count ordinary scalar attributes as rivals to conditional relationships, and evaluate general `when()` fields separately. + - Hint: compare relationship fields using `whenLoaded(` with unconditional relationship property access in `app/Http/Resources`. +45. Pagination contracts: within comparable endpoint categories, length-aware `paginate()` vs `simplePaginate()` vs `cursorPaginate()`. These have different totals, navigation, ordering, and performance contracts, so record only a stable path-scoped API policy, never a project-wide majority. + - Hint: grep those in `app/`, then group matches by endpoint type and client contract before comparing them. +46. Web redirects/URLs: `route('name')` vs `url('/path')` vs `action([...])`. + - Hint: grep `route('`, `url('/`, `action([` in `app/Http` and views. + +## J. Strings, collections & dates + +47. Iteration idiom: `collect()->map()->filter()` pipelines vs `array_map` / `foreach`. + - Hint: grep `collect(`, `->map(` vs `array_map`, `foreach` density in `app/`. +48. String API: fluent `Str::of()->...` (Stringable) vs static `Str::` vs native (`trim`, `strtoupper`). + - Hint: grep `Str::of(` vs `Str::` vs native string funcs. +49. Dates: compare equivalent construction call styles (`now()` / `today()` helpers vs `Carbon::`) separately from the application's mutable/immutable date policy. `Date::use(CarbonImmutable::class)` can make helpers return immutable dates, so those signals are complementary rather than conflicting. + - Hint: grep `now(` and `Carbon::` for call style; separately inspect `CarbonImmutable` and `Date::use` for mutability policy. + +--- + +Genuine forks only. Every row survived the "no tool can decide this, and it isn't the default" filter. Give each applicable dimension exactly one verdict: pattern, conflict, default, no-signal, tooling-owned, or already-recorded. The rows tagged (architecture) are where the highest-value rules come from. diff --git a/_api_app/.agents/skills/laravel-best-practices/SKILL.md b/_api_app/.agents/skills/laravel-best-practices/SKILL.md index 99018f3a..311ab844 100644 --- a/_api_app/.agents/skills/laravel-best-practices/SKILL.md +++ b/_api_app/.agents/skills/laravel-best-practices/SKILL.md @@ -8,183 +8,52 @@ metadata: # 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`. +Best practices for Laravel, organized as an index of rule files. Each rule file 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. +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches, and 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 +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. ## 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 \ No newline at end of file +1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out. +2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files. +3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job. +4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable. +5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them. +6. Re-read the diff against every mapped rule before finishing. + +## Rule Index + +Cross-cutting changes often need more than one rule file. + +| Concern | Read | +| --- | --- | +| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) | +| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) | +| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) | +| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) | +| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) | +| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) | +| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) | +| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) | +| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) | +| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) | +| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) | +| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) | +| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) | +| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) | +| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) | +| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) | +| Environment values and application configuration | [`rules/config.md`](rules/config.md) | +| Tests: coverage, factories, fakes, and assertions | the `testing-best-practices` skill | +| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) | +| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) | + +## Decision Rules + +- Prefer framework features and existing application abstractions over new helpers or dependencies. +- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable. +- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/advanced-queries.md b/_api_app/.agents/skills/laravel-best-practices/rules/advanced-queries.md index 920714a1..54dd783d 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/advanced-queries.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/advanced-queries.md @@ -1,8 +1,8 @@ -# Advanced Query Patterns +# Advanced Query Best Practices -## Use `addSelect()` Subqueries for Single Values from Has-Many +## Select Single Relationship Values with Subqueries -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. +When only one value from a has-many relationship is needed, consider a correlated subquery with `addSelect()` instead of loading the entire relationship. This selects the value as part of the main query without an additional relationship query. ```php public function scopeWithLastLoginAt($query): void @@ -16,14 +16,14 @@ public function scopeWithLastLoginAt($query): void } ``` -## Create Dynamic Relationships via Subquery FK +## Create Dynamic Relationships with a Subquery Foreign Key -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. +The same pattern can select a foreign key and expose the selected model through a `belongsTo` relationship. Eager loading that relationship still executes a separate query, but it avoids loading the full has-many collection. ```php public function lastLogin(): BelongsTo { - return $this->belongsTo(Login::class); + return $this->belongsTo(Login::class, 'last_login_id'); } public function scopeWithLastLogin($query): void @@ -37,9 +37,9 @@ public function scopeWithLastLogin($query): void } ``` -## Use Conditional Aggregates Instead of Multiple Count Queries +## Combine Related Counts with Conditional Aggregates -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. +Combine several counts over the same filtered data set into one query by using conditional aggregates. Use `toBase()` when only scalar values are needed and model hydration provides no benefit. Confirm the expression syntax against the application's database engine. ```php $statuses = Feature::toBase() @@ -49,50 +49,50 @@ $statuses = Feature::toBase() ->first(); ``` -## Use `setRelation()` to Prevent Circular N+1 +## Reuse Loaded Parent Models with `setRelation()` -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. +When a parent and its children are already loaded and code also accesses `$child->parent`, set the inverse relationship to the existing parent instance. This avoids an additional lazy-loading query for each child. ```php $feature->load('comments.user'); $feature->comments->each->setRelation('feature', $feature); ``` -## Prefer `whereIn` + Subquery Over `whereHas` +## Compare `whereHas()` with an `IN` Subquery -`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. +`whereHas()` typically produces an `EXISTS` subquery, while `whereIn()` can express the same filter with an `IN` subquery. Either form may be faster depending on the database engine, indexes, cardinality, and query plan. Measure both forms with representative data; neither subquery loads its result set into PHP memory. -Incorrect (correlated EXISTS re-executes per row): +Option using `EXISTS`: ```php $query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); ``` -Correct (index-friendly subquery, no PHP memory overhead): +Option using `IN`: ```php $query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); ``` -## Sometimes Two Simple Queries Beat One Complex Query +## Measure Two Simple Queries Against 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. +Two targeted queries can outperform one complex correlated subquery or join when the first query is highly selective. They also add a database round trip, can transfer a large identifier list, and do not provide a single-query consistency snapshot. Decide from query plans and production-like measurements. -## Use Compound Indexes Matching `orderBy` Column Order +## Design Composite Indexes for the Query -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. +For common multi-column sorts, consider a composite index whose column order supports the query's filters and ordering. Database engines may combine indexes or choose an explicit sort, so matching the `ORDER BY` list alone does not guarantee that an index will be used. Verify the query plan. ```php // Migration $table->index(['last_name', 'first_name']); -// Query — column order must match the index +// Query that this index may support User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); ``` -## Use Correlated Subqueries for Has-Many Ordering +## Consider a Correlated Subquery 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. +When sorting by one value from a has-many relationship, a direct join can duplicate parent rows unless it first reduces the related table to one row per parent. A correlated subquery in `orderBy()` is often simpler, but its performance depends on the query plan and supporting indexes. ```php public function scopeOrderByLastLogin($query): void @@ -103,4 +103,4 @@ public function scopeOrderByLastLogin($query): void ->take(1) ); } -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/architecture.md b/_api_app/.agents/skills/laravel-best-practices/rules/architecture.md index 16505642..5e7af23b 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/architecture.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/architecture.md @@ -1,15 +1,15 @@ # Architecture Best Practices -## Single-Purpose Action Classes +## Extract Focused Business Operations -Extract discrete business operations into invokable Action classes. +Extract a discrete business operation into an action class when doing so makes the operation easier to reuse or test. An action class has no special meaning to Laravel; follow the project's naming and invocation conventions. ```php class CreateOrderAction { public function __construct(private InventoryService $inventory) {} - public function execute(array $data): Order + public function handle(array $data): Order { $order = Order::create($data); $this->inventory->reserve($order); @@ -19,11 +19,12 @@ class CreateOrderAction } ``` -## Use Dependency Injection +## Inject Required Dependencies -Always use constructor injection. Avoid `app()` or `resolve()` inside classes. +Prefer constructor injection for dependencies required throughout an object's lifetime. Method injection is appropriate for dependencies needed by one controller action, listener, job handler, or other container-invoked method. Avoid `app()` and `resolve()` when normal injection can make a dependency explicit. + +Hidden dependency: -Incorrect: ```php class OrderController extends Controller { @@ -36,24 +37,24 @@ class OrderController extends Controller } ``` -Correct: +Injected dependency: + ```php class OrderController extends Controller { - public function __construct(private OrderService $service) {} - - public function store(StoreOrderRequest $request) + public function store(StoreOrderRequest $request, OrderService $service) { - return $this->service->create($request->validated()); + return $service->create($request->validated()); } } ``` -## Code to Interfaces +## Depend on Contracts at Boundaries + +Depend on contracts at system boundaries, such as payment gateways, notification channels, and external services, when testability or interchangeable implementations justify the abstraction. -Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. +Concrete boundary dependency: -Incorrect (concrete dependency): ```php class OrderService { @@ -61,7 +62,8 @@ class OrderService } ``` -Correct (interface dependency): +Contract boundary dependency: + ```php interface PaymentGateway { @@ -80,86 +82,99 @@ Bind in a service provider: $this->app->bind(PaymentGateway::class, StripeGateway::class); ``` -## Default Sort by Descending +## Specify a Deterministic Sort Order -When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres. +Without an explicit `ORDER BY`, row order is undefined. Choose an order that matches the feature, and add a unique tie-breaker when stable pagination matters. + +Unspecified order: -Incorrect: ```php $posts = Post::paginate(); ``` -Correct: +Newest first with a stable tie-breaker: + ```php -$posts = Post::latest()->paginate(); +$posts = Post::query() + ->orderByDesc('created_at') + ->orderByDesc('id') + ->paginate(); ``` ## Use Atomic Locks for Race Conditions -Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. +Use a lock when concurrent execution must be serialized. `Cache::lock()` provides an atomic lock when the configured cache store supports locks. `lockForUpdate()` locks selected database rows and must run inside a database transaction. These mechanisms solve different coordination problems. ```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(); +// Or at query level, inside a transaction +DB::transaction(function () use ($id) { + $product = Product::where('id', $id)->lockForUpdate()->first(); + + // Read and update the product while the database lock is held. +}); ``` ## 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. +When no Laravel helper exists, prefer multibyte-aware functions such as `mb_strlen()` and `mb_strtolower()` for UTF-8 text. For example, `strlen()` counts bytes, while `strtolower()` is not multibyte-aware. Incorrect: + ```php -strlen('José'); // 5 (bytes, not characters) -strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +strlen('José'); // 5 bytes, not 4 characters +strtolower('MÜNCHEN'); // Does not lowercase Ü ``` Correct: + ```php -mb_strlen('José'); // 4 (characters) -mb_strtolower('MÜNCHEN'); // 'münchen' +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' +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. +For lightweight work that does not need retries or crash durability, consider `defer()` instead of dispatching a job. During an HTTP request, the callback normally runs after the response has been sent but remains in the same PHP process. + +Queued and durable: -Incorrect (job overhead for trivial work): ```php dispatch(new LogPageView($page)); ``` -Correct (runs after response, same process): +Deferred in the current 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 a queued job when the work needs retries, queue controls, or durability across process failures. ## Use `Context` for Request-Scoped Data -The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. +The `Context` facade makes contextual data available across the current execution lifecycle without manually passing arguments through every layer. ```php // In middleware Context::add('tenant_id', $request->header('X-Tenant-ID')); -// Anywhere later — controllers, jobs, log context +// Later in the same execution lifecycle $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`. +Visible context is added to log context, and both visible and hidden context are captured and restored for queued jobs. Use `Context::addHidden()` for data that should propagate to queued jobs without appearing in logs. Do not place secrets in context unless that propagation is intended. ## Use `Concurrency::run()` for Parallel Execution -Run independent operations in parallel using child processes — no async libraries needed. +Run independent operations concurrently through Laravel's configured concurrency driver. ```php use Illuminate\Support\Facades\Concurrency; @@ -170,13 +185,14 @@ use Illuminate\Support\Facades\Concurrency; ]); ``` -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. +With a process-based driver, each closure runs in a separate PHP process that boots the application. Use concurrency when independent database queries, HTTP client calls, or computations benefit enough to offset process and serialization overhead. The `sync` driver executes closures sequentially and is useful primarily during testing. -## Convention Over Configuration +## Follow Framework Conventions -Follow Laravel conventions. Don't override defaults unnecessarily. +Follow Laravel conventions unless the domain or an existing schema requires an override. + +Customized schema: -Incorrect: ```php class Customer extends Model { @@ -190,7 +206,8 @@ class Customer extends Model } ``` -Correct: +Conventional schema: + ```php class Customer extends Model { @@ -199,4 +216,4 @@ class Customer extends Model return $this->belongsToMany(Role::class); } } -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/blade-views.md b/_api_app/.agents/skills/laravel-best-practices/rules/blade-views.md index c6f8aaf1..4ea2c6c3 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/blade-views.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/blade-views.md @@ -1,8 +1,8 @@ -# Blade & Views Best Practices +# Blade and View Best Practices ## Use `$attributes->merge()` in Component Templates -Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. +Use the component attribute bag so callers can add attributes. `merge()` combines default attributes with caller-provided values; class values receive special merging behavior. ```blade
merge(['class' => 'alert alert-'.$type]) }}> @@ -12,25 +12,25 @@ Hardcoding classes prevents consumers from adding their own. `merge()` combines ## 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. +If a component renders repeatedly, `@push` adds its script on every render. Use a consistently named `@pushOnce` block to add that content once per rendered response. -## Prefer Blade Components Over `@include` +## Prefer Components for Explicit Interfaces -`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. +Use a Blade component when a reusable interface benefits from explicit props, an attribute bag, or slots. An include remains suitable for a small partial that intentionally uses the current view data; pass an explicit data array when implicit variable sharing would obscure its dependencies. -## Use View Composers for Shared View Data +## Share Compatible View Data with a View Composer -If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. +Use a view composer to centralize data needed whenever one or more named Blade views are rendered. Keep the composer compatible with every view it targets, and avoid broad wildcards when views require different data shapes. A view composer runs when Laravel renders the matching view; it does not supply data to JSON, streamed, or other non-view responses. -## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) +## Return Blade Fragments for Partial Rendering -A single view can return either the full page or just a fragment, keeping routing clean. +A route can return either a full view or a named fragment for clients such as htmx or Turbo. ```php return view('dashboard', compact('users')) ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); ``` -## Use `@aware` for Deeply Nested Component Props +## Share Parent Component Props with `@aware` -Avoids re-passing parent props through every level of nested components. \ No newline at end of file +Use `@aware` when a nested component needs a prop explicitly passed to an ancestor component. It does not expose an ancestor's default prop value unless that value was passed through the attribute bag. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/caching.md b/_api_app/.agents/skills/laravel-best-practices/rules/caching.md index eb3ef3e6..cd2ffd60 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/caching.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/caching.md @@ -1,10 +1,13 @@ # Caching Best Practices -## Use `Cache::remember()` Instead of Manual Get/Put +## Use `Cache::remember()` for Cache-Aside Reads -Atomic pattern prevents race conditions and removes boilerplate. +`Cache::remember()` implements a cache-aside read without a separate truthiness check. It does not prevent concurrent requests from computing the same missing value; use an atomic lock when duplicate computation must be prevented. + +The manual version below incorrectly treats valid falsy values, such as `false` or `0`, as cache misses. Incorrect: + ```php $val = Cache::get('stats'); if (! $val) { @@ -14,27 +17,42 @@ if (! $val) { ``` Correct: + ```php $val = Cache::remember('stats', 60, fn () => $this->computeStats()); ``` -## Use `Cache::flexible()` for Stale-While-Revalidate +## Consider `Cache::flexible()` for Stale-While-Revalidate + +For frequently read keys, `Cache::flexible()` can serve stale data during a defined stale period and register a deferred refresh. During an HTTP request, that refresh normally runs after the response; it is not a durable background job. Once the stale period has elapsed, the request recomputes the value synchronously. + +Synchronous expiration: + +```php +Cache::remember('users', 300, fn () => User::all()); +``` + +Stale-while-revalidate tradeoff: -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. +```php +Cache::flexible('users', [300, 600], fn () => User::all()); +``` -Incorrect: `Cache::remember('users', 300, fn () => User::all());` +This value is fresh for five minutes and may be served stale until ten minutes after it was cached. -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 an Execution -## Use `Cache::memo()` to Avoid Redundant Hits Within a Request +If the same cache key is read repeatedly during one request or job, `memo()` decorates a cache store and retains resolved values in memory for that execution. -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. +```php +$settings = Cache::memo()->get('settings'); +``` -`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. +Repeated reads through the same memoized store avoid additional store lookups. Writes through the memoized store update or invalidate its in-memory values as appropriate. ## 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`. +Tags group related entries for invalidation without tracking each key. Cache tags are not supported by the `file`, `dynamodb`, or `database` drivers; confirm support before choosing a store. ```php Cache::tags(['user-1'])->flush(); @@ -42,15 +60,27 @@ 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. +`add()` atomically writes a value only when the key does not already exist. + +Incorrect: + +```php +if (! Cache::has('lock')) { + Cache::put('lock', true, 10); +} +``` + +Correct: -Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` +```php +Cache::add('lock', true, 10); +``` -Correct: `Cache::add('lock', true, 10);` +Use `Cache::lock()` rather than an ordinary cache key when lock ownership and safe release are required. -## Use `once()` for Per-Request Memoization +## Use `once()` for In-Process 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. +`once()` memoizes a callback's return value for the current request or job. Calls made from an object instance are scoped to that instance. Unlike `Cache::memo()`, `once()` does not read from an external cache store. ```php public function roles(): Collection @@ -59,12 +89,12 @@ public function roles(): Collection } ``` -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. +Repeated calls return the memoized result without rerunning the callback. Use `once()` for repeated computation within one execution. Use `Cache::memo()` to memoize access to an underlying store that can also persist values across executions. ## Configure Failover Cache Stores in Production -If Redis goes down, the app falls back to a secondary store automatically. +The failover driver tries each configured store in order when a store operation throws an exception. It does not consult later stores for an ordinary cache miss, and data is not replicated between stores. ```php 'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/collections.md b/_api_app/.agents/skills/laravel-best-practices/rules/collections.md index 14f683d3..211fec11 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/collections.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/collections.md @@ -2,43 +2,71 @@ ## Use Higher-Order Messages for Simple Operations -Incorrect: +Explicit closure: + ```php $users->each(function (User $user) { $user->markAsVip(); }); ``` -Correct: `$users->each->markAsVip();` +Concise equivalent: + +```php +$users->each->markAsVip(); +``` -Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. +Higher-order messages are available for supported collection methods such as `each`, `map`, `filter`, and `sum`. Use an explicit closure when arguments or nontrivial logic would be clearer. -## Choose `cursor()` vs. `lazy()` Correctly +## Choose Between `cursor()` and `lazy()` -- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). -- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. +`cursor()` executes one query and hydrates models individually, but it cannot eager load relationships. The database driver's result buffering can still consume substantial memory for very large results. Use it for low-memory, attribute-only iteration when one long-running query is acceptable. -Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. +`lazy()` executes multiple chunked queries and returns a flat `LazyCollection`. It supports eager loading relationships for each chunk and avoids holding one database cursor open for the entire iteration. -Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. +With relationships: + +```php +User::with('roles')->lazy()->each(function (User $user) { + // The roles for this chunk have been eager loaded. +}); +``` + +Without relationships: + +```php +User::cursor()->each(function (User $user) { + // Process model attributes. +}); +``` ## 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. +`lazy()` uses offset pagination, so updates to columns that affect the query can shift rows and cause records to be skipped or processed twice. `lazyById()` paginates by a monotonic key and is safer when updating other columns during iteration. Do not change the pagination key itself while iterating. ## Use `toQuery()` for Bulk Operations on Collections -Avoids manual `whereIn` construction. +Use `toQuery()` to build a query from the models in an Eloquent collection instead of manually constructing a `whereIn` clause. + +Manual query: + +```php +User::whereIn('id', $users->modelKeys())->update(['active' => false]); +``` + +Collection query: -Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` +```php +$users->toQuery()->update(['active' => false]); +``` -Correct: `$users->toQuery()->update([...]);` +`toQuery()` requires a non-empty Eloquent collection whose models are of the same type. Like other bulk Eloquent updates, it does not dispatch per-model update events, so use it only when those events are not required. ## Use `#[CollectedBy]` for Custom Collection Classes -More declarative than overriding `newCollection()`. +The `#[CollectedBy]` attribute declares the custom collection class without requiring a `newCollection()` override. ```php #[CollectedBy(UserCollection::class)] class User extends Model {} -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/config.md b/_api_app/.agents/skills/laravel-best-practices/rules/config.md index 8fd8f536..82691c42 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/config.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/config.md @@ -1,73 +1,85 @@ # Configuration Best Practices -## `env()` Only in Config Files +## Read Environment Variables in Configuration Files -Direct `env()` calls return `null` when config is cached. +Call `env()` only from configuration files. After configuration is cached, Laravel does not load the application's `.env` file, so application code should read configuration values through `config()`. Incorrect: + ```php $key = env('API_KEY'); ``` Correct: + ```php // config/services.php -'key' => env('API_KEY'), +return [ + 'key' => env('API_KEY'), +]; // Application code $key = config('services.key'); ``` -## Use Encrypted Env or External Secrets +## Protect Production Secrets -Never store production secrets in plain `.env` files in version control. +Do not commit plaintext production secrets. Laravel can encrypt an environment file so its encrypted form can be stored safely, while deployment platforms can supply secrets through their native secret stores. Incorrect: -```bash -# .env committed to repo or shared in Slack - -STRIPE_SECRET=sk_live_abc123 -AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +```bash +# A plaintext .env file committed to the repository +STRIPE_SECRET= +AWS_SECRET_ACCESS_KEY= ``` -Correct: +Encrypted environment file: + ```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. +For hosted deployments, consider the platform's native secret store, such as AWS Secrets Manager or Vault, and inject secrets 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 +## Name Repeated Domain Values -Use class constants instead of hardcoded magic strings for model states, types, and statuses. +Use an enum or class constant when a domain value is repeated or represents a constrained set. A one-off string literal does not always need a named constant. ```php -// Incorrect +// Repeated literal return $this->type === 'normal'; -// Correct +// Named domain value 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. +If the application supports localization, put user-facing strings in language files and retrieve them with `__()`. Simple literals are reasonable for applications that intentionally do not support multiple languages. ```php -// Only when lang files already exist in the project +// In a localized application return back()->with('message', __('app.article_added')); -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/db-performance.md b/_api_app/.agents/skills/laravel-best-practices/rules/db-performance.md index 8fb71937..3f339770 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/db-performance.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/db-performance.md @@ -1,10 +1,11 @@ # Database Performance Best Practices -## Always Eager Load Relationships +## Eager Load Relationships Before Iterating -Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. +When a relationship will be accessed for many models, eager load it with `with()` to avoid running one initial query plus one relationship query per model, commonly called an N+1 query pattern. Lazy loading is reasonable when the relationship may not be needed or only one model is involved. + +Lazy-loaded version: -Incorrect (N+1 — executes 1 + N queries): ```php $posts = Post::all(); foreach ($posts as $post) { @@ -12,7 +13,8 @@ foreach ($posts as $post) { } ``` -Correct (2 queries total): +Eager-loaded version: + ```php $posts = Post::with('author')->get(); foreach ($posts as $post) { @@ -20,7 +22,7 @@ foreach ($posts as $post) { } ``` -Constrain eager loads to select only needed columns (always include the foreign key): +Constrain eager loads when large columns are unnecessary. Include the related model's primary key and every column Eloquent needs to match the relationship. In this example, `users.id` and `posts.user_id` match posts to users, while selecting `posts.id` preserves each related model's primary key: ```php $users = User::with(['posts' => function ($query) { @@ -42,31 +44,34 @@ public function boot(): void } ``` -Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. +By default, accessing an unloaded relationship then throws a `LazyLoadingViolationException`. Applications can customize violation handling with `handleLazyLoadingViolationUsing()`. ## Select Only Needed Columns -Avoid `SELECT *` — especially when tables have large text or JSON columns. +Select only the columns the operation needs when omitting large text, binary, or JSON columns provides a meaningful benefit. + +All columns: -Incorrect: ```php $posts = Post::with('author')->get(); ``` -Correct: +Selected columns: + ```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. +When limiting selected columns, retain every key Eloquent needs for matching. A `belongsTo` relationship needs its foreign key on the parent query and the owner's key on the related query. A `hasMany` relationship needs the parent's local key and the related model's foreign key. -## Chunk Large Datasets +## Process Large Data Sets Incrementally -Never load thousands of records at once. Use chunking for batch processing. +Use chunking or lazy iteration when loading an entire result set would exceed the application's practical memory budget. + +Loads the complete result set: -Incorrect: ```php $users = User::all(); foreach ($users as $user) { @@ -74,7 +79,8 @@ foreach ($users as $user) { } ``` -Correct: +Processes bounded chunks: + ```php User::where('subscribed', true)->chunk(200, function ($users) { foreach ($users as $user) { @@ -83,7 +89,7 @@ User::where('subscribed', true)->chunk(200, function ($users) { }); ``` -Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: +Use `chunkById()` when updates can change which rows match the query. Standard `chunk()` uses offset pagination, whose result positions can shift as rows change: ```php User::where('active', false)->chunkById(200, function ($users) { @@ -91,11 +97,14 @@ User::where('active', false)->chunkById(200, function ($users) { }); ``` -## Add Database Indexes +For read-only, attribute-only iteration, `cursor()` hydrates models individually from one query, although some database drivers still buffer raw results. Use `lazy()` when relationships must be eager loaded in chunks, and use `lazyById()` or `chunkById()` when updates can affect query membership. See the collection rules for detailed tradeoffs. + +## Add Indexes for Measured Query Patterns -Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. +Design indexes around frequent, performance-sensitive query patterns. A column's presence in `WHERE`, `ORDER BY`, `JOIN`, or `GROUP BY` does not by itself justify an index; selectivity, write cost, existing indexes, and the database query plan all matter. + +Schema without an application-specific query index: -Incorrect: ```php Schema::create('orders', function (Blueprint $table) { $table->id(); @@ -105,24 +114,26 @@ Schema::create('orders', function (Blueprint $table) { }); ``` -Correct: +Schema optimized for `WHERE status = ? ORDER BY created_at`: + ```php Schema::create('orders', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id')->index()->constrained(); - $table->string('status')->index(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); $table->timestamps(); $table->index(['status', 'created_at']); }); ``` -Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). +Confirm composite index column order and effectiveness with production-like data and the database's query-plan tools. Also check whether the database already created an index to support a foreign key before adding another one. + +## Count Relationships Without Loading Them -## Use `withCount()` for Counting Relations +Use `withCount()` when only relationship counts are needed; loading and hydrating every related model wastes memory. -Never load entire collections just to count them. +Loads related models: -Incorrect: ```php $posts = Post::all(); foreach ($posts as $post) { @@ -130,7 +141,8 @@ foreach ($posts as $post) { } ``` -Correct: +Selects relationship counts: + ```php $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { @@ -149,39 +161,24 @@ $posts = Post::withCount([ ])->get(); ``` -## Use `cursor()` for Memory-Efficient Iteration +## Keep Queries Out of Blade Templates -For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. +Prepare data before rendering a Blade template, such as in a controller, query service, or view composer. This keeps query behavior visible and testable. -Incorrect: -```php -$users = User::where('active', true)->get(); -``` - -Correct: -```php -foreach (User::where('active', true)->cursor() as $user) { - ProcessUser::dispatch($user->id); -} -``` +Query in the template: -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: +Data prepared before rendering: + ```php // Controller $users = User::with('profile')->get(); + return view('users.index', compact('users')); ``` @@ -189,4 +186,4 @@ return view('users.index', compact('users')); @foreach ($users as $user) {{ $user->profile->name }} @endforeach -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/eloquent.md b/_api_app/.agents/skills/laravel-best-practices/rules/eloquent.md index 09cd66a0..5a89510d 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/eloquent.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/eloquent.md @@ -1,8 +1,8 @@ # Eloquent Best Practices -## Use Correct Relationship Types +## Define Precise Relationship Types -Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. +Define the relationship that matches the database association, and declare its concrete return type. ```php public function comments(): HasMany @@ -20,7 +20,8 @@ public function author(): BelongsTo Extract reusable query constraints into local scopes to avoid duplication. -Incorrect: +Duplicated constraints: + ```php $active = User::where('verified', true)->whereNotNull('activated_at')->get(); $articles = Article::whereHas('user', function ($q) { @@ -28,9 +29,11 @@ $articles = Article::whereHas('user', function ($q) { })->get(); ``` -Correct: +Reusable local scope: + ```php -public function scopeActive(Builder $query): Builder +#[Scope] +protected function active(Builder $query): Builder { return $query->where('verified', true)->whereNotNull('activated_at'); } @@ -44,7 +47,8 @@ $articles = Article::whereHas('user', fn ($q) => $q->active())->get(); 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): +Global scope tradeoff: + ```php class PublishedScope implements Scope { @@ -53,12 +57,15 @@ class PublishedScope implements Scope $builder->where('published', true); } } -// Now admin panels, reports, and background jobs all silently skip drafts + +// Admin panels, reports, and jobs now omit drafts unless the scope is removed. ``` -Correct (local scope you opt into): +Explicit local scope: + ```php -public function scopePublished(Builder $query): Builder +#[Scope] +protected function published(Builder $query): Builder { return $query->where('published', true); } @@ -82,16 +89,18 @@ protected function casts(): array } ``` -## Cast Date Columns Properly +## Cast Date and Time Attributes + +Cast a date or timestamp attribute when application code should treat it as a Carbon instance. Eloquent already casts the conventional `created_at` and `updated_at` timestamps. -Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. +Manual parsing in the template: -Incorrect: ```blade -{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +{{ Carbon::parse($order->ordered_at)->toDateString() }} ``` -Correct: +Model cast: + ```php protected function casts(): array { @@ -108,24 +117,27 @@ protected function casts(): array ## Use `whereBelongsTo()` for Relationship Queries -Cleaner than manually specifying foreign keys. +`whereBelongsTo()` expresses the relationship constraint without manually specifying its foreign key. + +Foreign key constraint: -Incorrect: ```php Post::where('user_id', $user->id)->get(); ``` -Correct: +Relationship-aware constraint: + ```php Post::whereBelongsTo($user)->get(); Post::whereBelongsTo($user, 'author')->get(); ``` -## Avoid Hardcoded Table Names in Queries +## Keep Application Queries Model-Aware -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). +Prefer Eloquent models and relationships for model-backed application queries. They preserve casts, scopes, and model table configuration. The query builder and raw SQL legitimately require table names, so use them when their lower-level behavior is intentional. + +Lower-level alternatives: -Incorrect: ```php DB::table('users')->where('active', true)->get(); @@ -134,15 +146,13 @@ $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(); +Model-aware queries: -// Even better — use Eloquent or the query builder instead of raw SQL +```php 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. +When a query builder operation should follow a model's configured table name, use `(new User)->getTable()`. For complex joins or raw SQL, explicit table names may be clearer; keep those references covered by tests when schema changes are possible. -**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. \ No newline at end of file +In migrations, use explicit table names rather than application models. Migrations are historical snapshots, while models and their scopes can change after a migration is deployed. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/error-handling.md b/_api_app/.agents/skills/laravel-best-practices/rules/error-handling.md index bb8e7a38..afcdf4ac 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/error-handling.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/error-handling.md @@ -1,15 +1,18 @@ # Error Handling Best Practices -## Exception Reporting and Rendering +## Choose Where to Report and Render Exceptions -There are two valid approaches — choose one and apply it consistently across the project. +Laravel supports exception-specific methods and centralized handler callbacks. Follow the pattern already established by the project. -**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: +Exception methods keep behavior beside the exception definition: ```php class InvalidOrderException extends Exception { - public function report(): void { /* custom reporting */ } + public function report(): void + { + // Send the exception to a custom reporter. + } public function render(Request $request): Response { @@ -18,38 +21,40 @@ class InvalidOrderException extends Exception } ``` -**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: +Centralized callbacks in `bootstrap/app.php` keep the application's exception policy together: ```php ->withExceptions(function (Exceptions $exceptions) { - $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->report(function (InvalidOrderException $e) { + // Send the exception to a custom reporter. + }); $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. +An exception's `report()` method suppresses Laravel's default reporting unless it returns `false`. A report callback allows default reporting unless it returns `false` or is chained with `stop()`. Use `ShouldntReport` or `dontReport()` when the handler should not report an exception at all. By contrast, returning `false` from a `render()` method or render callback defers to Laravel's default rendering. -## Use `ShouldntReport` for Exceptions That Should Never Log +## Mark Exceptions the Handler Should Not Report -More discoverable than listing classes in `dontReport()`. +Implementing `ShouldntReport` prevents Laravel's exception handler from reporting that exception type and keeps the policy visible on the class. It does not prevent application code from logging the exception explicitly. ```php class PodcastProcessingException extends Exception implements ShouldntReport {} ``` -## Throttle High-Volume Exceptions +## Throttle High-Volume Exception Reports -A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. +A failing integration can flood logs or error tracking. Configure `throttle()` with a `Lottery` or `Limit` result to sample or rate-limit matching exception reports. Choose keys deliberately when separate exception classes, tenants, or integrations need independent limits. -## Enable `dontReportDuplicates()` +## Prevent Duplicate Reports of One Exception Instance -Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. +Enable `dontReportDuplicates()` when the same exception object may pass through multiple `report($exception)` calls. It deduplicates by object identity, not by exception class or message. -## Force JSON Error Rendering for API Routes +## Define JSON Rendering for API Routes -Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. +Laravel normally uses request content negotiation to decide whether to render an exception as JSON. If the application's API contract requires JSON regardless of the `Accept` header, define that policy explicitly for the relevant routes. ```php $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { @@ -59,7 +64,7 @@ $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { ## 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. +Attach structured data to an exception through `context()`. Laravel merges that data into the exception's log context when the handler reports it. ```php class InvalidOrderException extends Exception @@ -69,4 +74,4 @@ class InvalidOrderException extends Exception return ['order_id' => $this->orderId]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/events-notifications.md b/_api_app/.agents/skills/laravel-best-practices/rules/events-notifications.md index bc43f199..a8cce31a 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/events-notifications.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/events-notifications.md @@ -1,24 +1,24 @@ -# Events & Notifications Best Practices +# Events and Notifications Best Practices ## Rely on Event Discovery -Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. +Laravel discovers listeners in the configured listener directories by inspecting type-hinted event arguments on `handle()` or `__invoke()` methods. Register listeners manually only when discovery is disabled, the listener is outside those directories, or explicit registration is clearer. -## Run `event:cache` in Production Deploy +## Cache Event Discovery During Production Deployment -Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. +Cache discovered listeners during production deployment with `php artisan optimize` or `php artisan event:cache`. Rebuild the cache whenever listener definitions change. ## Use `ShouldDispatchAfterCommit` Inside Transactions -Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. +When an event is dispatched inside a database transaction, `ShouldDispatchAfterCommit` delays dispatch until all open database transactions commit. If a transaction rolls back, Laravel discards the event. This affects synchronous and queued listeners; it is not limited to queue timing. ```php class OrderShipped implements ShouldDispatchAfterCommit {} ``` -## Always Queue Notifications +## Queue Slow Notifications -Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. +Queue notifications that call external services, such as email, text messaging, or Slack, when they do not need to complete before the response. Keep a notification synchronous when immediate completion or failure feedback is part of the operation. ```php class InvoicePaid extends Notification implements ShouldQueue @@ -27,13 +27,17 @@ class InvoicePaid extends Notification implements ShouldQueue } ``` -## Use `afterCommit()` on Notifications in Transactions +## Dispatch Queued Notifications After Commit -Same race condition as events — the queued notification job may run before the transaction commits. +A queued notification sent inside a database transaction can run before the transaction commits. Call `afterCommit()` on the queued notification, or enable the queue connection's `after_commit` option, when its delivery depends on committed data. This setting has no scheduling effect on a synchronous notification. + +```php +$user->notify((new InvoicePaid($invoice))->afterCommit()); +``` ## Route Notification Channels to Dedicated Queues -Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. +Different notification channels can have different latency and priority requirements. Implement `viaQueues()` when channels should use separate queues. ## Use On-Demand Notifications for Non-User Recipients @@ -45,4 +49,4 @@ 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. \ No newline at end of file +Implement `HasLocalePreference::preferredLocale()` on a notifiable model when notifications and mailables should use the recipient's locale. Laravel also preserves that locale for queued delivery. An explicit `locale()` call can still override the preference for an individual notification. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/http-client.md b/_api_app/.agents/skills/laravel-best-practices/rules/http-client.md index 0a7876ed..cc8fdd6a 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/http-client.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/http-client.md @@ -1,86 +1,100 @@ # HTTP Client Best Practices -## Always Set Explicit Timeouts +## Set Explicit Timeouts -The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. +Laravel's HTTP client has a 30-second response timeout by default. Choose response and connection timeouts that fit the service and the calling request or job. Remember that retries can multiply the total elapsed time. + +Less resilient: -Incorrect: ```php $response = Http::get('https://api.example.com/users'); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) - ->connectTimeout(3) +$response = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users'); ``` -For service-specific clients, define timeouts in a macro: +Define shared settings in a macro or a dedicated client: ```php Http::macro('github', function () { return Http::baseUrl('https://api.github.com') - ->timeout(10) ->connectTimeout(3) + ->timeout(10) ->withToken(config('services.github.token')); }); $response = Http::github()->get('/repos/laravel/framework'); ``` -## Use Retry with Backoff for External APIs +## Retry Only Safe Operations -External APIs have transient failures. Use `retry()` with increasing delays. +Retry transient connection failures, rate-limit responses, and server errors with an appropriate delay. Retry idempotent requests such as `GET` when the operation can safely run more than once. Retry a state-changing request only when the remote API supports an idempotency key or provides equivalent duplicate protection. -Incorrect: -```php -$response = Http::post('https://api.stripe.com/v1/charges', $data); +Unsafe without an idempotency guarantee: -if ($response->failed()) { - throw new PaymentFailedException('Charge failed'); -} +```php +$response = Http::retry([100, 500, 1000]) + ->post('https://api.example.com/v1/charges', $data); ``` -Correct: +Safe for an idempotent request: + ```php -$response = Http::retry([100, 500, 1000]) +$response = Http::connectTimeout(3) ->timeout(10) - ->post('https://api.stripe.com/v1/charges', $data); + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->get('https://api.example.com/data'); ``` -Only retry on specific errors: +For a supported state-changing API, send a stable idempotency key for every attempt: ```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'); +$response = Http::withHeaders(['Idempotency-Key' => $paymentAttempt->uuid]) + ->connectTimeout(3) + ->timeout(10) + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->post('https://api.example.com/v1/charges', $data); ``` ## Handle Errors Explicitly -The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. +The HTTP client returns responses for `4xx` and `5xx` status codes instead of throwing by default. Inspect the expected statuses or call `throw()` before consuming a success payload. + +Unsafe when a success payload is expected: -Incorrect: ```php -$response = Http::get('https://api.example.com/users/1'); -$user = $response->json(); // Could be an error body +$user = Http::get('https://api.example.com/users/1')->json(); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) +$user = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users/1') - ->throw(); - -$user = $response->json(); + ->throw() + ->json(); ``` -For graceful degradation: +Handle expected alternatives explicitly when graceful degradation is required: ```php -$response = Http::get('https://api.example.com/users/1'); +$response = Http::connectTimeout(3) + ->timeout(5) + ->get('https://api.example.com/users/1'); if ($response->successful()) { return $response->json(); @@ -93,46 +107,30 @@ if ($response->notFound()) { $response->throw(); ``` -## Use Request Pooling for Concurrent Requests - -When making multiple independent API calls, use `Http::pool()` instead of sequential calls. +## Pool Independent Requests -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(); -``` +Use `Http::pool()` when several independent requests can run concurrently. Pooling changes execution time, not error handling; inspect or throw for each response as needed. -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'), + $pool->as('users')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/users'), + $pool->as('posts')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/posts'), ]); -$users = $responses['users']->json(); -$posts = $responses['posts']->json(); +$users = $responses['users']->throw()->json(); +$posts = $responses['posts']->throw()->json(); ``` -## Fake HTTP Calls in Tests - -Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. +## Fake HTTP Requests in Tests -Incorrect: -```php -it('syncs user from API', function () { - $service = new UserSyncService; - $service->sync(1); // Hits the real API -}); -``` +Use `Http::fake()` for external integrations, and use `Http::preventStrayRequests()` when an unexpected real request should fail the test. Also test timeouts, connection failures, and error responses that the application handles. -Correct: ```php -it('syncs user from API', function () { +it('syncs a user from the API', function () { Http::preventStrayRequests(); Http::fake([ @@ -142,19 +140,18 @@ it('syncs user from API', function () { ]), ]); - $service = new UserSyncService; - $service->sync(1); + (new UserSyncService)->sync(1); - Http::assertSent(function (Request $request) { - return $request->url() === 'https://api.example.com/users/1'; - }); + Http::assertSent(fn (Request $request) => + $request->url() === 'https://api.example.com/users/1' + ); }); ``` -Test failure scenarios too: +For example, fake a connection failure when testing the integration's failure path: ```php Http::fake([ 'api.example.com/*' => Http::failedConnection(), ]); -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/mail.md b/_api_app/.agents/skills/laravel-best-practices/rules/mail.md index c7f67966..f6310437 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/mail.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/mail.md @@ -1,27 +1,54 @@ # Mail Best Practices -## Implement `ShouldQueue` on the Mailable Class +## Queue Slow Mail Delivery -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. +Implement `ShouldQueue` on a mailable when delivery should normally happen in the background. Laravel queues that mailable even when the call site uses `Mail::send()`. -## Use `afterCommit()` on Mailables Inside Transactions +```php +class OrderShipped extends Mailable implements ShouldQueue +{ + use Queueable, SerializesModels; +} +``` -A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. +Keep mail synchronous when the caller must know immediately whether delivery was accepted, or when no queue worker is available. -## Use `assertQueued()` Not `assertSent()` for Queued Mailables +## Dispatch Queued Mail After Commit -`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence. +A queued mailable dispatched during a database transaction can be processed before the transaction commits. Call `afterCommit()` on the mailable, or enable the queue connection's `after_commit` option, when the mail depends on committed records. -Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. +```php +Mail::to($user)->send( + (new OrderShipped($order))->afterCommit() +); +``` -Correct: `Mail::assertQueued(OrderShipped::class);` +If the transaction rolls back, an after-commit mailable is not dispatched. This setting affects queued mail only; it does not defer synchronous delivery. -## Use Markdown Mailables for Transactional Emails +## Assert the Delivery Mode -Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. +Use `Mail::assertQueued()` for queued mailables and `Mail::assertSent()` for synchronously sent mailables. -## Separate Content Tests from Sending Tests +Incorrect for a mailable that implements `ShouldQueue`: -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. \ No newline at end of file +```php +Mail::assertSent(OrderShipped::class); +``` + +Correct: + +```php +Mail::assertQueued(OrderShipped::class); +``` + +## Use Markdown Mailables When They Fit + +Markdown mailables render HTML and plain-text versions from Laravel's mail components and support publishable themes. They are useful for conventional transactional messages, but a custom HTML and text pair may be more appropriate for a specialized design. + +```bash +php artisan make:mail OrderShipped --markdown=mail.orders.shipped +``` + +## Separate Content and Delivery Tests + +Test rendered content by instantiating the mailable and using assertions such as `assertSeeInHtml()` and `assertSeeInText()`. Test delivery separately with `Mail::fake()` and `assertSent()` or `assertQueued()` so failures identify the affected behavior. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/migrations.md b/_api_app/.agents/skills/laravel-best-practices/rules/migrations.md index de25aa39..3345402e 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/migrations.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/migrations.md @@ -2,76 +2,51 @@ ## Generate Migrations with Artisan -Always use `php artisan make:migration` for consistent naming and timestamps. +Use `php artisan make:migration` to generate the timestamped filename and migration structure. -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 +## Define Foreign-Key Constraints Deliberately -Automatic naming and referential integrity. +Use `constrained()` when its naming conventions and default actions match the relationship. Specify the table or delete behavior when they do not. ```php $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - -// Non-standard names $table->foreignId('author_id')->constrained('users'); ``` -## Never Modify Deployed Migrations +Do not add a duplicate single-column index without checking the database driver's treatment of foreign-key indexes and the indexes already created by the migration. -Once a migration has run in production, treat it as immutable. Create a new migration to change the table. +## Treat Deployed Migrations as Immutable -Incorrect (editing a deployed migration): -```php -// 2024_01_01_create_posts_table.php — already in production -$table->string('slug')->unique(); // ← added after deployment -``` +After a migration has run in a shared or production environment, create a new migration for subsequent changes. Editing the old file makes fresh installations differ from upgraded installations. -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'); -}); -``` +For a local migration that has not been shared or deployed, editing and rerunning it may be simpler. -## Add Indexes in the Migration +## Design Indexes for Real Queries -Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. +Add indexes based on query patterns, selectivity, write cost, and the database's ability to use composite indexes. A column appearing in `WHERE`, `ORDER BY`, or `JOIN` does not automatically need its own index. -Incorrect: -```php -Schema::create('orders', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->constrained(); - $table->string('status'); - $table->timestamps(); -}); -``` +Declare each selected index in the schema migration that creates or changes the relevant table. Confirm important indexes with representative data and the database's query plan, and avoid redundant indexes whose leading columns duplicate an existing index without serving a distinct query. See the database performance and advanced query rules for index selection and column-order guidance. + +## Stage Changes That Affect Existing Rows + +Adding a required or unique column to a populated table often needs multiple deployment-safe steps. Add a nullable column, deploy code that can handle both states, backfill existing rows in bounded chunks, then add the required constraint or index after the data is valid. + +Do not assume this migration is safe on a populated table: -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(); -}); +$table->string('slug')->unique(); ``` -## Mirror Defaults in Model `$attributes` +Large backfills are usually better implemented as an observable, restartable command or job than inside a schema migration. Small deterministic data changes may be reasonable in a migration when their locking, transaction, and deployment behavior is understood. + +## Mirror Defaults Only When Unsaved Models Need Them -When a column has a database default, mirror it in the model so new instances have correct values before saving. +A database default is applied when a row is inserted, not when a model is instantiated. Mirror the value in the model's `$attributes` only when application code must observe that default before persistence, and keep both definitions synchronized. ```php // Migration @@ -83,39 +58,10 @@ protected $attributes = [ ]; ``` -## 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'); - }); -} -``` +## Make Rollbacks Honest -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. +Implement `down()` when the change can be safely reversed. A rollback that drops populated columns or cannot restore transformed data is destructive even if it is syntactically reversible; document that limitation and prefer a forward-fix migration in production. ## 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']); -``` \ No newline at end of file +Keep each migration small enough to reason about, deploy, and reverse. Separate long-running backfills from schema changes when doing so reduces locks and supports phased deployment, but do not split related operations merely to enforce a blanket separation between data definition and data manipulation. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/queue-jobs.md b/_api_app/.agents/skills/laravel-best-practices/rules/queue-jobs.md index 2f174dfc..011828a6 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/queue-jobs.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/queue-jobs.md @@ -1,82 +1,82 @@ -# Queue & Job Best Practices +# Queue and Job Best Practices -## Set `retry_after` Greater Than `timeout` +## Keep Reservation Time Longer Than Execution Time -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. +For queue drivers that use Laravel's `retry_after` setting, configure it to exceed the longest worker or job timeout by a safety margin. When a reservation expires, another worker can reserve the same job while the first process is still running. Keep the worker's `--timeout` several seconds shorter than `retry_after`. -Incorrect (`retry_after` ≤ `timeout`): ```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +// Job +public $timeout = 120; -// config/queue.php — retry_after: 90 ← job retried while still running! +// config/queue.php for the connection +'retry_after' => 150, ``` -Correct (`retry_after` > `timeout`): -```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +Amazon Simple Queue Service uses its visibility timeout instead of Laravel's `retry_after`; configure that timeout at the queue level. Because workers can also stop after side effects but before acknowledging a job, make important jobs idempotent even with correct timeout settings. -// config/queue.php — retry_after: 180 ← safely longer than any job timeout -``` - -## Use Exponential Backoff +## Back Off Transient Failures -Use progressively longer delays between retries to avoid hammering failing services. +Use progressively longer delays when a dependency needs time to recover. Do not retry permanent validation or business-rule failures. -Incorrect (fixed retry interval): ```php class SyncWithStripe implements ShouldQueue { - public $tries = 3; - // Default: retries immediately, overwhelming the API -} -``` + public $tries = 4; -Correct (exponential backoff): -```php -class SyncWithStripe implements ShouldQueue -{ - public $tries = 3; public $backoff = [1, 5, 10]; } ``` -## Implement `ShouldBeUnique` +Rate-limiting and exception-throttling middleware can release jobs back to the queue. Released attempts may still count toward the maximum attempt limit, so configure `$tries` or `retryUntil()` to allow the intended retry window. -Prevent duplicate job processing. +## Use Unique Jobs for Dispatch Deduplication + +Implement `ShouldBeUnique` when only one queued instance of a logical job should exist. Uniqueness uses a cache lock and is not a substitute for idempotent processing or a database constraint. ```php class GenerateInvoice implements ShouldQueue, ShouldBeUnique { + public $uniqueFor = 3600; + public function uniqueId(): string { - return $this->order->id; + return (string) $this->order->id; } +} +``` - public $uniqueFor = 3600; +All dispatching processes must use a shared cache that supports locks. Unique-job constraints do not apply to jobs within batches. + +Use `ShouldBeUniqueUntilProcessing` only when the lock should be released immediately before processing begins, allowing another instance to be dispatched while the first is running: + +```php +class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing +{ + // ... } ``` -## Always Implement `failed()` +## Handle Terminal Failure When Needed + +Implement `failed()` when the application must update state, alert an operator, or record domain-specific context after all attempts are exhausted. Logging every failure in each job may duplicate the queue system's failure reporting. -Handle errors explicitly — don't rely on silent failure. +Laravel invokes `failed()` on a new job instance, so mutations made to the job during `handle()` are not available there. ```php public function failed(?Throwable $exception): void { $this->podcast->update(['status' => 'failed']); - Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); + + Log::error('Podcast processing failed', [ + 'podcast_id' => $this->podcast->id, + 'exception' => $exception, + ]); } ``` -## Rate Limit External API Calls in Jobs +## Rate Limit External Calls -Use `RateLimited` middleware to throttle jobs calling third-party APIs. +Use queue middleware such as `RateLimited` when jobs share a third-party API quota. Define the named limiter and choose release delays and attempt limits together. ```php public function middleware(): array @@ -85,62 +85,33 @@ public function middleware(): array } ``` -## Batch Related Jobs +## Batch Jobs for Group Coordination -Use `Bus::batch()` when jobs should succeed or fail together. +Use `Bus::batch()` to monitor a group of jobs and run callbacks when the batch completes or encounters failures. A batch is not a database transaction: completed jobs are not rolled back when another job fails. By default, one failed job cancels the batch; call `allowFailures()` only when partial failure is acceptable. ```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(); + ->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) + ->catch(fn (Batch $batch, Throwable $exception) => Log::error('Import batch failed', [ + 'exception' => $exception, + ])) + ->dispatch(); ``` -## `retryUntil()` Needs `$tries = 0` +## Configure Time-Based Retry Limits Deliberately -When using time-based retry limits, set `$tries = 0` to avoid premature failure. +Use `retryUntil()` as the time-based alternative to a maximum attempt count. Laravel may attempt the job any number of times until this deadline, subject to other failure conditions such as maximum exceptions. The method takes precedence over attempt-based limits, so setting `$tries = 0` is not required. ```php -public $tries = 0; - -public function retryUntil(): \DateTimeInterface +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 Horizon for Redis Queue Operations -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, - ], - ], -], -``` \ No newline at end of file +Laravel Horizon provides monitoring, balancing, metrics, and supervisor configuration for Redis queues. It does not support non-Redis queue drivers. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/routing.md b/_api_app/.agents/skills/laravel-best-practices/rules/routing.md index e288375d..604a8d96 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/routing.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/routing.md @@ -1,98 +1,106 @@ -# Routing & Controllers Best Practices +# Routing and Controller Best Practices ## Use Implicit Route Model Binding -Let Laravel resolve models automatically from route parameters. +Let Laravel resolve models from route parameters when the default lookup and missing-model behavior fit the endpoint. + +Instead of manual lookup: -Incorrect: ```php -public function show(int $id) +public function show(int $id): View { $post = Post::findOrFail($id); + + return view('posts.show', ['post' => $post]); } ``` -Correct: +Use route model binding: + ```php -public function show(Post $post) +public function show(Post $post): View { return view('posts.show', ['post' => $post]); } ``` -## Use Scoped Bindings for Nested Resources +## Scope Nested Bindings -Enforce parent-child relationships automatically. +Use scoped bindings when a nested resource must belong to its parent. This constrains model resolution; it does not replace authorization. ```php Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { - // $post is automatically scoped to $user + // The resolved post belongs to the resolved user. })->scopeBindings(); ``` -## Use Resource Controllers +## Use Resource Routes for Resourceful Actions -Use `Route::resource()` or `apiResource()` for RESTful endpoints. +Use `Route::resource()` or `Route::apiResource()` when the endpoint follows Laravel's resource-controller actions. Define explicit routes when the behavior does not fit that vocabulary. ```php Route::resource('posts', PostController::class); -Route::apiResource('api/posts', Api\PostController::class); + +// Alternatively, for an API-only resource: +Route::apiResource('posts', ApiPostController::class); ``` -## Keep Controllers Thin +`apiResource()` omits the HTML-oriented `create` and `edit` routes. It does not itself add an `/api` prefix; that prefix comes from the application's API route configuration. + +## Organize Controllers Around Resources + +As a general default, organize each controller around one resource and use Laravel's standard resource actions: `index`, `show`, `create`, `store`, `edit`, `update`, and `destroy`. This keeps routes predictable and prevents controllers from accumulating unrelated behavior. -Aim for under 10 lines per method. Extract business logic to action or service classes. +When a controller needs a custom action such as `publish`, `approve`, or `archive`, first consider whether that behavior represents a separate resource. A focused resource controller gives the behavior its own authorization, validation, and middleware boundary. + +Custom action on the primary controller: -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); -} +Route::post('/podcasts/{podcast}/publish', [PodcastController::class, 'publish']); ``` -Correct: +The published podcast modeled as a resource: + ```php -public function store(StorePostRequest $request, CreatePostAction $create) -{ - $post = $create->execute($request->validated()); +Route::post('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'store']) + ->name('published-podcasts.store'); - return redirect()->route('posts.show', $post); -} +Route::delete('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'destroy']) + ->name('published-podcasts.destroy'); ``` -## 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 +class PublishedPodcastController extends Controller { - $validated = $request->validate([ - 'title' => ['required', 'max:255'], - 'body' => ['required'], - ]); + public function store(Podcast $podcast): RedirectResponse + { + $podcast->publish(); - Post::create($validated); + return back(); + } + + public function destroy(Podcast $podcast): RedirectResponse + { + $podcast->unpublish(); - return redirect()->route('posts.index'); + return back(); + } } ``` -Correct: +Treat a custom verb as a design signal, not proof that another controller is required. Use query parameters for simple filtering, and keep an explicit action route when modeling the operation as a resource would obscure the domain or conflict with established project conventions. + +## Keep Controllers Focused on HTTP Concerns + +Controllers should coordinate HTTP input, authorization, validation, an application operation, and the response. Extract substantial or reusable business logic, but do not introduce an action or service merely to satisfy an arbitrary line limit. + ```php -public function store(StorePostRequest $request): RedirectResponse +public function store(StorePostRequest $request, CreatePostAction $create): RedirectResponse { - Post::create($request->validated()); + $post = $create->handle($request->validated()); - return redirect()->route('posts.index'); + return redirect()->route('posts.show', $post); } -``` \ No newline at end of file +``` + +A form request can perform validation and authorization before the controller runs. Do not repeat its rules in the controller. Keep simple, endpoint-specific validation inline when extraction would not improve reuse or clarity; see the validation rules for detailed guidance. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/scheduling.md b/_api_app/.agents/skills/laravel-best-practices/rules/scheduling.md index dfaefa26..4c25134a 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/scheduling.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/scheduling.md @@ -1,32 +1,50 @@ # Task Scheduling Best Practices -## Use `withoutOverlapping()` on Variable-Duration Tasks +## Prevent Unwanted Overlap -Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. +Use `withoutOverlapping()` when a second run must not begin while the previous run holds the lock. This is appropriate for variable-duration tasks that are not safe to run concurrently. -## Use `onOneServer()` on Multi-Server Deployments +```php +Schedule::command('reports:generate') + ->everyFifteenMinutes() + ->withoutOverlapping(30); +``` -Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). +The optional value is the lock expiration time in minutes, not the task timeout. Choose it carefully: the default is 24 hours, stale locks can be cleared with `php artisan schedule:clear-cache`, and an expiration that is too short can permit overlap while the first task still runs. The task itself should still tolerate retries and partial execution where practical. -## Use `runInBackground()` for Concurrent Long Tasks +## Run a Task on One Server -By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. +Use `onOneServer()` when only one scheduler node should run an eligible task. Scheduler nodes must use the same default cache store, and that store must support atomic locks. Supported stores include `database`, `memcached`, `dynamodb`, and `redis`. -## Use `environments()` to Restrict Tasks +```php +Schedule::command('billing:charge')->daily()->onOneServer(); +``` -Prevent accidental execution of production-only tasks (billing, reporting) on staging. +Name scheduled closures before applying `onOneServer()`, especially when scheduling the same closure with different parameters, so each task has a distinct lock identity. + +## Run Eligible Commands in the Background + +Tasks due at the same time run sequentially by default. Use `runInBackground()` when an independent, long-running scheduled command should not delay later tasks. ```php -Schedule::command('billing:charge')->monthly()->environments(['production']); +Schedule::command('analytics:process')->hourly()->runInBackground(); ``` -## Use `takeUntilTimeout()` for Time-Bounded Processing +Laravel restricts `runInBackground()` to tasks scheduled with `command()` and `exec()`; it is not available for scheduled closures. Ensure background processes have appropriate logging and failure monitoring. -A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. +## Restrict Tasks by Environment -## Use Schedule Groups for Shared Configuration +Use `environments()` when a task should run only in named application environments. Treat this as an operational safeguard, not an authorization control. -Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. +```php +Schedule::command('billing:charge') + ->monthly() + ->environments(['production']); +``` + +## Group Shared Configuration + +Use schedule groups when several tasks genuinely share frequency or constraints. ```php Schedule::daily() @@ -36,4 +54,8 @@ Schedule::daily() Schedule::command('emails:send --force'); Schedule::command('emails:prune'); }); -``` \ No newline at end of file +``` + +## Bound Work Inside the Task + +The scheduler does not provide a `takeUntilTimeout()` event method or terminate arbitrary tasks at a deadline. Bound work in the command or job itself by processing finite chunks, checking a deadline, or dispatching queue jobs with suitable timeouts. Use operating-system or process controls when hard termination is required. diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/security.md b/_api_app/.agents/skills/laravel-best-practices/rules/security.md index 524d47e6..57b57f0f 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/security.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/security.md @@ -1,18 +1,9 @@ # Security Best Practices -## Mass Assignment Protection +## Control Mass Assignment -Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). +Define `$fillable` when a model is populated from request-derived arrays, or deliberately guard attributes by another consistent model convention. Laravel models guard all attributes by default; `$guarded = []` opts out of that protection. -Incorrect: -```php -class User extends Model -{ - protected $guarded = []; // All fields are mass assignable -} -``` - -Correct: ```php class User extends Model { @@ -24,82 +15,71 @@ class User extends Model } ``` -Never use `$guarded = []` on models that accept user input. +Do not pass untrusted request data to a model with `$guarded = []`. Mass-assignment protection controls which attributes `create()`, `fill()`, and `update()` may set; it does not validate values or authorize the operation. -## Authorize Every Action +## Authorize Protected Actions -Use policies or gates in controllers. Never skip authorization. +Use policies, gates, or form request authorization for actions that depend on the current user's permissions. Authentication alone does not establish permission, and validation is not authorization. -Incorrect: ```php -public function update(Request $request, Post $post) -{ - $post->update($request->validated()); -} -``` - -Correct: -```php -public function update(UpdatePostRequest $request, Post $post) +public function update(UpdatePostRequest $request, Post $post): RedirectResponse { Gate::authorize('update', $post); $post->update($request->validated()); + + return redirect()->route('posts.show', $post); } ``` -Or via Form Request: +Authorization may instead live in the form request: ```php public function authorize(): bool { - return $this->user()->can('update', $this->route('post')); + return $this->user()?->can('update', $this->route('post')) ?? false; } ``` -## Prevent SQL Injection +Public actions intentionally available to everyone do not need a redundant authorization check. -Always use parameter binding. Never interpolate user input into queries. +## Bind Query Parameters + +Use Eloquent, the query builder, or explicit bindings instead of interpolating untrusted values into Structured Query Language (SQL). Bindings protect values, not identifiers such as column names or sort directions; map user-selected identifiers to an allow-list. 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(); +User::whereRaw('LOWER(name) = ?', [$request->string('name')->lower()->toString()])->get(); ``` -## Escape Output to Prevent XSS +## Escape Output in Its Context -Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. +Blade's `{{ }}` syntax HTML-escapes output. Use `{!! !!}` only for content that has been sanitized for the exact HTML context in which it is rendered. Escaping rules differ for HTML, URLs, JavaScript, and Cascading Style Sheets. + +Incorrect for untrusted content: -Incorrect: ```blade {!! $user->bio !!} ``` Correct: + ```blade {{ $user->bio }} ``` -## CSRF Protection +## Apply Cross-Site Request Forgery Protection -Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia. +Include `@csrf` in state-changing Blade forms handled by Laravel's `web` middleware. Routes intentionally excluded from cross-site request forgery (CSRF) verification, such as validated third-party webhooks, need their own authenticity check. -Incorrect: -```blade -
- -
-``` - -Correct: ```blade
@csrf @@ -107,21 +87,27 @@ Correct:
``` -## Rate Limit Auth and API Routes +Inertia applications commonly use Axios, which returns the encrypted `XSRF-TOKEN` cookie in the `X-XSRF-TOKEN` header. Confirm equivalent configuration when using another HTTP client. Do not disable CSRF protection merely to fix a token mismatch. + +## Rate Limit Sensitive Endpoints -Apply `throttle` middleware to authentication and API routes. +Apply suitable rate limits to login attempts, password recovery, verification messages, and expensive or abuse-prone application programming interface (API) routes. Choose the limiter key deliberately; an Internet Protocol (IP) address alone can unfairly group users behind a shared network, while an account identifier alone can enable targeted denial of service. ```php RateLimiter::for('login', function (Request $request) { - return Limit::perMinute(5)->by($request->ip()); + return Limit::perMinute(5)->by(Str::transliterate( + Str::lower($request->string('email')).'|'.$request->ip() + )); }); Route::post('/login', LoginController::class)->middleware('throttle:login'); ``` -## Validate File Uploads +Rate limiting reduces abuse; it does not replace authentication, authorization, or upstream denial-of-service protection. -Validate MIME type, extension, and size. Never trust client-provided filenames. +## Validate and Store Uploads Safely + +Validate expected content type, dimensions where relevant, and size. Laravel's `mimes` rule reads the file contents and guesses a Multipurpose Internet Mail Extensions (MIME) type corresponding to the listed extensions; it does not validate the user-assigned filename extension. The `extensions` rule checks that extension and should not be used by itself. ```php public function rules(): array @@ -132,56 +118,28 @@ public function rules(): array } ``` -Store with generated filenames: - -```php -$path = $request->file('avatar')->store('avatars', 'public'); -``` - -## Keep Secrets Out of Code +Use Laravel's storage methods to generate a filename, and store untrusted files outside a publicly executable location. Public files can require additional controls, such as image re-encoding, content-disposition headers, and explicit blocking of active formats. -Never commit `.env`. Access secrets via `config()` only. - -Incorrect: ```php -$key = env('API_KEY'); +$path = $request->file('avatar')->store('avatars'); ``` -Correct: -```php -// config/services.php -'api_key' => env('API_KEY'), +## Keep Secrets Out of Application Code -// In application code -$key = config('services.api_key'); -``` +Do not commit populated environment files or hard-code credentials. Read environment variables in configuration files, then use `config()` in application code so configuration caching works correctly. See the configuration rules for encrypted environment files and external secret stores. ## Audit Dependencies -Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. +Run `composer audit` regularly and in continuous integration. Review findings for exploitability and update or mitigate affected packages promptly. ```bash composer audit ``` -## Encrypt Sensitive Database Fields +## Encrypt Sensitive Attributes When Appropriate -Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. +Use an `encrypted` cast for sensitive values that must be recoverable, and use `$hidden` to omit them from array and JavaScript Object Notation (JSON) serialization. Hidden attributes remain accessible in PHP, and encryption does not replace access control. Encrypted values cannot be meaningfully queried and should use a `TEXT` or larger column because ciphertext length is variable. -Incorrect: -```php -class Integration extends Model -{ - protected function casts(): array - { - return [ - 'api_key' => 'string', - ]; - } -} -``` - -Correct: ```php class Integration extends Model { @@ -195,4 +153,4 @@ class Integration extends Model ]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/style.md b/_api_app/.agents/skills/laravel-best-practices/rules/style.md index 67af9891..3f3f14fb 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/style.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/style.md @@ -1,125 +1,110 @@ -# 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 | -|---------|---------| +# Convention and Style Best Practices + +## Follow Project Naming Conventions + +Prefer Laravel's conventions in new code, but preserve an established project convention unless a coordinated rename is worthwhile. + +| Element | Convention | Example | +| --- | --- | --- | +| Controller | Singular resource name | `ArticleController` | +| Model | Singular StudlyCase | `User` | +| Table | Plural snake_case | `article_comments` | +| Pivot table | Singular model names in alphabetical order, in snake_case | `article_user` | +| Column | snake_case | `meta_title` | +| Conventional foreign key | Singular model name plus `_id`, in snake_case | `article_id` | +| Resource URI | Plural resource | `articles/1` | +| Route name | Dotted segments; snake_case within a segment when needed | `users.show_active` | +| Method | camelCase | `getAll` | +| Variable | camelCase | `$articlesWithAuthor` | +| Collection | Descriptive and plural | `$activeUsers` | +| Object | Descriptive and singular | `$activeUser` | +| View | kebab-case | `show-filtered.blade.php` | +| Configuration file | snake_case | `google_calendar.php` | +| Enumeration | Singular StudlyCase | `UserType` | + +## Prefer Clear, Idiomatic Syntax + +Use Laravel helpers and query methods when they communicate intent more directly. Do not shorten code when the result is ambiguous or loses useful type information. + +| More verbose | Idiomatic alternative | +| --- | --- | | `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')` | +| `->first()?->name` | `->value('name')` when only that value is needed | -## Use Laravel String & Array Helpers +Use typed request accessors such as `$request->string()`, `$request->integer()`, and `$request->boolean()` when their coercion matches the operation. -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. +## Use Utilities When They Clarify Intent -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); +Laravel's `Str`, `Arr`, `Number`, and `Uri` utilities provide expressive operations and framework-consistent behavior. Prefer them when they are clearer or safer than an equivalent PHP operation, not as an unconditional replacement for every built-in function. -// Correct +```php $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 +$class = class_basename(User::class); $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()`. +Use `Arr` for dot notation and common transformations: -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'); +$public = Arr::only($attributes, ['name', 'email']); ``` -Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`. +Use `Number` for localized display formatting rather than values that will be stored or calculated: -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%" +Number::format(1000000); +Number::currency(1500, 'USD'); +Number::fileSize(1024 * 1024); ``` -URIs — use `Uri` for URL manipulation: +Use `Uri` when constructing or transforming a uniform resource identifier (URI) benefits from a structured API: + ```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. +Check the documentation for the Laravel version supported by the project before using newer utility classes or methods. -Use `search-docs` for the full list of available methods — these helpers are extensive. +## Keep Presentation Code Maintainable -## No Inline JS/CSS in Blade +Prefer the project's asset pipeline, components, and existing conventions for substantial JavaScript and Cascading Style Sheets (CSS). Small page-specific scripts or styles can be reasonable in Blade layouts or stacks; avoid mixing large behavior and style blocks into templates. -Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes. +Pass server data with an encoding mechanism appropriate to its context. For example, Blade's `Js::from()` safely formats data for JavaScript: -Incorrect: ```blade -let article = `{{ json_encode($article) }}`; + ``` -Correct: -```blade - -``` +Data attributes are useful for small scalar values, but serializing a large model into an attribute can expose unnecessary fields and complicate escaping. -Pass data to JS via data attributes or use a dedicated PHP-to-JS package. +## Write Comments That Explain Why -## No Unnecessary Comments +Prefer clear names and small units of code over comments that merely restate an operation. Add concise comments for non-obvious constraints, tradeoffs, workarounds, regular expressions, or external behavior that the code cannot express by itself. Keep comments accurate when behavior changes. -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. +Unhelpful: -Incorrect: ```php -// Check if there are any joins -if (count((array) $builder->getQuery()->joins) > 0) +// Check whether the query has joins. +if (count((array) $builder->getQuery()->joins) > 0) { + // ... +} ``` -Correct: +Clearer: + ```php -if ($this->hasJoins()) -``` \ No newline at end of file +if ($this->hasJoins()) { + // ... +} +``` diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/testing.md b/_api_app/.agents/skills/laravel-best-practices/rules/testing.md deleted file mode 100644 index d39cc3ed..00000000 --- a/_api_app/.agents/skills/laravel-best-practices/rules/testing.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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(); -``` \ No newline at end of file diff --git a/_api_app/.agents/skills/laravel-best-practices/rules/validation.md b/_api_app/.agents/skills/laravel-best-practices/rules/validation.md index a20202ff..37ce6c5e 100644 --- a/_api_app/.agents/skills/laravel-best-practices/rules/validation.md +++ b/_api_app/.agents/skills/laravel-best-practices/rules/validation.md @@ -1,75 +1,89 @@ -# Validation & Forms Best Practices +# Validation and Forms Best Practices -## Use Form Request Classes +## Extract Validation When It Improves the Boundary -Extract validation from controllers into dedicated Form Request classes. +Use a form request when validation or authorization is substantial, reused, or clearer outside the controller. Inline `$request->validate()` remains appropriate for a small, endpoint-specific rule set. -Incorrect: ```php -public function store(Request $request) +public function store(StorePostRequest $request): RedirectResponse { - $request->validate([ - 'title' => 'required|max:255', - 'body' => 'required', - ]); -} -``` + $post = Post::create($request->validated()); -Correct: -```php -public function store(StorePostRequest $request) -{ - Post::create($request->validated()); + return redirect()->route('posts.show', $post); } ``` -## Array vs. String Notation for Rules +A form request's `authorize()` method can enforce access to the operation. Validation establishes the shape and values of input; it does not itself authorize the user. + +## Prefer Readable Rule Syntax -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. +Array syntax composes cleanly with rule objects and avoids delimiter issues. Prefer it in new code when it improves readability, while following a consistent local style. ```php -// Preferred for new code 'email' => ['required', 'email', Rule::unique('users')], +``` + +String syntax remains valid for simple rules: -// Follow existing convention if the project uses string notation +```php 'email' => 'required|email|unique:users', ``` -## Always Use `validated()` +## Use Only Intended Validated Data + +Use `validated()` or `safe()` instead of `$request->all()` when passing request data onward. Then select the fields intended for the operation when the validation rules also cover control fields or nested data. -Get only validated data. Never use `$request->all()` for mass operations. +Unsafe: -Incorrect: ```php Post::create($request->all()); ``` -Correct: +Preferred: + ```php -Post::create($request->validated()); +$post = Post::create($request->safe()->only(['title', 'body'])); ``` -## Use `Rule::when()` for Conditional Validation +Validated data is not automatically safe for mass assignment. Keep model `$fillable` or `$guarded` rules aligned with the operation, and never add a sensitive attribute to validation merely to make mass assignment convenient. + +## Express Conditional Rules Clearly + +Use conditional rules such as `Rule::when()`, `required_if`, or `exclude_unless` when they make the condition explicit. Choose the simplest form that remains easy to test. ```php 'company_name' => [ - Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), + 'string', + 'max:255', + Rule::when( + $this->input('account_type') === 'business', + ['required'], + ['nullable'], + ), ], ``` -## Use the `after()` Method for Custom Validation +## Add Cross-Field Validation After Base Rules -Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. +Use a form request's `after()` method for validation that depends on multiple fields or application state. Avoid expensive queries when prerequisite fields have already failed validation. ```php public function after(): array { return [ function (Validator $validator) { - if ($this->quantity > Product::find($this->product_id)?->stock) { + if ($validator->errors()->hasAny(['product_id', 'quantity'])) { + return; + } + + $stock = Product::find($this->integer('product_id'))?->stock; + + if ($stock !== null && $this->integer('quantity') > $stock) { $validator->errors()->add('quantity', 'Not enough stock.'); } }, ]; } -``` \ No newline at end of file +``` + +Validation against mutable state does not prevent a race between validation and persistence. Enforce inventory, uniqueness, and similar invariants with database constraints, atomic updates, or a database transaction as appropriate. diff --git a/_api_app/.agents/skills/pest-testing/SKILL.md b/_api_app/.agents/skills/pest-testing/SKILL.md deleted file mode 100644 index 0761b6e7..00000000 --- a/_api_app/.agents/skills/pest-testing/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -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) or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 3 features. Do not use for editing factories, seeders, migrations, controllers, models, or non-test PHP code." -license: MIT -metadata: - author: laravel ---- - -# Pest Testing 3 - -## Documentation - -Use `search-docs` for detailed Pest 3 patterns and documentation. - -## Basic Usage - -### Creating Tests - -All tests must be written using Pest. Use `php artisan make:test --pest {name}`. - -### Test Organization - -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Do NOT remove tests without approval - these are core application code. -- Test happy paths, failure paths, and edge cases. - -### 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()`. - - -```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()`: - - -```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.): - - -```php -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); -``` - -## Pest 3 Features - -### Architecture Testing - -Pest 3 includes architecture testing to enforce code conventions: - - -```php -arch('controllers') - ->expect('App\Http\Controllers') - ->toExtendNothing() - ->toHaveSuffix('Controller'); - -arch('models') - ->expect('App\Models') - ->toExtend('Illuminate\Database\Eloquent\Model'); - -arch('no debugging') - ->expect(['dd', 'dump', 'ray']) - ->not->toBeUsed(); -``` - -### Type Coverage - -Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. - -## 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 \ No newline at end of file diff --git a/_api_app/.agents/skills/testing-best-practices/SKILL.md b/_api_app/.agents/skills/testing-best-practices/SKILL.md new file mode 100644 index 00000000..35d02a1d --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/SKILL.md @@ -0,0 +1,58 @@ +--- +name: testing-best-practices +description: "Laravel test design and review. Use when selecting coverage, naming or structuring tests, choosing assertions or test data, isolating dependencies, testing HTTP or security boundaries, improving suite performance, or reviewing test value. Use framework guidance or search-docs for Pest and PHPUnit syntax." +license: MIT +metadata: + author: laravel +--- + +# Testing Best Practices + +This skill provides rules for designing Laravel tests. Each rule file explains what to do and why. Use `search-docs` for Laravel and Pest API syntax. +This project uses Pest. Follow the corresponding guidance in each rule. + +## Consistency First + +Read nearby tests before you choose syntax and organization. + +A pattern repeated throughout the project is a convention, and project conventions take precedence over this skill. Follow them and give new tests the same structure. + +These rules govern the tests you write now. An existing test that follows a project convention is not defective merely because it conflicts with this skill. Do not delete or rewrite it. If the convention has drawbacks, explain them and let the user decide. + +## What to Test + +Read this section before you write a test. + +- Test observable behavior and application contracts. A test must pass after an implementation change if the behavior stays the same. +- Cover every changed decision and each applicable high-value failure mode. A decision is a branch, a validation, a calculation, or an authorization. +- Exercise declarations through behavior instead of repeating their text. +- Leave framework behavior to framework tests. Testing project configuration is not testing the framework. A constrained relationship, cast, scope, or validation rule belongs to this project. +- Keep every test that can detect a distinct defect. When two tests detect the same defect, trim the higher-layer test to one case and report the duplication. Do not delete an existing test. +- Write a feature test first. Write a unit test only for logic that does not use the framework. +- Write a feature test for every behavior reachable through a request. Real-browser tests require `pestphp/pest-plugin-browser` and a browser download, neither of which this project installs. Mention the package only if the user asks for a real-browser test. +- Judge an architecture test by the convention it protects, not by the rules above. An `arch()` test declares a rule for an entire directory, such as the parent class of every model, the classes that may use an enum, or the methods every factory declares. It intentionally checks declarations and fails when a new file breaks the convention. +- Use the test tools that the project installs. Add a new test dependency, plugin, or browser only after the user asks for it. + +## How to Apply + +1. Read the code under test. Read the tests in the same directory. Identify every decision in the code. +2. Select every applicable branch in the rule index. Read every selected rule file. +3. Report each defect in the code before you write a test. Examples are a method with no body, a policy that no action calls, and a write action with no validation. Test the actual behavior. Report the defect to the user. +4. Write the tests. Run the smallest set of tests that covers the change. The tests must pass. +5. Check every applicable item in `rules/review.md` and every selected rule file. Resolve every mismatch before completion. + +## Rule Index + +Most changes need more than one rule file. + +| Subject | Rule File | +| --- | --- | +| Test framework features that may already do the work | [`rules/finding-features.md`](rules/finding-features.md) | +| File layout, test names, and groups | [`rules/naming.md`](rules/naming.md) | +| Arrange-act-assert and choosing the correct assertion | [`rules/assertions.md`](rules/assertions.md) | +| Endpoint coverage, authentication, authorization, tenant isolation, validation, and browser tests | [`rules/endpoint-tests.md`](rules/endpoint-tests.md) | +| Factories, test data ownership, and repeated input values | [`rules/test-data.md`](rules/test-data.md) | +| Fakes, mocks, outbound HTTP, time, randomness, and databases | [`rules/isolation.md`](rules/isolation.md) | +| Escaping, injection, cross-tenant access, and privilege checks | [`rules/security.md`](rules/security.md) | +| Environment and CI settings for a slow suite | [`rules/performance.md`](rules/performance.md) | +| Reviewing a test or suite | [`rules/review.md`](rules/review.md) | diff --git a/_api_app/.agents/skills/testing-best-practices/rules/assertions.md b/_api_app/.agents/skills/testing-best-practices/rules/assertions.md new file mode 100644 index 00000000..ba55fb25 --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/assertions.md @@ -0,0 +1,60 @@ +# Assertions + +## Arrange, Act, Assert + +Write each test in three parts: setup, one action, and assertions. Put one blank line between them so readers can identify each part without comments. + +Keep each test self-contained. Do not use values created by another test. + +## How to Find the Correct Assertion + +First identify the subject of the check, then find an assertion designed for it. A subject-specific assertion identifies the incorrect value when the test fails. + +1. Search Laravel's assertions for framework subjects such as responses, the database, sessions, models, queues, events, mail, and notifications. +2. Fetch `https://pestphp.com/docs/expectations.md` for the expectations of Pest for a plain value, a type, a format, or a shape. +3. Build the check by hand only if no assertion exists for the subject. +4. Confirm the name in the documentation before you use it. Do not write an assertion that you did not confirm. + +Use the assertion in this table for each subject. + +| Subject | Assertion to use | +| --- | --- | +| A return value, the state of an object, or a transformation of a value | an `expect()` chain | +| An HTTP status, JSON, a session, or Inertia | a Laravel response assertion | +| The state in the database | a Laravel database assertion | +| The existence of a model | `assertModelExists($model)` rather than `assertDatabaseHas('users', ['id' => $user->id])` | + +Use a PHPUnit assertion only if no Pest expectation and no Laravel assertion exists for the subject. + +Assert each fact once. Do not assert a 200 status before `assertSee`, because `assertSee` already shows that the page rendered. + +## Named Response Assertions + +Use a named response assertion, such as `assertNotFound()`, rather than `assertStatus(404)`. A failure then identifies the broken contract. Laravel provides named assertions for commonly tested status codes. + +Keep one `expect()` chain on one subject. Start a new chain when the subject changes, or when the chain is difficult to read. + +## Assert a Known Value + +Write the expected value in the test, or calculate the expected value by a different method. Do not calculate the expected value with the logic of the implementation, because the test then passes when that logic is wrong. + +```php +// The test calculates the value with the logic of the implementation... +$expected = now()->subHours(24)->floorSeconds(30)->toJson(); +expect($from)->toBe($expected); + +// The test sets a fixed input and asserts a known value... +travelTo('2025-01-01 00:00:00'); +expect($from)->toBe('2024-12-31T00:00:00.000000Z'); +``` + +## Assert the Complete Result + +A status code is not the complete result of a write operation. Assert each of the following if the operation changes it: + +- The response or the return value. +- The state in the database. +- The jobs and the events that the operation dispatches. +- The notifications and the mail that the operation sends. + +On the failure path, assert that the operation makes none of these changes. A test that asserts only `assertOk()` passes even when the application saves no record. diff --git a/_api_app/.agents/skills/testing-best-practices/rules/endpoint-tests.md b/_api_app/.agents/skills/testing-best-practices/rules/endpoint-tests.md new file mode 100644 index 00000000..32e32e64 --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/endpoint-tests.md @@ -0,0 +1,47 @@ +# Endpoint Tests + +## How to Write the Test + +Fetch `https://laravel.com/framework/docs/http-tests` for the request helpers, the authentication helpers, and the response assertions. Confirm the name before you use it, and do not guess an assertion. + +Choose an assertion based on the subject of the check: the status, a header, a redirect, the JSON body, the session, a validation error, or the view. Laravel provides a named assertion for each subject that identifies the incorrect value. + +## Endpoint Coverage + +Write a test for each applicable case: + +- The request has missing or invalid authentication. +- The request comes from a different tenant, team, or organization. +- The user has an insufficient role or permission. +- The request does not satisfy a route or scope constraint. +- The request fails the validation. +- The request is valid. Assert both the response and the persisted state. + +Assert the application's actual behavior rather than a generic status code. An API returns `401` for a missing or invalid token, while a browser endpoint redirects to the sign-in route. + +## Tenant Isolation + +Assert the status code returned for a cross-tenant request. Use `404` rather than `403` when one tenant must not learn that another tenant's record exists, because `403` confirms its existence. + +## Test Authorization at the Policy Level + +An HTTP test shows that the endpoint performs authorization. It cannot identify which mechanism refused the request because middleware, a policy, and a call to `abort()` can all return `403`. + +- Assert the complete matrix of the permissions against the policy or the gate. A failure then names the rule that is not correct. +- Write one HTTP test for one refused role, which shows that the endpoint calls the authorization. +- Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists. + +## Testing Validation + +- Write one test for each validation rule when each failure represents a separate contract. +- Write one test with an empty payload to assert several required fields together. +- Assert the text of the message that the user gets. A message that is present but wrong is a defect. +- Use a dataset for input values that need the same setup and the same assertions. + +Send an input value that is not valid through the application, and assert the error. Do not assert that an array of rules contains a string, because that assertion tests the declaration and not the behavior. Use such an assertion only for a rule that no request can reach, and write the reason in the test. + +### Which Layer Owns Which Case + +The rule-class test owns the matrix of values that pass and fail. The endpoint test proves that the endpoint applies the rule and that the user receives the message. + +When both tests contain the matrix, move it to the rule-class test and retain one case in the endpoint test. Never remove the last case, because the rule-class test still passes if the request omits the rule. The same division applies to policies, scopes, and other classes called by a request. diff --git a/_api_app/.agents/skills/testing-best-practices/rules/finding-features.md b/_api_app/.agents/skills/testing-best-practices/rules/finding-features.md new file mode 100644 index 00000000..83ce650b --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/finding-features.md @@ -0,0 +1,34 @@ +# How to Find Test Framework Features + +Pest adds features faster than this skill can list them. Find an existing feature before implementing the behavior by hand. + +- Give `search-docs` the capability you need rather than the name of a function you remember. It returns features available in the installed version. +- Fetch `https://pestphp.com/llms.txt` for the complete feature list and additions in each release. +- If a search returns no results, tell the user that the installed version does not provide the feature. Do not write an API that you have not confirmed. + +Search for a feature in this table before you write the code by hand. + +| Work that you need | Term to search for | +| --- | --- | +| Run one test with many input values | datasets, bound datasets | +| Assert over many values or over a collection | higher-order expectations | +| Remove the same setup from each test in a file | hooks, higher-order tests | +| Apply a convention to the complete codebase | architecture testing | +| Measure if the suite finds a defect | mutation testing | +| Find code with no types | type coverage | +| Reduce the time of a slow suite | parallel, profiling | +| Run one test while you debug | filtering, `--bail`, `--dirty` | + +## Built-in Laravel Assertion Methods + +Laravel provides assertions for each part of the framework. Fetch `https://laravel.com/framework/docs/testing` for the complete list, and search for an assertion before building a check by hand. Examples include `assertDatabaseHas()`, `assertModelExists()`, `assertSoftDeleted()`, response assertions such as `assertRedirectToRoute()` and `assertJsonPath()`, and fake assertions such as `Queue::assertPushed()` and `Notification::assertSentTo()`. + +A hand-built check fails with `false is not true`, which identifies nothing. A framework assertion names the incorrect table, value, or response, so the failure indicates what to fix. + +```php +// The failure says that false is not true. Instead of this... +expect(User::where('email', 'taylor@laravel.com')->exists())->toBeTrue(); + +// Use this... the failure names the table and the attributes that it did not find... +$this->assertDatabaseHas('users', ['email' => 'taylor@laravel.com']); +``` diff --git a/_api_app/.agents/skills/testing-best-practices/rules/isolation.md b/_api_app/.agents/skills/testing-best-practices/rules/isolation.md new file mode 100644 index 00000000..e1a21fdb --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/isolation.md @@ -0,0 +1,52 @@ +# Fakes, Mocks, and Determinism + +Tests that depend on actual time, randomness, sleeping, or network calls can fail for reasons unrelated to the code under test. Control all four. + +## How to Isolate a Dependency + +Fetch `https://laravel.com/framework/docs/mocking` for Laravel's fakes, facade doubles, and fake assertions. Confirm each name before using it. + +Identify the dependency, then choose the first applicable option. A framework fake preserves the real code path, while a mock replaces the dependency. + +1. Always use framework fakes for facades such as events, queues, mail, notifications, storage, the HTTP client, time, and sleep. +2. Use a developer-defined fake implementation of a service if the application provides one. +3. Use a mock for a container-resolved contract only when the real implementation leaves the process or is nondeterministic. +4. Use the real implementation for everything else, including the database. + +## Framework Fakes + +- Create each fake inside the test that needs it. Do not create fakes in a file-level `beforeEach()`. +- Pass class names to `Event::fake()` and `Queue::fake()` when you know which classes the code dispatches. A fake without class names can hide an unexpected dispatch. +- Use a fake without class names only when the test asserts the complete result, including a call to `assertNothingPushed()`. +- Write one assertion for each fake. The assertion states that the code dispatches the item, or that the code does not dispatch the item. +- Assert the data of a job or of an event if that data is part of the behavior. +- Use `Exceptions::fake()` to assert that the application reports the correct exception. Do not use `withoutExceptionHandling()`, because it changes the response under test. + +Create prerequisite factory records before calling `Event::fake()`. Factories use model events, such as a `creating` hook that generates a UUID, and a fake without class names suppresses those events and can produce an invalid model. Call the fake first only when a factory event is under test, and pass that event's class name. + +## Mocking + +Use `shouldReceive()` before the action to declare an expectation. Use `shouldHaveReceived()` after the action for a spy. Use `Mockery::on()` or `withArgs()` if an equality check cannot state the expected argument, such as a check of one field of a value object. + +Import the mock function before you use it: `use function Pest\Laravel\mock;`. + +## Outbound HTTP Testing + +Call `Http::preventStrayRequests()`. Any request without a matching fake then fails without reaching the network. + +Fake the exact endpoint used by each test. Do not call `Http::fake()` without an endpoint because it accepts unexpected requests and can hide defects. + +## Time and Randomness + +- Freeze the time or move the time in each test that depends on a date, a period, or a timestamp. +- Use the framework helpers `freezeTime()`, `travelTo()`, `travel()`, and `travelBack()`. Do not call `Carbon::setTestNow()`. +- Use `Str::createRandomStringsUsing()` to fix a generated string, if the test asserts an identifier or a slug. +- Use `Sleep::fake()` instead of a real sleep, and assert the sleeps that the code requests. +- Restore the time and the randomness after each test, if the suite does not restore them for every test. + +## Database + +- Run real queries against the real records in the test database. Do not mock the query builder, because the test then asserts the mock. +- Assert the exact keys of `toArray()` if the shape of the serialized model is a contract. The test then fails when the model exposes a new attribute. +- Test application behavior caused by the schema, such as deleting dependent records through a cascade. Do not test the database engine's cascade implementation. +- Use `LazilyRefreshDatabase` instead of `RefreshDatabase`. A test that does not use the database then does not run the migrations. diff --git a/_api_app/.agents/skills/testing-best-practices/rules/naming.md b/_api_app/.agents/skills/testing-best-practices/rules/naming.md new file mode 100644 index 00000000..4f3f5b1d --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/naming.md @@ -0,0 +1,45 @@ +# Naming and Structure + +## File Layout + +- Name each test file `{ClassName}Test.php`. +- Place each test file at the same relative path as the class under test. The class `app/Actions/DeleteTeam.php` gets the test `tests/Unit/Actions/DeleteTeamTest.php`. +- Follow the project's convention for fixture files. If none exists, put fixtures in `tests/Fixtures/` and load them by path. +- Move large literal values out of the test body and into fixture files. + +## Test Function + +Use the test function used by other files in the same directory. If no neighboring test files exist: + +- Use `it()` for the behavior of the code, and write the name as a verb phrase. +- Use `test()` for a declarative fact, such as a grant in a policy, the labels of an enum, or the shape of a serialized model. + +Use one Pest declaration style in each file. Use either `it()` or `test()` consistently. + +## Naming Tests + +The name of a test is a specification. State the user-visible result and the condition that causes it. + +- Name the behavior, and not the method under test. The file name already gives the class. +- Give the exact status code in the name of a test for an API error. +- Do not write `Given`, `When`, or `Then` in the name. + +```php +it('returns 401 when no token is provided', function () { ... }); +it('does not include deployments from deleted environments', function () { ... }); +it('falls back to the default region when none is configured', function () { ... }); +``` + +Use a verb that describes a result, such as `returns`, `renders`, `creates`, `dispatches`, `rejects`, `forbids`, `falls back`, or `does not`. + +Do not write `it('works correctly')` or `it('returns data')`, because neither specifies a meaningful result. Do not write `it('handleMethod creates record')`, because it names a method rather than behavior. + +## Grouping + +Use `describe()` if one file covers separate actions in a lifecycle. An example is a controller with the actions `index`, `show`, `store`, `update`, and `destroy`. + +Do not use `describe()` in these cases: + +- The file covers one action or one flow. +- The tests are different only in the input value. Use a dataset instead. +- The group adds a level but does not make the file easier to read. diff --git a/_api_app/.agents/skills/testing-best-practices/rules/performance.md b/_api_app/.agents/skills/testing-best-practices/rules/performance.md new file mode 100644 index 00000000..ee993fbb --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/performance.md @@ -0,0 +1,46 @@ +# Test Suite Performance + +These settings apply to the project and CI, not to individual tests. Read `rules/isolation.md` for choices within a test. + +Fetch `https://pestphp.com/docs/optimizing-tests` for Pest options that make test runs faster. +Verify each flag in the documentation before adding it to CI. + +Measure before changing a setting. Find the slow test first, and apply a project-wide setting only after identifying the costly work. + +## Test Environment + +- Set `BCRYPT_ROUNDS=4` in `.env.testing` or in `phpunit.xml`. The default value is 12, and the hash then takes most of the time of each test that signs a user in. +- Disable XDebug. Disable pcov also, unless the run needs the coverage. +- Disable packages that perform work on every request in the test environment. Examples are Pulse, Telescope, and Nightwatch. +- Use the `WithCachedConfig` and `WithCachedRoutes` traits, so the run does not parse the configuration and the routes for every test. +- Call `withoutVite()`, or `withoutMix()`, so the framework does not resolve a built asset. + +## Global Fakes + +Put these three calls in the base `Pest.php` of the project: + +- `Http::preventStrayRequests()`, because one request that reaches the network can slow the suite. This catches requests made through Laravel's HTTP client. Check direct Guzzle and cURL usage separately. +- `Sleep::fake(syncWithCarbon: true)`, so a retry and a backoff do not sleep. +- `Exceptions::fake()`, so the suite does not report an exception to an external service. + +## How to Run the Suite in Parallel + +Run `vendor/bin/pest --parallel` to spread tests across the machine's CPU cores. Add `--processes=N` if the default count is unsuitable for the machine or CI. + +A parallel run gives each process a separate database. Tests must meet these conditions; a test that fails only in parallel breaks one of them: + +- The test creates each record that it reads. It does not read a record that another test creates. +- The test does not depend on the order of the run. +- The test does not share a file, a cache key, or a queue with another test. Give each process a separate name for such a resource. + +## How to Find a Slow Test + +Run `vendor/bin/pest --profile` to list the slowest tests. Start with the ten slowest tests, because the same cause often applies to the complete suite. + +If the cause of a slow test is unclear, add an event listener or temporary log entry to identify its work. + +## Common Errors + +- The run loads XDebug for a test that does not need it. +- `BCRYPT_ROUNDS` keeps the default value, because the project has no `.env.testing`. +- The code under test calls the real `sleep()`, and `Sleep::fake()` then does not help. diff --git a/_api_app/.agents/skills/testing-best-practices/rules/review.md b/_api_app/.agents/skills/testing-best-practices/rules/review.md new file mode 100644 index 00000000..e0cfca14 --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/review.md @@ -0,0 +1,44 @@ +# Reviewing Tests + +Check every item in this file. A passing test may still provide no value. For each test, identify the defect it would catch. + +Report each finding. Do not delete or rewrite a test without the user's approval. When an issue appears throughout the suite as a convention, report the pattern once rather than every affected file. + +## Test Value + +Apply this section to behavioral tests. An architecture test states a convention for a directory, so these items do not apply to it. + +- [ ] Each test covers observable behavior or an application contract, and passes after a change to the implementation that keeps the behavior. +- [ ] Each tested declaration is exercised through behavior, and no test asserts the behavior of the framework. A test of what this project configures, such as a relation with a constraint, a cast, or a scope, belongs to this project. +- [ ] Each test detects a distinct defect that no other test covers. A duplicate shrinks at the higher layer to the one case that proves the wiring. +- [ ] Every changed decision and each applicable high-value failure mode has coverage. + +## Names and Structure + +- [ ] Each file has the name `{ClassName}Test.php` and the relative path of the class under test. +- [ ] Each name states a result, the condition that causes it, and the status code for an API error. +- [ ] Each file uses one declaration style consistently, and each `describe()` group holds separate behavior. + +## Coverage + +- [ ] HTTP tests cover authentication, authorization, role, scope, and validation when applicable. +- [ ] A request for a record of a different tenant gets a status code that does not confirm that the record exists. +- [ ] The complete permission matrix belongs in policy tests, not controller tests. +- [ ] Each validation rule has one test that asserts the user-visible message. When a unit test owns a matrix, reduce duplicate higher-level coverage to one case rather than deleting it. +- [ ] Rendered user input and each dynamic part of a query have a security test. + +## Data and Determinism + +- [ ] Each test creates its mutable records directly or through a helper that it calls, and every created record arranges the behavior or supports an assertion. +- [ ] Each `beforeEach()` holds configuration only. +- [ ] Each factory state and each relationship gives the meaning of the data. +- [ ] Each call to `make()` is in a test that does not need the database. +- [ ] Time, randomness, sleep, and outbound HTTP are controlled. +- [ ] Each test passes alone, and passes in the complete suite in any order. + +## Assertions + +- [ ] Each expected value is a known value, and the test does not calculate the value with the logic of the implementation. +- [ ] Each test of a write operation asserts the response, the state in the database, and the side effects. +- [ ] Each fake has one assertion, and gives the class names unless the test asserts the complete result. +- [ ] Each `expect()` chain stays on one subject. diff --git a/_api_app/.agents/skills/testing-best-practices/rules/security.md b/_api_app/.agents/skills/testing-best-practices/rules/security.md new file mode 100644 index 00000000..234f5b00 --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/security.md @@ -0,0 +1,27 @@ +# Security Tests + +Test each security boundary where user input affects authorization, rendered output, or query construction. A defect at such a boundary can be difficult to detect because the feature may continue to work. + +Write a test for each of these cases: + +- **Cross-tenant access.** Request a record of a different tenant, team, or organization. Read `rules/endpoint-tests.md` for why the response should possibly be `404` rather than `403`. +- **Each unprivileged role.** Use a dataset over the roles that the endpoint must refuse. +- **Escaping user-provided content.** Test escaping in HTML and mail. Include names and every free-text field a template renders. Assert that dangerous characters are escaped and the raw value is absent. Do not assert an exact entity for a quote, because Markdown and mail CSS inliners may decode it. +- **Injection into dynamic query components.** Examples include sort columns, filter fields, and sort directions. +- **An unexpected key** in a payload or configuration array. A merge that accepts every key can set an attribute the user must not control. + +```php +it('escapes dangerous content in the notification', function () { + $organization = Organization::factory()->make([ + 'name' => "O'Reilly ", + ]); + + $content = (new QuotaApproaching($organization, 80))->toMail()->render(); + + expect($content) + ->toContain('<script>') + ->not->toContain(""); +}); +``` + +Laravel provides defenses against mass assignment, unauthorized access, and unescaped output. Test that the application applies the appropriate defense to each attribute, route, and template. diff --git a/_api_app/.agents/skills/testing-best-practices/rules/test-data.md b/_api_app/.agents/skills/testing-best-practices/rules/test-data.md new file mode 100644 index 00000000..65124683 --- /dev/null +++ b/_api_app/.agents/skills/testing-best-practices/rules/test-data.md @@ -0,0 +1,56 @@ +# Factories and Test Data + +## Each Test Makes Its Own Data + +Create mutable records inside the test that uses them. This keeps setup visible and lets each test select its factory state. + +Use `beforeEach()` only for configuration that applies to every test in the file. Do not create records in it. + +## Record Construction + +- Use `create()` if the test needs the record in the database. +- Use `make()` only if the test does not need the database. Examples include rendering a notification and testing a value object's behavior. +- Use a named factory state instead of a raw attribute. `User::factory()->unverified()->create()` gives the state meaning; `create(['email_verified_at' => null])` gives only its value. +- Use `for()` or the relationship helper of the project to declare the owner of a record. +- Use `recycle()` if several records must share one parent record. +- Use `sequence()` if several records need different attributes. + +```php +$organization = Organization::factory()->onPlan(BillingPlan::PRO)->create(); + +$environment = Environment::factory()->recycle($organization)->create(); + +$organizations = Organization::factory() + ->count(3) + ->sequence( + ['created_at' => now()->setSeconds(30)], + ['created_at' => now()->setSeconds(1)], + ) + ->create(); +``` + +Create only the records required to arrange the behavior or support an assertion. + +## Datasets + +Use a dataset when the setup, test body, and assertions remain the same across input values. + +```php +it('forbids roles other than admin', function (Role $role) { + actingAs(User::factory()->hasOrganization($role)->create()) + ->post('/settings') + ->assertForbidden(); +})->with(collect(Role::cases())->reject(fn (Role $role) => $role === Role::ADMIN)); +``` + +Use parameterized tests for: + +- enum cases +- roles and plans +- boundary values +- input values that are invalid in the same way +- input and output value pairs + +Write separate tests if the cases need a different setup, a different behavior, or different assertions. One test function with a branch in the body is two tests in one function. + +Give each dataset case a name that states the difference. A failure then identifies the case without requiring you to count positions. diff --git a/_api_app/.claude/skills/ai-sdk-development/SKILL.md b/_api_app/.claude/skills/ai-sdk-development/SKILL.md index d74e342e..ef6d72a2 100644 --- a/_api_app/.claude/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.claude/skills/ai-sdk-development/SKILL.md @@ -410,4 +410,4 @@ Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text | OpenRouter | Y | - | - | - | - | - | - | - | | ElevenLabs | - | - | Y | Y | - | - | - | - | | Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file +| Jina | - | - | - | - | Y | Y | - | - | diff --git a/_api_app/.claude/skills/deploying-to-cloud/SKILL.md b/_api_app/.claude/skills/deploying-to-cloud/SKILL.md new file mode 100644 index 00000000..9584216a --- /dev/null +++ b/_api_app/.claude/skills/deploying-to-cloud/SKILL.md @@ -0,0 +1,267 @@ +--- +name: deploying-to-cloud +description: "Deploys and manages Laravel applications on Laravel Cloud using the `cloud` CLI. Use when the user wants to deploy an app, ship to Cloud, create or manage applications, environments, databases, caches, object storage, queues, domains, instances, background processes, secrets, compute, scheduled tasks, or Laravel Cloud infrastructure; or check billing, usage, or spend. Triggers on deployment, shipping, Cloud management, environment setup, database provisioning, billing or usage queries, the `cloud` CLI, and Laravel Cloud deployment troubleshooting." +license: MIT +metadata: + author: laravel +--- + +# Deploying with Laravel Cloud + +Use the [Laravel Cloud documentation](https://cloud.laravel.com/docs/llms.txt) for detailed, current feature behavior. Use the Cloud CLI for Cloud operations rather than guessing dashboard or API workflows. Install it in the project and invoke it as `./vendor/bin/cloud` by default; use a globally installed `cloud` command only as a fallback. Commands below are written as `cloud` for brevity; run them as `./vendor/bin/cloud` unless falling back to the global installation. + +## Application Setup + +- Laravel Cloud deploys from GitHub, GitLab, or Bitbucket. A new Laravel application requires PHP 8.2 or later, Laravel 9 or later, a connected Git provider, and a deployment region. +- Cloud creates environments for applications. Use separate production, staging, and preview environments; each environment has its own compute, resources, and deployment settings. +- Keep application compute and attached resources in the same region where possible. +- Cloud builds a Docker image using the selected PHP version, runs the configured build and deploy commands, and switches traffic to a successful deployment with zero downtime. Push-to-deploy is enabled by default, and manual deployments and deploy hooks are also available. + +## Build and Deploy + +- A typical Laravel build command is `composer install --no-dev && npm run build`. +- Run optimization and cache-building commands during the build, not during deployment. A typical deploy command is `php artisan migrate --force`. +- Build and deploy commands have a 15-minute timeout. Deploy commands run immediately before the release becomes live, and filesystem changes made by deploy commands are not persisted. +- Do not add `php artisan queue:restart`, `php artisan horizon:terminate`, `php artisan optimize:clear`, or `php artisan storage:link` to deploy commands. Cloud handles worker restarts and process management; deploy filesystem changes are not persistent. +- After changing environment settings, attached resources, or linked secrets, redeploy the environment for the changes to take effect. + +## Configuration and Resources + +- Attached databases, caches, and object storage inject their connection variables automatically. Custom environment variables take precedence over injected values. +- Use Secrets Manager for encrypted organization-level values shared across environments. Secret values cannot be read after creation; redeploy affected environments after creating, updating, linking, unlinking, or deleting a secret. +- Environment filesystems are ephemeral and are not shared between replicas. Use a database or Laravel Valkey for persistent cache and sessions, and Laravel Cloud Object Storage for persistent files. Do not rely on local files surviving a deployment. + +### Object Storage and File Visibility + +- Laravel Cloud Object Storage is backed by Cloudflare R2. R2 applies visibility at the bucket level; a bucket cannot contain a mix of private and public objects. +- For private files, always use `Storage::disk()` with no arguments, such as `Storage::disk()->put(...)`, so Laravel uses the environment's default disk. Do not pass a disk name for private/default storage. Attach a private Cloud bucket as the environment's default disk. +- For public files, use the named public disk: `Storage::disk('public')->put(...)`. Attach a second Cloud bucket configured as public and give it the `public` disk name. +- Applications that use both private and public files therefore need two Cloud Object Storage buckets attached to the environment: a private default bucket and a public bucket named `public`. +- Do not set per-file or Flysystem `visibility: 'public'` configuration for Cloud Object Storage. R2 does not support per-object ACL headers and rejects those requests; select the bucket visibility when creating the bucket. +- Private buckets are not internet-accessible, but Laravel can generate temporary public URLs with `Storage::temporaryUrl(...)`. Public buckets expose all objects through their Cloud-provided public URL. +- Install the S3 Flysystem adapter before using Cloud Object Storage: `composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies`. + +## Queues and Scheduling + +- Managed queues are the recommended queue option. They provision dedicated workers and autoscale based on queued work. +- Managed queues require a supported recent Laravel version and `aws/aws-sdk-php`; check the current Cloud documentation before changing dependencies. +- Enable the scheduler on an App or Worker cluster to run `php artisan schedule:run` every minute. When an environment has multiple replicas, use Laravel's `onOneServer` for tasks that must run once. +- Only Flex compute sizes can scale to zero. Scale-to-zero environments wake for Laravel scheduled tasks and queued jobs, but long-running jobs may be interrupted when the sleep timeout is reached. Use managed queues for workloads that must not be interrupted. + +## Domains and Storage + +- Each environment receives a `laravel.cloud` domain after its first successful deployment. Cloud automatically verifies custom domains and provisions SSL after the required DNS records are configured. +- Do not use the ephemeral filesystem for persistent uploads, generated files, cache, or session data. Use the appropriate Cloud resource instead. + +## Cloud CLI + +### Setup + +```shell +composer require --dev laravel/cloud-cli +./vendor/bin/cloud auth -n +``` + +If a project-local installation is not available, install the CLI globally with `composer global require laravel/cloud-cli` and invoke it as `cloud`. + +`cloud auth` opens a browser. If a browser is unavailable, set `LARAVEL_CLOUD_TOKEN` in the environment. It overrides any saved token and writes nothing to disk. To save a token instead, run `cloud auth:token --add --token= -n` or pipe it with `echo "$TOKEN" | cloud auth:token --add -n`. + +### Commands + +Commands follow a CRUD pattern: `resource:list`, `resource:get`, `resource:create`, `resource:update`, `resource:delete`. + +Available resources: `application`, `environment`, `instance`, `database-cluster`, `database`, `cache`, `bucket`, `domain`, `websocket-cluster`, `background-process`, `secret`, `command`, `deployment`. + +Some resources have additional commands (e.g., `domain:verify`, `database:open`, `instance:sizes`, `cache:types`). Discover these via `cloud -h`. + +Never hardcode command signatures. Always run `cloud -h` to discover options at runtime. + +### CLI Flags + +- Always add `-n` to every command to prevent the CLI from hanging. +- Never use `-q` or `--silent`; they suppress all output. + +Flag combos per operation: +- Read (`:list`, `:get`) → `--json -n` +- Create (`:create`) → `--json -n` +- Update (`:update`) → `--json -n --force` +- Delete (`:delete`) → `-n --force` (no `--json`) +- Environment variables → `-n --force` +- Deploy or ship → `-n` with all options passed explicitly (no `--json`) + +### Deployment Workflow + +Determine the task and follow the matching path: + +- First deployment: inspect `cloud ship -h`, then run `cloud ship -n` with all required values. + +- Existing application: + ```shell + cloud deploy {app_name} {environment} -n --open + cloud deploy:monitor -n + ``` + +- Environment variables: `cloud environment:variables -n --force` + +- Secrets: `echo "$VALUE" | cloud secret:create --name=NAME --json -n`, then `cloud environment-secret:attach` + +- Provision infrastructure: `cloud :create --json -n` + +- Monorepo (application in a subdirectory): add `--root-directory=` to `cloud ship` or `cloud application:create`. + +- Custom domain: `cloud domain:create --json -n`, then `cloud domain:verify -n`. + +- Repository defaults: `cloud repo:config {application} -n` sets repository-local application and organization defaults. Pass `--organization=` when the user has multiple organizations. + +For multi-step operations, see [reference/checklists.md](reference/checklists.md). + +- If the user's needs are unclear, ask before running anything. + +### When a Command Fails + +1. Read the error output. +2. Check the resource status with `:list --json -n` or `:get --json -n`. +3. For authentication errors, run `cloud auth -n`. +4. Fix the issue and rerun the command. +5. If the same error occurs after one fix, stop and ask the user rather than repeating the operation. + +Always run `cloud deploy:monitor -n` after every deploy. If it fails, inspect the deployment status and logs and show the user what went wrong before attempting a fix. + +### Subagent Delegation + +Delegate high-output operations to subagents using the Task tool to keep the main context window small. Only the summary returns; verbose output stays in the subagent's context. + +Delegate these to a subagent: +- `cloud deploy:monitor -n` — deployment logs can be very long +- `cloud deployment:get --json -n` — full deployment details +- `cloud :list --json -n` — listing many resources produces large JSON +- `cloud command:run` — when output may be long +- `cloud usage --detailed --json -n` — the payload includes details for every database, cache, bucket, websocket, and application +- Fetching docs from https://cloud.laravel.com/docs/llms.txt via `WebFetch` + +Keep in the main context: +- Short commands like `:create`, `:delete`, `:update` — output is small +- `cloud deploy -n` — you need the deployment ID immediately +- Any command where you need the result for the next step right away + +### Rules + +Follow these rules: +- Flag selection — always use the documented combos above +- Deploy sequence — deploy then monitor, never skip monitoring +- Destructive commands — always confirm with the user first, show the command and wait for approval. This includes deleting applications, environments, databases, caches, buckets, domains, or secrets. +- Error loop — diagnose, fix once, ask user if it fails again + +Use judgment for: +- Instance sizes, regions, cluster types — ask the user if not specified +- Which resources to provision — based on what the user describes +- Order of provisioning — no strict sequence required +- How to present output — summarize, show raw, or extract fields based on context + +### Secrets + +Secrets are encrypted values shared across the organization and attached to environments. The CLI encrypts values locally, so plaintext never reaches the API. + +```shell +cloud secret:list --json -n +echo "$VALUE" | cloud secret:create --name=STRIPE_KEY --json -n +cloud environment-secret:attach {environment} {secretId} -n +cloud environment-secret:list {environment} --json -n +``` + +Pipe the value in. `--value=` works, but leaves the plaintext in shell history and the process list. + +`secret:update`, `secret:delete`, and `environment-secret:attach` take secret IDs, not names — names are not unique. Read IDs from `cloud secret:list --json -n`. + +There is no `secret:get`, and no way to detach a secret from a single environment. `secret:delete` removes it and detaches it everywhere. + +Secrets need the `sodium` PHP extension. No other command does. + +Redeploy affected environments after creating, updating, attaching, or deleting a secret. + +### Remote Access + +#### Tinker (v0.2.0 or later) + +Run PHP code directly in a Cloud environment: + +```shell +cloud tinker {environment} --code='Your PHP code here' --timeout=60 -n +``` + +- `--code` — PHP code to execute (required in non-interactive mode) +- `--timeout` — maximum number of seconds to wait for output (default: 60) + +The code must explicitly output results using `echo`, `dump`, or similar; expressions alone produce no output. + +Always pass `--code` and `-n` to avoid interactive prompts. + +#### Remote Commands + +Run shell commands on a Cloud environment: + +```shell +cloud command:run {environment} --cmd='your command here' -n +``` + +- `--cmd` — the command to run (required in non-interactive mode) +- `--no-monitor` — skip real-time output streaming +- `--copy-output` — copy output to clipboard + +Review past commands: + +- `cloud command:list {environment} --json -n` — list command history +- `cloud command:get {commandId} --json -n` — get details and output of a specific command + +### Billing and Usage + +View billing and usage for the current organization: + +```shell +cloud usage --json -n +``` + +- `--period=current|previous|1|2|3` — Billing period (default: `current`; `1`, `2`, and `3` are one, two, and three periods back, respectively). Other values return an error. +- `--environment=` — filter usage to a single environment +- `--detailed` — include per-application, per-resource, and per-add-on breakdowns +- `--json` — machine-readable output (always pair with `-n`) + +Common queries: + +- Current spend: `cloud usage --json -n | jq '.currentSpendCents'` +- Last month's bill: `cloud usage --period=previous --json -n` +- One environment, full breakdown: `cloud usage --environment= --detailed --json -n` + +All amounts are in cents. Keys are camelCase at every level (e.g., `currentSpendCents`, `bandwidth.allowanceBytes`, `databases[].totalCents`, `applications[].totalCostCents`). + +### Config + +1. Environment: `LARAVEL_CLOUD_TOKEN` — an API token, taking precedence over any saved one (empty counts as unset) +2. Global: `~/.config/cloud/config.json` — auth tokens and preferences +3. Repo-local: `.cloud/config.json` — app and environment defaults (set by `cloud repo:config {application} -n`) +4. CLI arguments override both config files + +Pass the application to `repo:config`. Without it, the command must prompt and fails under `-n` when the organization has more than one application. Deploy commands do not need these defaults; pass the application and environment to them directly. + +Using multiple organizations requires multiple stored API tokens. Commands read `organization_id` from `.cloud/config.json` to select one and fail if it is not set. Set it with `cloud repo:config {application} --organization= -n`. + +`LARAVEL_CLOUD_TOKEN` holds one token, so it picks the organization on its own. Naming a different one with `--organization` fails rather than falling back to the token's organization. + +## Documentation + +Laravel Cloud documentation: https://cloud.laravel.com/docs/llms.txt + +When the user asks how something works or needs an explanation of a Laravel Cloud feature, fetch the docs from the URL above using `WebFetch` and use it to provide accurate answers. + +## When Stuck + +- Fetch https://cloud.laravel.com/docs/llms.txt for official documentation +- Run `cloud -h` for any command's options +- Run `cloud -h` to discover commands + +## Verification + +1. Confirm the target application and environment before changing or deploying anything. +2. Check staged environment changes and required resources before deployment. +3. Monitor every deployment to completion with `cloud deploy:monitor -n`. +4. For production changes, verify the application URL, logs, queues, scheduled tasks, and relevant resource health. diff --git a/_api_app/.claude/skills/deploying-to-cloud/reference/checklists.md b/_api_app/.claude/skills/deploying-to-cloud/reference/checklists.md new file mode 100644 index 00000000..d3cb54bd --- /dev/null +++ b/_api_app/.claude/skills/deploying-to-cloud/reference/checklists.md @@ -0,0 +1,55 @@ +# Checklists for Multi-Step Operations + +Use these checklists when performing complex setups. Copy the relevant checklist and track progress as you go. + +## Full environment setup (app + database + cache + domain) + +``` +- [ ] Create or select the application +- [ ] Create the environment +- [ ] Provision the database (`database-cluster:create` then `database:create`) +- [ ] Provision the cache (`cache:create`) +- [ ] Set environment variables (`environment:variables`) +- [ ] Create and attach any secrets (`secret:create` then `environment-secret:attach`) +- [ ] Attach custom domain (`domain:create` then `domain:verify`) +- [ ] Deploy (`deploy` then `deploy:monitor`) +- [ ] Verify the deployment is live +``` + +## New app from scratch + +``` +- [ ] Run `cloud ship -n` with required options +- [ ] Monitor deployment status +- [ ] Set environment variables if needed +- [ ] Verify the app is accessible +``` + +## Adding a database to an existing environment + +``` +- [ ] Check existing database clusters (`database-cluster:list --json -n`) +- [ ] Create a new cluster if needed (`database-cluster:create --json -n`) +- [ ] Create the database on the cluster (`database:create --json -n`) +- [ ] Update environment variables with connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Adding a cache to an existing environment + +``` +- [ ] Check existing caches (`cache:list --json -n`) +- [ ] Create the cache (`cache:create --json -n`) +- [ ] Update environment variables with cache connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Custom domain setup + +``` +- [ ] Create the domain (`domain:create --json -n`) +- [ ] Get DNS records to configure (`domain:get --json -n`) +- [ ] Tell the user to add the DNS records at their registrar +- [ ] Verify the domain (`domain:verify -n`) +- [ ] Confirm verification passed +``` diff --git a/_api_app/.claude/skills/infer-conventions/SKILL.md b/_api_app/.claude/skills/infer-conventions/SKILL.md new file mode 100644 index 00000000..eb3fc846 --- /dev/null +++ b/_api_app/.claude/skills/infer-conventions/SKILL.md @@ -0,0 +1,105 @@ +--- +name: infer-conventions +description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Only run this skill when the user explicitly asks for it; never start a sweep as part of another task. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand." +disable-model-invocation: true +license: MIT +metadata: + author: laravel +--- + +# Infer Conventions + +Learn how this application writes Laravel, then record what you learn as durable, path-scoped rules other agents will read. You are documenting reality, not improving it. + +## Ground Rules (read before you start) + +- Consistency first. The codebase's majority style is the convention. Never judge it, never propose a "better" pattern, never record what the code should do. If the app validates inline everywhere, that is the rule, even if Form Requests would be nicer. +- Skip what an active tool produces, keep what a tool would fight. Inspect the project's Pint and Rector configuration first; a Rector transformation is tooling-owned only when its package and relevant rule or set are installed and enabled. Active tools may rewrite code toward one canonical form: `$casts` to `casts()`, `$fillable` to attributes, magic accessors to the `Attribute` class, pipe-string rules to arrays, `$signature` to `#[Signature]`, named migrations to anonymous, and many more. When the app already sits at an active tool's target form, the tool owns it, so record nothing. But when the app deliberately holds a form an active tool would refactor away, such as legacy `getXxxAttribute()` accessors the `Attribute` class would replace, no tool can reproduce that choice and an agent defaults the other way. That against-the-grain hold is exactly what to record. +- Record decisions, not defaults. A consistent pattern earns a rule only when it reflects a choice: the app took one valid option where the framework or common practice offered others, or the pattern would surprise a competent agent. Framework defaults steer nothing, so skip them: anonymous migrations, `$signature` commands, `ShouldQueue` jobs, `casts()` on Laravel 11+, named routes, Rule objects in `app/Rules`, and `Mail::fake()` or `Bus::fake()` to isolate framework services. A real fork is not enough on its own. Weigh the side the app took, and record only the side an agent would not reach for by itself: inline closures everywhere, legacy accessors, a bespoke query layer. Watch for the false fork too. "No Mockery" next to facade fakes is not a choice against Mockery, because they double different things. The test for every candidate: without this rule, would the next agent plausibly write it differently? Only "yes" earns a rule. +- Architecture choices are the gold. Record presence and deliberate absence. The structural pattern the app commits to is the highest-signal convention and the one no tool can decide: Action classes and how they are invoked (`handle` / `execute` / `__invoke`), service objects, dedicated query objects exposing `builder()`, DTOs (spatie/laravel-data vs readonly classes), Form Request validation vs inline, an events and listeners spine vs direct calls, and domain or module folders. Also record a consistent non-pattern, such as "query Eloquent directly in controllers, no repository layer", so the next agent matches the app's altitude instead of over-engineering. +- Never duplicate `.ai/rules`. Read `.ai/rules/index.md` and the area files before the sweep. A dimension already covered there is marked done and skipped. +- Evidence or silence. A convention needs at least 3 consistent examples and no meaningful rival to become a candidate. Every Step 1 verdict applies this bar. +- The recorded rule states the convention, nothing else. One or two imperative lines: this project does X, so do X here. Keep detection evidence out. No counts, ratios, current usage, file lists, or example paths, because that is proof for the confirm step, not part of the rule. One short syntax fragment at most, and point to `search-docs` for API details. + +## Process + +Each step ends on a checkable completion criterion. Do not advance until it holds. + +Fan out when you can. The sweep is embarrassingly parallel. If your environment can spawn subagents (a Task, dispatch, or equivalent tool), do Step 0 yourself, then hand each checklist group (A to J) and the architecture map to its own subagent. Each subagent runs the greps, reads a few representative files, and returns structured verdicts (dimension, verdict, evidence, proposed glob / title / note). You aggregate, dedupe, then run Steps 3 to 5. It is far faster on a real app. No subagents available? Run the steps in sequence, with the same bar and the same output. + +### Step 0: Orient + +Read `composer.json` (installed packages tell you which checklist groups apply), the `pint.json` / PHPStan / Rector config, `.ai/rules/index.md` if present, and most important, map the `app/` tree. List every directory under `app/` (and any `Modules/`, `src/`, `packages/`, or domain root). Every folder beyond Laravel's default skeleton (`Http`, `Models`, `Providers`, `Console`, `Exceptions`) is a structural pattern the app committed to and a high-value rule waiting to be written: `Actions`, `Services`, `Data` or DTOs, `Queries`, `Repositories`, `ViewModels`, `Pipelines`, `Support`, `Enums`, `Contracts`, `Observers`, or `Domain` and module roots. Note each one. You will confirm how it is used in Step 2. + +This app has no Livewire/Inertia/Flux packages installed. Treat the frontend group as likely API-only: confirm from `resources/views` before spending time there, and skip the Livewire/Inertia/Flux dimensions. + +Done when: you have the applicable checklist groups, the dimensions already recorded in `.ai/rules`, and a list of every non-default `app/` directory mapped to the pattern it represents. + +### Step 1: Predefined sweep + +Open `references/checklist.md` and work every applicable dimension using its search hints. Give each exactly one verdict: + +- Pattern. Clears the bar, rival under ~20% of sites, and reflects a real choice (passes the decisions-not-defaults test). A recording candidate. Cite 2 to 3 example files. +- Conflict. Both styles present in meaningful numbers. Report the split with counts and example files. Never record a preferred winner while the code remains mixed, even in yolo, because that would describe an aspiration rather than reality. Record only if the user identifies a stable path or context boundary that explains both styles; otherwise defer until the code is reconciled. +- Default. Consistent, but a framework or common-practice default the agent already writes unprompted. Skip it as a no-op, not a convention. +- No signal. Under the bar: feature unused, or too few examples. Skip silently (one summary line at most). +- Tooling-owned or Already-recorded. Skip per the ground rules. + +Done when: every applicable dimension carries exactly one of those verdicts. + +### Step 2: Open-ended pass + +First, close out the architecture map from Step 0. For every non-default `app/` directory you listed, confirm how the pattern is used and apply the same evidence and decisions-not-defaults tests as Step 1. Generator-standard or sparsely used directories such as `Rules`, `Observers`, `Mail`, and `Notifications` are signals to inspect, not automatic conventions. Make genuine structural patterns candidates: Action classes invoked via `handle` / `execute` / `__invoke`, Services constructor-injected, `Queries` objects exposing `builder(): Builder`, DTOs as readonly classes or spatie/laravel-data, module or domain folders as the unit of organization. Scope each qualifying pattern to its own directory glob. Also record a consistent deliberate absence, such as "no repository layer, controllers query Eloquent directly", so the next agent matches the app's altitude. + +Then find what else makes this codebase itself: base or abstract classes most code extends, traits used everywhere, tenancy or authorization scoping woven through queries, naming schemes, and custom helpers. Same evidence bar, cite files. Record every genuine structural pattern, and cap the other house findings at ~5 so the pass stays high-signal. + +Done when: every non-default `app/` directory from Step 0 has a verdict, and the pass has produced its cited house findings (or concluded there are none). + +### Step 3: Confirm + +Present every candidate in one batch. Per item: dimension, verdict, evidence (counts and files), and the exact proposed `glob` or `globs` / `title` / `note`. Conflicts are presented as questions about an existing context boundary or deferred cleanup, not as a choice of future style. + +Default mode is confirm: record only what the user approves. Switch to yolo only when the invocation said so ("yolo", "don't ask", "just record them"), then record all pattern candidates without asking. Conflicts still go to the user in yolo. + +Done when: every candidate is approved, rejected, or (conflicts) decided. + +### Step 4: Record + +Make one `record-rule` call for each glob an approved convention applies to. Choose the most specific globs that cover the cited evidence from the mapping table below; if a convention spans models and migrations, record it under both domains so agents discover it from either path. The `note` is the bare convention: strip every trace of detection (see the ground rule). If `record-rule` is unavailable (rules disabled), report the full rule text so the user can enable `BOOST_RULES_ENABLED` or add it by hand. + +Record this: + +> Accessors and mutators: use the legacy magic-method style (`getXxxAttribute()` / `setXxxAttribute()`), not the `Attribute` class. Match it in models. + +Not this: + +> Accessors/mutators use the legacy magic-method style; the `Attribute`-class style is not used anywhere (13 legacy, 0 Attribute-class), e.g. `app/Models/Post.php`. Match the legacy style in existing models. + +Done when: every approved item has a successful tool response, and any failure is reported with its rule text. + +### Step 5: Summarize + +List recorded rules (file and title), conflicts the user deferred, notable no-signals, and remind the user to commit `.ai/rules` so their team and agents share the conventions. + +## Glob mapping + +Attach each rule to the most specific path that covers its evidence. Never a lazy `app/**` when a subtree fits. Match the glob to where the code actually lives, which is not the same in a default skeleton and in a modular or DDD layout. Use the Step 0 `app/` map to pick the real path. + +Examples: + +- Models: `app/Models/**` in a default app, or `app/Modules/Blog/Models/**` / `src/Domain/Blog/**` in a modular one. +- Controllers, routing, validation, responses: `app/Http/**`, or `app/Modules/*/Http/**` when each module owns its HTTP layer. +- Actions, Services, DTOs: `app/Actions/**`, `app/Services/**`, `app/Data/**`, or the module path the app actually uses. +- Tests: `tests/**`. +- Migrations and database: `database/migrations/**`. +- Truly app-wide (rare, e.g. auth retrieval): `app/**`. + +`record-rule` takes one glob. When a convention genuinely spans two domains (e.g. UUID keys touch models and migrations), call it once per domain with the same title and note; mentioning another path in the note does not make the rule discoverable there. + +## Edge cases + +- Rules disabled or `record-rule` missing: detection is read-only, so Steps 0 to 3 still run, and recording falls back to the manual path in Step 4. +- Tiny or fresh app: most dimensions land on no-signal. Say so honestly ("not enough code to infer conventions yet") and record nothing. +- Huge app: each dimension is a bounded grep plus a handful of file reads. Sample representative files, do not read everything. +- Re-runs: reading `.ai/rules` in Step 0 makes re-runs incremental, so only new or undecided dimensions surface. +- Non-standard layout (modules, DDD): the open-ended pass catches the layout itself as convention #1. Adapt the globs in the mapping table to the observed paths. diff --git a/_api_app/.claude/skills/infer-conventions/references/checklist.md b/_api_app/.claude/skills/infer-conventions/references/checklist.md new file mode 100644 index 00000000..80cef34b --- /dev/null +++ b/_api_app/.claude/skills/infer-conventions/references/checklist.md @@ -0,0 +1,137 @@ +# Detection Checklist + +Every dimension here is a genuine fork: Laravel offers two or more valid approaches, the app's choice changes what the next agent writes, and no active project tool can pick for you. Left out on purpose: pure formatting (Pint owns it), any form an installed and enabled Rector rule rewrites to one canonical shape (`$casts` to `casts()`, `$fillable` to attributes, pipe-string rules to arrays, named to anonymous migrations, `$signature` to `#[Signature]`), and framework defaults any agent writes unprompted (`ShouldQueue` jobs, relation return types, `HasFactory`). + +Each item gives the fork, then a hint (a grep or dir to spot which side the app takes). Hints are only a start. Read the matched files, never record on a raw count. Apply the ground rules to every verdict: a consistent choice that is a default or a tool's target form is not a pattern. Rows tagged (architecture) are the highest-signal, so record presence and deliberate absence. + +--- + +## A. Validation & HTTP input + +1. Validation entry point: inline `$request->validate()` vs Form Request classes vs `Validator::make()`. + - Hint: `ls app/Http/Requests`; grep `->validate(` / `Validator::make(` in `app/Http/Controllers`. +2. Custom rule location: invokable rule objects in `app/Rules` vs inline closures vs `Validator::extend()` in a provider. Rule objects are the default `make:rule` path, so record only if the app leans on closures or `Validator::extend` instead. "No rule objects" alone is just no-signal. + - Hint: `ls app/Rules`; grep `Validator::extend` in `app/Providers`. +3. Typed input retrieval: typed getters (`$request->string()`, `->integer()`, `->enum()`, `->date()`) vs raw `$request->input()` / dynamic properties. + - Hint: grep `->string(` / `->integer(` / `->enum(` vs `->input(` in `app/Http`. +4. Custom messages/attributes: `lang/*/validation.php` vs Form Request `messages()` / `attributes()` methods. + - Hint: `ls lang`; grep `function messages`, `function attributes` in `app/Http/Requests`. + +## B. Controllers & routing + +5. Controller shape: invokable single-action (`__invoke`) vs resource controllers vs plain multi-method. + - Hint: grep `__invoke` in controllers; `Route::resource` / `apiResource` vs verb routes. +6. Business-logic location (architecture): fat controllers vs delegated to Actions / Services / Jobs. + - Hint: read a few controller methods; `ls app/Actions app/Services`. +7. Route handler style: closures in `routes/*.php` vs controller classes. + - Hint: count `function ()` vs `::class` in `routes/web.php`, `routes/api.php`. +8. Middleware assignment: route/group `->middleware()` vs controller `HasMiddleware::middleware()` vs `#[Middleware]` attribute. + - Hint: grep `implements HasMiddleware`, `#[Middleware(` in controllers vs `->middleware(` in routes. +9. Route model binding: implicit (type-hinted models) vs explicit `Route::bind` vs manual `findOrFail`. + - Hint: typed model params in signatures vs `findOrFail(` in controllers; grep `Route::bind`. +10. Rate limiting: named `RateLimiter::for()` + `throttle:name` vs inline `throttle:60,1`. + - Hint: grep `RateLimiter::for` in providers vs `throttle:` in route files. + +## C. Authorization + +11. Authorization home: Gates (`Gate::define`) vs Policy classes in `app/Policies`. + - Hint: `ls app/Policies`; grep `Gate::define` in `app/Providers`. +12. Authorization call site: `$this->authorize()` / `Gate::authorize()` vs `$user->can()` vs `can` middleware vs `#[Authorize]` vs `@can` in Blade. + - Hint: grep `authorize(`, `->can(`, `middleware('can:`, `#[Authorize(`, `@can(`. + +## D. Eloquent & models + +13. Mass assignment: `$fillable` allow-list vs `$guarded` block-list. + - Hint: grep `protected $fillable` / `protected $guarded` in `app/Models`. +14. Accessors/mutators: modern `Attribute` class vs legacy `getXxxAttribute()` / `setXxxAttribute()`. Record a legacy hold, it goes against the tool's grain. + - Hint: grep `: Attribute` / `Attribute::make` vs `function get[A-Z].*Attribute` in `app/Models`. +15. Primary keys: auto-increment vs `HasUuids` vs `HasUlids`. + - Hint: grep `HasUuids` / `HasUlids` in `app/Models`; migration `id()` vs `uuid('id')`. +16. Custom casts: dedicated `CastsAttributes` classes (`app/Casts`) vs inline `Attribute` vs built-in cast strings. + - Hint: `ls app/Casts`; grep `Cast::class`, `AsStringable::class` in models. +17. Data/query layer (architecture): Eloquent directly in controllers vs repositories vs dedicated query objects (e.g. classes exposing `builder(): Builder`). + - Hint: `ls app/Repositories app/Queries`; see where non-trivial queries are built. +18. Query scopes: local `scope`/`#[Scope]` methods vs dedicated builder classes. + - Hint: grep `function scope` / `#[Scope]` in models; `ls app/*/Builders`. +19. Model events: observers (`app/Observers`, `#[ObservedBy]`) vs `booted()` closures vs event classes. + - Hint: `ls app/Observers`; grep `booted`, `::observe`, `#[ObservedBy]`. +20. Eager-load posture: explicit per-query `->with()` vs model-level `$with` defaults. Treat `preventLazyLoading()` separately as a development guard because it can complement either posture. + - Hint: grep `protected $with`, `->with(`, and separately `preventLazyLoading` in `app/`. + +## E. Architecture & organization + +21. Action/Service structure (architecture): Action classes (invoked via `handle` / `execute` / `__invoke`) vs service objects vs neither. Cross-check the Step 0 `app/` map: any `Actions`/`Services`/`Pipelines`/`Jobs`-as-actions folder is this pattern, so record how it is invoked. + - Hint: `ls app/` (the whole tree, not just `Actions`/`Services`); grep the invocation method in the folder you find. +22. DTOs (architecture): spatie/laravel-data vs plain readonly classes vs arrays everywhere. + - Hint: `ls app/Data`; grep `extends Data`, `readonly class` in `app/`. +23. Dependency acquisition: constructor/method injection vs `app()` / `resolve()` / `App::make()` service location. + - Hint: grep `app(` / `resolve(` / `::make(` in `app/` vs promoted constructor deps. +24. Decoupling: events + listeners vs direct service calls. + - Hint: `ls app/Events app/Listeners`; grep `event(`, `::dispatch(`. +25. Helper vs facade idiom: global helpers (`config()`, `auth()`, `response()`) vs facades (`Config::`, `Auth::`, `Response::`). + - Hint: ratio of `config(` vs `Config::` (etc.) across `app/`. +26. Namespace layout (architecture): default `app/` skeleton vs domain/module folders (`app/Domain/**`, modules). + - Hint: `ls app/`, look for `Domain/`, `Modules/`, bounded-context folders. +27. Enums: backed vs pure; case naming; where they live. + - Hint: `ls app/Enums`; grep `enum .*: string`, `enum .*: int`. + +## F. Frontend & views + +No Livewire/Inertia/Flux package is installed. This app may be API-only. Confirm from `resources/views` before sweeping, and treat the Livewire/Flux dimensions as not applicable. + +28. Frontend stack: Blade+Livewire vs Inertia (Vue/React/Svelte) vs Blade-only / API + separate SPA. + - Hint: `composer.json` + `package.json`; `ls resources/js/pages`, `resources/views`. +29. Blade composition: class `` components vs anonymous components (`@props`) vs `@include` partials. + - Hint: `ls app/View/Components`; grep `constrained()` vs `foreignIdFor(Model::class)` vs manual `foreign()->references()->on()`. + - Hint: grep `foreignId(`, `foreignIdFor(`, `->foreign(` in `database/migrations`. +34. `down()` methods: real reverse logic vs omitted / one-way migrations. + - Hint: grep `function down` vs the migration count. +35. Enum storage: DB `enum()` column vs `string()` + PHP-enum cast on the model. + - Hint: grep `->enum(` in migrations vs string columns cast to enums. +36. Transactions: `DB::transaction(fn ...)` closure vs manual `beginTransaction` / `commit` / `rollBack`. + - Hint: grep `DB::transaction`, `beginTransaction` in `app/`. +37. Idempotent writes: `upsert` / `updateOrCreate` / `firstOrCreate` vs find-then-save. + - Hint: grep `upsert(`, `updateOrCreate(`, `firstOrCreate(` in `app/`. + +## H. Testing + +38. Framework: Pest (`it()` / `test()` / `expect()`) vs PHPUnit classes. + - Hint: `ls tests/Pest.php`; grep `it(` / `test(` vs `extends TestCase`. +39. DB reset: `RefreshDatabase` vs `DatabaseTruncation` vs `DatabaseMigrations`. + - Hint: grep those trait names in `tests/`. +40. Fixtures: compare how equivalent test-owned records are created, such as factories vs manual inserts. Track seeders separately for shared reference data because `$this->seed()` commonly and legitimately coexists with factories. + - Hint: grep `::factory(` and direct inserts in `tests/`; separately inspect `$this->seed(` calls and what those seeders provide. +41. Collaborator isolation: how the app doubles its own classes, Mockery `mock()` / `spy()` vs real integration. Ignore facade fakes like `Mail::fake()` here, they isolate framework services by default and are not a fork against Mockery. + - Hint: grep `->mock(`, `->spy(`, `Mockery::` in `tests/`. +42. Endpoint assertions: array `assertJson([...])` / `assertJsonFragment` vs fluent `AssertableJson`. + - Hint: grep `AssertableJson`, `assertJsonFragment` in `tests/`. + +## I. Responses & API resources + +43. Response shape: API Resource classes vs `response()->json()` vs returning models/arrays directly. + - Hint: `ls app/Http/Resources`; grep `JsonResource`, `->json(` in controllers. +44. Resource relationship inclusion: `whenLoaded()` guards vs unconditional relationship access. Do not count ordinary scalar attributes as rivals to conditional relationships, and evaluate general `when()` fields separately. + - Hint: compare relationship fields using `whenLoaded(` with unconditional relationship property access in `app/Http/Resources`. +45. Pagination contracts: within comparable endpoint categories, length-aware `paginate()` vs `simplePaginate()` vs `cursorPaginate()`. These have different totals, navigation, ordering, and performance contracts, so record only a stable path-scoped API policy, never a project-wide majority. + - Hint: grep those in `app/`, then group matches by endpoint type and client contract before comparing them. +46. Web redirects/URLs: `route('name')` vs `url('/path')` vs `action([...])`. + - Hint: grep `route('`, `url('/`, `action([` in `app/Http` and views. + +## J. Strings, collections & dates + +47. Iteration idiom: `collect()->map()->filter()` pipelines vs `array_map` / `foreach`. + - Hint: grep `collect(`, `->map(` vs `array_map`, `foreach` density in `app/`. +48. String API: fluent `Str::of()->...` (Stringable) vs static `Str::` vs native (`trim`, `strtoupper`). + - Hint: grep `Str::of(` vs `Str::` vs native string funcs. +49. Dates: compare equivalent construction call styles (`now()` / `today()` helpers vs `Carbon::`) separately from the application's mutable/immutable date policy. `Date::use(CarbonImmutable::class)` can make helpers return immutable dates, so those signals are complementary rather than conflicting. + - Hint: grep `now(` and `Carbon::` for call style; separately inspect `CarbonImmutable` and `Date::use` for mutability policy. + +--- + +Genuine forks only. Every row survived the "no tool can decide this, and it isn't the default" filter. Give each applicable dimension exactly one verdict: pattern, conflict, default, no-signal, tooling-owned, or already-recorded. The rows tagged (architecture) are where the highest-value rules come from. diff --git a/_api_app/.claude/skills/laravel-best-practices/SKILL.md b/_api_app/.claude/skills/laravel-best-practices/SKILL.md index 99018f3a..311ab844 100644 --- a/_api_app/.claude/skills/laravel-best-practices/SKILL.md +++ b/_api_app/.claude/skills/laravel-best-practices/SKILL.md @@ -8,183 +8,52 @@ metadata: # 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`. +Best practices for Laravel, organized as an index of rule files. Each rule file 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. +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches, and 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 +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. ## 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 \ No newline at end of file +1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out. +2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files. +3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job. +4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable. +5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them. +6. Re-read the diff against every mapped rule before finishing. + +## Rule Index + +Cross-cutting changes often need more than one rule file. + +| Concern | Read | +| --- | --- | +| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) | +| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) | +| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) | +| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) | +| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) | +| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) | +| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) | +| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) | +| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) | +| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) | +| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) | +| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) | +| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) | +| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) | +| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) | +| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) | +| Environment values and application configuration | [`rules/config.md`](rules/config.md) | +| Tests: coverage, factories, fakes, and assertions | the `testing-best-practices` skill | +| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) | +| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) | + +## Decision Rules + +- Prefer framework features and existing application abstractions over new helpers or dependencies. +- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable. +- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/advanced-queries.md b/_api_app/.claude/skills/laravel-best-practices/rules/advanced-queries.md index 920714a1..54dd783d 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/advanced-queries.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/advanced-queries.md @@ -1,8 +1,8 @@ -# Advanced Query Patterns +# Advanced Query Best Practices -## Use `addSelect()` Subqueries for Single Values from Has-Many +## Select Single Relationship Values with Subqueries -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. +When only one value from a has-many relationship is needed, consider a correlated subquery with `addSelect()` instead of loading the entire relationship. This selects the value as part of the main query without an additional relationship query. ```php public function scopeWithLastLoginAt($query): void @@ -16,14 +16,14 @@ public function scopeWithLastLoginAt($query): void } ``` -## Create Dynamic Relationships via Subquery FK +## Create Dynamic Relationships with a Subquery Foreign Key -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. +The same pattern can select a foreign key and expose the selected model through a `belongsTo` relationship. Eager loading that relationship still executes a separate query, but it avoids loading the full has-many collection. ```php public function lastLogin(): BelongsTo { - return $this->belongsTo(Login::class); + return $this->belongsTo(Login::class, 'last_login_id'); } public function scopeWithLastLogin($query): void @@ -37,9 +37,9 @@ public function scopeWithLastLogin($query): void } ``` -## Use Conditional Aggregates Instead of Multiple Count Queries +## Combine Related Counts with Conditional Aggregates -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. +Combine several counts over the same filtered data set into one query by using conditional aggregates. Use `toBase()` when only scalar values are needed and model hydration provides no benefit. Confirm the expression syntax against the application's database engine. ```php $statuses = Feature::toBase() @@ -49,50 +49,50 @@ $statuses = Feature::toBase() ->first(); ``` -## Use `setRelation()` to Prevent Circular N+1 +## Reuse Loaded Parent Models with `setRelation()` -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. +When a parent and its children are already loaded and code also accesses `$child->parent`, set the inverse relationship to the existing parent instance. This avoids an additional lazy-loading query for each child. ```php $feature->load('comments.user'); $feature->comments->each->setRelation('feature', $feature); ``` -## Prefer `whereIn` + Subquery Over `whereHas` +## Compare `whereHas()` with an `IN` Subquery -`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. +`whereHas()` typically produces an `EXISTS` subquery, while `whereIn()` can express the same filter with an `IN` subquery. Either form may be faster depending on the database engine, indexes, cardinality, and query plan. Measure both forms with representative data; neither subquery loads its result set into PHP memory. -Incorrect (correlated EXISTS re-executes per row): +Option using `EXISTS`: ```php $query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); ``` -Correct (index-friendly subquery, no PHP memory overhead): +Option using `IN`: ```php $query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); ``` -## Sometimes Two Simple Queries Beat One Complex Query +## Measure Two Simple Queries Against 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. +Two targeted queries can outperform one complex correlated subquery or join when the first query is highly selective. They also add a database round trip, can transfer a large identifier list, and do not provide a single-query consistency snapshot. Decide from query plans and production-like measurements. -## Use Compound Indexes Matching `orderBy` Column Order +## Design Composite Indexes for the Query -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. +For common multi-column sorts, consider a composite index whose column order supports the query's filters and ordering. Database engines may combine indexes or choose an explicit sort, so matching the `ORDER BY` list alone does not guarantee that an index will be used. Verify the query plan. ```php // Migration $table->index(['last_name', 'first_name']); -// Query — column order must match the index +// Query that this index may support User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); ``` -## Use Correlated Subqueries for Has-Many Ordering +## Consider a Correlated Subquery 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. +When sorting by one value from a has-many relationship, a direct join can duplicate parent rows unless it first reduces the related table to one row per parent. A correlated subquery in `orderBy()` is often simpler, but its performance depends on the query plan and supporting indexes. ```php public function scopeOrderByLastLogin($query): void @@ -103,4 +103,4 @@ public function scopeOrderByLastLogin($query): void ->take(1) ); } -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/architecture.md b/_api_app/.claude/skills/laravel-best-practices/rules/architecture.md index 16505642..5e7af23b 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/architecture.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/architecture.md @@ -1,15 +1,15 @@ # Architecture Best Practices -## Single-Purpose Action Classes +## Extract Focused Business Operations -Extract discrete business operations into invokable Action classes. +Extract a discrete business operation into an action class when doing so makes the operation easier to reuse or test. An action class has no special meaning to Laravel; follow the project's naming and invocation conventions. ```php class CreateOrderAction { public function __construct(private InventoryService $inventory) {} - public function execute(array $data): Order + public function handle(array $data): Order { $order = Order::create($data); $this->inventory->reserve($order); @@ -19,11 +19,12 @@ class CreateOrderAction } ``` -## Use Dependency Injection +## Inject Required Dependencies -Always use constructor injection. Avoid `app()` or `resolve()` inside classes. +Prefer constructor injection for dependencies required throughout an object's lifetime. Method injection is appropriate for dependencies needed by one controller action, listener, job handler, or other container-invoked method. Avoid `app()` and `resolve()` when normal injection can make a dependency explicit. + +Hidden dependency: -Incorrect: ```php class OrderController extends Controller { @@ -36,24 +37,24 @@ class OrderController extends Controller } ``` -Correct: +Injected dependency: + ```php class OrderController extends Controller { - public function __construct(private OrderService $service) {} - - public function store(StoreOrderRequest $request) + public function store(StoreOrderRequest $request, OrderService $service) { - return $this->service->create($request->validated()); + return $service->create($request->validated()); } } ``` -## Code to Interfaces +## Depend on Contracts at Boundaries + +Depend on contracts at system boundaries, such as payment gateways, notification channels, and external services, when testability or interchangeable implementations justify the abstraction. -Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. +Concrete boundary dependency: -Incorrect (concrete dependency): ```php class OrderService { @@ -61,7 +62,8 @@ class OrderService } ``` -Correct (interface dependency): +Contract boundary dependency: + ```php interface PaymentGateway { @@ -80,86 +82,99 @@ Bind in a service provider: $this->app->bind(PaymentGateway::class, StripeGateway::class); ``` -## Default Sort by Descending +## Specify a Deterministic Sort Order -When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres. +Without an explicit `ORDER BY`, row order is undefined. Choose an order that matches the feature, and add a unique tie-breaker when stable pagination matters. + +Unspecified order: -Incorrect: ```php $posts = Post::paginate(); ``` -Correct: +Newest first with a stable tie-breaker: + ```php -$posts = Post::latest()->paginate(); +$posts = Post::query() + ->orderByDesc('created_at') + ->orderByDesc('id') + ->paginate(); ``` ## Use Atomic Locks for Race Conditions -Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. +Use a lock when concurrent execution must be serialized. `Cache::lock()` provides an atomic lock when the configured cache store supports locks. `lockForUpdate()` locks selected database rows and must run inside a database transaction. These mechanisms solve different coordination problems. ```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(); +// Or at query level, inside a transaction +DB::transaction(function () use ($id) { + $product = Product::where('id', $id)->lockForUpdate()->first(); + + // Read and update the product while the database lock is held. +}); ``` ## 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. +When no Laravel helper exists, prefer multibyte-aware functions such as `mb_strlen()` and `mb_strtolower()` for UTF-8 text. For example, `strlen()` counts bytes, while `strtolower()` is not multibyte-aware. Incorrect: + ```php -strlen('José'); // 5 (bytes, not characters) -strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +strlen('José'); // 5 bytes, not 4 characters +strtolower('MÜNCHEN'); // Does not lowercase Ü ``` Correct: + ```php -mb_strlen('José'); // 4 (characters) -mb_strtolower('MÜNCHEN'); // 'münchen' +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' +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. +For lightweight work that does not need retries or crash durability, consider `defer()` instead of dispatching a job. During an HTTP request, the callback normally runs after the response has been sent but remains in the same PHP process. + +Queued and durable: -Incorrect (job overhead for trivial work): ```php dispatch(new LogPageView($page)); ``` -Correct (runs after response, same process): +Deferred in the current 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 a queued job when the work needs retries, queue controls, or durability across process failures. ## Use `Context` for Request-Scoped Data -The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. +The `Context` facade makes contextual data available across the current execution lifecycle without manually passing arguments through every layer. ```php // In middleware Context::add('tenant_id', $request->header('X-Tenant-ID')); -// Anywhere later — controllers, jobs, log context +// Later in the same execution lifecycle $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`. +Visible context is added to log context, and both visible and hidden context are captured and restored for queued jobs. Use `Context::addHidden()` for data that should propagate to queued jobs without appearing in logs. Do not place secrets in context unless that propagation is intended. ## Use `Concurrency::run()` for Parallel Execution -Run independent operations in parallel using child processes — no async libraries needed. +Run independent operations concurrently through Laravel's configured concurrency driver. ```php use Illuminate\Support\Facades\Concurrency; @@ -170,13 +185,14 @@ use Illuminate\Support\Facades\Concurrency; ]); ``` -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. +With a process-based driver, each closure runs in a separate PHP process that boots the application. Use concurrency when independent database queries, HTTP client calls, or computations benefit enough to offset process and serialization overhead. The `sync` driver executes closures sequentially and is useful primarily during testing. -## Convention Over Configuration +## Follow Framework Conventions -Follow Laravel conventions. Don't override defaults unnecessarily. +Follow Laravel conventions unless the domain or an existing schema requires an override. + +Customized schema: -Incorrect: ```php class Customer extends Model { @@ -190,7 +206,8 @@ class Customer extends Model } ``` -Correct: +Conventional schema: + ```php class Customer extends Model { @@ -199,4 +216,4 @@ class Customer extends Model return $this->belongsToMany(Role::class); } } -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/blade-views.md b/_api_app/.claude/skills/laravel-best-practices/rules/blade-views.md index c6f8aaf1..4ea2c6c3 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/blade-views.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/blade-views.md @@ -1,8 +1,8 @@ -# Blade & Views Best Practices +# Blade and View Best Practices ## Use `$attributes->merge()` in Component Templates -Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. +Use the component attribute bag so callers can add attributes. `merge()` combines default attributes with caller-provided values; class values receive special merging behavior. ```blade
merge(['class' => 'alert alert-'.$type]) }}> @@ -12,25 +12,25 @@ Hardcoding classes prevents consumers from adding their own. `merge()` combines ## 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. +If a component renders repeatedly, `@push` adds its script on every render. Use a consistently named `@pushOnce` block to add that content once per rendered response. -## Prefer Blade Components Over `@include` +## Prefer Components for Explicit Interfaces -`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. +Use a Blade component when a reusable interface benefits from explicit props, an attribute bag, or slots. An include remains suitable for a small partial that intentionally uses the current view data; pass an explicit data array when implicit variable sharing would obscure its dependencies. -## Use View Composers for Shared View Data +## Share Compatible View Data with a View Composer -If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. +Use a view composer to centralize data needed whenever one or more named Blade views are rendered. Keep the composer compatible with every view it targets, and avoid broad wildcards when views require different data shapes. A view composer runs when Laravel renders the matching view; it does not supply data to JSON, streamed, or other non-view responses. -## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) +## Return Blade Fragments for Partial Rendering -A single view can return either the full page or just a fragment, keeping routing clean. +A route can return either a full view or a named fragment for clients such as htmx or Turbo. ```php return view('dashboard', compact('users')) ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); ``` -## Use `@aware` for Deeply Nested Component Props +## Share Parent Component Props with `@aware` -Avoids re-passing parent props through every level of nested components. \ No newline at end of file +Use `@aware` when a nested component needs a prop explicitly passed to an ancestor component. It does not expose an ancestor's default prop value unless that value was passed through the attribute bag. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/caching.md b/_api_app/.claude/skills/laravel-best-practices/rules/caching.md index eb3ef3e6..cd2ffd60 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/caching.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/caching.md @@ -1,10 +1,13 @@ # Caching Best Practices -## Use `Cache::remember()` Instead of Manual Get/Put +## Use `Cache::remember()` for Cache-Aside Reads -Atomic pattern prevents race conditions and removes boilerplate. +`Cache::remember()` implements a cache-aside read without a separate truthiness check. It does not prevent concurrent requests from computing the same missing value; use an atomic lock when duplicate computation must be prevented. + +The manual version below incorrectly treats valid falsy values, such as `false` or `0`, as cache misses. Incorrect: + ```php $val = Cache::get('stats'); if (! $val) { @@ -14,27 +17,42 @@ if (! $val) { ``` Correct: + ```php $val = Cache::remember('stats', 60, fn () => $this->computeStats()); ``` -## Use `Cache::flexible()` for Stale-While-Revalidate +## Consider `Cache::flexible()` for Stale-While-Revalidate + +For frequently read keys, `Cache::flexible()` can serve stale data during a defined stale period and register a deferred refresh. During an HTTP request, that refresh normally runs after the response; it is not a durable background job. Once the stale period has elapsed, the request recomputes the value synchronously. + +Synchronous expiration: + +```php +Cache::remember('users', 300, fn () => User::all()); +``` + +Stale-while-revalidate tradeoff: -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. +```php +Cache::flexible('users', [300, 600], fn () => User::all()); +``` -Incorrect: `Cache::remember('users', 300, fn () => User::all());` +This value is fresh for five minutes and may be served stale until ten minutes after it was cached. -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 an Execution -## Use `Cache::memo()` to Avoid Redundant Hits Within a Request +If the same cache key is read repeatedly during one request or job, `memo()` decorates a cache store and retains resolved values in memory for that execution. -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. +```php +$settings = Cache::memo()->get('settings'); +``` -`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. +Repeated reads through the same memoized store avoid additional store lookups. Writes through the memoized store update or invalidate its in-memory values as appropriate. ## 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`. +Tags group related entries for invalidation without tracking each key. Cache tags are not supported by the `file`, `dynamodb`, or `database` drivers; confirm support before choosing a store. ```php Cache::tags(['user-1'])->flush(); @@ -42,15 +60,27 @@ 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. +`add()` atomically writes a value only when the key does not already exist. + +Incorrect: + +```php +if (! Cache::has('lock')) { + Cache::put('lock', true, 10); +} +``` + +Correct: -Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` +```php +Cache::add('lock', true, 10); +``` -Correct: `Cache::add('lock', true, 10);` +Use `Cache::lock()` rather than an ordinary cache key when lock ownership and safe release are required. -## Use `once()` for Per-Request Memoization +## Use `once()` for In-Process 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. +`once()` memoizes a callback's return value for the current request or job. Calls made from an object instance are scoped to that instance. Unlike `Cache::memo()`, `once()` does not read from an external cache store. ```php public function roles(): Collection @@ -59,12 +89,12 @@ public function roles(): Collection } ``` -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. +Repeated calls return the memoized result without rerunning the callback. Use `once()` for repeated computation within one execution. Use `Cache::memo()` to memoize access to an underlying store that can also persist values across executions. ## Configure Failover Cache Stores in Production -If Redis goes down, the app falls back to a secondary store automatically. +The failover driver tries each configured store in order when a store operation throws an exception. It does not consult later stores for an ordinary cache miss, and data is not replicated between stores. ```php 'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/collections.md b/_api_app/.claude/skills/laravel-best-practices/rules/collections.md index 14f683d3..211fec11 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/collections.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/collections.md @@ -2,43 +2,71 @@ ## Use Higher-Order Messages for Simple Operations -Incorrect: +Explicit closure: + ```php $users->each(function (User $user) { $user->markAsVip(); }); ``` -Correct: `$users->each->markAsVip();` +Concise equivalent: + +```php +$users->each->markAsVip(); +``` -Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. +Higher-order messages are available for supported collection methods such as `each`, `map`, `filter`, and `sum`. Use an explicit closure when arguments or nontrivial logic would be clearer. -## Choose `cursor()` vs. `lazy()` Correctly +## Choose Between `cursor()` and `lazy()` -- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). -- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. +`cursor()` executes one query and hydrates models individually, but it cannot eager load relationships. The database driver's result buffering can still consume substantial memory for very large results. Use it for low-memory, attribute-only iteration when one long-running query is acceptable. -Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. +`lazy()` executes multiple chunked queries and returns a flat `LazyCollection`. It supports eager loading relationships for each chunk and avoids holding one database cursor open for the entire iteration. -Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. +With relationships: + +```php +User::with('roles')->lazy()->each(function (User $user) { + // The roles for this chunk have been eager loaded. +}); +``` + +Without relationships: + +```php +User::cursor()->each(function (User $user) { + // Process model attributes. +}); +``` ## 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. +`lazy()` uses offset pagination, so updates to columns that affect the query can shift rows and cause records to be skipped or processed twice. `lazyById()` paginates by a monotonic key and is safer when updating other columns during iteration. Do not change the pagination key itself while iterating. ## Use `toQuery()` for Bulk Operations on Collections -Avoids manual `whereIn` construction. +Use `toQuery()` to build a query from the models in an Eloquent collection instead of manually constructing a `whereIn` clause. + +Manual query: + +```php +User::whereIn('id', $users->modelKeys())->update(['active' => false]); +``` + +Collection query: -Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` +```php +$users->toQuery()->update(['active' => false]); +``` -Correct: `$users->toQuery()->update([...]);` +`toQuery()` requires a non-empty Eloquent collection whose models are of the same type. Like other bulk Eloquent updates, it does not dispatch per-model update events, so use it only when those events are not required. ## Use `#[CollectedBy]` for Custom Collection Classes -More declarative than overriding `newCollection()`. +The `#[CollectedBy]` attribute declares the custom collection class without requiring a `newCollection()` override. ```php #[CollectedBy(UserCollection::class)] class User extends Model {} -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/config.md b/_api_app/.claude/skills/laravel-best-practices/rules/config.md index 8fd8f536..82691c42 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/config.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/config.md @@ -1,73 +1,85 @@ # Configuration Best Practices -## `env()` Only in Config Files +## Read Environment Variables in Configuration Files -Direct `env()` calls return `null` when config is cached. +Call `env()` only from configuration files. After configuration is cached, Laravel does not load the application's `.env` file, so application code should read configuration values through `config()`. Incorrect: + ```php $key = env('API_KEY'); ``` Correct: + ```php // config/services.php -'key' => env('API_KEY'), +return [ + 'key' => env('API_KEY'), +]; // Application code $key = config('services.key'); ``` -## Use Encrypted Env or External Secrets +## Protect Production Secrets -Never store production secrets in plain `.env` files in version control. +Do not commit plaintext production secrets. Laravel can encrypt an environment file so its encrypted form can be stored safely, while deployment platforms can supply secrets through their native secret stores. Incorrect: -```bash -# .env committed to repo or shared in Slack - -STRIPE_SECRET=sk_live_abc123 -AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +```bash +# A plaintext .env file committed to the repository +STRIPE_SECRET= +AWS_SECRET_ACCESS_KEY= ``` -Correct: +Encrypted environment file: + ```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. +For hosted deployments, consider the platform's native secret store, such as AWS Secrets Manager or Vault, and inject secrets 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 +## Name Repeated Domain Values -Use class constants instead of hardcoded magic strings for model states, types, and statuses. +Use an enum or class constant when a domain value is repeated or represents a constrained set. A one-off string literal does not always need a named constant. ```php -// Incorrect +// Repeated literal return $this->type === 'normal'; -// Correct +// Named domain value 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. +If the application supports localization, put user-facing strings in language files and retrieve them with `__()`. Simple literals are reasonable for applications that intentionally do not support multiple languages. ```php -// Only when lang files already exist in the project +// In a localized application return back()->with('message', __('app.article_added')); -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/db-performance.md b/_api_app/.claude/skills/laravel-best-practices/rules/db-performance.md index 8fb71937..3f339770 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/db-performance.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/db-performance.md @@ -1,10 +1,11 @@ # Database Performance Best Practices -## Always Eager Load Relationships +## Eager Load Relationships Before Iterating -Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. +When a relationship will be accessed for many models, eager load it with `with()` to avoid running one initial query plus one relationship query per model, commonly called an N+1 query pattern. Lazy loading is reasonable when the relationship may not be needed or only one model is involved. + +Lazy-loaded version: -Incorrect (N+1 — executes 1 + N queries): ```php $posts = Post::all(); foreach ($posts as $post) { @@ -12,7 +13,8 @@ foreach ($posts as $post) { } ``` -Correct (2 queries total): +Eager-loaded version: + ```php $posts = Post::with('author')->get(); foreach ($posts as $post) { @@ -20,7 +22,7 @@ foreach ($posts as $post) { } ``` -Constrain eager loads to select only needed columns (always include the foreign key): +Constrain eager loads when large columns are unnecessary. Include the related model's primary key and every column Eloquent needs to match the relationship. In this example, `users.id` and `posts.user_id` match posts to users, while selecting `posts.id` preserves each related model's primary key: ```php $users = User::with(['posts' => function ($query) { @@ -42,31 +44,34 @@ public function boot(): void } ``` -Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. +By default, accessing an unloaded relationship then throws a `LazyLoadingViolationException`. Applications can customize violation handling with `handleLazyLoadingViolationUsing()`. ## Select Only Needed Columns -Avoid `SELECT *` — especially when tables have large text or JSON columns. +Select only the columns the operation needs when omitting large text, binary, or JSON columns provides a meaningful benefit. + +All columns: -Incorrect: ```php $posts = Post::with('author')->get(); ``` -Correct: +Selected columns: + ```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. +When limiting selected columns, retain every key Eloquent needs for matching. A `belongsTo` relationship needs its foreign key on the parent query and the owner's key on the related query. A `hasMany` relationship needs the parent's local key and the related model's foreign key. -## Chunk Large Datasets +## Process Large Data Sets Incrementally -Never load thousands of records at once. Use chunking for batch processing. +Use chunking or lazy iteration when loading an entire result set would exceed the application's practical memory budget. + +Loads the complete result set: -Incorrect: ```php $users = User::all(); foreach ($users as $user) { @@ -74,7 +79,8 @@ foreach ($users as $user) { } ``` -Correct: +Processes bounded chunks: + ```php User::where('subscribed', true)->chunk(200, function ($users) { foreach ($users as $user) { @@ -83,7 +89,7 @@ User::where('subscribed', true)->chunk(200, function ($users) { }); ``` -Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: +Use `chunkById()` when updates can change which rows match the query. Standard `chunk()` uses offset pagination, whose result positions can shift as rows change: ```php User::where('active', false)->chunkById(200, function ($users) { @@ -91,11 +97,14 @@ User::where('active', false)->chunkById(200, function ($users) { }); ``` -## Add Database Indexes +For read-only, attribute-only iteration, `cursor()` hydrates models individually from one query, although some database drivers still buffer raw results. Use `lazy()` when relationships must be eager loaded in chunks, and use `lazyById()` or `chunkById()` when updates can affect query membership. See the collection rules for detailed tradeoffs. + +## Add Indexes for Measured Query Patterns -Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. +Design indexes around frequent, performance-sensitive query patterns. A column's presence in `WHERE`, `ORDER BY`, `JOIN`, or `GROUP BY` does not by itself justify an index; selectivity, write cost, existing indexes, and the database query plan all matter. + +Schema without an application-specific query index: -Incorrect: ```php Schema::create('orders', function (Blueprint $table) { $table->id(); @@ -105,24 +114,26 @@ Schema::create('orders', function (Blueprint $table) { }); ``` -Correct: +Schema optimized for `WHERE status = ? ORDER BY created_at`: + ```php Schema::create('orders', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id')->index()->constrained(); - $table->string('status')->index(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); $table->timestamps(); $table->index(['status', 'created_at']); }); ``` -Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). +Confirm composite index column order and effectiveness with production-like data and the database's query-plan tools. Also check whether the database already created an index to support a foreign key before adding another one. + +## Count Relationships Without Loading Them -## Use `withCount()` for Counting Relations +Use `withCount()` when only relationship counts are needed; loading and hydrating every related model wastes memory. -Never load entire collections just to count them. +Loads related models: -Incorrect: ```php $posts = Post::all(); foreach ($posts as $post) { @@ -130,7 +141,8 @@ foreach ($posts as $post) { } ``` -Correct: +Selects relationship counts: + ```php $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { @@ -149,39 +161,24 @@ $posts = Post::withCount([ ])->get(); ``` -## Use `cursor()` for Memory-Efficient Iteration +## Keep Queries Out of Blade Templates -For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. +Prepare data before rendering a Blade template, such as in a controller, query service, or view composer. This keeps query behavior visible and testable. -Incorrect: -```php -$users = User::where('active', true)->get(); -``` - -Correct: -```php -foreach (User::where('active', true)->cursor() as $user) { - ProcessUser::dispatch($user->id); -} -``` +Query in the template: -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: +Data prepared before rendering: + ```php // Controller $users = User::with('profile')->get(); + return view('users.index', compact('users')); ``` @@ -189,4 +186,4 @@ return view('users.index', compact('users')); @foreach ($users as $user) {{ $user->profile->name }} @endforeach -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/eloquent.md b/_api_app/.claude/skills/laravel-best-practices/rules/eloquent.md index 09cd66a0..5a89510d 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/eloquent.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/eloquent.md @@ -1,8 +1,8 @@ # Eloquent Best Practices -## Use Correct Relationship Types +## Define Precise Relationship Types -Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. +Define the relationship that matches the database association, and declare its concrete return type. ```php public function comments(): HasMany @@ -20,7 +20,8 @@ public function author(): BelongsTo Extract reusable query constraints into local scopes to avoid duplication. -Incorrect: +Duplicated constraints: + ```php $active = User::where('verified', true)->whereNotNull('activated_at')->get(); $articles = Article::whereHas('user', function ($q) { @@ -28,9 +29,11 @@ $articles = Article::whereHas('user', function ($q) { })->get(); ``` -Correct: +Reusable local scope: + ```php -public function scopeActive(Builder $query): Builder +#[Scope] +protected function active(Builder $query): Builder { return $query->where('verified', true)->whereNotNull('activated_at'); } @@ -44,7 +47,8 @@ $articles = Article::whereHas('user', fn ($q) => $q->active())->get(); 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): +Global scope tradeoff: + ```php class PublishedScope implements Scope { @@ -53,12 +57,15 @@ class PublishedScope implements Scope $builder->where('published', true); } } -// Now admin panels, reports, and background jobs all silently skip drafts + +// Admin panels, reports, and jobs now omit drafts unless the scope is removed. ``` -Correct (local scope you opt into): +Explicit local scope: + ```php -public function scopePublished(Builder $query): Builder +#[Scope] +protected function published(Builder $query): Builder { return $query->where('published', true); } @@ -82,16 +89,18 @@ protected function casts(): array } ``` -## Cast Date Columns Properly +## Cast Date and Time Attributes + +Cast a date or timestamp attribute when application code should treat it as a Carbon instance. Eloquent already casts the conventional `created_at` and `updated_at` timestamps. -Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. +Manual parsing in the template: -Incorrect: ```blade -{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +{{ Carbon::parse($order->ordered_at)->toDateString() }} ``` -Correct: +Model cast: + ```php protected function casts(): array { @@ -108,24 +117,27 @@ protected function casts(): array ## Use `whereBelongsTo()` for Relationship Queries -Cleaner than manually specifying foreign keys. +`whereBelongsTo()` expresses the relationship constraint without manually specifying its foreign key. + +Foreign key constraint: -Incorrect: ```php Post::where('user_id', $user->id)->get(); ``` -Correct: +Relationship-aware constraint: + ```php Post::whereBelongsTo($user)->get(); Post::whereBelongsTo($user, 'author')->get(); ``` -## Avoid Hardcoded Table Names in Queries +## Keep Application Queries Model-Aware -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). +Prefer Eloquent models and relationships for model-backed application queries. They preserve casts, scopes, and model table configuration. The query builder and raw SQL legitimately require table names, so use them when their lower-level behavior is intentional. + +Lower-level alternatives: -Incorrect: ```php DB::table('users')->where('active', true)->get(); @@ -134,15 +146,13 @@ $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(); +Model-aware queries: -// Even better — use Eloquent or the query builder instead of raw SQL +```php 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. +When a query builder operation should follow a model's configured table name, use `(new User)->getTable()`. For complex joins or raw SQL, explicit table names may be clearer; keep those references covered by tests when schema changes are possible. -**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. \ No newline at end of file +In migrations, use explicit table names rather than application models. Migrations are historical snapshots, while models and their scopes can change after a migration is deployed. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/error-handling.md b/_api_app/.claude/skills/laravel-best-practices/rules/error-handling.md index bb8e7a38..afcdf4ac 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/error-handling.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/error-handling.md @@ -1,15 +1,18 @@ # Error Handling Best Practices -## Exception Reporting and Rendering +## Choose Where to Report and Render Exceptions -There are two valid approaches — choose one and apply it consistently across the project. +Laravel supports exception-specific methods and centralized handler callbacks. Follow the pattern already established by the project. -**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: +Exception methods keep behavior beside the exception definition: ```php class InvalidOrderException extends Exception { - public function report(): void { /* custom reporting */ } + public function report(): void + { + // Send the exception to a custom reporter. + } public function render(Request $request): Response { @@ -18,38 +21,40 @@ class InvalidOrderException extends Exception } ``` -**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: +Centralized callbacks in `bootstrap/app.php` keep the application's exception policy together: ```php ->withExceptions(function (Exceptions $exceptions) { - $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->report(function (InvalidOrderException $e) { + // Send the exception to a custom reporter. + }); $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. +An exception's `report()` method suppresses Laravel's default reporting unless it returns `false`. A report callback allows default reporting unless it returns `false` or is chained with `stop()`. Use `ShouldntReport` or `dontReport()` when the handler should not report an exception at all. By contrast, returning `false` from a `render()` method or render callback defers to Laravel's default rendering. -## Use `ShouldntReport` for Exceptions That Should Never Log +## Mark Exceptions the Handler Should Not Report -More discoverable than listing classes in `dontReport()`. +Implementing `ShouldntReport` prevents Laravel's exception handler from reporting that exception type and keeps the policy visible on the class. It does not prevent application code from logging the exception explicitly. ```php class PodcastProcessingException extends Exception implements ShouldntReport {} ``` -## Throttle High-Volume Exceptions +## Throttle High-Volume Exception Reports -A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. +A failing integration can flood logs or error tracking. Configure `throttle()` with a `Lottery` or `Limit` result to sample or rate-limit matching exception reports. Choose keys deliberately when separate exception classes, tenants, or integrations need independent limits. -## Enable `dontReportDuplicates()` +## Prevent Duplicate Reports of One Exception Instance -Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. +Enable `dontReportDuplicates()` when the same exception object may pass through multiple `report($exception)` calls. It deduplicates by object identity, not by exception class or message. -## Force JSON Error Rendering for API Routes +## Define JSON Rendering for API Routes -Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. +Laravel normally uses request content negotiation to decide whether to render an exception as JSON. If the application's API contract requires JSON regardless of the `Accept` header, define that policy explicitly for the relevant routes. ```php $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { @@ -59,7 +64,7 @@ $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { ## 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. +Attach structured data to an exception through `context()`. Laravel merges that data into the exception's log context when the handler reports it. ```php class InvalidOrderException extends Exception @@ -69,4 +74,4 @@ class InvalidOrderException extends Exception return ['order_id' => $this->orderId]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/events-notifications.md b/_api_app/.claude/skills/laravel-best-practices/rules/events-notifications.md index bc43f199..a8cce31a 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/events-notifications.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/events-notifications.md @@ -1,24 +1,24 @@ -# Events & Notifications Best Practices +# Events and Notifications Best Practices ## Rely on Event Discovery -Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. +Laravel discovers listeners in the configured listener directories by inspecting type-hinted event arguments on `handle()` or `__invoke()` methods. Register listeners manually only when discovery is disabled, the listener is outside those directories, or explicit registration is clearer. -## Run `event:cache` in Production Deploy +## Cache Event Discovery During Production Deployment -Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. +Cache discovered listeners during production deployment with `php artisan optimize` or `php artisan event:cache`. Rebuild the cache whenever listener definitions change. ## Use `ShouldDispatchAfterCommit` Inside Transactions -Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. +When an event is dispatched inside a database transaction, `ShouldDispatchAfterCommit` delays dispatch until all open database transactions commit. If a transaction rolls back, Laravel discards the event. This affects synchronous and queued listeners; it is not limited to queue timing. ```php class OrderShipped implements ShouldDispatchAfterCommit {} ``` -## Always Queue Notifications +## Queue Slow Notifications -Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. +Queue notifications that call external services, such as email, text messaging, or Slack, when they do not need to complete before the response. Keep a notification synchronous when immediate completion or failure feedback is part of the operation. ```php class InvoicePaid extends Notification implements ShouldQueue @@ -27,13 +27,17 @@ class InvoicePaid extends Notification implements ShouldQueue } ``` -## Use `afterCommit()` on Notifications in Transactions +## Dispatch Queued Notifications After Commit -Same race condition as events — the queued notification job may run before the transaction commits. +A queued notification sent inside a database transaction can run before the transaction commits. Call `afterCommit()` on the queued notification, or enable the queue connection's `after_commit` option, when its delivery depends on committed data. This setting has no scheduling effect on a synchronous notification. + +```php +$user->notify((new InvoicePaid($invoice))->afterCommit()); +``` ## Route Notification Channels to Dedicated Queues -Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. +Different notification channels can have different latency and priority requirements. Implement `viaQueues()` when channels should use separate queues. ## Use On-Demand Notifications for Non-User Recipients @@ -45,4 +49,4 @@ 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. \ No newline at end of file +Implement `HasLocalePreference::preferredLocale()` on a notifiable model when notifications and mailables should use the recipient's locale. Laravel also preserves that locale for queued delivery. An explicit `locale()` call can still override the preference for an individual notification. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/http-client.md b/_api_app/.claude/skills/laravel-best-practices/rules/http-client.md index 0a7876ed..cc8fdd6a 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/http-client.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/http-client.md @@ -1,86 +1,100 @@ # HTTP Client Best Practices -## Always Set Explicit Timeouts +## Set Explicit Timeouts -The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. +Laravel's HTTP client has a 30-second response timeout by default. Choose response and connection timeouts that fit the service and the calling request or job. Remember that retries can multiply the total elapsed time. + +Less resilient: -Incorrect: ```php $response = Http::get('https://api.example.com/users'); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) - ->connectTimeout(3) +$response = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users'); ``` -For service-specific clients, define timeouts in a macro: +Define shared settings in a macro or a dedicated client: ```php Http::macro('github', function () { return Http::baseUrl('https://api.github.com') - ->timeout(10) ->connectTimeout(3) + ->timeout(10) ->withToken(config('services.github.token')); }); $response = Http::github()->get('/repos/laravel/framework'); ``` -## Use Retry with Backoff for External APIs +## Retry Only Safe Operations -External APIs have transient failures. Use `retry()` with increasing delays. +Retry transient connection failures, rate-limit responses, and server errors with an appropriate delay. Retry idempotent requests such as `GET` when the operation can safely run more than once. Retry a state-changing request only when the remote API supports an idempotency key or provides equivalent duplicate protection. -Incorrect: -```php -$response = Http::post('https://api.stripe.com/v1/charges', $data); +Unsafe without an idempotency guarantee: -if ($response->failed()) { - throw new PaymentFailedException('Charge failed'); -} +```php +$response = Http::retry([100, 500, 1000]) + ->post('https://api.example.com/v1/charges', $data); ``` -Correct: +Safe for an idempotent request: + ```php -$response = Http::retry([100, 500, 1000]) +$response = Http::connectTimeout(3) ->timeout(10) - ->post('https://api.stripe.com/v1/charges', $data); + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->get('https://api.example.com/data'); ``` -Only retry on specific errors: +For a supported state-changing API, send a stable idempotency key for every attempt: ```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'); +$response = Http::withHeaders(['Idempotency-Key' => $paymentAttempt->uuid]) + ->connectTimeout(3) + ->timeout(10) + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->post('https://api.example.com/v1/charges', $data); ``` ## Handle Errors Explicitly -The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. +The HTTP client returns responses for `4xx` and `5xx` status codes instead of throwing by default. Inspect the expected statuses or call `throw()` before consuming a success payload. + +Unsafe when a success payload is expected: -Incorrect: ```php -$response = Http::get('https://api.example.com/users/1'); -$user = $response->json(); // Could be an error body +$user = Http::get('https://api.example.com/users/1')->json(); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) +$user = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users/1') - ->throw(); - -$user = $response->json(); + ->throw() + ->json(); ``` -For graceful degradation: +Handle expected alternatives explicitly when graceful degradation is required: ```php -$response = Http::get('https://api.example.com/users/1'); +$response = Http::connectTimeout(3) + ->timeout(5) + ->get('https://api.example.com/users/1'); if ($response->successful()) { return $response->json(); @@ -93,46 +107,30 @@ if ($response->notFound()) { $response->throw(); ``` -## Use Request Pooling for Concurrent Requests - -When making multiple independent API calls, use `Http::pool()` instead of sequential calls. +## Pool Independent Requests -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(); -``` +Use `Http::pool()` when several independent requests can run concurrently. Pooling changes execution time, not error handling; inspect or throw for each response as needed. -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'), + $pool->as('users')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/users'), + $pool->as('posts')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/posts'), ]); -$users = $responses['users']->json(); -$posts = $responses['posts']->json(); +$users = $responses['users']->throw()->json(); +$posts = $responses['posts']->throw()->json(); ``` -## Fake HTTP Calls in Tests - -Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. +## Fake HTTP Requests in Tests -Incorrect: -```php -it('syncs user from API', function () { - $service = new UserSyncService; - $service->sync(1); // Hits the real API -}); -``` +Use `Http::fake()` for external integrations, and use `Http::preventStrayRequests()` when an unexpected real request should fail the test. Also test timeouts, connection failures, and error responses that the application handles. -Correct: ```php -it('syncs user from API', function () { +it('syncs a user from the API', function () { Http::preventStrayRequests(); Http::fake([ @@ -142,19 +140,18 @@ it('syncs user from API', function () { ]), ]); - $service = new UserSyncService; - $service->sync(1); + (new UserSyncService)->sync(1); - Http::assertSent(function (Request $request) { - return $request->url() === 'https://api.example.com/users/1'; - }); + Http::assertSent(fn (Request $request) => + $request->url() === 'https://api.example.com/users/1' + ); }); ``` -Test failure scenarios too: +For example, fake a connection failure when testing the integration's failure path: ```php Http::fake([ 'api.example.com/*' => Http::failedConnection(), ]); -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/mail.md b/_api_app/.claude/skills/laravel-best-practices/rules/mail.md index c7f67966..f6310437 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/mail.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/mail.md @@ -1,27 +1,54 @@ # Mail Best Practices -## Implement `ShouldQueue` on the Mailable Class +## Queue Slow Mail Delivery -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. +Implement `ShouldQueue` on a mailable when delivery should normally happen in the background. Laravel queues that mailable even when the call site uses `Mail::send()`. -## Use `afterCommit()` on Mailables Inside Transactions +```php +class OrderShipped extends Mailable implements ShouldQueue +{ + use Queueable, SerializesModels; +} +``` -A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. +Keep mail synchronous when the caller must know immediately whether delivery was accepted, or when no queue worker is available. -## Use `assertQueued()` Not `assertSent()` for Queued Mailables +## Dispatch Queued Mail After Commit -`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence. +A queued mailable dispatched during a database transaction can be processed before the transaction commits. Call `afterCommit()` on the mailable, or enable the queue connection's `after_commit` option, when the mail depends on committed records. -Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. +```php +Mail::to($user)->send( + (new OrderShipped($order))->afterCommit() +); +``` -Correct: `Mail::assertQueued(OrderShipped::class);` +If the transaction rolls back, an after-commit mailable is not dispatched. This setting affects queued mail only; it does not defer synchronous delivery. -## Use Markdown Mailables for Transactional Emails +## Assert the Delivery Mode -Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. +Use `Mail::assertQueued()` for queued mailables and `Mail::assertSent()` for synchronously sent mailables. -## Separate Content Tests from Sending Tests +Incorrect for a mailable that implements `ShouldQueue`: -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. \ No newline at end of file +```php +Mail::assertSent(OrderShipped::class); +``` + +Correct: + +```php +Mail::assertQueued(OrderShipped::class); +``` + +## Use Markdown Mailables When They Fit + +Markdown mailables render HTML and plain-text versions from Laravel's mail components and support publishable themes. They are useful for conventional transactional messages, but a custom HTML and text pair may be more appropriate for a specialized design. + +```bash +php artisan make:mail OrderShipped --markdown=mail.orders.shipped +``` + +## Separate Content and Delivery Tests + +Test rendered content by instantiating the mailable and using assertions such as `assertSeeInHtml()` and `assertSeeInText()`. Test delivery separately with `Mail::fake()` and `assertSent()` or `assertQueued()` so failures identify the affected behavior. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/migrations.md b/_api_app/.claude/skills/laravel-best-practices/rules/migrations.md index de25aa39..3345402e 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/migrations.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/migrations.md @@ -2,76 +2,51 @@ ## Generate Migrations with Artisan -Always use `php artisan make:migration` for consistent naming and timestamps. +Use `php artisan make:migration` to generate the timestamped filename and migration structure. -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 +## Define Foreign-Key Constraints Deliberately -Automatic naming and referential integrity. +Use `constrained()` when its naming conventions and default actions match the relationship. Specify the table or delete behavior when they do not. ```php $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - -// Non-standard names $table->foreignId('author_id')->constrained('users'); ``` -## Never Modify Deployed Migrations +Do not add a duplicate single-column index without checking the database driver's treatment of foreign-key indexes and the indexes already created by the migration. -Once a migration has run in production, treat it as immutable. Create a new migration to change the table. +## Treat Deployed Migrations as Immutable -Incorrect (editing a deployed migration): -```php -// 2024_01_01_create_posts_table.php — already in production -$table->string('slug')->unique(); // ← added after deployment -``` +After a migration has run in a shared or production environment, create a new migration for subsequent changes. Editing the old file makes fresh installations differ from upgraded installations. -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'); -}); -``` +For a local migration that has not been shared or deployed, editing and rerunning it may be simpler. -## Add Indexes in the Migration +## Design Indexes for Real Queries -Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. +Add indexes based on query patterns, selectivity, write cost, and the database's ability to use composite indexes. A column appearing in `WHERE`, `ORDER BY`, or `JOIN` does not automatically need its own index. -Incorrect: -```php -Schema::create('orders', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->constrained(); - $table->string('status'); - $table->timestamps(); -}); -``` +Declare each selected index in the schema migration that creates or changes the relevant table. Confirm important indexes with representative data and the database's query plan, and avoid redundant indexes whose leading columns duplicate an existing index without serving a distinct query. See the database performance and advanced query rules for index selection and column-order guidance. + +## Stage Changes That Affect Existing Rows + +Adding a required or unique column to a populated table often needs multiple deployment-safe steps. Add a nullable column, deploy code that can handle both states, backfill existing rows in bounded chunks, then add the required constraint or index after the data is valid. + +Do not assume this migration is safe on a populated table: -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(); -}); +$table->string('slug')->unique(); ``` -## Mirror Defaults in Model `$attributes` +Large backfills are usually better implemented as an observable, restartable command or job than inside a schema migration. Small deterministic data changes may be reasonable in a migration when their locking, transaction, and deployment behavior is understood. + +## Mirror Defaults Only When Unsaved Models Need Them -When a column has a database default, mirror it in the model so new instances have correct values before saving. +A database default is applied when a row is inserted, not when a model is instantiated. Mirror the value in the model's `$attributes` only when application code must observe that default before persistence, and keep both definitions synchronized. ```php // Migration @@ -83,39 +58,10 @@ protected $attributes = [ ]; ``` -## 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'); - }); -} -``` +## Make Rollbacks Honest -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. +Implement `down()` when the change can be safely reversed. A rollback that drops populated columns or cannot restore transformed data is destructive even if it is syntactically reversible; document that limitation and prefer a forward-fix migration in production. ## 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']); -``` \ No newline at end of file +Keep each migration small enough to reason about, deploy, and reverse. Separate long-running backfills from schema changes when doing so reduces locks and supports phased deployment, but do not split related operations merely to enforce a blanket separation between data definition and data manipulation. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/queue-jobs.md b/_api_app/.claude/skills/laravel-best-practices/rules/queue-jobs.md index 2f174dfc..011828a6 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/queue-jobs.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/queue-jobs.md @@ -1,82 +1,82 @@ -# Queue & Job Best Practices +# Queue and Job Best Practices -## Set `retry_after` Greater Than `timeout` +## Keep Reservation Time Longer Than Execution Time -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. +For queue drivers that use Laravel's `retry_after` setting, configure it to exceed the longest worker or job timeout by a safety margin. When a reservation expires, another worker can reserve the same job while the first process is still running. Keep the worker's `--timeout` several seconds shorter than `retry_after`. -Incorrect (`retry_after` ≤ `timeout`): ```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +// Job +public $timeout = 120; -// config/queue.php — retry_after: 90 ← job retried while still running! +// config/queue.php for the connection +'retry_after' => 150, ``` -Correct (`retry_after` > `timeout`): -```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +Amazon Simple Queue Service uses its visibility timeout instead of Laravel's `retry_after`; configure that timeout at the queue level. Because workers can also stop after side effects but before acknowledging a job, make important jobs idempotent even with correct timeout settings. -// config/queue.php — retry_after: 180 ← safely longer than any job timeout -``` - -## Use Exponential Backoff +## Back Off Transient Failures -Use progressively longer delays between retries to avoid hammering failing services. +Use progressively longer delays when a dependency needs time to recover. Do not retry permanent validation or business-rule failures. -Incorrect (fixed retry interval): ```php class SyncWithStripe implements ShouldQueue { - public $tries = 3; - // Default: retries immediately, overwhelming the API -} -``` + public $tries = 4; -Correct (exponential backoff): -```php -class SyncWithStripe implements ShouldQueue -{ - public $tries = 3; public $backoff = [1, 5, 10]; } ``` -## Implement `ShouldBeUnique` +Rate-limiting and exception-throttling middleware can release jobs back to the queue. Released attempts may still count toward the maximum attempt limit, so configure `$tries` or `retryUntil()` to allow the intended retry window. -Prevent duplicate job processing. +## Use Unique Jobs for Dispatch Deduplication + +Implement `ShouldBeUnique` when only one queued instance of a logical job should exist. Uniqueness uses a cache lock and is not a substitute for idempotent processing or a database constraint. ```php class GenerateInvoice implements ShouldQueue, ShouldBeUnique { + public $uniqueFor = 3600; + public function uniqueId(): string { - return $this->order->id; + return (string) $this->order->id; } +} +``` - public $uniqueFor = 3600; +All dispatching processes must use a shared cache that supports locks. Unique-job constraints do not apply to jobs within batches. + +Use `ShouldBeUniqueUntilProcessing` only when the lock should be released immediately before processing begins, allowing another instance to be dispatched while the first is running: + +```php +class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing +{ + // ... } ``` -## Always Implement `failed()` +## Handle Terminal Failure When Needed + +Implement `failed()` when the application must update state, alert an operator, or record domain-specific context after all attempts are exhausted. Logging every failure in each job may duplicate the queue system's failure reporting. -Handle errors explicitly — don't rely on silent failure. +Laravel invokes `failed()` on a new job instance, so mutations made to the job during `handle()` are not available there. ```php public function failed(?Throwable $exception): void { $this->podcast->update(['status' => 'failed']); - Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); + + Log::error('Podcast processing failed', [ + 'podcast_id' => $this->podcast->id, + 'exception' => $exception, + ]); } ``` -## Rate Limit External API Calls in Jobs +## Rate Limit External Calls -Use `RateLimited` middleware to throttle jobs calling third-party APIs. +Use queue middleware such as `RateLimited` when jobs share a third-party API quota. Define the named limiter and choose release delays and attempt limits together. ```php public function middleware(): array @@ -85,62 +85,33 @@ public function middleware(): array } ``` -## Batch Related Jobs +## Batch Jobs for Group Coordination -Use `Bus::batch()` when jobs should succeed or fail together. +Use `Bus::batch()` to monitor a group of jobs and run callbacks when the batch completes or encounters failures. A batch is not a database transaction: completed jobs are not rolled back when another job fails. By default, one failed job cancels the batch; call `allowFailures()` only when partial failure is acceptable. ```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(); + ->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) + ->catch(fn (Batch $batch, Throwable $exception) => Log::error('Import batch failed', [ + 'exception' => $exception, + ])) + ->dispatch(); ``` -## `retryUntil()` Needs `$tries = 0` +## Configure Time-Based Retry Limits Deliberately -When using time-based retry limits, set `$tries = 0` to avoid premature failure. +Use `retryUntil()` as the time-based alternative to a maximum attempt count. Laravel may attempt the job any number of times until this deadline, subject to other failure conditions such as maximum exceptions. The method takes precedence over attempt-based limits, so setting `$tries = 0` is not required. ```php -public $tries = 0; - -public function retryUntil(): \DateTimeInterface +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 Horizon for Redis Queue Operations -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, - ], - ], -], -``` \ No newline at end of file +Laravel Horizon provides monitoring, balancing, metrics, and supervisor configuration for Redis queues. It does not support non-Redis queue drivers. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/routing.md b/_api_app/.claude/skills/laravel-best-practices/rules/routing.md index e288375d..604a8d96 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/routing.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/routing.md @@ -1,98 +1,106 @@ -# Routing & Controllers Best Practices +# Routing and Controller Best Practices ## Use Implicit Route Model Binding -Let Laravel resolve models automatically from route parameters. +Let Laravel resolve models from route parameters when the default lookup and missing-model behavior fit the endpoint. + +Instead of manual lookup: -Incorrect: ```php -public function show(int $id) +public function show(int $id): View { $post = Post::findOrFail($id); + + return view('posts.show', ['post' => $post]); } ``` -Correct: +Use route model binding: + ```php -public function show(Post $post) +public function show(Post $post): View { return view('posts.show', ['post' => $post]); } ``` -## Use Scoped Bindings for Nested Resources +## Scope Nested Bindings -Enforce parent-child relationships automatically. +Use scoped bindings when a nested resource must belong to its parent. This constrains model resolution; it does not replace authorization. ```php Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { - // $post is automatically scoped to $user + // The resolved post belongs to the resolved user. })->scopeBindings(); ``` -## Use Resource Controllers +## Use Resource Routes for Resourceful Actions -Use `Route::resource()` or `apiResource()` for RESTful endpoints. +Use `Route::resource()` or `Route::apiResource()` when the endpoint follows Laravel's resource-controller actions. Define explicit routes when the behavior does not fit that vocabulary. ```php Route::resource('posts', PostController::class); -Route::apiResource('api/posts', Api\PostController::class); + +// Alternatively, for an API-only resource: +Route::apiResource('posts', ApiPostController::class); ``` -## Keep Controllers Thin +`apiResource()` omits the HTML-oriented `create` and `edit` routes. It does not itself add an `/api` prefix; that prefix comes from the application's API route configuration. + +## Organize Controllers Around Resources + +As a general default, organize each controller around one resource and use Laravel's standard resource actions: `index`, `show`, `create`, `store`, `edit`, `update`, and `destroy`. This keeps routes predictable and prevents controllers from accumulating unrelated behavior. -Aim for under 10 lines per method. Extract business logic to action or service classes. +When a controller needs a custom action such as `publish`, `approve`, or `archive`, first consider whether that behavior represents a separate resource. A focused resource controller gives the behavior its own authorization, validation, and middleware boundary. + +Custom action on the primary controller: -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); -} +Route::post('/podcasts/{podcast}/publish', [PodcastController::class, 'publish']); ``` -Correct: +The published podcast modeled as a resource: + ```php -public function store(StorePostRequest $request, CreatePostAction $create) -{ - $post = $create->execute($request->validated()); +Route::post('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'store']) + ->name('published-podcasts.store'); - return redirect()->route('posts.show', $post); -} +Route::delete('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'destroy']) + ->name('published-podcasts.destroy'); ``` -## 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 +class PublishedPodcastController extends Controller { - $validated = $request->validate([ - 'title' => ['required', 'max:255'], - 'body' => ['required'], - ]); + public function store(Podcast $podcast): RedirectResponse + { + $podcast->publish(); - Post::create($validated); + return back(); + } + + public function destroy(Podcast $podcast): RedirectResponse + { + $podcast->unpublish(); - return redirect()->route('posts.index'); + return back(); + } } ``` -Correct: +Treat a custom verb as a design signal, not proof that another controller is required. Use query parameters for simple filtering, and keep an explicit action route when modeling the operation as a resource would obscure the domain or conflict with established project conventions. + +## Keep Controllers Focused on HTTP Concerns + +Controllers should coordinate HTTP input, authorization, validation, an application operation, and the response. Extract substantial or reusable business logic, but do not introduce an action or service merely to satisfy an arbitrary line limit. + ```php -public function store(StorePostRequest $request): RedirectResponse +public function store(StorePostRequest $request, CreatePostAction $create): RedirectResponse { - Post::create($request->validated()); + $post = $create->handle($request->validated()); - return redirect()->route('posts.index'); + return redirect()->route('posts.show', $post); } -``` \ No newline at end of file +``` + +A form request can perform validation and authorization before the controller runs. Do not repeat its rules in the controller. Keep simple, endpoint-specific validation inline when extraction would not improve reuse or clarity; see the validation rules for detailed guidance. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/scheduling.md b/_api_app/.claude/skills/laravel-best-practices/rules/scheduling.md index dfaefa26..4c25134a 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/scheduling.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/scheduling.md @@ -1,32 +1,50 @@ # Task Scheduling Best Practices -## Use `withoutOverlapping()` on Variable-Duration Tasks +## Prevent Unwanted Overlap -Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. +Use `withoutOverlapping()` when a second run must not begin while the previous run holds the lock. This is appropriate for variable-duration tasks that are not safe to run concurrently. -## Use `onOneServer()` on Multi-Server Deployments +```php +Schedule::command('reports:generate') + ->everyFifteenMinutes() + ->withoutOverlapping(30); +``` -Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). +The optional value is the lock expiration time in minutes, not the task timeout. Choose it carefully: the default is 24 hours, stale locks can be cleared with `php artisan schedule:clear-cache`, and an expiration that is too short can permit overlap while the first task still runs. The task itself should still tolerate retries and partial execution where practical. -## Use `runInBackground()` for Concurrent Long Tasks +## Run a Task on One Server -By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. +Use `onOneServer()` when only one scheduler node should run an eligible task. Scheduler nodes must use the same default cache store, and that store must support atomic locks. Supported stores include `database`, `memcached`, `dynamodb`, and `redis`. -## Use `environments()` to Restrict Tasks +```php +Schedule::command('billing:charge')->daily()->onOneServer(); +``` -Prevent accidental execution of production-only tasks (billing, reporting) on staging. +Name scheduled closures before applying `onOneServer()`, especially when scheduling the same closure with different parameters, so each task has a distinct lock identity. + +## Run Eligible Commands in the Background + +Tasks due at the same time run sequentially by default. Use `runInBackground()` when an independent, long-running scheduled command should not delay later tasks. ```php -Schedule::command('billing:charge')->monthly()->environments(['production']); +Schedule::command('analytics:process')->hourly()->runInBackground(); ``` -## Use `takeUntilTimeout()` for Time-Bounded Processing +Laravel restricts `runInBackground()` to tasks scheduled with `command()` and `exec()`; it is not available for scheduled closures. Ensure background processes have appropriate logging and failure monitoring. -A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. +## Restrict Tasks by Environment -## Use Schedule Groups for Shared Configuration +Use `environments()` when a task should run only in named application environments. Treat this as an operational safeguard, not an authorization control. -Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. +```php +Schedule::command('billing:charge') + ->monthly() + ->environments(['production']); +``` + +## Group Shared Configuration + +Use schedule groups when several tasks genuinely share frequency or constraints. ```php Schedule::daily() @@ -36,4 +54,8 @@ Schedule::daily() Schedule::command('emails:send --force'); Schedule::command('emails:prune'); }); -``` \ No newline at end of file +``` + +## Bound Work Inside the Task + +The scheduler does not provide a `takeUntilTimeout()` event method or terminate arbitrary tasks at a deadline. Bound work in the command or job itself by processing finite chunks, checking a deadline, or dispatching queue jobs with suitable timeouts. Use operating-system or process controls when hard termination is required. diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/security.md b/_api_app/.claude/skills/laravel-best-practices/rules/security.md index 524d47e6..57b57f0f 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/security.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/security.md @@ -1,18 +1,9 @@ # Security Best Practices -## Mass Assignment Protection +## Control Mass Assignment -Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). +Define `$fillable` when a model is populated from request-derived arrays, or deliberately guard attributes by another consistent model convention. Laravel models guard all attributes by default; `$guarded = []` opts out of that protection. -Incorrect: -```php -class User extends Model -{ - protected $guarded = []; // All fields are mass assignable -} -``` - -Correct: ```php class User extends Model { @@ -24,82 +15,71 @@ class User extends Model } ``` -Never use `$guarded = []` on models that accept user input. +Do not pass untrusted request data to a model with `$guarded = []`. Mass-assignment protection controls which attributes `create()`, `fill()`, and `update()` may set; it does not validate values or authorize the operation. -## Authorize Every Action +## Authorize Protected Actions -Use policies or gates in controllers. Never skip authorization. +Use policies, gates, or form request authorization for actions that depend on the current user's permissions. Authentication alone does not establish permission, and validation is not authorization. -Incorrect: ```php -public function update(Request $request, Post $post) -{ - $post->update($request->validated()); -} -``` - -Correct: -```php -public function update(UpdatePostRequest $request, Post $post) +public function update(UpdatePostRequest $request, Post $post): RedirectResponse { Gate::authorize('update', $post); $post->update($request->validated()); + + return redirect()->route('posts.show', $post); } ``` -Or via Form Request: +Authorization may instead live in the form request: ```php public function authorize(): bool { - return $this->user()->can('update', $this->route('post')); + return $this->user()?->can('update', $this->route('post')) ?? false; } ``` -## Prevent SQL Injection +Public actions intentionally available to everyone do not need a redundant authorization check. -Always use parameter binding. Never interpolate user input into queries. +## Bind Query Parameters + +Use Eloquent, the query builder, or explicit bindings instead of interpolating untrusted values into Structured Query Language (SQL). Bindings protect values, not identifiers such as column names or sort directions; map user-selected identifiers to an allow-list. 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(); +User::whereRaw('LOWER(name) = ?', [$request->string('name')->lower()->toString()])->get(); ``` -## Escape Output to Prevent XSS +## Escape Output in Its Context -Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. +Blade's `{{ }}` syntax HTML-escapes output. Use `{!! !!}` only for content that has been sanitized for the exact HTML context in which it is rendered. Escaping rules differ for HTML, URLs, JavaScript, and Cascading Style Sheets. + +Incorrect for untrusted content: -Incorrect: ```blade {!! $user->bio !!} ``` Correct: + ```blade {{ $user->bio }} ``` -## CSRF Protection +## Apply Cross-Site Request Forgery Protection -Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia. +Include `@csrf` in state-changing Blade forms handled by Laravel's `web` middleware. Routes intentionally excluded from cross-site request forgery (CSRF) verification, such as validated third-party webhooks, need their own authenticity check. -Incorrect: -```blade -
- -
-``` - -Correct: ```blade
@csrf @@ -107,21 +87,27 @@ Correct:
``` -## Rate Limit Auth and API Routes +Inertia applications commonly use Axios, which returns the encrypted `XSRF-TOKEN` cookie in the `X-XSRF-TOKEN` header. Confirm equivalent configuration when using another HTTP client. Do not disable CSRF protection merely to fix a token mismatch. + +## Rate Limit Sensitive Endpoints -Apply `throttle` middleware to authentication and API routes. +Apply suitable rate limits to login attempts, password recovery, verification messages, and expensive or abuse-prone application programming interface (API) routes. Choose the limiter key deliberately; an Internet Protocol (IP) address alone can unfairly group users behind a shared network, while an account identifier alone can enable targeted denial of service. ```php RateLimiter::for('login', function (Request $request) { - return Limit::perMinute(5)->by($request->ip()); + return Limit::perMinute(5)->by(Str::transliterate( + Str::lower($request->string('email')).'|'.$request->ip() + )); }); Route::post('/login', LoginController::class)->middleware('throttle:login'); ``` -## Validate File Uploads +Rate limiting reduces abuse; it does not replace authentication, authorization, or upstream denial-of-service protection. -Validate MIME type, extension, and size. Never trust client-provided filenames. +## Validate and Store Uploads Safely + +Validate expected content type, dimensions where relevant, and size. Laravel's `mimes` rule reads the file contents and guesses a Multipurpose Internet Mail Extensions (MIME) type corresponding to the listed extensions; it does not validate the user-assigned filename extension. The `extensions` rule checks that extension and should not be used by itself. ```php public function rules(): array @@ -132,56 +118,28 @@ public function rules(): array } ``` -Store with generated filenames: - -```php -$path = $request->file('avatar')->store('avatars', 'public'); -``` - -## Keep Secrets Out of Code +Use Laravel's storage methods to generate a filename, and store untrusted files outside a publicly executable location. Public files can require additional controls, such as image re-encoding, content-disposition headers, and explicit blocking of active formats. -Never commit `.env`. Access secrets via `config()` only. - -Incorrect: ```php -$key = env('API_KEY'); +$path = $request->file('avatar')->store('avatars'); ``` -Correct: -```php -// config/services.php -'api_key' => env('API_KEY'), +## Keep Secrets Out of Application Code -// In application code -$key = config('services.api_key'); -``` +Do not commit populated environment files or hard-code credentials. Read environment variables in configuration files, then use `config()` in application code so configuration caching works correctly. See the configuration rules for encrypted environment files and external secret stores. ## Audit Dependencies -Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. +Run `composer audit` regularly and in continuous integration. Review findings for exploitability and update or mitigate affected packages promptly. ```bash composer audit ``` -## Encrypt Sensitive Database Fields +## Encrypt Sensitive Attributes When Appropriate -Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. +Use an `encrypted` cast for sensitive values that must be recoverable, and use `$hidden` to omit them from array and JavaScript Object Notation (JSON) serialization. Hidden attributes remain accessible in PHP, and encryption does not replace access control. Encrypted values cannot be meaningfully queried and should use a `TEXT` or larger column because ciphertext length is variable. -Incorrect: -```php -class Integration extends Model -{ - protected function casts(): array - { - return [ - 'api_key' => 'string', - ]; - } -} -``` - -Correct: ```php class Integration extends Model { @@ -195,4 +153,4 @@ class Integration extends Model ]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/style.md b/_api_app/.claude/skills/laravel-best-practices/rules/style.md index 67af9891..3f3f14fb 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/style.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/style.md @@ -1,125 +1,110 @@ -# 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 | -|---------|---------| +# Convention and Style Best Practices + +## Follow Project Naming Conventions + +Prefer Laravel's conventions in new code, but preserve an established project convention unless a coordinated rename is worthwhile. + +| Element | Convention | Example | +| --- | --- | --- | +| Controller | Singular resource name | `ArticleController` | +| Model | Singular StudlyCase | `User` | +| Table | Plural snake_case | `article_comments` | +| Pivot table | Singular model names in alphabetical order, in snake_case | `article_user` | +| Column | snake_case | `meta_title` | +| Conventional foreign key | Singular model name plus `_id`, in snake_case | `article_id` | +| Resource URI | Plural resource | `articles/1` | +| Route name | Dotted segments; snake_case within a segment when needed | `users.show_active` | +| Method | camelCase | `getAll` | +| Variable | camelCase | `$articlesWithAuthor` | +| Collection | Descriptive and plural | `$activeUsers` | +| Object | Descriptive and singular | `$activeUser` | +| View | kebab-case | `show-filtered.blade.php` | +| Configuration file | snake_case | `google_calendar.php` | +| Enumeration | Singular StudlyCase | `UserType` | + +## Prefer Clear, Idiomatic Syntax + +Use Laravel helpers and query methods when they communicate intent more directly. Do not shorten code when the result is ambiguous or loses useful type information. + +| More verbose | Idiomatic alternative | +| --- | --- | | `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')` | +| `->first()?->name` | `->value('name')` when only that value is needed | -## Use Laravel String & Array Helpers +Use typed request accessors such as `$request->string()`, `$request->integer()`, and `$request->boolean()` when their coercion matches the operation. -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. +## Use Utilities When They Clarify Intent -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); +Laravel's `Str`, `Arr`, `Number`, and `Uri` utilities provide expressive operations and framework-consistent behavior. Prefer them when they are clearer or safer than an equivalent PHP operation, not as an unconditional replacement for every built-in function. -// Correct +```php $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 +$class = class_basename(User::class); $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()`. +Use `Arr` for dot notation and common transformations: -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'); +$public = Arr::only($attributes, ['name', 'email']); ``` -Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`. +Use `Number` for localized display formatting rather than values that will be stored or calculated: -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%" +Number::format(1000000); +Number::currency(1500, 'USD'); +Number::fileSize(1024 * 1024); ``` -URIs — use `Uri` for URL manipulation: +Use `Uri` when constructing or transforming a uniform resource identifier (URI) benefits from a structured API: + ```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. +Check the documentation for the Laravel version supported by the project before using newer utility classes or methods. -Use `search-docs` for the full list of available methods — these helpers are extensive. +## Keep Presentation Code Maintainable -## No Inline JS/CSS in Blade +Prefer the project's asset pipeline, components, and existing conventions for substantial JavaScript and Cascading Style Sheets (CSS). Small page-specific scripts or styles can be reasonable in Blade layouts or stacks; avoid mixing large behavior and style blocks into templates. -Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes. +Pass server data with an encoding mechanism appropriate to its context. For example, Blade's `Js::from()` safely formats data for JavaScript: -Incorrect: ```blade -let article = `{{ json_encode($article) }}`; + ``` -Correct: -```blade - -``` +Data attributes are useful for small scalar values, but serializing a large model into an attribute can expose unnecessary fields and complicate escaping. -Pass data to JS via data attributes or use a dedicated PHP-to-JS package. +## Write Comments That Explain Why -## No Unnecessary Comments +Prefer clear names and small units of code over comments that merely restate an operation. Add concise comments for non-obvious constraints, tradeoffs, workarounds, regular expressions, or external behavior that the code cannot express by itself. Keep comments accurate when behavior changes. -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. +Unhelpful: -Incorrect: ```php -// Check if there are any joins -if (count((array) $builder->getQuery()->joins) > 0) +// Check whether the query has joins. +if (count((array) $builder->getQuery()->joins) > 0) { + // ... +} ``` -Correct: +Clearer: + ```php -if ($this->hasJoins()) -``` \ No newline at end of file +if ($this->hasJoins()) { + // ... +} +``` diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/testing.md b/_api_app/.claude/skills/laravel-best-practices/rules/testing.md deleted file mode 100644 index d39cc3ed..00000000 --- a/_api_app/.claude/skills/laravel-best-practices/rules/testing.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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(); -``` \ No newline at end of file diff --git a/_api_app/.claude/skills/laravel-best-practices/rules/validation.md b/_api_app/.claude/skills/laravel-best-practices/rules/validation.md index a20202ff..37ce6c5e 100644 --- a/_api_app/.claude/skills/laravel-best-practices/rules/validation.md +++ b/_api_app/.claude/skills/laravel-best-practices/rules/validation.md @@ -1,75 +1,89 @@ -# Validation & Forms Best Practices +# Validation and Forms Best Practices -## Use Form Request Classes +## Extract Validation When It Improves the Boundary -Extract validation from controllers into dedicated Form Request classes. +Use a form request when validation or authorization is substantial, reused, or clearer outside the controller. Inline `$request->validate()` remains appropriate for a small, endpoint-specific rule set. -Incorrect: ```php -public function store(Request $request) +public function store(StorePostRequest $request): RedirectResponse { - $request->validate([ - 'title' => 'required|max:255', - 'body' => 'required', - ]); -} -``` + $post = Post::create($request->validated()); -Correct: -```php -public function store(StorePostRequest $request) -{ - Post::create($request->validated()); + return redirect()->route('posts.show', $post); } ``` -## Array vs. String Notation for Rules +A form request's `authorize()` method can enforce access to the operation. Validation establishes the shape and values of input; it does not itself authorize the user. + +## Prefer Readable Rule Syntax -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. +Array syntax composes cleanly with rule objects and avoids delimiter issues. Prefer it in new code when it improves readability, while following a consistent local style. ```php -// Preferred for new code 'email' => ['required', 'email', Rule::unique('users')], +``` + +String syntax remains valid for simple rules: -// Follow existing convention if the project uses string notation +```php 'email' => 'required|email|unique:users', ``` -## Always Use `validated()` +## Use Only Intended Validated Data + +Use `validated()` or `safe()` instead of `$request->all()` when passing request data onward. Then select the fields intended for the operation when the validation rules also cover control fields or nested data. -Get only validated data. Never use `$request->all()` for mass operations. +Unsafe: -Incorrect: ```php Post::create($request->all()); ``` -Correct: +Preferred: + ```php -Post::create($request->validated()); +$post = Post::create($request->safe()->only(['title', 'body'])); ``` -## Use `Rule::when()` for Conditional Validation +Validated data is not automatically safe for mass assignment. Keep model `$fillable` or `$guarded` rules aligned with the operation, and never add a sensitive attribute to validation merely to make mass assignment convenient. + +## Express Conditional Rules Clearly + +Use conditional rules such as `Rule::when()`, `required_if`, or `exclude_unless` when they make the condition explicit. Choose the simplest form that remains easy to test. ```php 'company_name' => [ - Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), + 'string', + 'max:255', + Rule::when( + $this->input('account_type') === 'business', + ['required'], + ['nullable'], + ), ], ``` -## Use the `after()` Method for Custom Validation +## Add Cross-Field Validation After Base Rules -Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. +Use a form request's `after()` method for validation that depends on multiple fields or application state. Avoid expensive queries when prerequisite fields have already failed validation. ```php public function after(): array { return [ function (Validator $validator) { - if ($this->quantity > Product::find($this->product_id)?->stock) { + if ($validator->errors()->hasAny(['product_id', 'quantity'])) { + return; + } + + $stock = Product::find($this->integer('product_id'))?->stock; + + if ($stock !== null && $this->integer('quantity') > $stock) { $validator->errors()->add('quantity', 'Not enough stock.'); } }, ]; } -``` \ No newline at end of file +``` + +Validation against mutable state does not prevent a race between validation and persistence. Enforce inventory, uniqueness, and similar invariants with database constraints, atomic updates, or a database transaction as appropriate. diff --git a/_api_app/.claude/skills/pest-testing/SKILL.md b/_api_app/.claude/skills/pest-testing/SKILL.md deleted file mode 100644 index 0761b6e7..00000000 --- a/_api_app/.claude/skills/pest-testing/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -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) or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 3 features. Do not use for editing factories, seeders, migrations, controllers, models, or non-test PHP code." -license: MIT -metadata: - author: laravel ---- - -# Pest Testing 3 - -## Documentation - -Use `search-docs` for detailed Pest 3 patterns and documentation. - -## Basic Usage - -### Creating Tests - -All tests must be written using Pest. Use `php artisan make:test --pest {name}`. - -### Test Organization - -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Do NOT remove tests without approval - these are core application code. -- Test happy paths, failure paths, and edge cases. - -### 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()`. - - -```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()`: - - -```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.): - - -```php -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); -``` - -## Pest 3 Features - -### Architecture Testing - -Pest 3 includes architecture testing to enforce code conventions: - - -```php -arch('controllers') - ->expect('App\Http\Controllers') - ->toExtendNothing() - ->toHaveSuffix('Controller'); - -arch('models') - ->expect('App\Models') - ->toExtend('Illuminate\Database\Eloquent\Model'); - -arch('no debugging') - ->expect(['dd', 'dump', 'ray']) - ->not->toBeUsed(); -``` - -### Type Coverage - -Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. - -## 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 \ No newline at end of file diff --git a/_api_app/.claude/skills/testing-best-practices/SKILL.md b/_api_app/.claude/skills/testing-best-practices/SKILL.md new file mode 100644 index 00000000..35d02a1d --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/SKILL.md @@ -0,0 +1,58 @@ +--- +name: testing-best-practices +description: "Laravel test design and review. Use when selecting coverage, naming or structuring tests, choosing assertions or test data, isolating dependencies, testing HTTP or security boundaries, improving suite performance, or reviewing test value. Use framework guidance or search-docs for Pest and PHPUnit syntax." +license: MIT +metadata: + author: laravel +--- + +# Testing Best Practices + +This skill provides rules for designing Laravel tests. Each rule file explains what to do and why. Use `search-docs` for Laravel and Pest API syntax. +This project uses Pest. Follow the corresponding guidance in each rule. + +## Consistency First + +Read nearby tests before you choose syntax and organization. + +A pattern repeated throughout the project is a convention, and project conventions take precedence over this skill. Follow them and give new tests the same structure. + +These rules govern the tests you write now. An existing test that follows a project convention is not defective merely because it conflicts with this skill. Do not delete or rewrite it. If the convention has drawbacks, explain them and let the user decide. + +## What to Test + +Read this section before you write a test. + +- Test observable behavior and application contracts. A test must pass after an implementation change if the behavior stays the same. +- Cover every changed decision and each applicable high-value failure mode. A decision is a branch, a validation, a calculation, or an authorization. +- Exercise declarations through behavior instead of repeating their text. +- Leave framework behavior to framework tests. Testing project configuration is not testing the framework. A constrained relationship, cast, scope, or validation rule belongs to this project. +- Keep every test that can detect a distinct defect. When two tests detect the same defect, trim the higher-layer test to one case and report the duplication. Do not delete an existing test. +- Write a feature test first. Write a unit test only for logic that does not use the framework. +- Write a feature test for every behavior reachable through a request. Real-browser tests require `pestphp/pest-plugin-browser` and a browser download, neither of which this project installs. Mention the package only if the user asks for a real-browser test. +- Judge an architecture test by the convention it protects, not by the rules above. An `arch()` test declares a rule for an entire directory, such as the parent class of every model, the classes that may use an enum, or the methods every factory declares. It intentionally checks declarations and fails when a new file breaks the convention. +- Use the test tools that the project installs. Add a new test dependency, plugin, or browser only after the user asks for it. + +## How to Apply + +1. Read the code under test. Read the tests in the same directory. Identify every decision in the code. +2. Select every applicable branch in the rule index. Read every selected rule file. +3. Report each defect in the code before you write a test. Examples are a method with no body, a policy that no action calls, and a write action with no validation. Test the actual behavior. Report the defect to the user. +4. Write the tests. Run the smallest set of tests that covers the change. The tests must pass. +5. Check every applicable item in `rules/review.md` and every selected rule file. Resolve every mismatch before completion. + +## Rule Index + +Most changes need more than one rule file. + +| Subject | Rule File | +| --- | --- | +| Test framework features that may already do the work | [`rules/finding-features.md`](rules/finding-features.md) | +| File layout, test names, and groups | [`rules/naming.md`](rules/naming.md) | +| Arrange-act-assert and choosing the correct assertion | [`rules/assertions.md`](rules/assertions.md) | +| Endpoint coverage, authentication, authorization, tenant isolation, validation, and browser tests | [`rules/endpoint-tests.md`](rules/endpoint-tests.md) | +| Factories, test data ownership, and repeated input values | [`rules/test-data.md`](rules/test-data.md) | +| Fakes, mocks, outbound HTTP, time, randomness, and databases | [`rules/isolation.md`](rules/isolation.md) | +| Escaping, injection, cross-tenant access, and privilege checks | [`rules/security.md`](rules/security.md) | +| Environment and CI settings for a slow suite | [`rules/performance.md`](rules/performance.md) | +| Reviewing a test or suite | [`rules/review.md`](rules/review.md) | diff --git a/_api_app/.claude/skills/testing-best-practices/rules/assertions.md b/_api_app/.claude/skills/testing-best-practices/rules/assertions.md new file mode 100644 index 00000000..ba55fb25 --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/assertions.md @@ -0,0 +1,60 @@ +# Assertions + +## Arrange, Act, Assert + +Write each test in three parts: setup, one action, and assertions. Put one blank line between them so readers can identify each part without comments. + +Keep each test self-contained. Do not use values created by another test. + +## How to Find the Correct Assertion + +First identify the subject of the check, then find an assertion designed for it. A subject-specific assertion identifies the incorrect value when the test fails. + +1. Search Laravel's assertions for framework subjects such as responses, the database, sessions, models, queues, events, mail, and notifications. +2. Fetch `https://pestphp.com/docs/expectations.md` for the expectations of Pest for a plain value, a type, a format, or a shape. +3. Build the check by hand only if no assertion exists for the subject. +4. Confirm the name in the documentation before you use it. Do not write an assertion that you did not confirm. + +Use the assertion in this table for each subject. + +| Subject | Assertion to use | +| --- | --- | +| A return value, the state of an object, or a transformation of a value | an `expect()` chain | +| An HTTP status, JSON, a session, or Inertia | a Laravel response assertion | +| The state in the database | a Laravel database assertion | +| The existence of a model | `assertModelExists($model)` rather than `assertDatabaseHas('users', ['id' => $user->id])` | + +Use a PHPUnit assertion only if no Pest expectation and no Laravel assertion exists for the subject. + +Assert each fact once. Do not assert a 200 status before `assertSee`, because `assertSee` already shows that the page rendered. + +## Named Response Assertions + +Use a named response assertion, such as `assertNotFound()`, rather than `assertStatus(404)`. A failure then identifies the broken contract. Laravel provides named assertions for commonly tested status codes. + +Keep one `expect()` chain on one subject. Start a new chain when the subject changes, or when the chain is difficult to read. + +## Assert a Known Value + +Write the expected value in the test, or calculate the expected value by a different method. Do not calculate the expected value with the logic of the implementation, because the test then passes when that logic is wrong. + +```php +// The test calculates the value with the logic of the implementation... +$expected = now()->subHours(24)->floorSeconds(30)->toJson(); +expect($from)->toBe($expected); + +// The test sets a fixed input and asserts a known value... +travelTo('2025-01-01 00:00:00'); +expect($from)->toBe('2024-12-31T00:00:00.000000Z'); +``` + +## Assert the Complete Result + +A status code is not the complete result of a write operation. Assert each of the following if the operation changes it: + +- The response or the return value. +- The state in the database. +- The jobs and the events that the operation dispatches. +- The notifications and the mail that the operation sends. + +On the failure path, assert that the operation makes none of these changes. A test that asserts only `assertOk()` passes even when the application saves no record. diff --git a/_api_app/.claude/skills/testing-best-practices/rules/endpoint-tests.md b/_api_app/.claude/skills/testing-best-practices/rules/endpoint-tests.md new file mode 100644 index 00000000..32e32e64 --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/endpoint-tests.md @@ -0,0 +1,47 @@ +# Endpoint Tests + +## How to Write the Test + +Fetch `https://laravel.com/framework/docs/http-tests` for the request helpers, the authentication helpers, and the response assertions. Confirm the name before you use it, and do not guess an assertion. + +Choose an assertion based on the subject of the check: the status, a header, a redirect, the JSON body, the session, a validation error, or the view. Laravel provides a named assertion for each subject that identifies the incorrect value. + +## Endpoint Coverage + +Write a test for each applicable case: + +- The request has missing or invalid authentication. +- The request comes from a different tenant, team, or organization. +- The user has an insufficient role or permission. +- The request does not satisfy a route or scope constraint. +- The request fails the validation. +- The request is valid. Assert both the response and the persisted state. + +Assert the application's actual behavior rather than a generic status code. An API returns `401` for a missing or invalid token, while a browser endpoint redirects to the sign-in route. + +## Tenant Isolation + +Assert the status code returned for a cross-tenant request. Use `404` rather than `403` when one tenant must not learn that another tenant's record exists, because `403` confirms its existence. + +## Test Authorization at the Policy Level + +An HTTP test shows that the endpoint performs authorization. It cannot identify which mechanism refused the request because middleware, a policy, and a call to `abort()` can all return `403`. + +- Assert the complete matrix of the permissions against the policy or the gate. A failure then names the rule that is not correct. +- Write one HTTP test for one refused role, which shows that the endpoint calls the authorization. +- Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists. + +## Testing Validation + +- Write one test for each validation rule when each failure represents a separate contract. +- Write one test with an empty payload to assert several required fields together. +- Assert the text of the message that the user gets. A message that is present but wrong is a defect. +- Use a dataset for input values that need the same setup and the same assertions. + +Send an input value that is not valid through the application, and assert the error. Do not assert that an array of rules contains a string, because that assertion tests the declaration and not the behavior. Use such an assertion only for a rule that no request can reach, and write the reason in the test. + +### Which Layer Owns Which Case + +The rule-class test owns the matrix of values that pass and fail. The endpoint test proves that the endpoint applies the rule and that the user receives the message. + +When both tests contain the matrix, move it to the rule-class test and retain one case in the endpoint test. Never remove the last case, because the rule-class test still passes if the request omits the rule. The same division applies to policies, scopes, and other classes called by a request. diff --git a/_api_app/.claude/skills/testing-best-practices/rules/finding-features.md b/_api_app/.claude/skills/testing-best-practices/rules/finding-features.md new file mode 100644 index 00000000..83ce650b --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/finding-features.md @@ -0,0 +1,34 @@ +# How to Find Test Framework Features + +Pest adds features faster than this skill can list them. Find an existing feature before implementing the behavior by hand. + +- Give `search-docs` the capability you need rather than the name of a function you remember. It returns features available in the installed version. +- Fetch `https://pestphp.com/llms.txt` for the complete feature list and additions in each release. +- If a search returns no results, tell the user that the installed version does not provide the feature. Do not write an API that you have not confirmed. + +Search for a feature in this table before you write the code by hand. + +| Work that you need | Term to search for | +| --- | --- | +| Run one test with many input values | datasets, bound datasets | +| Assert over many values or over a collection | higher-order expectations | +| Remove the same setup from each test in a file | hooks, higher-order tests | +| Apply a convention to the complete codebase | architecture testing | +| Measure if the suite finds a defect | mutation testing | +| Find code with no types | type coverage | +| Reduce the time of a slow suite | parallel, profiling | +| Run one test while you debug | filtering, `--bail`, `--dirty` | + +## Built-in Laravel Assertion Methods + +Laravel provides assertions for each part of the framework. Fetch `https://laravel.com/framework/docs/testing` for the complete list, and search for an assertion before building a check by hand. Examples include `assertDatabaseHas()`, `assertModelExists()`, `assertSoftDeleted()`, response assertions such as `assertRedirectToRoute()` and `assertJsonPath()`, and fake assertions such as `Queue::assertPushed()` and `Notification::assertSentTo()`. + +A hand-built check fails with `false is not true`, which identifies nothing. A framework assertion names the incorrect table, value, or response, so the failure indicates what to fix. + +```php +// The failure says that false is not true. Instead of this... +expect(User::where('email', 'taylor@laravel.com')->exists())->toBeTrue(); + +// Use this... the failure names the table and the attributes that it did not find... +$this->assertDatabaseHas('users', ['email' => 'taylor@laravel.com']); +``` diff --git a/_api_app/.claude/skills/testing-best-practices/rules/isolation.md b/_api_app/.claude/skills/testing-best-practices/rules/isolation.md new file mode 100644 index 00000000..e1a21fdb --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/isolation.md @@ -0,0 +1,52 @@ +# Fakes, Mocks, and Determinism + +Tests that depend on actual time, randomness, sleeping, or network calls can fail for reasons unrelated to the code under test. Control all four. + +## How to Isolate a Dependency + +Fetch `https://laravel.com/framework/docs/mocking` for Laravel's fakes, facade doubles, and fake assertions. Confirm each name before using it. + +Identify the dependency, then choose the first applicable option. A framework fake preserves the real code path, while a mock replaces the dependency. + +1. Always use framework fakes for facades such as events, queues, mail, notifications, storage, the HTTP client, time, and sleep. +2. Use a developer-defined fake implementation of a service if the application provides one. +3. Use a mock for a container-resolved contract only when the real implementation leaves the process or is nondeterministic. +4. Use the real implementation for everything else, including the database. + +## Framework Fakes + +- Create each fake inside the test that needs it. Do not create fakes in a file-level `beforeEach()`. +- Pass class names to `Event::fake()` and `Queue::fake()` when you know which classes the code dispatches. A fake without class names can hide an unexpected dispatch. +- Use a fake without class names only when the test asserts the complete result, including a call to `assertNothingPushed()`. +- Write one assertion for each fake. The assertion states that the code dispatches the item, or that the code does not dispatch the item. +- Assert the data of a job or of an event if that data is part of the behavior. +- Use `Exceptions::fake()` to assert that the application reports the correct exception. Do not use `withoutExceptionHandling()`, because it changes the response under test. + +Create prerequisite factory records before calling `Event::fake()`. Factories use model events, such as a `creating` hook that generates a UUID, and a fake without class names suppresses those events and can produce an invalid model. Call the fake first only when a factory event is under test, and pass that event's class name. + +## Mocking + +Use `shouldReceive()` before the action to declare an expectation. Use `shouldHaveReceived()` after the action for a spy. Use `Mockery::on()` or `withArgs()` if an equality check cannot state the expected argument, such as a check of one field of a value object. + +Import the mock function before you use it: `use function Pest\Laravel\mock;`. + +## Outbound HTTP Testing + +Call `Http::preventStrayRequests()`. Any request without a matching fake then fails without reaching the network. + +Fake the exact endpoint used by each test. Do not call `Http::fake()` without an endpoint because it accepts unexpected requests and can hide defects. + +## Time and Randomness + +- Freeze the time or move the time in each test that depends on a date, a period, or a timestamp. +- Use the framework helpers `freezeTime()`, `travelTo()`, `travel()`, and `travelBack()`. Do not call `Carbon::setTestNow()`. +- Use `Str::createRandomStringsUsing()` to fix a generated string, if the test asserts an identifier or a slug. +- Use `Sleep::fake()` instead of a real sleep, and assert the sleeps that the code requests. +- Restore the time and the randomness after each test, if the suite does not restore them for every test. + +## Database + +- Run real queries against the real records in the test database. Do not mock the query builder, because the test then asserts the mock. +- Assert the exact keys of `toArray()` if the shape of the serialized model is a contract. The test then fails when the model exposes a new attribute. +- Test application behavior caused by the schema, such as deleting dependent records through a cascade. Do not test the database engine's cascade implementation. +- Use `LazilyRefreshDatabase` instead of `RefreshDatabase`. A test that does not use the database then does not run the migrations. diff --git a/_api_app/.claude/skills/testing-best-practices/rules/naming.md b/_api_app/.claude/skills/testing-best-practices/rules/naming.md new file mode 100644 index 00000000..4f3f5b1d --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/naming.md @@ -0,0 +1,45 @@ +# Naming and Structure + +## File Layout + +- Name each test file `{ClassName}Test.php`. +- Place each test file at the same relative path as the class under test. The class `app/Actions/DeleteTeam.php` gets the test `tests/Unit/Actions/DeleteTeamTest.php`. +- Follow the project's convention for fixture files. If none exists, put fixtures in `tests/Fixtures/` and load them by path. +- Move large literal values out of the test body and into fixture files. + +## Test Function + +Use the test function used by other files in the same directory. If no neighboring test files exist: + +- Use `it()` for the behavior of the code, and write the name as a verb phrase. +- Use `test()` for a declarative fact, such as a grant in a policy, the labels of an enum, or the shape of a serialized model. + +Use one Pest declaration style in each file. Use either `it()` or `test()` consistently. + +## Naming Tests + +The name of a test is a specification. State the user-visible result and the condition that causes it. + +- Name the behavior, and not the method under test. The file name already gives the class. +- Give the exact status code in the name of a test for an API error. +- Do not write `Given`, `When`, or `Then` in the name. + +```php +it('returns 401 when no token is provided', function () { ... }); +it('does not include deployments from deleted environments', function () { ... }); +it('falls back to the default region when none is configured', function () { ... }); +``` + +Use a verb that describes a result, such as `returns`, `renders`, `creates`, `dispatches`, `rejects`, `forbids`, `falls back`, or `does not`. + +Do not write `it('works correctly')` or `it('returns data')`, because neither specifies a meaningful result. Do not write `it('handleMethod creates record')`, because it names a method rather than behavior. + +## Grouping + +Use `describe()` if one file covers separate actions in a lifecycle. An example is a controller with the actions `index`, `show`, `store`, `update`, and `destroy`. + +Do not use `describe()` in these cases: + +- The file covers one action or one flow. +- The tests are different only in the input value. Use a dataset instead. +- The group adds a level but does not make the file easier to read. diff --git a/_api_app/.claude/skills/testing-best-practices/rules/performance.md b/_api_app/.claude/skills/testing-best-practices/rules/performance.md new file mode 100644 index 00000000..ee993fbb --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/performance.md @@ -0,0 +1,46 @@ +# Test Suite Performance + +These settings apply to the project and CI, not to individual tests. Read `rules/isolation.md` for choices within a test. + +Fetch `https://pestphp.com/docs/optimizing-tests` for Pest options that make test runs faster. +Verify each flag in the documentation before adding it to CI. + +Measure before changing a setting. Find the slow test first, and apply a project-wide setting only after identifying the costly work. + +## Test Environment + +- Set `BCRYPT_ROUNDS=4` in `.env.testing` or in `phpunit.xml`. The default value is 12, and the hash then takes most of the time of each test that signs a user in. +- Disable XDebug. Disable pcov also, unless the run needs the coverage. +- Disable packages that perform work on every request in the test environment. Examples are Pulse, Telescope, and Nightwatch. +- Use the `WithCachedConfig` and `WithCachedRoutes` traits, so the run does not parse the configuration and the routes for every test. +- Call `withoutVite()`, or `withoutMix()`, so the framework does not resolve a built asset. + +## Global Fakes + +Put these three calls in the base `Pest.php` of the project: + +- `Http::preventStrayRequests()`, because one request that reaches the network can slow the suite. This catches requests made through Laravel's HTTP client. Check direct Guzzle and cURL usage separately. +- `Sleep::fake(syncWithCarbon: true)`, so a retry and a backoff do not sleep. +- `Exceptions::fake()`, so the suite does not report an exception to an external service. + +## How to Run the Suite in Parallel + +Run `vendor/bin/pest --parallel` to spread tests across the machine's CPU cores. Add `--processes=N` if the default count is unsuitable for the machine or CI. + +A parallel run gives each process a separate database. Tests must meet these conditions; a test that fails only in parallel breaks one of them: + +- The test creates each record that it reads. It does not read a record that another test creates. +- The test does not depend on the order of the run. +- The test does not share a file, a cache key, or a queue with another test. Give each process a separate name for such a resource. + +## How to Find a Slow Test + +Run `vendor/bin/pest --profile` to list the slowest tests. Start with the ten slowest tests, because the same cause often applies to the complete suite. + +If the cause of a slow test is unclear, add an event listener or temporary log entry to identify its work. + +## Common Errors + +- The run loads XDebug for a test that does not need it. +- `BCRYPT_ROUNDS` keeps the default value, because the project has no `.env.testing`. +- The code under test calls the real `sleep()`, and `Sleep::fake()` then does not help. diff --git a/_api_app/.claude/skills/testing-best-practices/rules/review.md b/_api_app/.claude/skills/testing-best-practices/rules/review.md new file mode 100644 index 00000000..e0cfca14 --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/review.md @@ -0,0 +1,44 @@ +# Reviewing Tests + +Check every item in this file. A passing test may still provide no value. For each test, identify the defect it would catch. + +Report each finding. Do not delete or rewrite a test without the user's approval. When an issue appears throughout the suite as a convention, report the pattern once rather than every affected file. + +## Test Value + +Apply this section to behavioral tests. An architecture test states a convention for a directory, so these items do not apply to it. + +- [ ] Each test covers observable behavior or an application contract, and passes after a change to the implementation that keeps the behavior. +- [ ] Each tested declaration is exercised through behavior, and no test asserts the behavior of the framework. A test of what this project configures, such as a relation with a constraint, a cast, or a scope, belongs to this project. +- [ ] Each test detects a distinct defect that no other test covers. A duplicate shrinks at the higher layer to the one case that proves the wiring. +- [ ] Every changed decision and each applicable high-value failure mode has coverage. + +## Names and Structure + +- [ ] Each file has the name `{ClassName}Test.php` and the relative path of the class under test. +- [ ] Each name states a result, the condition that causes it, and the status code for an API error. +- [ ] Each file uses one declaration style consistently, and each `describe()` group holds separate behavior. + +## Coverage + +- [ ] HTTP tests cover authentication, authorization, role, scope, and validation when applicable. +- [ ] A request for a record of a different tenant gets a status code that does not confirm that the record exists. +- [ ] The complete permission matrix belongs in policy tests, not controller tests. +- [ ] Each validation rule has one test that asserts the user-visible message. When a unit test owns a matrix, reduce duplicate higher-level coverage to one case rather than deleting it. +- [ ] Rendered user input and each dynamic part of a query have a security test. + +## Data and Determinism + +- [ ] Each test creates its mutable records directly or through a helper that it calls, and every created record arranges the behavior or supports an assertion. +- [ ] Each `beforeEach()` holds configuration only. +- [ ] Each factory state and each relationship gives the meaning of the data. +- [ ] Each call to `make()` is in a test that does not need the database. +- [ ] Time, randomness, sleep, and outbound HTTP are controlled. +- [ ] Each test passes alone, and passes in the complete suite in any order. + +## Assertions + +- [ ] Each expected value is a known value, and the test does not calculate the value with the logic of the implementation. +- [ ] Each test of a write operation asserts the response, the state in the database, and the side effects. +- [ ] Each fake has one assertion, and gives the class names unless the test asserts the complete result. +- [ ] Each `expect()` chain stays on one subject. diff --git a/_api_app/.claude/skills/testing-best-practices/rules/security.md b/_api_app/.claude/skills/testing-best-practices/rules/security.md new file mode 100644 index 00000000..234f5b00 --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/security.md @@ -0,0 +1,27 @@ +# Security Tests + +Test each security boundary where user input affects authorization, rendered output, or query construction. A defect at such a boundary can be difficult to detect because the feature may continue to work. + +Write a test for each of these cases: + +- **Cross-tenant access.** Request a record of a different tenant, team, or organization. Read `rules/endpoint-tests.md` for why the response should possibly be `404` rather than `403`. +- **Each unprivileged role.** Use a dataset over the roles that the endpoint must refuse. +- **Escaping user-provided content.** Test escaping in HTML and mail. Include names and every free-text field a template renders. Assert that dangerous characters are escaped and the raw value is absent. Do not assert an exact entity for a quote, because Markdown and mail CSS inliners may decode it. +- **Injection into dynamic query components.** Examples include sort columns, filter fields, and sort directions. +- **An unexpected key** in a payload or configuration array. A merge that accepts every key can set an attribute the user must not control. + +```php +it('escapes dangerous content in the notification', function () { + $organization = Organization::factory()->make([ + 'name' => "O'Reilly ", + ]); + + $content = (new QuotaApproaching($organization, 80))->toMail()->render(); + + expect($content) + ->toContain('<script>') + ->not->toContain(""); +}); +``` + +Laravel provides defenses against mass assignment, unauthorized access, and unescaped output. Test that the application applies the appropriate defense to each attribute, route, and template. diff --git a/_api_app/.claude/skills/testing-best-practices/rules/test-data.md b/_api_app/.claude/skills/testing-best-practices/rules/test-data.md new file mode 100644 index 00000000..65124683 --- /dev/null +++ b/_api_app/.claude/skills/testing-best-practices/rules/test-data.md @@ -0,0 +1,56 @@ +# Factories and Test Data + +## Each Test Makes Its Own Data + +Create mutable records inside the test that uses them. This keeps setup visible and lets each test select its factory state. + +Use `beforeEach()` only for configuration that applies to every test in the file. Do not create records in it. + +## Record Construction + +- Use `create()` if the test needs the record in the database. +- Use `make()` only if the test does not need the database. Examples include rendering a notification and testing a value object's behavior. +- Use a named factory state instead of a raw attribute. `User::factory()->unverified()->create()` gives the state meaning; `create(['email_verified_at' => null])` gives only its value. +- Use `for()` or the relationship helper of the project to declare the owner of a record. +- Use `recycle()` if several records must share one parent record. +- Use `sequence()` if several records need different attributes. + +```php +$organization = Organization::factory()->onPlan(BillingPlan::PRO)->create(); + +$environment = Environment::factory()->recycle($organization)->create(); + +$organizations = Organization::factory() + ->count(3) + ->sequence( + ['created_at' => now()->setSeconds(30)], + ['created_at' => now()->setSeconds(1)], + ) + ->create(); +``` + +Create only the records required to arrange the behavior or support an assertion. + +## Datasets + +Use a dataset when the setup, test body, and assertions remain the same across input values. + +```php +it('forbids roles other than admin', function (Role $role) { + actingAs(User::factory()->hasOrganization($role)->create()) + ->post('/settings') + ->assertForbidden(); +})->with(collect(Role::cases())->reject(fn (Role $role) => $role === Role::ADMIN)); +``` + +Use parameterized tests for: + +- enum cases +- roles and plans +- boundary values +- input values that are invalid in the same way +- input and output value pairs + +Write separate tests if the cases need a different setup, a different behavior, or different assertions. One test function with a branch in the body is two tests in one function. + +Give each dataset case a name that states the difference. A failure then identifies the case without requiring you to count positions. diff --git a/_api_app/.cursor/skills/ai-sdk-development/SKILL.md b/_api_app/.cursor/skills/ai-sdk-development/SKILL.md index d74e342e..ef6d72a2 100644 --- a/_api_app/.cursor/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.cursor/skills/ai-sdk-development/SKILL.md @@ -410,4 +410,4 @@ Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text | OpenRouter | Y | - | - | - | - | - | - | - | | ElevenLabs | - | - | Y | Y | - | - | - | - | | Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file +| Jina | - | - | - | - | Y | Y | - | - | diff --git a/_api_app/.cursor/skills/deploying-to-cloud/SKILL.md b/_api_app/.cursor/skills/deploying-to-cloud/SKILL.md new file mode 100644 index 00000000..9584216a --- /dev/null +++ b/_api_app/.cursor/skills/deploying-to-cloud/SKILL.md @@ -0,0 +1,267 @@ +--- +name: deploying-to-cloud +description: "Deploys and manages Laravel applications on Laravel Cloud using the `cloud` CLI. Use when the user wants to deploy an app, ship to Cloud, create or manage applications, environments, databases, caches, object storage, queues, domains, instances, background processes, secrets, compute, scheduled tasks, or Laravel Cloud infrastructure; or check billing, usage, or spend. Triggers on deployment, shipping, Cloud management, environment setup, database provisioning, billing or usage queries, the `cloud` CLI, and Laravel Cloud deployment troubleshooting." +license: MIT +metadata: + author: laravel +--- + +# Deploying with Laravel Cloud + +Use the [Laravel Cloud documentation](https://cloud.laravel.com/docs/llms.txt) for detailed, current feature behavior. Use the Cloud CLI for Cloud operations rather than guessing dashboard or API workflows. Install it in the project and invoke it as `./vendor/bin/cloud` by default; use a globally installed `cloud` command only as a fallback. Commands below are written as `cloud` for brevity; run them as `./vendor/bin/cloud` unless falling back to the global installation. + +## Application Setup + +- Laravel Cloud deploys from GitHub, GitLab, or Bitbucket. A new Laravel application requires PHP 8.2 or later, Laravel 9 or later, a connected Git provider, and a deployment region. +- Cloud creates environments for applications. Use separate production, staging, and preview environments; each environment has its own compute, resources, and deployment settings. +- Keep application compute and attached resources in the same region where possible. +- Cloud builds a Docker image using the selected PHP version, runs the configured build and deploy commands, and switches traffic to a successful deployment with zero downtime. Push-to-deploy is enabled by default, and manual deployments and deploy hooks are also available. + +## Build and Deploy + +- A typical Laravel build command is `composer install --no-dev && npm run build`. +- Run optimization and cache-building commands during the build, not during deployment. A typical deploy command is `php artisan migrate --force`. +- Build and deploy commands have a 15-minute timeout. Deploy commands run immediately before the release becomes live, and filesystem changes made by deploy commands are not persisted. +- Do not add `php artisan queue:restart`, `php artisan horizon:terminate`, `php artisan optimize:clear`, or `php artisan storage:link` to deploy commands. Cloud handles worker restarts and process management; deploy filesystem changes are not persistent. +- After changing environment settings, attached resources, or linked secrets, redeploy the environment for the changes to take effect. + +## Configuration and Resources + +- Attached databases, caches, and object storage inject their connection variables automatically. Custom environment variables take precedence over injected values. +- Use Secrets Manager for encrypted organization-level values shared across environments. Secret values cannot be read after creation; redeploy affected environments after creating, updating, linking, unlinking, or deleting a secret. +- Environment filesystems are ephemeral and are not shared between replicas. Use a database or Laravel Valkey for persistent cache and sessions, and Laravel Cloud Object Storage for persistent files. Do not rely on local files surviving a deployment. + +### Object Storage and File Visibility + +- Laravel Cloud Object Storage is backed by Cloudflare R2. R2 applies visibility at the bucket level; a bucket cannot contain a mix of private and public objects. +- For private files, always use `Storage::disk()` with no arguments, such as `Storage::disk()->put(...)`, so Laravel uses the environment's default disk. Do not pass a disk name for private/default storage. Attach a private Cloud bucket as the environment's default disk. +- For public files, use the named public disk: `Storage::disk('public')->put(...)`. Attach a second Cloud bucket configured as public and give it the `public` disk name. +- Applications that use both private and public files therefore need two Cloud Object Storage buckets attached to the environment: a private default bucket and a public bucket named `public`. +- Do not set per-file or Flysystem `visibility: 'public'` configuration for Cloud Object Storage. R2 does not support per-object ACL headers and rejects those requests; select the bucket visibility when creating the bucket. +- Private buckets are not internet-accessible, but Laravel can generate temporary public URLs with `Storage::temporaryUrl(...)`. Public buckets expose all objects through their Cloud-provided public URL. +- Install the S3 Flysystem adapter before using Cloud Object Storage: `composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies`. + +## Queues and Scheduling + +- Managed queues are the recommended queue option. They provision dedicated workers and autoscale based on queued work. +- Managed queues require a supported recent Laravel version and `aws/aws-sdk-php`; check the current Cloud documentation before changing dependencies. +- Enable the scheduler on an App or Worker cluster to run `php artisan schedule:run` every minute. When an environment has multiple replicas, use Laravel's `onOneServer` for tasks that must run once. +- Only Flex compute sizes can scale to zero. Scale-to-zero environments wake for Laravel scheduled tasks and queued jobs, but long-running jobs may be interrupted when the sleep timeout is reached. Use managed queues for workloads that must not be interrupted. + +## Domains and Storage + +- Each environment receives a `laravel.cloud` domain after its first successful deployment. Cloud automatically verifies custom domains and provisions SSL after the required DNS records are configured. +- Do not use the ephemeral filesystem for persistent uploads, generated files, cache, or session data. Use the appropriate Cloud resource instead. + +## Cloud CLI + +### Setup + +```shell +composer require --dev laravel/cloud-cli +./vendor/bin/cloud auth -n +``` + +If a project-local installation is not available, install the CLI globally with `composer global require laravel/cloud-cli` and invoke it as `cloud`. + +`cloud auth` opens a browser. If a browser is unavailable, set `LARAVEL_CLOUD_TOKEN` in the environment. It overrides any saved token and writes nothing to disk. To save a token instead, run `cloud auth:token --add --token= -n` or pipe it with `echo "$TOKEN" | cloud auth:token --add -n`. + +### Commands + +Commands follow a CRUD pattern: `resource:list`, `resource:get`, `resource:create`, `resource:update`, `resource:delete`. + +Available resources: `application`, `environment`, `instance`, `database-cluster`, `database`, `cache`, `bucket`, `domain`, `websocket-cluster`, `background-process`, `secret`, `command`, `deployment`. + +Some resources have additional commands (e.g., `domain:verify`, `database:open`, `instance:sizes`, `cache:types`). Discover these via `cloud -h`. + +Never hardcode command signatures. Always run `cloud -h` to discover options at runtime. + +### CLI Flags + +- Always add `-n` to every command to prevent the CLI from hanging. +- Never use `-q` or `--silent`; they suppress all output. + +Flag combos per operation: +- Read (`:list`, `:get`) → `--json -n` +- Create (`:create`) → `--json -n` +- Update (`:update`) → `--json -n --force` +- Delete (`:delete`) → `-n --force` (no `--json`) +- Environment variables → `-n --force` +- Deploy or ship → `-n` with all options passed explicitly (no `--json`) + +### Deployment Workflow + +Determine the task and follow the matching path: + +- First deployment: inspect `cloud ship -h`, then run `cloud ship -n` with all required values. + +- Existing application: + ```shell + cloud deploy {app_name} {environment} -n --open + cloud deploy:monitor -n + ``` + +- Environment variables: `cloud environment:variables -n --force` + +- Secrets: `echo "$VALUE" | cloud secret:create --name=NAME --json -n`, then `cloud environment-secret:attach` + +- Provision infrastructure: `cloud :create --json -n` + +- Monorepo (application in a subdirectory): add `--root-directory=` to `cloud ship` or `cloud application:create`. + +- Custom domain: `cloud domain:create --json -n`, then `cloud domain:verify -n`. + +- Repository defaults: `cloud repo:config {application} -n` sets repository-local application and organization defaults. Pass `--organization=` when the user has multiple organizations. + +For multi-step operations, see [reference/checklists.md](reference/checklists.md). + +- If the user's needs are unclear, ask before running anything. + +### When a Command Fails + +1. Read the error output. +2. Check the resource status with `:list --json -n` or `:get --json -n`. +3. For authentication errors, run `cloud auth -n`. +4. Fix the issue and rerun the command. +5. If the same error occurs after one fix, stop and ask the user rather than repeating the operation. + +Always run `cloud deploy:monitor -n` after every deploy. If it fails, inspect the deployment status and logs and show the user what went wrong before attempting a fix. + +### Subagent Delegation + +Delegate high-output operations to subagents using the Task tool to keep the main context window small. Only the summary returns; verbose output stays in the subagent's context. + +Delegate these to a subagent: +- `cloud deploy:monitor -n` — deployment logs can be very long +- `cloud deployment:get --json -n` — full deployment details +- `cloud :list --json -n` — listing many resources produces large JSON +- `cloud command:run` — when output may be long +- `cloud usage --detailed --json -n` — the payload includes details for every database, cache, bucket, websocket, and application +- Fetching docs from https://cloud.laravel.com/docs/llms.txt via `WebFetch` + +Keep in the main context: +- Short commands like `:create`, `:delete`, `:update` — output is small +- `cloud deploy -n` — you need the deployment ID immediately +- Any command where you need the result for the next step right away + +### Rules + +Follow these rules: +- Flag selection — always use the documented combos above +- Deploy sequence — deploy then monitor, never skip monitoring +- Destructive commands — always confirm with the user first, show the command and wait for approval. This includes deleting applications, environments, databases, caches, buckets, domains, or secrets. +- Error loop — diagnose, fix once, ask user if it fails again + +Use judgment for: +- Instance sizes, regions, cluster types — ask the user if not specified +- Which resources to provision — based on what the user describes +- Order of provisioning — no strict sequence required +- How to present output — summarize, show raw, or extract fields based on context + +### Secrets + +Secrets are encrypted values shared across the organization and attached to environments. The CLI encrypts values locally, so plaintext never reaches the API. + +```shell +cloud secret:list --json -n +echo "$VALUE" | cloud secret:create --name=STRIPE_KEY --json -n +cloud environment-secret:attach {environment} {secretId} -n +cloud environment-secret:list {environment} --json -n +``` + +Pipe the value in. `--value=` works, but leaves the plaintext in shell history and the process list. + +`secret:update`, `secret:delete`, and `environment-secret:attach` take secret IDs, not names — names are not unique. Read IDs from `cloud secret:list --json -n`. + +There is no `secret:get`, and no way to detach a secret from a single environment. `secret:delete` removes it and detaches it everywhere. + +Secrets need the `sodium` PHP extension. No other command does. + +Redeploy affected environments after creating, updating, attaching, or deleting a secret. + +### Remote Access + +#### Tinker (v0.2.0 or later) + +Run PHP code directly in a Cloud environment: + +```shell +cloud tinker {environment} --code='Your PHP code here' --timeout=60 -n +``` + +- `--code` — PHP code to execute (required in non-interactive mode) +- `--timeout` — maximum number of seconds to wait for output (default: 60) + +The code must explicitly output results using `echo`, `dump`, or similar; expressions alone produce no output. + +Always pass `--code` and `-n` to avoid interactive prompts. + +#### Remote Commands + +Run shell commands on a Cloud environment: + +```shell +cloud command:run {environment} --cmd='your command here' -n +``` + +- `--cmd` — the command to run (required in non-interactive mode) +- `--no-monitor` — skip real-time output streaming +- `--copy-output` — copy output to clipboard + +Review past commands: + +- `cloud command:list {environment} --json -n` — list command history +- `cloud command:get {commandId} --json -n` — get details and output of a specific command + +### Billing and Usage + +View billing and usage for the current organization: + +```shell +cloud usage --json -n +``` + +- `--period=current|previous|1|2|3` — Billing period (default: `current`; `1`, `2`, and `3` are one, two, and three periods back, respectively). Other values return an error. +- `--environment=` — filter usage to a single environment +- `--detailed` — include per-application, per-resource, and per-add-on breakdowns +- `--json` — machine-readable output (always pair with `-n`) + +Common queries: + +- Current spend: `cloud usage --json -n | jq '.currentSpendCents'` +- Last month's bill: `cloud usage --period=previous --json -n` +- One environment, full breakdown: `cloud usage --environment= --detailed --json -n` + +All amounts are in cents. Keys are camelCase at every level (e.g., `currentSpendCents`, `bandwidth.allowanceBytes`, `databases[].totalCents`, `applications[].totalCostCents`). + +### Config + +1. Environment: `LARAVEL_CLOUD_TOKEN` — an API token, taking precedence over any saved one (empty counts as unset) +2. Global: `~/.config/cloud/config.json` — auth tokens and preferences +3. Repo-local: `.cloud/config.json` — app and environment defaults (set by `cloud repo:config {application} -n`) +4. CLI arguments override both config files + +Pass the application to `repo:config`. Without it, the command must prompt and fails under `-n` when the organization has more than one application. Deploy commands do not need these defaults; pass the application and environment to them directly. + +Using multiple organizations requires multiple stored API tokens. Commands read `organization_id` from `.cloud/config.json` to select one and fail if it is not set. Set it with `cloud repo:config {application} --organization= -n`. + +`LARAVEL_CLOUD_TOKEN` holds one token, so it picks the organization on its own. Naming a different one with `--organization` fails rather than falling back to the token's organization. + +## Documentation + +Laravel Cloud documentation: https://cloud.laravel.com/docs/llms.txt + +When the user asks how something works or needs an explanation of a Laravel Cloud feature, fetch the docs from the URL above using `WebFetch` and use it to provide accurate answers. + +## When Stuck + +- Fetch https://cloud.laravel.com/docs/llms.txt for official documentation +- Run `cloud -h` for any command's options +- Run `cloud -h` to discover commands + +## Verification + +1. Confirm the target application and environment before changing or deploying anything. +2. Check staged environment changes and required resources before deployment. +3. Monitor every deployment to completion with `cloud deploy:monitor -n`. +4. For production changes, verify the application URL, logs, queues, scheduled tasks, and relevant resource health. diff --git a/_api_app/.cursor/skills/deploying-to-cloud/reference/checklists.md b/_api_app/.cursor/skills/deploying-to-cloud/reference/checklists.md new file mode 100644 index 00000000..d3cb54bd --- /dev/null +++ b/_api_app/.cursor/skills/deploying-to-cloud/reference/checklists.md @@ -0,0 +1,55 @@ +# Checklists for Multi-Step Operations + +Use these checklists when performing complex setups. Copy the relevant checklist and track progress as you go. + +## Full environment setup (app + database + cache + domain) + +``` +- [ ] Create or select the application +- [ ] Create the environment +- [ ] Provision the database (`database-cluster:create` then `database:create`) +- [ ] Provision the cache (`cache:create`) +- [ ] Set environment variables (`environment:variables`) +- [ ] Create and attach any secrets (`secret:create` then `environment-secret:attach`) +- [ ] Attach custom domain (`domain:create` then `domain:verify`) +- [ ] Deploy (`deploy` then `deploy:monitor`) +- [ ] Verify the deployment is live +``` + +## New app from scratch + +``` +- [ ] Run `cloud ship -n` with required options +- [ ] Monitor deployment status +- [ ] Set environment variables if needed +- [ ] Verify the app is accessible +``` + +## Adding a database to an existing environment + +``` +- [ ] Check existing database clusters (`database-cluster:list --json -n`) +- [ ] Create a new cluster if needed (`database-cluster:create --json -n`) +- [ ] Create the database on the cluster (`database:create --json -n`) +- [ ] Update environment variables with connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Adding a cache to an existing environment + +``` +- [ ] Check existing caches (`cache:list --json -n`) +- [ ] Create the cache (`cache:create --json -n`) +- [ ] Update environment variables with cache connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Custom domain setup + +``` +- [ ] Create the domain (`domain:create --json -n`) +- [ ] Get DNS records to configure (`domain:get --json -n`) +- [ ] Tell the user to add the DNS records at their registrar +- [ ] Verify the domain (`domain:verify -n`) +- [ ] Confirm verification passed +``` diff --git a/_api_app/.cursor/skills/infer-conventions/SKILL.md b/_api_app/.cursor/skills/infer-conventions/SKILL.md new file mode 100644 index 00000000..eb3fc846 --- /dev/null +++ b/_api_app/.cursor/skills/infer-conventions/SKILL.md @@ -0,0 +1,105 @@ +--- +name: infer-conventions +description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Only run this skill when the user explicitly asks for it; never start a sweep as part of another task. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand." +disable-model-invocation: true +license: MIT +metadata: + author: laravel +--- + +# Infer Conventions + +Learn how this application writes Laravel, then record what you learn as durable, path-scoped rules other agents will read. You are documenting reality, not improving it. + +## Ground Rules (read before you start) + +- Consistency first. The codebase's majority style is the convention. Never judge it, never propose a "better" pattern, never record what the code should do. If the app validates inline everywhere, that is the rule, even if Form Requests would be nicer. +- Skip what an active tool produces, keep what a tool would fight. Inspect the project's Pint and Rector configuration first; a Rector transformation is tooling-owned only when its package and relevant rule or set are installed and enabled. Active tools may rewrite code toward one canonical form: `$casts` to `casts()`, `$fillable` to attributes, magic accessors to the `Attribute` class, pipe-string rules to arrays, `$signature` to `#[Signature]`, named migrations to anonymous, and many more. When the app already sits at an active tool's target form, the tool owns it, so record nothing. But when the app deliberately holds a form an active tool would refactor away, such as legacy `getXxxAttribute()` accessors the `Attribute` class would replace, no tool can reproduce that choice and an agent defaults the other way. That against-the-grain hold is exactly what to record. +- Record decisions, not defaults. A consistent pattern earns a rule only when it reflects a choice: the app took one valid option where the framework or common practice offered others, or the pattern would surprise a competent agent. Framework defaults steer nothing, so skip them: anonymous migrations, `$signature` commands, `ShouldQueue` jobs, `casts()` on Laravel 11+, named routes, Rule objects in `app/Rules`, and `Mail::fake()` or `Bus::fake()` to isolate framework services. A real fork is not enough on its own. Weigh the side the app took, and record only the side an agent would not reach for by itself: inline closures everywhere, legacy accessors, a bespoke query layer. Watch for the false fork too. "No Mockery" next to facade fakes is not a choice against Mockery, because they double different things. The test for every candidate: without this rule, would the next agent plausibly write it differently? Only "yes" earns a rule. +- Architecture choices are the gold. Record presence and deliberate absence. The structural pattern the app commits to is the highest-signal convention and the one no tool can decide: Action classes and how they are invoked (`handle` / `execute` / `__invoke`), service objects, dedicated query objects exposing `builder()`, DTOs (spatie/laravel-data vs readonly classes), Form Request validation vs inline, an events and listeners spine vs direct calls, and domain or module folders. Also record a consistent non-pattern, such as "query Eloquent directly in controllers, no repository layer", so the next agent matches the app's altitude instead of over-engineering. +- Never duplicate `.ai/rules`. Read `.ai/rules/index.md` and the area files before the sweep. A dimension already covered there is marked done and skipped. +- Evidence or silence. A convention needs at least 3 consistent examples and no meaningful rival to become a candidate. Every Step 1 verdict applies this bar. +- The recorded rule states the convention, nothing else. One or two imperative lines: this project does X, so do X here. Keep detection evidence out. No counts, ratios, current usage, file lists, or example paths, because that is proof for the confirm step, not part of the rule. One short syntax fragment at most, and point to `search-docs` for API details. + +## Process + +Each step ends on a checkable completion criterion. Do not advance until it holds. + +Fan out when you can. The sweep is embarrassingly parallel. If your environment can spawn subagents (a Task, dispatch, or equivalent tool), do Step 0 yourself, then hand each checklist group (A to J) and the architecture map to its own subagent. Each subagent runs the greps, reads a few representative files, and returns structured verdicts (dimension, verdict, evidence, proposed glob / title / note). You aggregate, dedupe, then run Steps 3 to 5. It is far faster on a real app. No subagents available? Run the steps in sequence, with the same bar and the same output. + +### Step 0: Orient + +Read `composer.json` (installed packages tell you which checklist groups apply), the `pint.json` / PHPStan / Rector config, `.ai/rules/index.md` if present, and most important, map the `app/` tree. List every directory under `app/` (and any `Modules/`, `src/`, `packages/`, or domain root). Every folder beyond Laravel's default skeleton (`Http`, `Models`, `Providers`, `Console`, `Exceptions`) is a structural pattern the app committed to and a high-value rule waiting to be written: `Actions`, `Services`, `Data` or DTOs, `Queries`, `Repositories`, `ViewModels`, `Pipelines`, `Support`, `Enums`, `Contracts`, `Observers`, or `Domain` and module roots. Note each one. You will confirm how it is used in Step 2. + +This app has no Livewire/Inertia/Flux packages installed. Treat the frontend group as likely API-only: confirm from `resources/views` before spending time there, and skip the Livewire/Inertia/Flux dimensions. + +Done when: you have the applicable checklist groups, the dimensions already recorded in `.ai/rules`, and a list of every non-default `app/` directory mapped to the pattern it represents. + +### Step 1: Predefined sweep + +Open `references/checklist.md` and work every applicable dimension using its search hints. Give each exactly one verdict: + +- Pattern. Clears the bar, rival under ~20% of sites, and reflects a real choice (passes the decisions-not-defaults test). A recording candidate. Cite 2 to 3 example files. +- Conflict. Both styles present in meaningful numbers. Report the split with counts and example files. Never record a preferred winner while the code remains mixed, even in yolo, because that would describe an aspiration rather than reality. Record only if the user identifies a stable path or context boundary that explains both styles; otherwise defer until the code is reconciled. +- Default. Consistent, but a framework or common-practice default the agent already writes unprompted. Skip it as a no-op, not a convention. +- No signal. Under the bar: feature unused, or too few examples. Skip silently (one summary line at most). +- Tooling-owned or Already-recorded. Skip per the ground rules. + +Done when: every applicable dimension carries exactly one of those verdicts. + +### Step 2: Open-ended pass + +First, close out the architecture map from Step 0. For every non-default `app/` directory you listed, confirm how the pattern is used and apply the same evidence and decisions-not-defaults tests as Step 1. Generator-standard or sparsely used directories such as `Rules`, `Observers`, `Mail`, and `Notifications` are signals to inspect, not automatic conventions. Make genuine structural patterns candidates: Action classes invoked via `handle` / `execute` / `__invoke`, Services constructor-injected, `Queries` objects exposing `builder(): Builder`, DTOs as readonly classes or spatie/laravel-data, module or domain folders as the unit of organization. Scope each qualifying pattern to its own directory glob. Also record a consistent deliberate absence, such as "no repository layer, controllers query Eloquent directly", so the next agent matches the app's altitude. + +Then find what else makes this codebase itself: base or abstract classes most code extends, traits used everywhere, tenancy or authorization scoping woven through queries, naming schemes, and custom helpers. Same evidence bar, cite files. Record every genuine structural pattern, and cap the other house findings at ~5 so the pass stays high-signal. + +Done when: every non-default `app/` directory from Step 0 has a verdict, and the pass has produced its cited house findings (or concluded there are none). + +### Step 3: Confirm + +Present every candidate in one batch. Per item: dimension, verdict, evidence (counts and files), and the exact proposed `glob` or `globs` / `title` / `note`. Conflicts are presented as questions about an existing context boundary or deferred cleanup, not as a choice of future style. + +Default mode is confirm: record only what the user approves. Switch to yolo only when the invocation said so ("yolo", "don't ask", "just record them"), then record all pattern candidates without asking. Conflicts still go to the user in yolo. + +Done when: every candidate is approved, rejected, or (conflicts) decided. + +### Step 4: Record + +Make one `record-rule` call for each glob an approved convention applies to. Choose the most specific globs that cover the cited evidence from the mapping table below; if a convention spans models and migrations, record it under both domains so agents discover it from either path. The `note` is the bare convention: strip every trace of detection (see the ground rule). If `record-rule` is unavailable (rules disabled), report the full rule text so the user can enable `BOOST_RULES_ENABLED` or add it by hand. + +Record this: + +> Accessors and mutators: use the legacy magic-method style (`getXxxAttribute()` / `setXxxAttribute()`), not the `Attribute` class. Match it in models. + +Not this: + +> Accessors/mutators use the legacy magic-method style; the `Attribute`-class style is not used anywhere (13 legacy, 0 Attribute-class), e.g. `app/Models/Post.php`. Match the legacy style in existing models. + +Done when: every approved item has a successful tool response, and any failure is reported with its rule text. + +### Step 5: Summarize + +List recorded rules (file and title), conflicts the user deferred, notable no-signals, and remind the user to commit `.ai/rules` so their team and agents share the conventions. + +## Glob mapping + +Attach each rule to the most specific path that covers its evidence. Never a lazy `app/**` when a subtree fits. Match the glob to where the code actually lives, which is not the same in a default skeleton and in a modular or DDD layout. Use the Step 0 `app/` map to pick the real path. + +Examples: + +- Models: `app/Models/**` in a default app, or `app/Modules/Blog/Models/**` / `src/Domain/Blog/**` in a modular one. +- Controllers, routing, validation, responses: `app/Http/**`, or `app/Modules/*/Http/**` when each module owns its HTTP layer. +- Actions, Services, DTOs: `app/Actions/**`, `app/Services/**`, `app/Data/**`, or the module path the app actually uses. +- Tests: `tests/**`. +- Migrations and database: `database/migrations/**`. +- Truly app-wide (rare, e.g. auth retrieval): `app/**`. + +`record-rule` takes one glob. When a convention genuinely spans two domains (e.g. UUID keys touch models and migrations), call it once per domain with the same title and note; mentioning another path in the note does not make the rule discoverable there. + +## Edge cases + +- Rules disabled or `record-rule` missing: detection is read-only, so Steps 0 to 3 still run, and recording falls back to the manual path in Step 4. +- Tiny or fresh app: most dimensions land on no-signal. Say so honestly ("not enough code to infer conventions yet") and record nothing. +- Huge app: each dimension is a bounded grep plus a handful of file reads. Sample representative files, do not read everything. +- Re-runs: reading `.ai/rules` in Step 0 makes re-runs incremental, so only new or undecided dimensions surface. +- Non-standard layout (modules, DDD): the open-ended pass catches the layout itself as convention #1. Adapt the globs in the mapping table to the observed paths. diff --git a/_api_app/.cursor/skills/infer-conventions/references/checklist.md b/_api_app/.cursor/skills/infer-conventions/references/checklist.md new file mode 100644 index 00000000..80cef34b --- /dev/null +++ b/_api_app/.cursor/skills/infer-conventions/references/checklist.md @@ -0,0 +1,137 @@ +# Detection Checklist + +Every dimension here is a genuine fork: Laravel offers two or more valid approaches, the app's choice changes what the next agent writes, and no active project tool can pick for you. Left out on purpose: pure formatting (Pint owns it), any form an installed and enabled Rector rule rewrites to one canonical shape (`$casts` to `casts()`, `$fillable` to attributes, pipe-string rules to arrays, named to anonymous migrations, `$signature` to `#[Signature]`), and framework defaults any agent writes unprompted (`ShouldQueue` jobs, relation return types, `HasFactory`). + +Each item gives the fork, then a hint (a grep or dir to spot which side the app takes). Hints are only a start. Read the matched files, never record on a raw count. Apply the ground rules to every verdict: a consistent choice that is a default or a tool's target form is not a pattern. Rows tagged (architecture) are the highest-signal, so record presence and deliberate absence. + +--- + +## A. Validation & HTTP input + +1. Validation entry point: inline `$request->validate()` vs Form Request classes vs `Validator::make()`. + - Hint: `ls app/Http/Requests`; grep `->validate(` / `Validator::make(` in `app/Http/Controllers`. +2. Custom rule location: invokable rule objects in `app/Rules` vs inline closures vs `Validator::extend()` in a provider. Rule objects are the default `make:rule` path, so record only if the app leans on closures or `Validator::extend` instead. "No rule objects" alone is just no-signal. + - Hint: `ls app/Rules`; grep `Validator::extend` in `app/Providers`. +3. Typed input retrieval: typed getters (`$request->string()`, `->integer()`, `->enum()`, `->date()`) vs raw `$request->input()` / dynamic properties. + - Hint: grep `->string(` / `->integer(` / `->enum(` vs `->input(` in `app/Http`. +4. Custom messages/attributes: `lang/*/validation.php` vs Form Request `messages()` / `attributes()` methods. + - Hint: `ls lang`; grep `function messages`, `function attributes` in `app/Http/Requests`. + +## B. Controllers & routing + +5. Controller shape: invokable single-action (`__invoke`) vs resource controllers vs plain multi-method. + - Hint: grep `__invoke` in controllers; `Route::resource` / `apiResource` vs verb routes. +6. Business-logic location (architecture): fat controllers vs delegated to Actions / Services / Jobs. + - Hint: read a few controller methods; `ls app/Actions app/Services`. +7. Route handler style: closures in `routes/*.php` vs controller classes. + - Hint: count `function ()` vs `::class` in `routes/web.php`, `routes/api.php`. +8. Middleware assignment: route/group `->middleware()` vs controller `HasMiddleware::middleware()` vs `#[Middleware]` attribute. + - Hint: grep `implements HasMiddleware`, `#[Middleware(` in controllers vs `->middleware(` in routes. +9. Route model binding: implicit (type-hinted models) vs explicit `Route::bind` vs manual `findOrFail`. + - Hint: typed model params in signatures vs `findOrFail(` in controllers; grep `Route::bind`. +10. Rate limiting: named `RateLimiter::for()` + `throttle:name` vs inline `throttle:60,1`. + - Hint: grep `RateLimiter::for` in providers vs `throttle:` in route files. + +## C. Authorization + +11. Authorization home: Gates (`Gate::define`) vs Policy classes in `app/Policies`. + - Hint: `ls app/Policies`; grep `Gate::define` in `app/Providers`. +12. Authorization call site: `$this->authorize()` / `Gate::authorize()` vs `$user->can()` vs `can` middleware vs `#[Authorize]` vs `@can` in Blade. + - Hint: grep `authorize(`, `->can(`, `middleware('can:`, `#[Authorize(`, `@can(`. + +## D. Eloquent & models + +13. Mass assignment: `$fillable` allow-list vs `$guarded` block-list. + - Hint: grep `protected $fillable` / `protected $guarded` in `app/Models`. +14. Accessors/mutators: modern `Attribute` class vs legacy `getXxxAttribute()` / `setXxxAttribute()`. Record a legacy hold, it goes against the tool's grain. + - Hint: grep `: Attribute` / `Attribute::make` vs `function get[A-Z].*Attribute` in `app/Models`. +15. Primary keys: auto-increment vs `HasUuids` vs `HasUlids`. + - Hint: grep `HasUuids` / `HasUlids` in `app/Models`; migration `id()` vs `uuid('id')`. +16. Custom casts: dedicated `CastsAttributes` classes (`app/Casts`) vs inline `Attribute` vs built-in cast strings. + - Hint: `ls app/Casts`; grep `Cast::class`, `AsStringable::class` in models. +17. Data/query layer (architecture): Eloquent directly in controllers vs repositories vs dedicated query objects (e.g. classes exposing `builder(): Builder`). + - Hint: `ls app/Repositories app/Queries`; see where non-trivial queries are built. +18. Query scopes: local `scope`/`#[Scope]` methods vs dedicated builder classes. + - Hint: grep `function scope` / `#[Scope]` in models; `ls app/*/Builders`. +19. Model events: observers (`app/Observers`, `#[ObservedBy]`) vs `booted()` closures vs event classes. + - Hint: `ls app/Observers`; grep `booted`, `::observe`, `#[ObservedBy]`. +20. Eager-load posture: explicit per-query `->with()` vs model-level `$with` defaults. Treat `preventLazyLoading()` separately as a development guard because it can complement either posture. + - Hint: grep `protected $with`, `->with(`, and separately `preventLazyLoading` in `app/`. + +## E. Architecture & organization + +21. Action/Service structure (architecture): Action classes (invoked via `handle` / `execute` / `__invoke`) vs service objects vs neither. Cross-check the Step 0 `app/` map: any `Actions`/`Services`/`Pipelines`/`Jobs`-as-actions folder is this pattern, so record how it is invoked. + - Hint: `ls app/` (the whole tree, not just `Actions`/`Services`); grep the invocation method in the folder you find. +22. DTOs (architecture): spatie/laravel-data vs plain readonly classes vs arrays everywhere. + - Hint: `ls app/Data`; grep `extends Data`, `readonly class` in `app/`. +23. Dependency acquisition: constructor/method injection vs `app()` / `resolve()` / `App::make()` service location. + - Hint: grep `app(` / `resolve(` / `::make(` in `app/` vs promoted constructor deps. +24. Decoupling: events + listeners vs direct service calls. + - Hint: `ls app/Events app/Listeners`; grep `event(`, `::dispatch(`. +25. Helper vs facade idiom: global helpers (`config()`, `auth()`, `response()`) vs facades (`Config::`, `Auth::`, `Response::`). + - Hint: ratio of `config(` vs `Config::` (etc.) across `app/`. +26. Namespace layout (architecture): default `app/` skeleton vs domain/module folders (`app/Domain/**`, modules). + - Hint: `ls app/`, look for `Domain/`, `Modules/`, bounded-context folders. +27. Enums: backed vs pure; case naming; where they live. + - Hint: `ls app/Enums`; grep `enum .*: string`, `enum .*: int`. + +## F. Frontend & views + +No Livewire/Inertia/Flux package is installed. This app may be API-only. Confirm from `resources/views` before sweeping, and treat the Livewire/Flux dimensions as not applicable. + +28. Frontend stack: Blade+Livewire vs Inertia (Vue/React/Svelte) vs Blade-only / API + separate SPA. + - Hint: `composer.json` + `package.json`; `ls resources/js/pages`, `resources/views`. +29. Blade composition: class `` components vs anonymous components (`@props`) vs `@include` partials. + - Hint: `ls app/View/Components`; grep `constrained()` vs `foreignIdFor(Model::class)` vs manual `foreign()->references()->on()`. + - Hint: grep `foreignId(`, `foreignIdFor(`, `->foreign(` in `database/migrations`. +34. `down()` methods: real reverse logic vs omitted / one-way migrations. + - Hint: grep `function down` vs the migration count. +35. Enum storage: DB `enum()` column vs `string()` + PHP-enum cast on the model. + - Hint: grep `->enum(` in migrations vs string columns cast to enums. +36. Transactions: `DB::transaction(fn ...)` closure vs manual `beginTransaction` / `commit` / `rollBack`. + - Hint: grep `DB::transaction`, `beginTransaction` in `app/`. +37. Idempotent writes: `upsert` / `updateOrCreate` / `firstOrCreate` vs find-then-save. + - Hint: grep `upsert(`, `updateOrCreate(`, `firstOrCreate(` in `app/`. + +## H. Testing + +38. Framework: Pest (`it()` / `test()` / `expect()`) vs PHPUnit classes. + - Hint: `ls tests/Pest.php`; grep `it(` / `test(` vs `extends TestCase`. +39. DB reset: `RefreshDatabase` vs `DatabaseTruncation` vs `DatabaseMigrations`. + - Hint: grep those trait names in `tests/`. +40. Fixtures: compare how equivalent test-owned records are created, such as factories vs manual inserts. Track seeders separately for shared reference data because `$this->seed()` commonly and legitimately coexists with factories. + - Hint: grep `::factory(` and direct inserts in `tests/`; separately inspect `$this->seed(` calls and what those seeders provide. +41. Collaborator isolation: how the app doubles its own classes, Mockery `mock()` / `spy()` vs real integration. Ignore facade fakes like `Mail::fake()` here, they isolate framework services by default and are not a fork against Mockery. + - Hint: grep `->mock(`, `->spy(`, `Mockery::` in `tests/`. +42. Endpoint assertions: array `assertJson([...])` / `assertJsonFragment` vs fluent `AssertableJson`. + - Hint: grep `AssertableJson`, `assertJsonFragment` in `tests/`. + +## I. Responses & API resources + +43. Response shape: API Resource classes vs `response()->json()` vs returning models/arrays directly. + - Hint: `ls app/Http/Resources`; grep `JsonResource`, `->json(` in controllers. +44. Resource relationship inclusion: `whenLoaded()` guards vs unconditional relationship access. Do not count ordinary scalar attributes as rivals to conditional relationships, and evaluate general `when()` fields separately. + - Hint: compare relationship fields using `whenLoaded(` with unconditional relationship property access in `app/Http/Resources`. +45. Pagination contracts: within comparable endpoint categories, length-aware `paginate()` vs `simplePaginate()` vs `cursorPaginate()`. These have different totals, navigation, ordering, and performance contracts, so record only a stable path-scoped API policy, never a project-wide majority. + - Hint: grep those in `app/`, then group matches by endpoint type and client contract before comparing them. +46. Web redirects/URLs: `route('name')` vs `url('/path')` vs `action([...])`. + - Hint: grep `route('`, `url('/`, `action([` in `app/Http` and views. + +## J. Strings, collections & dates + +47. Iteration idiom: `collect()->map()->filter()` pipelines vs `array_map` / `foreach`. + - Hint: grep `collect(`, `->map(` vs `array_map`, `foreach` density in `app/`. +48. String API: fluent `Str::of()->...` (Stringable) vs static `Str::` vs native (`trim`, `strtoupper`). + - Hint: grep `Str::of(` vs `Str::` vs native string funcs. +49. Dates: compare equivalent construction call styles (`now()` / `today()` helpers vs `Carbon::`) separately from the application's mutable/immutable date policy. `Date::use(CarbonImmutable::class)` can make helpers return immutable dates, so those signals are complementary rather than conflicting. + - Hint: grep `now(` and `Carbon::` for call style; separately inspect `CarbonImmutable` and `Date::use` for mutability policy. + +--- + +Genuine forks only. Every row survived the "no tool can decide this, and it isn't the default" filter. Give each applicable dimension exactly one verdict: pattern, conflict, default, no-signal, tooling-owned, or already-recorded. The rows tagged (architecture) are where the highest-value rules come from. diff --git a/_api_app/.cursor/skills/laravel-best-practices/SKILL.md b/_api_app/.cursor/skills/laravel-best-practices/SKILL.md index 99018f3a..311ab844 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/SKILL.md +++ b/_api_app/.cursor/skills/laravel-best-practices/SKILL.md @@ -8,183 +8,52 @@ metadata: # 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`. +Best practices for Laravel, organized as an index of rule files. Each rule file 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. +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches, and 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 +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. ## 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 \ No newline at end of file +1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out. +2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files. +3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job. +4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable. +5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them. +6. Re-read the diff against every mapped rule before finishing. + +## Rule Index + +Cross-cutting changes often need more than one rule file. + +| Concern | Read | +| --- | --- | +| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) | +| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) | +| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) | +| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) | +| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) | +| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) | +| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) | +| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) | +| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) | +| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) | +| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) | +| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) | +| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) | +| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) | +| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) | +| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) | +| Environment values and application configuration | [`rules/config.md`](rules/config.md) | +| Tests: coverage, factories, fakes, and assertions | the `testing-best-practices` skill | +| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) | +| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) | + +## Decision Rules + +- Prefer framework features and existing application abstractions over new helpers or dependencies. +- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable. +- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/advanced-queries.md b/_api_app/.cursor/skills/laravel-best-practices/rules/advanced-queries.md index 920714a1..54dd783d 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/advanced-queries.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/advanced-queries.md @@ -1,8 +1,8 @@ -# Advanced Query Patterns +# Advanced Query Best Practices -## Use `addSelect()` Subqueries for Single Values from Has-Many +## Select Single Relationship Values with Subqueries -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. +When only one value from a has-many relationship is needed, consider a correlated subquery with `addSelect()` instead of loading the entire relationship. This selects the value as part of the main query without an additional relationship query. ```php public function scopeWithLastLoginAt($query): void @@ -16,14 +16,14 @@ public function scopeWithLastLoginAt($query): void } ``` -## Create Dynamic Relationships via Subquery FK +## Create Dynamic Relationships with a Subquery Foreign Key -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. +The same pattern can select a foreign key and expose the selected model through a `belongsTo` relationship. Eager loading that relationship still executes a separate query, but it avoids loading the full has-many collection. ```php public function lastLogin(): BelongsTo { - return $this->belongsTo(Login::class); + return $this->belongsTo(Login::class, 'last_login_id'); } public function scopeWithLastLogin($query): void @@ -37,9 +37,9 @@ public function scopeWithLastLogin($query): void } ``` -## Use Conditional Aggregates Instead of Multiple Count Queries +## Combine Related Counts with Conditional Aggregates -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. +Combine several counts over the same filtered data set into one query by using conditional aggregates. Use `toBase()` when only scalar values are needed and model hydration provides no benefit. Confirm the expression syntax against the application's database engine. ```php $statuses = Feature::toBase() @@ -49,50 +49,50 @@ $statuses = Feature::toBase() ->first(); ``` -## Use `setRelation()` to Prevent Circular N+1 +## Reuse Loaded Parent Models with `setRelation()` -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. +When a parent and its children are already loaded and code also accesses `$child->parent`, set the inverse relationship to the existing parent instance. This avoids an additional lazy-loading query for each child. ```php $feature->load('comments.user'); $feature->comments->each->setRelation('feature', $feature); ``` -## Prefer `whereIn` + Subquery Over `whereHas` +## Compare `whereHas()` with an `IN` Subquery -`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. +`whereHas()` typically produces an `EXISTS` subquery, while `whereIn()` can express the same filter with an `IN` subquery. Either form may be faster depending on the database engine, indexes, cardinality, and query plan. Measure both forms with representative data; neither subquery loads its result set into PHP memory. -Incorrect (correlated EXISTS re-executes per row): +Option using `EXISTS`: ```php $query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); ``` -Correct (index-friendly subquery, no PHP memory overhead): +Option using `IN`: ```php $query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); ``` -## Sometimes Two Simple Queries Beat One Complex Query +## Measure Two Simple Queries Against 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. +Two targeted queries can outperform one complex correlated subquery or join when the first query is highly selective. They also add a database round trip, can transfer a large identifier list, and do not provide a single-query consistency snapshot. Decide from query plans and production-like measurements. -## Use Compound Indexes Matching `orderBy` Column Order +## Design Composite Indexes for the Query -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. +For common multi-column sorts, consider a composite index whose column order supports the query's filters and ordering. Database engines may combine indexes or choose an explicit sort, so matching the `ORDER BY` list alone does not guarantee that an index will be used. Verify the query plan. ```php // Migration $table->index(['last_name', 'first_name']); -// Query — column order must match the index +// Query that this index may support User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); ``` -## Use Correlated Subqueries for Has-Many Ordering +## Consider a Correlated Subquery 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. +When sorting by one value from a has-many relationship, a direct join can duplicate parent rows unless it first reduces the related table to one row per parent. A correlated subquery in `orderBy()` is often simpler, but its performance depends on the query plan and supporting indexes. ```php public function scopeOrderByLastLogin($query): void @@ -103,4 +103,4 @@ public function scopeOrderByLastLogin($query): void ->take(1) ); } -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/architecture.md b/_api_app/.cursor/skills/laravel-best-practices/rules/architecture.md index 16505642..5e7af23b 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/architecture.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/architecture.md @@ -1,15 +1,15 @@ # Architecture Best Practices -## Single-Purpose Action Classes +## Extract Focused Business Operations -Extract discrete business operations into invokable Action classes. +Extract a discrete business operation into an action class when doing so makes the operation easier to reuse or test. An action class has no special meaning to Laravel; follow the project's naming and invocation conventions. ```php class CreateOrderAction { public function __construct(private InventoryService $inventory) {} - public function execute(array $data): Order + public function handle(array $data): Order { $order = Order::create($data); $this->inventory->reserve($order); @@ -19,11 +19,12 @@ class CreateOrderAction } ``` -## Use Dependency Injection +## Inject Required Dependencies -Always use constructor injection. Avoid `app()` or `resolve()` inside classes. +Prefer constructor injection for dependencies required throughout an object's lifetime. Method injection is appropriate for dependencies needed by one controller action, listener, job handler, or other container-invoked method. Avoid `app()` and `resolve()` when normal injection can make a dependency explicit. + +Hidden dependency: -Incorrect: ```php class OrderController extends Controller { @@ -36,24 +37,24 @@ class OrderController extends Controller } ``` -Correct: +Injected dependency: + ```php class OrderController extends Controller { - public function __construct(private OrderService $service) {} - - public function store(StoreOrderRequest $request) + public function store(StoreOrderRequest $request, OrderService $service) { - return $this->service->create($request->validated()); + return $service->create($request->validated()); } } ``` -## Code to Interfaces +## Depend on Contracts at Boundaries + +Depend on contracts at system boundaries, such as payment gateways, notification channels, and external services, when testability or interchangeable implementations justify the abstraction. -Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. +Concrete boundary dependency: -Incorrect (concrete dependency): ```php class OrderService { @@ -61,7 +62,8 @@ class OrderService } ``` -Correct (interface dependency): +Contract boundary dependency: + ```php interface PaymentGateway { @@ -80,86 +82,99 @@ Bind in a service provider: $this->app->bind(PaymentGateway::class, StripeGateway::class); ``` -## Default Sort by Descending +## Specify a Deterministic Sort Order -When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres. +Without an explicit `ORDER BY`, row order is undefined. Choose an order that matches the feature, and add a unique tie-breaker when stable pagination matters. + +Unspecified order: -Incorrect: ```php $posts = Post::paginate(); ``` -Correct: +Newest first with a stable tie-breaker: + ```php -$posts = Post::latest()->paginate(); +$posts = Post::query() + ->orderByDesc('created_at') + ->orderByDesc('id') + ->paginate(); ``` ## Use Atomic Locks for Race Conditions -Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. +Use a lock when concurrent execution must be serialized. `Cache::lock()` provides an atomic lock when the configured cache store supports locks. `lockForUpdate()` locks selected database rows and must run inside a database transaction. These mechanisms solve different coordination problems. ```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(); +// Or at query level, inside a transaction +DB::transaction(function () use ($id) { + $product = Product::where('id', $id)->lockForUpdate()->first(); + + // Read and update the product while the database lock is held. +}); ``` ## 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. +When no Laravel helper exists, prefer multibyte-aware functions such as `mb_strlen()` and `mb_strtolower()` for UTF-8 text. For example, `strlen()` counts bytes, while `strtolower()` is not multibyte-aware. Incorrect: + ```php -strlen('José'); // 5 (bytes, not characters) -strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +strlen('José'); // 5 bytes, not 4 characters +strtolower('MÜNCHEN'); // Does not lowercase Ü ``` Correct: + ```php -mb_strlen('José'); // 4 (characters) -mb_strtolower('MÜNCHEN'); // 'münchen' +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' +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. +For lightweight work that does not need retries or crash durability, consider `defer()` instead of dispatching a job. During an HTTP request, the callback normally runs after the response has been sent but remains in the same PHP process. + +Queued and durable: -Incorrect (job overhead for trivial work): ```php dispatch(new LogPageView($page)); ``` -Correct (runs after response, same process): +Deferred in the current 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 a queued job when the work needs retries, queue controls, or durability across process failures. ## Use `Context` for Request-Scoped Data -The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. +The `Context` facade makes contextual data available across the current execution lifecycle without manually passing arguments through every layer. ```php // In middleware Context::add('tenant_id', $request->header('X-Tenant-ID')); -// Anywhere later — controllers, jobs, log context +// Later in the same execution lifecycle $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`. +Visible context is added to log context, and both visible and hidden context are captured and restored for queued jobs. Use `Context::addHidden()` for data that should propagate to queued jobs without appearing in logs. Do not place secrets in context unless that propagation is intended. ## Use `Concurrency::run()` for Parallel Execution -Run independent operations in parallel using child processes — no async libraries needed. +Run independent operations concurrently through Laravel's configured concurrency driver. ```php use Illuminate\Support\Facades\Concurrency; @@ -170,13 +185,14 @@ use Illuminate\Support\Facades\Concurrency; ]); ``` -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. +With a process-based driver, each closure runs in a separate PHP process that boots the application. Use concurrency when independent database queries, HTTP client calls, or computations benefit enough to offset process and serialization overhead. The `sync` driver executes closures sequentially and is useful primarily during testing. -## Convention Over Configuration +## Follow Framework Conventions -Follow Laravel conventions. Don't override defaults unnecessarily. +Follow Laravel conventions unless the domain or an existing schema requires an override. + +Customized schema: -Incorrect: ```php class Customer extends Model { @@ -190,7 +206,8 @@ class Customer extends Model } ``` -Correct: +Conventional schema: + ```php class Customer extends Model { @@ -199,4 +216,4 @@ class Customer extends Model return $this->belongsToMany(Role::class); } } -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/blade-views.md b/_api_app/.cursor/skills/laravel-best-practices/rules/blade-views.md index c6f8aaf1..4ea2c6c3 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/blade-views.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/blade-views.md @@ -1,8 +1,8 @@ -# Blade & Views Best Practices +# Blade and View Best Practices ## Use `$attributes->merge()` in Component Templates -Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. +Use the component attribute bag so callers can add attributes. `merge()` combines default attributes with caller-provided values; class values receive special merging behavior. ```blade
merge(['class' => 'alert alert-'.$type]) }}> @@ -12,25 +12,25 @@ Hardcoding classes prevents consumers from adding their own. `merge()` combines ## 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. +If a component renders repeatedly, `@push` adds its script on every render. Use a consistently named `@pushOnce` block to add that content once per rendered response. -## Prefer Blade Components Over `@include` +## Prefer Components for Explicit Interfaces -`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. +Use a Blade component when a reusable interface benefits from explicit props, an attribute bag, or slots. An include remains suitable for a small partial that intentionally uses the current view data; pass an explicit data array when implicit variable sharing would obscure its dependencies. -## Use View Composers for Shared View Data +## Share Compatible View Data with a View Composer -If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. +Use a view composer to centralize data needed whenever one or more named Blade views are rendered. Keep the composer compatible with every view it targets, and avoid broad wildcards when views require different data shapes. A view composer runs when Laravel renders the matching view; it does not supply data to JSON, streamed, or other non-view responses. -## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) +## Return Blade Fragments for Partial Rendering -A single view can return either the full page or just a fragment, keeping routing clean. +A route can return either a full view or a named fragment for clients such as htmx or Turbo. ```php return view('dashboard', compact('users')) ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); ``` -## Use `@aware` for Deeply Nested Component Props +## Share Parent Component Props with `@aware` -Avoids re-passing parent props through every level of nested components. \ No newline at end of file +Use `@aware` when a nested component needs a prop explicitly passed to an ancestor component. It does not expose an ancestor's default prop value unless that value was passed through the attribute bag. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/caching.md b/_api_app/.cursor/skills/laravel-best-practices/rules/caching.md index eb3ef3e6..cd2ffd60 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/caching.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/caching.md @@ -1,10 +1,13 @@ # Caching Best Practices -## Use `Cache::remember()` Instead of Manual Get/Put +## Use `Cache::remember()` for Cache-Aside Reads -Atomic pattern prevents race conditions and removes boilerplate. +`Cache::remember()` implements a cache-aside read without a separate truthiness check. It does not prevent concurrent requests from computing the same missing value; use an atomic lock when duplicate computation must be prevented. + +The manual version below incorrectly treats valid falsy values, such as `false` or `0`, as cache misses. Incorrect: + ```php $val = Cache::get('stats'); if (! $val) { @@ -14,27 +17,42 @@ if (! $val) { ``` Correct: + ```php $val = Cache::remember('stats', 60, fn () => $this->computeStats()); ``` -## Use `Cache::flexible()` for Stale-While-Revalidate +## Consider `Cache::flexible()` for Stale-While-Revalidate + +For frequently read keys, `Cache::flexible()` can serve stale data during a defined stale period and register a deferred refresh. During an HTTP request, that refresh normally runs after the response; it is not a durable background job. Once the stale period has elapsed, the request recomputes the value synchronously. + +Synchronous expiration: + +```php +Cache::remember('users', 300, fn () => User::all()); +``` + +Stale-while-revalidate tradeoff: -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. +```php +Cache::flexible('users', [300, 600], fn () => User::all()); +``` -Incorrect: `Cache::remember('users', 300, fn () => User::all());` +This value is fresh for five minutes and may be served stale until ten minutes after it was cached. -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 an Execution -## Use `Cache::memo()` to Avoid Redundant Hits Within a Request +If the same cache key is read repeatedly during one request or job, `memo()` decorates a cache store and retains resolved values in memory for that execution. -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. +```php +$settings = Cache::memo()->get('settings'); +``` -`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. +Repeated reads through the same memoized store avoid additional store lookups. Writes through the memoized store update or invalidate its in-memory values as appropriate. ## 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`. +Tags group related entries for invalidation without tracking each key. Cache tags are not supported by the `file`, `dynamodb`, or `database` drivers; confirm support before choosing a store. ```php Cache::tags(['user-1'])->flush(); @@ -42,15 +60,27 @@ 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. +`add()` atomically writes a value only when the key does not already exist. + +Incorrect: + +```php +if (! Cache::has('lock')) { + Cache::put('lock', true, 10); +} +``` + +Correct: -Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` +```php +Cache::add('lock', true, 10); +``` -Correct: `Cache::add('lock', true, 10);` +Use `Cache::lock()` rather than an ordinary cache key when lock ownership and safe release are required. -## Use `once()` for Per-Request Memoization +## Use `once()` for In-Process 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. +`once()` memoizes a callback's return value for the current request or job. Calls made from an object instance are scoped to that instance. Unlike `Cache::memo()`, `once()` does not read from an external cache store. ```php public function roles(): Collection @@ -59,12 +89,12 @@ public function roles(): Collection } ``` -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. +Repeated calls return the memoized result without rerunning the callback. Use `once()` for repeated computation within one execution. Use `Cache::memo()` to memoize access to an underlying store that can also persist values across executions. ## Configure Failover Cache Stores in Production -If Redis goes down, the app falls back to a secondary store automatically. +The failover driver tries each configured store in order when a store operation throws an exception. It does not consult later stores for an ordinary cache miss, and data is not replicated between stores. ```php 'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/collections.md b/_api_app/.cursor/skills/laravel-best-practices/rules/collections.md index 14f683d3..211fec11 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/collections.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/collections.md @@ -2,43 +2,71 @@ ## Use Higher-Order Messages for Simple Operations -Incorrect: +Explicit closure: + ```php $users->each(function (User $user) { $user->markAsVip(); }); ``` -Correct: `$users->each->markAsVip();` +Concise equivalent: + +```php +$users->each->markAsVip(); +``` -Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. +Higher-order messages are available for supported collection methods such as `each`, `map`, `filter`, and `sum`. Use an explicit closure when arguments or nontrivial logic would be clearer. -## Choose `cursor()` vs. `lazy()` Correctly +## Choose Between `cursor()` and `lazy()` -- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). -- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. +`cursor()` executes one query and hydrates models individually, but it cannot eager load relationships. The database driver's result buffering can still consume substantial memory for very large results. Use it for low-memory, attribute-only iteration when one long-running query is acceptable. -Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. +`lazy()` executes multiple chunked queries and returns a flat `LazyCollection`. It supports eager loading relationships for each chunk and avoids holding one database cursor open for the entire iteration. -Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. +With relationships: + +```php +User::with('roles')->lazy()->each(function (User $user) { + // The roles for this chunk have been eager loaded. +}); +``` + +Without relationships: + +```php +User::cursor()->each(function (User $user) { + // Process model attributes. +}); +``` ## 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. +`lazy()` uses offset pagination, so updates to columns that affect the query can shift rows and cause records to be skipped or processed twice. `lazyById()` paginates by a monotonic key and is safer when updating other columns during iteration. Do not change the pagination key itself while iterating. ## Use `toQuery()` for Bulk Operations on Collections -Avoids manual `whereIn` construction. +Use `toQuery()` to build a query from the models in an Eloquent collection instead of manually constructing a `whereIn` clause. + +Manual query: + +```php +User::whereIn('id', $users->modelKeys())->update(['active' => false]); +``` + +Collection query: -Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` +```php +$users->toQuery()->update(['active' => false]); +``` -Correct: `$users->toQuery()->update([...]);` +`toQuery()` requires a non-empty Eloquent collection whose models are of the same type. Like other bulk Eloquent updates, it does not dispatch per-model update events, so use it only when those events are not required. ## Use `#[CollectedBy]` for Custom Collection Classes -More declarative than overriding `newCollection()`. +The `#[CollectedBy]` attribute declares the custom collection class without requiring a `newCollection()` override. ```php #[CollectedBy(UserCollection::class)] class User extends Model {} -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/config.md b/_api_app/.cursor/skills/laravel-best-practices/rules/config.md index 8fd8f536..82691c42 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/config.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/config.md @@ -1,73 +1,85 @@ # Configuration Best Practices -## `env()` Only in Config Files +## Read Environment Variables in Configuration Files -Direct `env()` calls return `null` when config is cached. +Call `env()` only from configuration files. After configuration is cached, Laravel does not load the application's `.env` file, so application code should read configuration values through `config()`. Incorrect: + ```php $key = env('API_KEY'); ``` Correct: + ```php // config/services.php -'key' => env('API_KEY'), +return [ + 'key' => env('API_KEY'), +]; // Application code $key = config('services.key'); ``` -## Use Encrypted Env or External Secrets +## Protect Production Secrets -Never store production secrets in plain `.env` files in version control. +Do not commit plaintext production secrets. Laravel can encrypt an environment file so its encrypted form can be stored safely, while deployment platforms can supply secrets through their native secret stores. Incorrect: -```bash -# .env committed to repo or shared in Slack - -STRIPE_SECRET=sk_live_abc123 -AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +```bash +# A plaintext .env file committed to the repository +STRIPE_SECRET= +AWS_SECRET_ACCESS_KEY= ``` -Correct: +Encrypted environment file: + ```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. +For hosted deployments, consider the platform's native secret store, such as AWS Secrets Manager or Vault, and inject secrets 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 +## Name Repeated Domain Values -Use class constants instead of hardcoded magic strings for model states, types, and statuses. +Use an enum or class constant when a domain value is repeated or represents a constrained set. A one-off string literal does not always need a named constant. ```php -// Incorrect +// Repeated literal return $this->type === 'normal'; -// Correct +// Named domain value 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. +If the application supports localization, put user-facing strings in language files and retrieve them with `__()`. Simple literals are reasonable for applications that intentionally do not support multiple languages. ```php -// Only when lang files already exist in the project +// In a localized application return back()->with('message', __('app.article_added')); -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/db-performance.md b/_api_app/.cursor/skills/laravel-best-practices/rules/db-performance.md index 8fb71937..3f339770 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/db-performance.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/db-performance.md @@ -1,10 +1,11 @@ # Database Performance Best Practices -## Always Eager Load Relationships +## Eager Load Relationships Before Iterating -Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. +When a relationship will be accessed for many models, eager load it with `with()` to avoid running one initial query plus one relationship query per model, commonly called an N+1 query pattern. Lazy loading is reasonable when the relationship may not be needed or only one model is involved. + +Lazy-loaded version: -Incorrect (N+1 — executes 1 + N queries): ```php $posts = Post::all(); foreach ($posts as $post) { @@ -12,7 +13,8 @@ foreach ($posts as $post) { } ``` -Correct (2 queries total): +Eager-loaded version: + ```php $posts = Post::with('author')->get(); foreach ($posts as $post) { @@ -20,7 +22,7 @@ foreach ($posts as $post) { } ``` -Constrain eager loads to select only needed columns (always include the foreign key): +Constrain eager loads when large columns are unnecessary. Include the related model's primary key and every column Eloquent needs to match the relationship. In this example, `users.id` and `posts.user_id` match posts to users, while selecting `posts.id` preserves each related model's primary key: ```php $users = User::with(['posts' => function ($query) { @@ -42,31 +44,34 @@ public function boot(): void } ``` -Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. +By default, accessing an unloaded relationship then throws a `LazyLoadingViolationException`. Applications can customize violation handling with `handleLazyLoadingViolationUsing()`. ## Select Only Needed Columns -Avoid `SELECT *` — especially when tables have large text or JSON columns. +Select only the columns the operation needs when omitting large text, binary, or JSON columns provides a meaningful benefit. + +All columns: -Incorrect: ```php $posts = Post::with('author')->get(); ``` -Correct: +Selected columns: + ```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. +When limiting selected columns, retain every key Eloquent needs for matching. A `belongsTo` relationship needs its foreign key on the parent query and the owner's key on the related query. A `hasMany` relationship needs the parent's local key and the related model's foreign key. -## Chunk Large Datasets +## Process Large Data Sets Incrementally -Never load thousands of records at once. Use chunking for batch processing. +Use chunking or lazy iteration when loading an entire result set would exceed the application's practical memory budget. + +Loads the complete result set: -Incorrect: ```php $users = User::all(); foreach ($users as $user) { @@ -74,7 +79,8 @@ foreach ($users as $user) { } ``` -Correct: +Processes bounded chunks: + ```php User::where('subscribed', true)->chunk(200, function ($users) { foreach ($users as $user) { @@ -83,7 +89,7 @@ User::where('subscribed', true)->chunk(200, function ($users) { }); ``` -Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: +Use `chunkById()` when updates can change which rows match the query. Standard `chunk()` uses offset pagination, whose result positions can shift as rows change: ```php User::where('active', false)->chunkById(200, function ($users) { @@ -91,11 +97,14 @@ User::where('active', false)->chunkById(200, function ($users) { }); ``` -## Add Database Indexes +For read-only, attribute-only iteration, `cursor()` hydrates models individually from one query, although some database drivers still buffer raw results. Use `lazy()` when relationships must be eager loaded in chunks, and use `lazyById()` or `chunkById()` when updates can affect query membership. See the collection rules for detailed tradeoffs. + +## Add Indexes for Measured Query Patterns -Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. +Design indexes around frequent, performance-sensitive query patterns. A column's presence in `WHERE`, `ORDER BY`, `JOIN`, or `GROUP BY` does not by itself justify an index; selectivity, write cost, existing indexes, and the database query plan all matter. + +Schema without an application-specific query index: -Incorrect: ```php Schema::create('orders', function (Blueprint $table) { $table->id(); @@ -105,24 +114,26 @@ Schema::create('orders', function (Blueprint $table) { }); ``` -Correct: +Schema optimized for `WHERE status = ? ORDER BY created_at`: + ```php Schema::create('orders', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id')->index()->constrained(); - $table->string('status')->index(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); $table->timestamps(); $table->index(['status', 'created_at']); }); ``` -Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). +Confirm composite index column order and effectiveness with production-like data and the database's query-plan tools. Also check whether the database already created an index to support a foreign key before adding another one. + +## Count Relationships Without Loading Them -## Use `withCount()` for Counting Relations +Use `withCount()` when only relationship counts are needed; loading and hydrating every related model wastes memory. -Never load entire collections just to count them. +Loads related models: -Incorrect: ```php $posts = Post::all(); foreach ($posts as $post) { @@ -130,7 +141,8 @@ foreach ($posts as $post) { } ``` -Correct: +Selects relationship counts: + ```php $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { @@ -149,39 +161,24 @@ $posts = Post::withCount([ ])->get(); ``` -## Use `cursor()` for Memory-Efficient Iteration +## Keep Queries Out of Blade Templates -For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. +Prepare data before rendering a Blade template, such as in a controller, query service, or view composer. This keeps query behavior visible and testable. -Incorrect: -```php -$users = User::where('active', true)->get(); -``` - -Correct: -```php -foreach (User::where('active', true)->cursor() as $user) { - ProcessUser::dispatch($user->id); -} -``` +Query in the template: -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: +Data prepared before rendering: + ```php // Controller $users = User::with('profile')->get(); + return view('users.index', compact('users')); ``` @@ -189,4 +186,4 @@ return view('users.index', compact('users')); @foreach ($users as $user) {{ $user->profile->name }} @endforeach -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/eloquent.md b/_api_app/.cursor/skills/laravel-best-practices/rules/eloquent.md index 09cd66a0..5a89510d 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/eloquent.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/eloquent.md @@ -1,8 +1,8 @@ # Eloquent Best Practices -## Use Correct Relationship Types +## Define Precise Relationship Types -Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. +Define the relationship that matches the database association, and declare its concrete return type. ```php public function comments(): HasMany @@ -20,7 +20,8 @@ public function author(): BelongsTo Extract reusable query constraints into local scopes to avoid duplication. -Incorrect: +Duplicated constraints: + ```php $active = User::where('verified', true)->whereNotNull('activated_at')->get(); $articles = Article::whereHas('user', function ($q) { @@ -28,9 +29,11 @@ $articles = Article::whereHas('user', function ($q) { })->get(); ``` -Correct: +Reusable local scope: + ```php -public function scopeActive(Builder $query): Builder +#[Scope] +protected function active(Builder $query): Builder { return $query->where('verified', true)->whereNotNull('activated_at'); } @@ -44,7 +47,8 @@ $articles = Article::whereHas('user', fn ($q) => $q->active())->get(); 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): +Global scope tradeoff: + ```php class PublishedScope implements Scope { @@ -53,12 +57,15 @@ class PublishedScope implements Scope $builder->where('published', true); } } -// Now admin panels, reports, and background jobs all silently skip drafts + +// Admin panels, reports, and jobs now omit drafts unless the scope is removed. ``` -Correct (local scope you opt into): +Explicit local scope: + ```php -public function scopePublished(Builder $query): Builder +#[Scope] +protected function published(Builder $query): Builder { return $query->where('published', true); } @@ -82,16 +89,18 @@ protected function casts(): array } ``` -## Cast Date Columns Properly +## Cast Date and Time Attributes + +Cast a date or timestamp attribute when application code should treat it as a Carbon instance. Eloquent already casts the conventional `created_at` and `updated_at` timestamps. -Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. +Manual parsing in the template: -Incorrect: ```blade -{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +{{ Carbon::parse($order->ordered_at)->toDateString() }} ``` -Correct: +Model cast: + ```php protected function casts(): array { @@ -108,24 +117,27 @@ protected function casts(): array ## Use `whereBelongsTo()` for Relationship Queries -Cleaner than manually specifying foreign keys. +`whereBelongsTo()` expresses the relationship constraint without manually specifying its foreign key. + +Foreign key constraint: -Incorrect: ```php Post::where('user_id', $user->id)->get(); ``` -Correct: +Relationship-aware constraint: + ```php Post::whereBelongsTo($user)->get(); Post::whereBelongsTo($user, 'author')->get(); ``` -## Avoid Hardcoded Table Names in Queries +## Keep Application Queries Model-Aware -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). +Prefer Eloquent models and relationships for model-backed application queries. They preserve casts, scopes, and model table configuration. The query builder and raw SQL legitimately require table names, so use them when their lower-level behavior is intentional. + +Lower-level alternatives: -Incorrect: ```php DB::table('users')->where('active', true)->get(); @@ -134,15 +146,13 @@ $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(); +Model-aware queries: -// Even better — use Eloquent or the query builder instead of raw SQL +```php 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. +When a query builder operation should follow a model's configured table name, use `(new User)->getTable()`. For complex joins or raw SQL, explicit table names may be clearer; keep those references covered by tests when schema changes are possible. -**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. \ No newline at end of file +In migrations, use explicit table names rather than application models. Migrations are historical snapshots, while models and their scopes can change after a migration is deployed. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/error-handling.md b/_api_app/.cursor/skills/laravel-best-practices/rules/error-handling.md index bb8e7a38..afcdf4ac 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/error-handling.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/error-handling.md @@ -1,15 +1,18 @@ # Error Handling Best Practices -## Exception Reporting and Rendering +## Choose Where to Report and Render Exceptions -There are two valid approaches — choose one and apply it consistently across the project. +Laravel supports exception-specific methods and centralized handler callbacks. Follow the pattern already established by the project. -**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: +Exception methods keep behavior beside the exception definition: ```php class InvalidOrderException extends Exception { - public function report(): void { /* custom reporting */ } + public function report(): void + { + // Send the exception to a custom reporter. + } public function render(Request $request): Response { @@ -18,38 +21,40 @@ class InvalidOrderException extends Exception } ``` -**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: +Centralized callbacks in `bootstrap/app.php` keep the application's exception policy together: ```php ->withExceptions(function (Exceptions $exceptions) { - $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->report(function (InvalidOrderException $e) { + // Send the exception to a custom reporter. + }); $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. +An exception's `report()` method suppresses Laravel's default reporting unless it returns `false`. A report callback allows default reporting unless it returns `false` or is chained with `stop()`. Use `ShouldntReport` or `dontReport()` when the handler should not report an exception at all. By contrast, returning `false` from a `render()` method or render callback defers to Laravel's default rendering. -## Use `ShouldntReport` for Exceptions That Should Never Log +## Mark Exceptions the Handler Should Not Report -More discoverable than listing classes in `dontReport()`. +Implementing `ShouldntReport` prevents Laravel's exception handler from reporting that exception type and keeps the policy visible on the class. It does not prevent application code from logging the exception explicitly. ```php class PodcastProcessingException extends Exception implements ShouldntReport {} ``` -## Throttle High-Volume Exceptions +## Throttle High-Volume Exception Reports -A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. +A failing integration can flood logs or error tracking. Configure `throttle()` with a `Lottery` or `Limit` result to sample or rate-limit matching exception reports. Choose keys deliberately when separate exception classes, tenants, or integrations need independent limits. -## Enable `dontReportDuplicates()` +## Prevent Duplicate Reports of One Exception Instance -Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. +Enable `dontReportDuplicates()` when the same exception object may pass through multiple `report($exception)` calls. It deduplicates by object identity, not by exception class or message. -## Force JSON Error Rendering for API Routes +## Define JSON Rendering for API Routes -Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. +Laravel normally uses request content negotiation to decide whether to render an exception as JSON. If the application's API contract requires JSON regardless of the `Accept` header, define that policy explicitly for the relevant routes. ```php $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { @@ -59,7 +64,7 @@ $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { ## 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. +Attach structured data to an exception through `context()`. Laravel merges that data into the exception's log context when the handler reports it. ```php class InvalidOrderException extends Exception @@ -69,4 +74,4 @@ class InvalidOrderException extends Exception return ['order_id' => $this->orderId]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/events-notifications.md b/_api_app/.cursor/skills/laravel-best-practices/rules/events-notifications.md index bc43f199..a8cce31a 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/events-notifications.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/events-notifications.md @@ -1,24 +1,24 @@ -# Events & Notifications Best Practices +# Events and Notifications Best Practices ## Rely on Event Discovery -Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. +Laravel discovers listeners in the configured listener directories by inspecting type-hinted event arguments on `handle()` or `__invoke()` methods. Register listeners manually only when discovery is disabled, the listener is outside those directories, or explicit registration is clearer. -## Run `event:cache` in Production Deploy +## Cache Event Discovery During Production Deployment -Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. +Cache discovered listeners during production deployment with `php artisan optimize` or `php artisan event:cache`. Rebuild the cache whenever listener definitions change. ## Use `ShouldDispatchAfterCommit` Inside Transactions -Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. +When an event is dispatched inside a database transaction, `ShouldDispatchAfterCommit` delays dispatch until all open database transactions commit. If a transaction rolls back, Laravel discards the event. This affects synchronous and queued listeners; it is not limited to queue timing. ```php class OrderShipped implements ShouldDispatchAfterCommit {} ``` -## Always Queue Notifications +## Queue Slow Notifications -Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. +Queue notifications that call external services, such as email, text messaging, or Slack, when they do not need to complete before the response. Keep a notification synchronous when immediate completion or failure feedback is part of the operation. ```php class InvoicePaid extends Notification implements ShouldQueue @@ -27,13 +27,17 @@ class InvoicePaid extends Notification implements ShouldQueue } ``` -## Use `afterCommit()` on Notifications in Transactions +## Dispatch Queued Notifications After Commit -Same race condition as events — the queued notification job may run before the transaction commits. +A queued notification sent inside a database transaction can run before the transaction commits. Call `afterCommit()` on the queued notification, or enable the queue connection's `after_commit` option, when its delivery depends on committed data. This setting has no scheduling effect on a synchronous notification. + +```php +$user->notify((new InvoicePaid($invoice))->afterCommit()); +``` ## Route Notification Channels to Dedicated Queues -Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. +Different notification channels can have different latency and priority requirements. Implement `viaQueues()` when channels should use separate queues. ## Use On-Demand Notifications for Non-User Recipients @@ -45,4 +49,4 @@ 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. \ No newline at end of file +Implement `HasLocalePreference::preferredLocale()` on a notifiable model when notifications and mailables should use the recipient's locale. Laravel also preserves that locale for queued delivery. An explicit `locale()` call can still override the preference for an individual notification. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/http-client.md b/_api_app/.cursor/skills/laravel-best-practices/rules/http-client.md index 0a7876ed..cc8fdd6a 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/http-client.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/http-client.md @@ -1,86 +1,100 @@ # HTTP Client Best Practices -## Always Set Explicit Timeouts +## Set Explicit Timeouts -The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. +Laravel's HTTP client has a 30-second response timeout by default. Choose response and connection timeouts that fit the service and the calling request or job. Remember that retries can multiply the total elapsed time. + +Less resilient: -Incorrect: ```php $response = Http::get('https://api.example.com/users'); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) - ->connectTimeout(3) +$response = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users'); ``` -For service-specific clients, define timeouts in a macro: +Define shared settings in a macro or a dedicated client: ```php Http::macro('github', function () { return Http::baseUrl('https://api.github.com') - ->timeout(10) ->connectTimeout(3) + ->timeout(10) ->withToken(config('services.github.token')); }); $response = Http::github()->get('/repos/laravel/framework'); ``` -## Use Retry with Backoff for External APIs +## Retry Only Safe Operations -External APIs have transient failures. Use `retry()` with increasing delays. +Retry transient connection failures, rate-limit responses, and server errors with an appropriate delay. Retry idempotent requests such as `GET` when the operation can safely run more than once. Retry a state-changing request only when the remote API supports an idempotency key or provides equivalent duplicate protection. -Incorrect: -```php -$response = Http::post('https://api.stripe.com/v1/charges', $data); +Unsafe without an idempotency guarantee: -if ($response->failed()) { - throw new PaymentFailedException('Charge failed'); -} +```php +$response = Http::retry([100, 500, 1000]) + ->post('https://api.example.com/v1/charges', $data); ``` -Correct: +Safe for an idempotent request: + ```php -$response = Http::retry([100, 500, 1000]) +$response = Http::connectTimeout(3) ->timeout(10) - ->post('https://api.stripe.com/v1/charges', $data); + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->get('https://api.example.com/data'); ``` -Only retry on specific errors: +For a supported state-changing API, send a stable idempotency key for every attempt: ```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'); +$response = Http::withHeaders(['Idempotency-Key' => $paymentAttempt->uuid]) + ->connectTimeout(3) + ->timeout(10) + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->post('https://api.example.com/v1/charges', $data); ``` ## Handle Errors Explicitly -The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. +The HTTP client returns responses for `4xx` and `5xx` status codes instead of throwing by default. Inspect the expected statuses or call `throw()` before consuming a success payload. + +Unsafe when a success payload is expected: -Incorrect: ```php -$response = Http::get('https://api.example.com/users/1'); -$user = $response->json(); // Could be an error body +$user = Http::get('https://api.example.com/users/1')->json(); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) +$user = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users/1') - ->throw(); - -$user = $response->json(); + ->throw() + ->json(); ``` -For graceful degradation: +Handle expected alternatives explicitly when graceful degradation is required: ```php -$response = Http::get('https://api.example.com/users/1'); +$response = Http::connectTimeout(3) + ->timeout(5) + ->get('https://api.example.com/users/1'); if ($response->successful()) { return $response->json(); @@ -93,46 +107,30 @@ if ($response->notFound()) { $response->throw(); ``` -## Use Request Pooling for Concurrent Requests - -When making multiple independent API calls, use `Http::pool()` instead of sequential calls. +## Pool Independent Requests -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(); -``` +Use `Http::pool()` when several independent requests can run concurrently. Pooling changes execution time, not error handling; inspect or throw for each response as needed. -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'), + $pool->as('users')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/users'), + $pool->as('posts')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/posts'), ]); -$users = $responses['users']->json(); -$posts = $responses['posts']->json(); +$users = $responses['users']->throw()->json(); +$posts = $responses['posts']->throw()->json(); ``` -## Fake HTTP Calls in Tests - -Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. +## Fake HTTP Requests in Tests -Incorrect: -```php -it('syncs user from API', function () { - $service = new UserSyncService; - $service->sync(1); // Hits the real API -}); -``` +Use `Http::fake()` for external integrations, and use `Http::preventStrayRequests()` when an unexpected real request should fail the test. Also test timeouts, connection failures, and error responses that the application handles. -Correct: ```php -it('syncs user from API', function () { +it('syncs a user from the API', function () { Http::preventStrayRequests(); Http::fake([ @@ -142,19 +140,18 @@ it('syncs user from API', function () { ]), ]); - $service = new UserSyncService; - $service->sync(1); + (new UserSyncService)->sync(1); - Http::assertSent(function (Request $request) { - return $request->url() === 'https://api.example.com/users/1'; - }); + Http::assertSent(fn (Request $request) => + $request->url() === 'https://api.example.com/users/1' + ); }); ``` -Test failure scenarios too: +For example, fake a connection failure when testing the integration's failure path: ```php Http::fake([ 'api.example.com/*' => Http::failedConnection(), ]); -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/mail.md b/_api_app/.cursor/skills/laravel-best-practices/rules/mail.md index c7f67966..f6310437 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/mail.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/mail.md @@ -1,27 +1,54 @@ # Mail Best Practices -## Implement `ShouldQueue` on the Mailable Class +## Queue Slow Mail Delivery -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. +Implement `ShouldQueue` on a mailable when delivery should normally happen in the background. Laravel queues that mailable even when the call site uses `Mail::send()`. -## Use `afterCommit()` on Mailables Inside Transactions +```php +class OrderShipped extends Mailable implements ShouldQueue +{ + use Queueable, SerializesModels; +} +``` -A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. +Keep mail synchronous when the caller must know immediately whether delivery was accepted, or when no queue worker is available. -## Use `assertQueued()` Not `assertSent()` for Queued Mailables +## Dispatch Queued Mail After Commit -`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence. +A queued mailable dispatched during a database transaction can be processed before the transaction commits. Call `afterCommit()` on the mailable, or enable the queue connection's `after_commit` option, when the mail depends on committed records. -Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. +```php +Mail::to($user)->send( + (new OrderShipped($order))->afterCommit() +); +``` -Correct: `Mail::assertQueued(OrderShipped::class);` +If the transaction rolls back, an after-commit mailable is not dispatched. This setting affects queued mail only; it does not defer synchronous delivery. -## Use Markdown Mailables for Transactional Emails +## Assert the Delivery Mode -Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. +Use `Mail::assertQueued()` for queued mailables and `Mail::assertSent()` for synchronously sent mailables. -## Separate Content Tests from Sending Tests +Incorrect for a mailable that implements `ShouldQueue`: -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. \ No newline at end of file +```php +Mail::assertSent(OrderShipped::class); +``` + +Correct: + +```php +Mail::assertQueued(OrderShipped::class); +``` + +## Use Markdown Mailables When They Fit + +Markdown mailables render HTML and plain-text versions from Laravel's mail components and support publishable themes. They are useful for conventional transactional messages, but a custom HTML and text pair may be more appropriate for a specialized design. + +```bash +php artisan make:mail OrderShipped --markdown=mail.orders.shipped +``` + +## Separate Content and Delivery Tests + +Test rendered content by instantiating the mailable and using assertions such as `assertSeeInHtml()` and `assertSeeInText()`. Test delivery separately with `Mail::fake()` and `assertSent()` or `assertQueued()` so failures identify the affected behavior. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/migrations.md b/_api_app/.cursor/skills/laravel-best-practices/rules/migrations.md index de25aa39..3345402e 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/migrations.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/migrations.md @@ -2,76 +2,51 @@ ## Generate Migrations with Artisan -Always use `php artisan make:migration` for consistent naming and timestamps. +Use `php artisan make:migration` to generate the timestamped filename and migration structure. -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 +## Define Foreign-Key Constraints Deliberately -Automatic naming and referential integrity. +Use `constrained()` when its naming conventions and default actions match the relationship. Specify the table or delete behavior when they do not. ```php $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - -// Non-standard names $table->foreignId('author_id')->constrained('users'); ``` -## Never Modify Deployed Migrations +Do not add a duplicate single-column index without checking the database driver's treatment of foreign-key indexes and the indexes already created by the migration. -Once a migration has run in production, treat it as immutable. Create a new migration to change the table. +## Treat Deployed Migrations as Immutable -Incorrect (editing a deployed migration): -```php -// 2024_01_01_create_posts_table.php — already in production -$table->string('slug')->unique(); // ← added after deployment -``` +After a migration has run in a shared or production environment, create a new migration for subsequent changes. Editing the old file makes fresh installations differ from upgraded installations. -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'); -}); -``` +For a local migration that has not been shared or deployed, editing and rerunning it may be simpler. -## Add Indexes in the Migration +## Design Indexes for Real Queries -Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. +Add indexes based on query patterns, selectivity, write cost, and the database's ability to use composite indexes. A column appearing in `WHERE`, `ORDER BY`, or `JOIN` does not automatically need its own index. -Incorrect: -```php -Schema::create('orders', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->constrained(); - $table->string('status'); - $table->timestamps(); -}); -``` +Declare each selected index in the schema migration that creates or changes the relevant table. Confirm important indexes with representative data and the database's query plan, and avoid redundant indexes whose leading columns duplicate an existing index without serving a distinct query. See the database performance and advanced query rules for index selection and column-order guidance. + +## Stage Changes That Affect Existing Rows + +Adding a required or unique column to a populated table often needs multiple deployment-safe steps. Add a nullable column, deploy code that can handle both states, backfill existing rows in bounded chunks, then add the required constraint or index after the data is valid. + +Do not assume this migration is safe on a populated table: -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(); -}); +$table->string('slug')->unique(); ``` -## Mirror Defaults in Model `$attributes` +Large backfills are usually better implemented as an observable, restartable command or job than inside a schema migration. Small deterministic data changes may be reasonable in a migration when their locking, transaction, and deployment behavior is understood. + +## Mirror Defaults Only When Unsaved Models Need Them -When a column has a database default, mirror it in the model so new instances have correct values before saving. +A database default is applied when a row is inserted, not when a model is instantiated. Mirror the value in the model's `$attributes` only when application code must observe that default before persistence, and keep both definitions synchronized. ```php // Migration @@ -83,39 +58,10 @@ protected $attributes = [ ]; ``` -## 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'); - }); -} -``` +## Make Rollbacks Honest -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. +Implement `down()` when the change can be safely reversed. A rollback that drops populated columns or cannot restore transformed data is destructive even if it is syntactically reversible; document that limitation and prefer a forward-fix migration in production. ## 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']); -``` \ No newline at end of file +Keep each migration small enough to reason about, deploy, and reverse. Separate long-running backfills from schema changes when doing so reduces locks and supports phased deployment, but do not split related operations merely to enforce a blanket separation between data definition and data manipulation. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/queue-jobs.md b/_api_app/.cursor/skills/laravel-best-practices/rules/queue-jobs.md index 2f174dfc..011828a6 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/queue-jobs.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/queue-jobs.md @@ -1,82 +1,82 @@ -# Queue & Job Best Practices +# Queue and Job Best Practices -## Set `retry_after` Greater Than `timeout` +## Keep Reservation Time Longer Than Execution Time -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. +For queue drivers that use Laravel's `retry_after` setting, configure it to exceed the longest worker or job timeout by a safety margin. When a reservation expires, another worker can reserve the same job while the first process is still running. Keep the worker's `--timeout` several seconds shorter than `retry_after`. -Incorrect (`retry_after` ≤ `timeout`): ```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +// Job +public $timeout = 120; -// config/queue.php — retry_after: 90 ← job retried while still running! +// config/queue.php for the connection +'retry_after' => 150, ``` -Correct (`retry_after` > `timeout`): -```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +Amazon Simple Queue Service uses its visibility timeout instead of Laravel's `retry_after`; configure that timeout at the queue level. Because workers can also stop after side effects but before acknowledging a job, make important jobs idempotent even with correct timeout settings. -// config/queue.php — retry_after: 180 ← safely longer than any job timeout -``` - -## Use Exponential Backoff +## Back Off Transient Failures -Use progressively longer delays between retries to avoid hammering failing services. +Use progressively longer delays when a dependency needs time to recover. Do not retry permanent validation or business-rule failures. -Incorrect (fixed retry interval): ```php class SyncWithStripe implements ShouldQueue { - public $tries = 3; - // Default: retries immediately, overwhelming the API -} -``` + public $tries = 4; -Correct (exponential backoff): -```php -class SyncWithStripe implements ShouldQueue -{ - public $tries = 3; public $backoff = [1, 5, 10]; } ``` -## Implement `ShouldBeUnique` +Rate-limiting and exception-throttling middleware can release jobs back to the queue. Released attempts may still count toward the maximum attempt limit, so configure `$tries` or `retryUntil()` to allow the intended retry window. -Prevent duplicate job processing. +## Use Unique Jobs for Dispatch Deduplication + +Implement `ShouldBeUnique` when only one queued instance of a logical job should exist. Uniqueness uses a cache lock and is not a substitute for idempotent processing or a database constraint. ```php class GenerateInvoice implements ShouldQueue, ShouldBeUnique { + public $uniqueFor = 3600; + public function uniqueId(): string { - return $this->order->id; + return (string) $this->order->id; } +} +``` - public $uniqueFor = 3600; +All dispatching processes must use a shared cache that supports locks. Unique-job constraints do not apply to jobs within batches. + +Use `ShouldBeUniqueUntilProcessing` only when the lock should be released immediately before processing begins, allowing another instance to be dispatched while the first is running: + +```php +class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing +{ + // ... } ``` -## Always Implement `failed()` +## Handle Terminal Failure When Needed + +Implement `failed()` when the application must update state, alert an operator, or record domain-specific context after all attempts are exhausted. Logging every failure in each job may duplicate the queue system's failure reporting. -Handle errors explicitly — don't rely on silent failure. +Laravel invokes `failed()` on a new job instance, so mutations made to the job during `handle()` are not available there. ```php public function failed(?Throwable $exception): void { $this->podcast->update(['status' => 'failed']); - Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); + + Log::error('Podcast processing failed', [ + 'podcast_id' => $this->podcast->id, + 'exception' => $exception, + ]); } ``` -## Rate Limit External API Calls in Jobs +## Rate Limit External Calls -Use `RateLimited` middleware to throttle jobs calling third-party APIs. +Use queue middleware such as `RateLimited` when jobs share a third-party API quota. Define the named limiter and choose release delays and attempt limits together. ```php public function middleware(): array @@ -85,62 +85,33 @@ public function middleware(): array } ``` -## Batch Related Jobs +## Batch Jobs for Group Coordination -Use `Bus::batch()` when jobs should succeed or fail together. +Use `Bus::batch()` to monitor a group of jobs and run callbacks when the batch completes or encounters failures. A batch is not a database transaction: completed jobs are not rolled back when another job fails. By default, one failed job cancels the batch; call `allowFailures()` only when partial failure is acceptable. ```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(); + ->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) + ->catch(fn (Batch $batch, Throwable $exception) => Log::error('Import batch failed', [ + 'exception' => $exception, + ])) + ->dispatch(); ``` -## `retryUntil()` Needs `$tries = 0` +## Configure Time-Based Retry Limits Deliberately -When using time-based retry limits, set `$tries = 0` to avoid premature failure. +Use `retryUntil()` as the time-based alternative to a maximum attempt count. Laravel may attempt the job any number of times until this deadline, subject to other failure conditions such as maximum exceptions. The method takes precedence over attempt-based limits, so setting `$tries = 0` is not required. ```php -public $tries = 0; - -public function retryUntil(): \DateTimeInterface +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 Horizon for Redis Queue Operations -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, - ], - ], -], -``` \ No newline at end of file +Laravel Horizon provides monitoring, balancing, metrics, and supervisor configuration for Redis queues. It does not support non-Redis queue drivers. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/routing.md b/_api_app/.cursor/skills/laravel-best-practices/rules/routing.md index e288375d..604a8d96 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/routing.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/routing.md @@ -1,98 +1,106 @@ -# Routing & Controllers Best Practices +# Routing and Controller Best Practices ## Use Implicit Route Model Binding -Let Laravel resolve models automatically from route parameters. +Let Laravel resolve models from route parameters when the default lookup and missing-model behavior fit the endpoint. + +Instead of manual lookup: -Incorrect: ```php -public function show(int $id) +public function show(int $id): View { $post = Post::findOrFail($id); + + return view('posts.show', ['post' => $post]); } ``` -Correct: +Use route model binding: + ```php -public function show(Post $post) +public function show(Post $post): View { return view('posts.show', ['post' => $post]); } ``` -## Use Scoped Bindings for Nested Resources +## Scope Nested Bindings -Enforce parent-child relationships automatically. +Use scoped bindings when a nested resource must belong to its parent. This constrains model resolution; it does not replace authorization. ```php Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { - // $post is automatically scoped to $user + // The resolved post belongs to the resolved user. })->scopeBindings(); ``` -## Use Resource Controllers +## Use Resource Routes for Resourceful Actions -Use `Route::resource()` or `apiResource()` for RESTful endpoints. +Use `Route::resource()` or `Route::apiResource()` when the endpoint follows Laravel's resource-controller actions. Define explicit routes when the behavior does not fit that vocabulary. ```php Route::resource('posts', PostController::class); -Route::apiResource('api/posts', Api\PostController::class); + +// Alternatively, for an API-only resource: +Route::apiResource('posts', ApiPostController::class); ``` -## Keep Controllers Thin +`apiResource()` omits the HTML-oriented `create` and `edit` routes. It does not itself add an `/api` prefix; that prefix comes from the application's API route configuration. + +## Organize Controllers Around Resources + +As a general default, organize each controller around one resource and use Laravel's standard resource actions: `index`, `show`, `create`, `store`, `edit`, `update`, and `destroy`. This keeps routes predictable and prevents controllers from accumulating unrelated behavior. -Aim for under 10 lines per method. Extract business logic to action or service classes. +When a controller needs a custom action such as `publish`, `approve`, or `archive`, first consider whether that behavior represents a separate resource. A focused resource controller gives the behavior its own authorization, validation, and middleware boundary. + +Custom action on the primary controller: -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); -} +Route::post('/podcasts/{podcast}/publish', [PodcastController::class, 'publish']); ``` -Correct: +The published podcast modeled as a resource: + ```php -public function store(StorePostRequest $request, CreatePostAction $create) -{ - $post = $create->execute($request->validated()); +Route::post('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'store']) + ->name('published-podcasts.store'); - return redirect()->route('posts.show', $post); -} +Route::delete('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'destroy']) + ->name('published-podcasts.destroy'); ``` -## 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 +class PublishedPodcastController extends Controller { - $validated = $request->validate([ - 'title' => ['required', 'max:255'], - 'body' => ['required'], - ]); + public function store(Podcast $podcast): RedirectResponse + { + $podcast->publish(); - Post::create($validated); + return back(); + } + + public function destroy(Podcast $podcast): RedirectResponse + { + $podcast->unpublish(); - return redirect()->route('posts.index'); + return back(); + } } ``` -Correct: +Treat a custom verb as a design signal, not proof that another controller is required. Use query parameters for simple filtering, and keep an explicit action route when modeling the operation as a resource would obscure the domain or conflict with established project conventions. + +## Keep Controllers Focused on HTTP Concerns + +Controllers should coordinate HTTP input, authorization, validation, an application operation, and the response. Extract substantial or reusable business logic, but do not introduce an action or service merely to satisfy an arbitrary line limit. + ```php -public function store(StorePostRequest $request): RedirectResponse +public function store(StorePostRequest $request, CreatePostAction $create): RedirectResponse { - Post::create($request->validated()); + $post = $create->handle($request->validated()); - return redirect()->route('posts.index'); + return redirect()->route('posts.show', $post); } -``` \ No newline at end of file +``` + +A form request can perform validation and authorization before the controller runs. Do not repeat its rules in the controller. Keep simple, endpoint-specific validation inline when extraction would not improve reuse or clarity; see the validation rules for detailed guidance. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/scheduling.md b/_api_app/.cursor/skills/laravel-best-practices/rules/scheduling.md index dfaefa26..4c25134a 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/scheduling.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/scheduling.md @@ -1,32 +1,50 @@ # Task Scheduling Best Practices -## Use `withoutOverlapping()` on Variable-Duration Tasks +## Prevent Unwanted Overlap -Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. +Use `withoutOverlapping()` when a second run must not begin while the previous run holds the lock. This is appropriate for variable-duration tasks that are not safe to run concurrently. -## Use `onOneServer()` on Multi-Server Deployments +```php +Schedule::command('reports:generate') + ->everyFifteenMinutes() + ->withoutOverlapping(30); +``` -Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). +The optional value is the lock expiration time in minutes, not the task timeout. Choose it carefully: the default is 24 hours, stale locks can be cleared with `php artisan schedule:clear-cache`, and an expiration that is too short can permit overlap while the first task still runs. The task itself should still tolerate retries and partial execution where practical. -## Use `runInBackground()` for Concurrent Long Tasks +## Run a Task on One Server -By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. +Use `onOneServer()` when only one scheduler node should run an eligible task. Scheduler nodes must use the same default cache store, and that store must support atomic locks. Supported stores include `database`, `memcached`, `dynamodb`, and `redis`. -## Use `environments()` to Restrict Tasks +```php +Schedule::command('billing:charge')->daily()->onOneServer(); +``` -Prevent accidental execution of production-only tasks (billing, reporting) on staging. +Name scheduled closures before applying `onOneServer()`, especially when scheduling the same closure with different parameters, so each task has a distinct lock identity. + +## Run Eligible Commands in the Background + +Tasks due at the same time run sequentially by default. Use `runInBackground()` when an independent, long-running scheduled command should not delay later tasks. ```php -Schedule::command('billing:charge')->monthly()->environments(['production']); +Schedule::command('analytics:process')->hourly()->runInBackground(); ``` -## Use `takeUntilTimeout()` for Time-Bounded Processing +Laravel restricts `runInBackground()` to tasks scheduled with `command()` and `exec()`; it is not available for scheduled closures. Ensure background processes have appropriate logging and failure monitoring. -A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. +## Restrict Tasks by Environment -## Use Schedule Groups for Shared Configuration +Use `environments()` when a task should run only in named application environments. Treat this as an operational safeguard, not an authorization control. -Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. +```php +Schedule::command('billing:charge') + ->monthly() + ->environments(['production']); +``` + +## Group Shared Configuration + +Use schedule groups when several tasks genuinely share frequency or constraints. ```php Schedule::daily() @@ -36,4 +54,8 @@ Schedule::daily() Schedule::command('emails:send --force'); Schedule::command('emails:prune'); }); -``` \ No newline at end of file +``` + +## Bound Work Inside the Task + +The scheduler does not provide a `takeUntilTimeout()` event method or terminate arbitrary tasks at a deadline. Bound work in the command or job itself by processing finite chunks, checking a deadline, or dispatching queue jobs with suitable timeouts. Use operating-system or process controls when hard termination is required. diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/security.md b/_api_app/.cursor/skills/laravel-best-practices/rules/security.md index 524d47e6..57b57f0f 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/security.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/security.md @@ -1,18 +1,9 @@ # Security Best Practices -## Mass Assignment Protection +## Control Mass Assignment -Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). +Define `$fillable` when a model is populated from request-derived arrays, or deliberately guard attributes by another consistent model convention. Laravel models guard all attributes by default; `$guarded = []` opts out of that protection. -Incorrect: -```php -class User extends Model -{ - protected $guarded = []; // All fields are mass assignable -} -``` - -Correct: ```php class User extends Model { @@ -24,82 +15,71 @@ class User extends Model } ``` -Never use `$guarded = []` on models that accept user input. +Do not pass untrusted request data to a model with `$guarded = []`. Mass-assignment protection controls which attributes `create()`, `fill()`, and `update()` may set; it does not validate values or authorize the operation. -## Authorize Every Action +## Authorize Protected Actions -Use policies or gates in controllers. Never skip authorization. +Use policies, gates, or form request authorization for actions that depend on the current user's permissions. Authentication alone does not establish permission, and validation is not authorization. -Incorrect: ```php -public function update(Request $request, Post $post) -{ - $post->update($request->validated()); -} -``` - -Correct: -```php -public function update(UpdatePostRequest $request, Post $post) +public function update(UpdatePostRequest $request, Post $post): RedirectResponse { Gate::authorize('update', $post); $post->update($request->validated()); + + return redirect()->route('posts.show', $post); } ``` -Or via Form Request: +Authorization may instead live in the form request: ```php public function authorize(): bool { - return $this->user()->can('update', $this->route('post')); + return $this->user()?->can('update', $this->route('post')) ?? false; } ``` -## Prevent SQL Injection +Public actions intentionally available to everyone do not need a redundant authorization check. -Always use parameter binding. Never interpolate user input into queries. +## Bind Query Parameters + +Use Eloquent, the query builder, or explicit bindings instead of interpolating untrusted values into Structured Query Language (SQL). Bindings protect values, not identifiers such as column names or sort directions; map user-selected identifiers to an allow-list. 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(); +User::whereRaw('LOWER(name) = ?', [$request->string('name')->lower()->toString()])->get(); ``` -## Escape Output to Prevent XSS +## Escape Output in Its Context -Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. +Blade's `{{ }}` syntax HTML-escapes output. Use `{!! !!}` only for content that has been sanitized for the exact HTML context in which it is rendered. Escaping rules differ for HTML, URLs, JavaScript, and Cascading Style Sheets. + +Incorrect for untrusted content: -Incorrect: ```blade {!! $user->bio !!} ``` Correct: + ```blade {{ $user->bio }} ``` -## CSRF Protection +## Apply Cross-Site Request Forgery Protection -Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia. +Include `@csrf` in state-changing Blade forms handled by Laravel's `web` middleware. Routes intentionally excluded from cross-site request forgery (CSRF) verification, such as validated third-party webhooks, need their own authenticity check. -Incorrect: -```blade -
- -
-``` - -Correct: ```blade
@csrf @@ -107,21 +87,27 @@ Correct:
``` -## Rate Limit Auth and API Routes +Inertia applications commonly use Axios, which returns the encrypted `XSRF-TOKEN` cookie in the `X-XSRF-TOKEN` header. Confirm equivalent configuration when using another HTTP client. Do not disable CSRF protection merely to fix a token mismatch. + +## Rate Limit Sensitive Endpoints -Apply `throttle` middleware to authentication and API routes. +Apply suitable rate limits to login attempts, password recovery, verification messages, and expensive or abuse-prone application programming interface (API) routes. Choose the limiter key deliberately; an Internet Protocol (IP) address alone can unfairly group users behind a shared network, while an account identifier alone can enable targeted denial of service. ```php RateLimiter::for('login', function (Request $request) { - return Limit::perMinute(5)->by($request->ip()); + return Limit::perMinute(5)->by(Str::transliterate( + Str::lower($request->string('email')).'|'.$request->ip() + )); }); Route::post('/login', LoginController::class)->middleware('throttle:login'); ``` -## Validate File Uploads +Rate limiting reduces abuse; it does not replace authentication, authorization, or upstream denial-of-service protection. -Validate MIME type, extension, and size. Never trust client-provided filenames. +## Validate and Store Uploads Safely + +Validate expected content type, dimensions where relevant, and size. Laravel's `mimes` rule reads the file contents and guesses a Multipurpose Internet Mail Extensions (MIME) type corresponding to the listed extensions; it does not validate the user-assigned filename extension. The `extensions` rule checks that extension and should not be used by itself. ```php public function rules(): array @@ -132,56 +118,28 @@ public function rules(): array } ``` -Store with generated filenames: - -```php -$path = $request->file('avatar')->store('avatars', 'public'); -``` - -## Keep Secrets Out of Code +Use Laravel's storage methods to generate a filename, and store untrusted files outside a publicly executable location. Public files can require additional controls, such as image re-encoding, content-disposition headers, and explicit blocking of active formats. -Never commit `.env`. Access secrets via `config()` only. - -Incorrect: ```php -$key = env('API_KEY'); +$path = $request->file('avatar')->store('avatars'); ``` -Correct: -```php -// config/services.php -'api_key' => env('API_KEY'), +## Keep Secrets Out of Application Code -// In application code -$key = config('services.api_key'); -``` +Do not commit populated environment files or hard-code credentials. Read environment variables in configuration files, then use `config()` in application code so configuration caching works correctly. See the configuration rules for encrypted environment files and external secret stores. ## Audit Dependencies -Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. +Run `composer audit` regularly and in continuous integration. Review findings for exploitability and update or mitigate affected packages promptly. ```bash composer audit ``` -## Encrypt Sensitive Database Fields +## Encrypt Sensitive Attributes When Appropriate -Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. +Use an `encrypted` cast for sensitive values that must be recoverable, and use `$hidden` to omit them from array and JavaScript Object Notation (JSON) serialization. Hidden attributes remain accessible in PHP, and encryption does not replace access control. Encrypted values cannot be meaningfully queried and should use a `TEXT` or larger column because ciphertext length is variable. -Incorrect: -```php -class Integration extends Model -{ - protected function casts(): array - { - return [ - 'api_key' => 'string', - ]; - } -} -``` - -Correct: ```php class Integration extends Model { @@ -195,4 +153,4 @@ class Integration extends Model ]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/style.md b/_api_app/.cursor/skills/laravel-best-practices/rules/style.md index 67af9891..3f3f14fb 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/style.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/style.md @@ -1,125 +1,110 @@ -# 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 | -|---------|---------| +# Convention and Style Best Practices + +## Follow Project Naming Conventions + +Prefer Laravel's conventions in new code, but preserve an established project convention unless a coordinated rename is worthwhile. + +| Element | Convention | Example | +| --- | --- | --- | +| Controller | Singular resource name | `ArticleController` | +| Model | Singular StudlyCase | `User` | +| Table | Plural snake_case | `article_comments` | +| Pivot table | Singular model names in alphabetical order, in snake_case | `article_user` | +| Column | snake_case | `meta_title` | +| Conventional foreign key | Singular model name plus `_id`, in snake_case | `article_id` | +| Resource URI | Plural resource | `articles/1` | +| Route name | Dotted segments; snake_case within a segment when needed | `users.show_active` | +| Method | camelCase | `getAll` | +| Variable | camelCase | `$articlesWithAuthor` | +| Collection | Descriptive and plural | `$activeUsers` | +| Object | Descriptive and singular | `$activeUser` | +| View | kebab-case | `show-filtered.blade.php` | +| Configuration file | snake_case | `google_calendar.php` | +| Enumeration | Singular StudlyCase | `UserType` | + +## Prefer Clear, Idiomatic Syntax + +Use Laravel helpers and query methods when they communicate intent more directly. Do not shorten code when the result is ambiguous or loses useful type information. + +| More verbose | Idiomatic alternative | +| --- | --- | | `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')` | +| `->first()?->name` | `->value('name')` when only that value is needed | -## Use Laravel String & Array Helpers +Use typed request accessors such as `$request->string()`, `$request->integer()`, and `$request->boolean()` when their coercion matches the operation. -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. +## Use Utilities When They Clarify Intent -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); +Laravel's `Str`, `Arr`, `Number`, and `Uri` utilities provide expressive operations and framework-consistent behavior. Prefer them when they are clearer or safer than an equivalent PHP operation, not as an unconditional replacement for every built-in function. -// Correct +```php $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 +$class = class_basename(User::class); $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()`. +Use `Arr` for dot notation and common transformations: -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'); +$public = Arr::only($attributes, ['name', 'email']); ``` -Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`. +Use `Number` for localized display formatting rather than values that will be stored or calculated: -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%" +Number::format(1000000); +Number::currency(1500, 'USD'); +Number::fileSize(1024 * 1024); ``` -URIs — use `Uri` for URL manipulation: +Use `Uri` when constructing or transforming a uniform resource identifier (URI) benefits from a structured API: + ```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. +Check the documentation for the Laravel version supported by the project before using newer utility classes or methods. -Use `search-docs` for the full list of available methods — these helpers are extensive. +## Keep Presentation Code Maintainable -## No Inline JS/CSS in Blade +Prefer the project's asset pipeline, components, and existing conventions for substantial JavaScript and Cascading Style Sheets (CSS). Small page-specific scripts or styles can be reasonable in Blade layouts or stacks; avoid mixing large behavior and style blocks into templates. -Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes. +Pass server data with an encoding mechanism appropriate to its context. For example, Blade's `Js::from()` safely formats data for JavaScript: -Incorrect: ```blade -let article = `{{ json_encode($article) }}`; + ``` -Correct: -```blade - -``` +Data attributes are useful for small scalar values, but serializing a large model into an attribute can expose unnecessary fields and complicate escaping. -Pass data to JS via data attributes or use a dedicated PHP-to-JS package. +## Write Comments That Explain Why -## No Unnecessary Comments +Prefer clear names and small units of code over comments that merely restate an operation. Add concise comments for non-obvious constraints, tradeoffs, workarounds, regular expressions, or external behavior that the code cannot express by itself. Keep comments accurate when behavior changes. -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. +Unhelpful: -Incorrect: ```php -// Check if there are any joins -if (count((array) $builder->getQuery()->joins) > 0) +// Check whether the query has joins. +if (count((array) $builder->getQuery()->joins) > 0) { + // ... +} ``` -Correct: +Clearer: + ```php -if ($this->hasJoins()) -``` \ No newline at end of file +if ($this->hasJoins()) { + // ... +} +``` diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/testing.md b/_api_app/.cursor/skills/laravel-best-practices/rules/testing.md deleted file mode 100644 index d39cc3ed..00000000 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/testing.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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(); -``` \ No newline at end of file diff --git a/_api_app/.cursor/skills/laravel-best-practices/rules/validation.md b/_api_app/.cursor/skills/laravel-best-practices/rules/validation.md index a20202ff..37ce6c5e 100644 --- a/_api_app/.cursor/skills/laravel-best-practices/rules/validation.md +++ b/_api_app/.cursor/skills/laravel-best-practices/rules/validation.md @@ -1,75 +1,89 @@ -# Validation & Forms Best Practices +# Validation and Forms Best Practices -## Use Form Request Classes +## Extract Validation When It Improves the Boundary -Extract validation from controllers into dedicated Form Request classes. +Use a form request when validation or authorization is substantial, reused, or clearer outside the controller. Inline `$request->validate()` remains appropriate for a small, endpoint-specific rule set. -Incorrect: ```php -public function store(Request $request) +public function store(StorePostRequest $request): RedirectResponse { - $request->validate([ - 'title' => 'required|max:255', - 'body' => 'required', - ]); -} -``` + $post = Post::create($request->validated()); -Correct: -```php -public function store(StorePostRequest $request) -{ - Post::create($request->validated()); + return redirect()->route('posts.show', $post); } ``` -## Array vs. String Notation for Rules +A form request's `authorize()` method can enforce access to the operation. Validation establishes the shape and values of input; it does not itself authorize the user. + +## Prefer Readable Rule Syntax -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. +Array syntax composes cleanly with rule objects and avoids delimiter issues. Prefer it in new code when it improves readability, while following a consistent local style. ```php -// Preferred for new code 'email' => ['required', 'email', Rule::unique('users')], +``` + +String syntax remains valid for simple rules: -// Follow existing convention if the project uses string notation +```php 'email' => 'required|email|unique:users', ``` -## Always Use `validated()` +## Use Only Intended Validated Data + +Use `validated()` or `safe()` instead of `$request->all()` when passing request data onward. Then select the fields intended for the operation when the validation rules also cover control fields or nested data. -Get only validated data. Never use `$request->all()` for mass operations. +Unsafe: -Incorrect: ```php Post::create($request->all()); ``` -Correct: +Preferred: + ```php -Post::create($request->validated()); +$post = Post::create($request->safe()->only(['title', 'body'])); ``` -## Use `Rule::when()` for Conditional Validation +Validated data is not automatically safe for mass assignment. Keep model `$fillable` or `$guarded` rules aligned with the operation, and never add a sensitive attribute to validation merely to make mass assignment convenient. + +## Express Conditional Rules Clearly + +Use conditional rules such as `Rule::when()`, `required_if`, or `exclude_unless` when they make the condition explicit. Choose the simplest form that remains easy to test. ```php 'company_name' => [ - Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), + 'string', + 'max:255', + Rule::when( + $this->input('account_type') === 'business', + ['required'], + ['nullable'], + ), ], ``` -## Use the `after()` Method for Custom Validation +## Add Cross-Field Validation After Base Rules -Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. +Use a form request's `after()` method for validation that depends on multiple fields or application state. Avoid expensive queries when prerequisite fields have already failed validation. ```php public function after(): array { return [ function (Validator $validator) { - if ($this->quantity > Product::find($this->product_id)?->stock) { + if ($validator->errors()->hasAny(['product_id', 'quantity'])) { + return; + } + + $stock = Product::find($this->integer('product_id'))?->stock; + + if ($stock !== null && $this->integer('quantity') > $stock) { $validator->errors()->add('quantity', 'Not enough stock.'); } }, ]; } -``` \ No newline at end of file +``` + +Validation against mutable state does not prevent a race between validation and persistence. Enforce inventory, uniqueness, and similar invariants with database constraints, atomic updates, or a database transaction as appropriate. diff --git a/_api_app/.cursor/skills/pest-testing/SKILL.md b/_api_app/.cursor/skills/pest-testing/SKILL.md deleted file mode 100644 index 0761b6e7..00000000 --- a/_api_app/.cursor/skills/pest-testing/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -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) or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 3 features. Do not use for editing factories, seeders, migrations, controllers, models, or non-test PHP code." -license: MIT -metadata: - author: laravel ---- - -# Pest Testing 3 - -## Documentation - -Use `search-docs` for detailed Pest 3 patterns and documentation. - -## Basic Usage - -### Creating Tests - -All tests must be written using Pest. Use `php artisan make:test --pest {name}`. - -### Test Organization - -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Do NOT remove tests without approval - these are core application code. -- Test happy paths, failure paths, and edge cases. - -### 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()`. - - -```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()`: - - -```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.): - - -```php -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); -``` - -## Pest 3 Features - -### Architecture Testing - -Pest 3 includes architecture testing to enforce code conventions: - - -```php -arch('controllers') - ->expect('App\Http\Controllers') - ->toExtendNothing() - ->toHaveSuffix('Controller'); - -arch('models') - ->expect('App\Models') - ->toExtend('Illuminate\Database\Eloquent\Model'); - -arch('no debugging') - ->expect(['dd', 'dump', 'ray']) - ->not->toBeUsed(); -``` - -### Type Coverage - -Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. - -## 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 \ No newline at end of file diff --git a/_api_app/.cursor/skills/testing-best-practices/SKILL.md b/_api_app/.cursor/skills/testing-best-practices/SKILL.md new file mode 100644 index 00000000..35d02a1d --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/SKILL.md @@ -0,0 +1,58 @@ +--- +name: testing-best-practices +description: "Laravel test design and review. Use when selecting coverage, naming or structuring tests, choosing assertions or test data, isolating dependencies, testing HTTP or security boundaries, improving suite performance, or reviewing test value. Use framework guidance or search-docs for Pest and PHPUnit syntax." +license: MIT +metadata: + author: laravel +--- + +# Testing Best Practices + +This skill provides rules for designing Laravel tests. Each rule file explains what to do and why. Use `search-docs` for Laravel and Pest API syntax. +This project uses Pest. Follow the corresponding guidance in each rule. + +## Consistency First + +Read nearby tests before you choose syntax and organization. + +A pattern repeated throughout the project is a convention, and project conventions take precedence over this skill. Follow them and give new tests the same structure. + +These rules govern the tests you write now. An existing test that follows a project convention is not defective merely because it conflicts with this skill. Do not delete or rewrite it. If the convention has drawbacks, explain them and let the user decide. + +## What to Test + +Read this section before you write a test. + +- Test observable behavior and application contracts. A test must pass after an implementation change if the behavior stays the same. +- Cover every changed decision and each applicable high-value failure mode. A decision is a branch, a validation, a calculation, or an authorization. +- Exercise declarations through behavior instead of repeating their text. +- Leave framework behavior to framework tests. Testing project configuration is not testing the framework. A constrained relationship, cast, scope, or validation rule belongs to this project. +- Keep every test that can detect a distinct defect. When two tests detect the same defect, trim the higher-layer test to one case and report the duplication. Do not delete an existing test. +- Write a feature test first. Write a unit test only for logic that does not use the framework. +- Write a feature test for every behavior reachable through a request. Real-browser tests require `pestphp/pest-plugin-browser` and a browser download, neither of which this project installs. Mention the package only if the user asks for a real-browser test. +- Judge an architecture test by the convention it protects, not by the rules above. An `arch()` test declares a rule for an entire directory, such as the parent class of every model, the classes that may use an enum, or the methods every factory declares. It intentionally checks declarations and fails when a new file breaks the convention. +- Use the test tools that the project installs. Add a new test dependency, plugin, or browser only after the user asks for it. + +## How to Apply + +1. Read the code under test. Read the tests in the same directory. Identify every decision in the code. +2. Select every applicable branch in the rule index. Read every selected rule file. +3. Report each defect in the code before you write a test. Examples are a method with no body, a policy that no action calls, and a write action with no validation. Test the actual behavior. Report the defect to the user. +4. Write the tests. Run the smallest set of tests that covers the change. The tests must pass. +5. Check every applicable item in `rules/review.md` and every selected rule file. Resolve every mismatch before completion. + +## Rule Index + +Most changes need more than one rule file. + +| Subject | Rule File | +| --- | --- | +| Test framework features that may already do the work | [`rules/finding-features.md`](rules/finding-features.md) | +| File layout, test names, and groups | [`rules/naming.md`](rules/naming.md) | +| Arrange-act-assert and choosing the correct assertion | [`rules/assertions.md`](rules/assertions.md) | +| Endpoint coverage, authentication, authorization, tenant isolation, validation, and browser tests | [`rules/endpoint-tests.md`](rules/endpoint-tests.md) | +| Factories, test data ownership, and repeated input values | [`rules/test-data.md`](rules/test-data.md) | +| Fakes, mocks, outbound HTTP, time, randomness, and databases | [`rules/isolation.md`](rules/isolation.md) | +| Escaping, injection, cross-tenant access, and privilege checks | [`rules/security.md`](rules/security.md) | +| Environment and CI settings for a slow suite | [`rules/performance.md`](rules/performance.md) | +| Reviewing a test or suite | [`rules/review.md`](rules/review.md) | diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/assertions.md b/_api_app/.cursor/skills/testing-best-practices/rules/assertions.md new file mode 100644 index 00000000..ba55fb25 --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/assertions.md @@ -0,0 +1,60 @@ +# Assertions + +## Arrange, Act, Assert + +Write each test in three parts: setup, one action, and assertions. Put one blank line between them so readers can identify each part without comments. + +Keep each test self-contained. Do not use values created by another test. + +## How to Find the Correct Assertion + +First identify the subject of the check, then find an assertion designed for it. A subject-specific assertion identifies the incorrect value when the test fails. + +1. Search Laravel's assertions for framework subjects such as responses, the database, sessions, models, queues, events, mail, and notifications. +2. Fetch `https://pestphp.com/docs/expectations.md` for the expectations of Pest for a plain value, a type, a format, or a shape. +3. Build the check by hand only if no assertion exists for the subject. +4. Confirm the name in the documentation before you use it. Do not write an assertion that you did not confirm. + +Use the assertion in this table for each subject. + +| Subject | Assertion to use | +| --- | --- | +| A return value, the state of an object, or a transformation of a value | an `expect()` chain | +| An HTTP status, JSON, a session, or Inertia | a Laravel response assertion | +| The state in the database | a Laravel database assertion | +| The existence of a model | `assertModelExists($model)` rather than `assertDatabaseHas('users', ['id' => $user->id])` | + +Use a PHPUnit assertion only if no Pest expectation and no Laravel assertion exists for the subject. + +Assert each fact once. Do not assert a 200 status before `assertSee`, because `assertSee` already shows that the page rendered. + +## Named Response Assertions + +Use a named response assertion, such as `assertNotFound()`, rather than `assertStatus(404)`. A failure then identifies the broken contract. Laravel provides named assertions for commonly tested status codes. + +Keep one `expect()` chain on one subject. Start a new chain when the subject changes, or when the chain is difficult to read. + +## Assert a Known Value + +Write the expected value in the test, or calculate the expected value by a different method. Do not calculate the expected value with the logic of the implementation, because the test then passes when that logic is wrong. + +```php +// The test calculates the value with the logic of the implementation... +$expected = now()->subHours(24)->floorSeconds(30)->toJson(); +expect($from)->toBe($expected); + +// The test sets a fixed input and asserts a known value... +travelTo('2025-01-01 00:00:00'); +expect($from)->toBe('2024-12-31T00:00:00.000000Z'); +``` + +## Assert the Complete Result + +A status code is not the complete result of a write operation. Assert each of the following if the operation changes it: + +- The response or the return value. +- The state in the database. +- The jobs and the events that the operation dispatches. +- The notifications and the mail that the operation sends. + +On the failure path, assert that the operation makes none of these changes. A test that asserts only `assertOk()` passes even when the application saves no record. diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/endpoint-tests.md b/_api_app/.cursor/skills/testing-best-practices/rules/endpoint-tests.md new file mode 100644 index 00000000..32e32e64 --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/endpoint-tests.md @@ -0,0 +1,47 @@ +# Endpoint Tests + +## How to Write the Test + +Fetch `https://laravel.com/framework/docs/http-tests` for the request helpers, the authentication helpers, and the response assertions. Confirm the name before you use it, and do not guess an assertion. + +Choose an assertion based on the subject of the check: the status, a header, a redirect, the JSON body, the session, a validation error, or the view. Laravel provides a named assertion for each subject that identifies the incorrect value. + +## Endpoint Coverage + +Write a test for each applicable case: + +- The request has missing or invalid authentication. +- The request comes from a different tenant, team, or organization. +- The user has an insufficient role or permission. +- The request does not satisfy a route or scope constraint. +- The request fails the validation. +- The request is valid. Assert both the response and the persisted state. + +Assert the application's actual behavior rather than a generic status code. An API returns `401` for a missing or invalid token, while a browser endpoint redirects to the sign-in route. + +## Tenant Isolation + +Assert the status code returned for a cross-tenant request. Use `404` rather than `403` when one tenant must not learn that another tenant's record exists, because `403` confirms its existence. + +## Test Authorization at the Policy Level + +An HTTP test shows that the endpoint performs authorization. It cannot identify which mechanism refused the request because middleware, a policy, and a call to `abort()` can all return `403`. + +- Assert the complete matrix of the permissions against the policy or the gate. A failure then names the rule that is not correct. +- Write one HTTP test for one refused role, which shows that the endpoint calls the authorization. +- Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists. + +## Testing Validation + +- Write one test for each validation rule when each failure represents a separate contract. +- Write one test with an empty payload to assert several required fields together. +- Assert the text of the message that the user gets. A message that is present but wrong is a defect. +- Use a dataset for input values that need the same setup and the same assertions. + +Send an input value that is not valid through the application, and assert the error. Do not assert that an array of rules contains a string, because that assertion tests the declaration and not the behavior. Use such an assertion only for a rule that no request can reach, and write the reason in the test. + +### Which Layer Owns Which Case + +The rule-class test owns the matrix of values that pass and fail. The endpoint test proves that the endpoint applies the rule and that the user receives the message. + +When both tests contain the matrix, move it to the rule-class test and retain one case in the endpoint test. Never remove the last case, because the rule-class test still passes if the request omits the rule. The same division applies to policies, scopes, and other classes called by a request. diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/finding-features.md b/_api_app/.cursor/skills/testing-best-practices/rules/finding-features.md new file mode 100644 index 00000000..83ce650b --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/finding-features.md @@ -0,0 +1,34 @@ +# How to Find Test Framework Features + +Pest adds features faster than this skill can list them. Find an existing feature before implementing the behavior by hand. + +- Give `search-docs` the capability you need rather than the name of a function you remember. It returns features available in the installed version. +- Fetch `https://pestphp.com/llms.txt` for the complete feature list and additions in each release. +- If a search returns no results, tell the user that the installed version does not provide the feature. Do not write an API that you have not confirmed. + +Search for a feature in this table before you write the code by hand. + +| Work that you need | Term to search for | +| --- | --- | +| Run one test with many input values | datasets, bound datasets | +| Assert over many values or over a collection | higher-order expectations | +| Remove the same setup from each test in a file | hooks, higher-order tests | +| Apply a convention to the complete codebase | architecture testing | +| Measure if the suite finds a defect | mutation testing | +| Find code with no types | type coverage | +| Reduce the time of a slow suite | parallel, profiling | +| Run one test while you debug | filtering, `--bail`, `--dirty` | + +## Built-in Laravel Assertion Methods + +Laravel provides assertions for each part of the framework. Fetch `https://laravel.com/framework/docs/testing` for the complete list, and search for an assertion before building a check by hand. Examples include `assertDatabaseHas()`, `assertModelExists()`, `assertSoftDeleted()`, response assertions such as `assertRedirectToRoute()` and `assertJsonPath()`, and fake assertions such as `Queue::assertPushed()` and `Notification::assertSentTo()`. + +A hand-built check fails with `false is not true`, which identifies nothing. A framework assertion names the incorrect table, value, or response, so the failure indicates what to fix. + +```php +// The failure says that false is not true. Instead of this... +expect(User::where('email', 'taylor@laravel.com')->exists())->toBeTrue(); + +// Use this... the failure names the table and the attributes that it did not find... +$this->assertDatabaseHas('users', ['email' => 'taylor@laravel.com']); +``` diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/isolation.md b/_api_app/.cursor/skills/testing-best-practices/rules/isolation.md new file mode 100644 index 00000000..e1a21fdb --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/isolation.md @@ -0,0 +1,52 @@ +# Fakes, Mocks, and Determinism + +Tests that depend on actual time, randomness, sleeping, or network calls can fail for reasons unrelated to the code under test. Control all four. + +## How to Isolate a Dependency + +Fetch `https://laravel.com/framework/docs/mocking` for Laravel's fakes, facade doubles, and fake assertions. Confirm each name before using it. + +Identify the dependency, then choose the first applicable option. A framework fake preserves the real code path, while a mock replaces the dependency. + +1. Always use framework fakes for facades such as events, queues, mail, notifications, storage, the HTTP client, time, and sleep. +2. Use a developer-defined fake implementation of a service if the application provides one. +3. Use a mock for a container-resolved contract only when the real implementation leaves the process or is nondeterministic. +4. Use the real implementation for everything else, including the database. + +## Framework Fakes + +- Create each fake inside the test that needs it. Do not create fakes in a file-level `beforeEach()`. +- Pass class names to `Event::fake()` and `Queue::fake()` when you know which classes the code dispatches. A fake without class names can hide an unexpected dispatch. +- Use a fake without class names only when the test asserts the complete result, including a call to `assertNothingPushed()`. +- Write one assertion for each fake. The assertion states that the code dispatches the item, or that the code does not dispatch the item. +- Assert the data of a job or of an event if that data is part of the behavior. +- Use `Exceptions::fake()` to assert that the application reports the correct exception. Do not use `withoutExceptionHandling()`, because it changes the response under test. + +Create prerequisite factory records before calling `Event::fake()`. Factories use model events, such as a `creating` hook that generates a UUID, and a fake without class names suppresses those events and can produce an invalid model. Call the fake first only when a factory event is under test, and pass that event's class name. + +## Mocking + +Use `shouldReceive()` before the action to declare an expectation. Use `shouldHaveReceived()` after the action for a spy. Use `Mockery::on()` or `withArgs()` if an equality check cannot state the expected argument, such as a check of one field of a value object. + +Import the mock function before you use it: `use function Pest\Laravel\mock;`. + +## Outbound HTTP Testing + +Call `Http::preventStrayRequests()`. Any request without a matching fake then fails without reaching the network. + +Fake the exact endpoint used by each test. Do not call `Http::fake()` without an endpoint because it accepts unexpected requests and can hide defects. + +## Time and Randomness + +- Freeze the time or move the time in each test that depends on a date, a period, or a timestamp. +- Use the framework helpers `freezeTime()`, `travelTo()`, `travel()`, and `travelBack()`. Do not call `Carbon::setTestNow()`. +- Use `Str::createRandomStringsUsing()` to fix a generated string, if the test asserts an identifier or a slug. +- Use `Sleep::fake()` instead of a real sleep, and assert the sleeps that the code requests. +- Restore the time and the randomness after each test, if the suite does not restore them for every test. + +## Database + +- Run real queries against the real records in the test database. Do not mock the query builder, because the test then asserts the mock. +- Assert the exact keys of `toArray()` if the shape of the serialized model is a contract. The test then fails when the model exposes a new attribute. +- Test application behavior caused by the schema, such as deleting dependent records through a cascade. Do not test the database engine's cascade implementation. +- Use `LazilyRefreshDatabase` instead of `RefreshDatabase`. A test that does not use the database then does not run the migrations. diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/naming.md b/_api_app/.cursor/skills/testing-best-practices/rules/naming.md new file mode 100644 index 00000000..4f3f5b1d --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/naming.md @@ -0,0 +1,45 @@ +# Naming and Structure + +## File Layout + +- Name each test file `{ClassName}Test.php`. +- Place each test file at the same relative path as the class under test. The class `app/Actions/DeleteTeam.php` gets the test `tests/Unit/Actions/DeleteTeamTest.php`. +- Follow the project's convention for fixture files. If none exists, put fixtures in `tests/Fixtures/` and load them by path. +- Move large literal values out of the test body and into fixture files. + +## Test Function + +Use the test function used by other files in the same directory. If no neighboring test files exist: + +- Use `it()` for the behavior of the code, and write the name as a verb phrase. +- Use `test()` for a declarative fact, such as a grant in a policy, the labels of an enum, or the shape of a serialized model. + +Use one Pest declaration style in each file. Use either `it()` or `test()` consistently. + +## Naming Tests + +The name of a test is a specification. State the user-visible result and the condition that causes it. + +- Name the behavior, and not the method under test. The file name already gives the class. +- Give the exact status code in the name of a test for an API error. +- Do not write `Given`, `When`, or `Then` in the name. + +```php +it('returns 401 when no token is provided', function () { ... }); +it('does not include deployments from deleted environments', function () { ... }); +it('falls back to the default region when none is configured', function () { ... }); +``` + +Use a verb that describes a result, such as `returns`, `renders`, `creates`, `dispatches`, `rejects`, `forbids`, `falls back`, or `does not`. + +Do not write `it('works correctly')` or `it('returns data')`, because neither specifies a meaningful result. Do not write `it('handleMethod creates record')`, because it names a method rather than behavior. + +## Grouping + +Use `describe()` if one file covers separate actions in a lifecycle. An example is a controller with the actions `index`, `show`, `store`, `update`, and `destroy`. + +Do not use `describe()` in these cases: + +- The file covers one action or one flow. +- The tests are different only in the input value. Use a dataset instead. +- The group adds a level but does not make the file easier to read. diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/performance.md b/_api_app/.cursor/skills/testing-best-practices/rules/performance.md new file mode 100644 index 00000000..ee993fbb --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/performance.md @@ -0,0 +1,46 @@ +# Test Suite Performance + +These settings apply to the project and CI, not to individual tests. Read `rules/isolation.md` for choices within a test. + +Fetch `https://pestphp.com/docs/optimizing-tests` for Pest options that make test runs faster. +Verify each flag in the documentation before adding it to CI. + +Measure before changing a setting. Find the slow test first, and apply a project-wide setting only after identifying the costly work. + +## Test Environment + +- Set `BCRYPT_ROUNDS=4` in `.env.testing` or in `phpunit.xml`. The default value is 12, and the hash then takes most of the time of each test that signs a user in. +- Disable XDebug. Disable pcov also, unless the run needs the coverage. +- Disable packages that perform work on every request in the test environment. Examples are Pulse, Telescope, and Nightwatch. +- Use the `WithCachedConfig` and `WithCachedRoutes` traits, so the run does not parse the configuration and the routes for every test. +- Call `withoutVite()`, or `withoutMix()`, so the framework does not resolve a built asset. + +## Global Fakes + +Put these three calls in the base `Pest.php` of the project: + +- `Http::preventStrayRequests()`, because one request that reaches the network can slow the suite. This catches requests made through Laravel's HTTP client. Check direct Guzzle and cURL usage separately. +- `Sleep::fake(syncWithCarbon: true)`, so a retry and a backoff do not sleep. +- `Exceptions::fake()`, so the suite does not report an exception to an external service. + +## How to Run the Suite in Parallel + +Run `vendor/bin/pest --parallel` to spread tests across the machine's CPU cores. Add `--processes=N` if the default count is unsuitable for the machine or CI. + +A parallel run gives each process a separate database. Tests must meet these conditions; a test that fails only in parallel breaks one of them: + +- The test creates each record that it reads. It does not read a record that another test creates. +- The test does not depend on the order of the run. +- The test does not share a file, a cache key, or a queue with another test. Give each process a separate name for such a resource. + +## How to Find a Slow Test + +Run `vendor/bin/pest --profile` to list the slowest tests. Start with the ten slowest tests, because the same cause often applies to the complete suite. + +If the cause of a slow test is unclear, add an event listener or temporary log entry to identify its work. + +## Common Errors + +- The run loads XDebug for a test that does not need it. +- `BCRYPT_ROUNDS` keeps the default value, because the project has no `.env.testing`. +- The code under test calls the real `sleep()`, and `Sleep::fake()` then does not help. diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/review.md b/_api_app/.cursor/skills/testing-best-practices/rules/review.md new file mode 100644 index 00000000..e0cfca14 --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/review.md @@ -0,0 +1,44 @@ +# Reviewing Tests + +Check every item in this file. A passing test may still provide no value. For each test, identify the defect it would catch. + +Report each finding. Do not delete or rewrite a test without the user's approval. When an issue appears throughout the suite as a convention, report the pattern once rather than every affected file. + +## Test Value + +Apply this section to behavioral tests. An architecture test states a convention for a directory, so these items do not apply to it. + +- [ ] Each test covers observable behavior or an application contract, and passes after a change to the implementation that keeps the behavior. +- [ ] Each tested declaration is exercised through behavior, and no test asserts the behavior of the framework. A test of what this project configures, such as a relation with a constraint, a cast, or a scope, belongs to this project. +- [ ] Each test detects a distinct defect that no other test covers. A duplicate shrinks at the higher layer to the one case that proves the wiring. +- [ ] Every changed decision and each applicable high-value failure mode has coverage. + +## Names and Structure + +- [ ] Each file has the name `{ClassName}Test.php` and the relative path of the class under test. +- [ ] Each name states a result, the condition that causes it, and the status code for an API error. +- [ ] Each file uses one declaration style consistently, and each `describe()` group holds separate behavior. + +## Coverage + +- [ ] HTTP tests cover authentication, authorization, role, scope, and validation when applicable. +- [ ] A request for a record of a different tenant gets a status code that does not confirm that the record exists. +- [ ] The complete permission matrix belongs in policy tests, not controller tests. +- [ ] Each validation rule has one test that asserts the user-visible message. When a unit test owns a matrix, reduce duplicate higher-level coverage to one case rather than deleting it. +- [ ] Rendered user input and each dynamic part of a query have a security test. + +## Data and Determinism + +- [ ] Each test creates its mutable records directly or through a helper that it calls, and every created record arranges the behavior or supports an assertion. +- [ ] Each `beforeEach()` holds configuration only. +- [ ] Each factory state and each relationship gives the meaning of the data. +- [ ] Each call to `make()` is in a test that does not need the database. +- [ ] Time, randomness, sleep, and outbound HTTP are controlled. +- [ ] Each test passes alone, and passes in the complete suite in any order. + +## Assertions + +- [ ] Each expected value is a known value, and the test does not calculate the value with the logic of the implementation. +- [ ] Each test of a write operation asserts the response, the state in the database, and the side effects. +- [ ] Each fake has one assertion, and gives the class names unless the test asserts the complete result. +- [ ] Each `expect()` chain stays on one subject. diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/security.md b/_api_app/.cursor/skills/testing-best-practices/rules/security.md new file mode 100644 index 00000000..234f5b00 --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/security.md @@ -0,0 +1,27 @@ +# Security Tests + +Test each security boundary where user input affects authorization, rendered output, or query construction. A defect at such a boundary can be difficult to detect because the feature may continue to work. + +Write a test for each of these cases: + +- **Cross-tenant access.** Request a record of a different tenant, team, or organization. Read `rules/endpoint-tests.md` for why the response should possibly be `404` rather than `403`. +- **Each unprivileged role.** Use a dataset over the roles that the endpoint must refuse. +- **Escaping user-provided content.** Test escaping in HTML and mail. Include names and every free-text field a template renders. Assert that dangerous characters are escaped and the raw value is absent. Do not assert an exact entity for a quote, because Markdown and mail CSS inliners may decode it. +- **Injection into dynamic query components.** Examples include sort columns, filter fields, and sort directions. +- **An unexpected key** in a payload or configuration array. A merge that accepts every key can set an attribute the user must not control. + +```php +it('escapes dangerous content in the notification', function () { + $organization = Organization::factory()->make([ + 'name' => "O'Reilly ", + ]); + + $content = (new QuotaApproaching($organization, 80))->toMail()->render(); + + expect($content) + ->toContain('<script>') + ->not->toContain(""); +}); +``` + +Laravel provides defenses against mass assignment, unauthorized access, and unescaped output. Test that the application applies the appropriate defense to each attribute, route, and template. diff --git a/_api_app/.cursor/skills/testing-best-practices/rules/test-data.md b/_api_app/.cursor/skills/testing-best-practices/rules/test-data.md new file mode 100644 index 00000000..65124683 --- /dev/null +++ b/_api_app/.cursor/skills/testing-best-practices/rules/test-data.md @@ -0,0 +1,56 @@ +# Factories and Test Data + +## Each Test Makes Its Own Data + +Create mutable records inside the test that uses them. This keeps setup visible and lets each test select its factory state. + +Use `beforeEach()` only for configuration that applies to every test in the file. Do not create records in it. + +## Record Construction + +- Use `create()` if the test needs the record in the database. +- Use `make()` only if the test does not need the database. Examples include rendering a notification and testing a value object's behavior. +- Use a named factory state instead of a raw attribute. `User::factory()->unverified()->create()` gives the state meaning; `create(['email_verified_at' => null])` gives only its value. +- Use `for()` or the relationship helper of the project to declare the owner of a record. +- Use `recycle()` if several records must share one parent record. +- Use `sequence()` if several records need different attributes. + +```php +$organization = Organization::factory()->onPlan(BillingPlan::PRO)->create(); + +$environment = Environment::factory()->recycle($organization)->create(); + +$organizations = Organization::factory() + ->count(3) + ->sequence( + ['created_at' => now()->setSeconds(30)], + ['created_at' => now()->setSeconds(1)], + ) + ->create(); +``` + +Create only the records required to arrange the behavior or support an assertion. + +## Datasets + +Use a dataset when the setup, test body, and assertions remain the same across input values. + +```php +it('forbids roles other than admin', function (Role $role) { + actingAs(User::factory()->hasOrganization($role)->create()) + ->post('/settings') + ->assertForbidden(); +})->with(collect(Role::cases())->reject(fn (Role $role) => $role === Role::ADMIN)); +``` + +Use parameterized tests for: + +- enum cases +- roles and plans +- boundary values +- input values that are invalid in the same way +- input and output value pairs + +Write separate tests if the cases need a different setup, a different behavior, or different assertions. One test function with a branch in the body is two tests in one function. + +Give each dataset case a name that states the difference. A failure then identifies the case without requiring you to count positions. diff --git a/_api_app/.github/skills/ai-sdk-development/SKILL.md b/_api_app/.github/skills/ai-sdk-development/SKILL.md index d74e342e..ef6d72a2 100644 --- a/_api_app/.github/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.github/skills/ai-sdk-development/SKILL.md @@ -410,4 +410,4 @@ Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text | OpenRouter | Y | - | - | - | - | - | - | - | | ElevenLabs | - | - | Y | Y | - | - | - | - | | Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | \ No newline at end of file +| Jina | - | - | - | - | Y | Y | - | - | diff --git a/_api_app/.github/skills/deploying-to-cloud/SKILL.md b/_api_app/.github/skills/deploying-to-cloud/SKILL.md new file mode 100644 index 00000000..9584216a --- /dev/null +++ b/_api_app/.github/skills/deploying-to-cloud/SKILL.md @@ -0,0 +1,267 @@ +--- +name: deploying-to-cloud +description: "Deploys and manages Laravel applications on Laravel Cloud using the `cloud` CLI. Use when the user wants to deploy an app, ship to Cloud, create or manage applications, environments, databases, caches, object storage, queues, domains, instances, background processes, secrets, compute, scheduled tasks, or Laravel Cloud infrastructure; or check billing, usage, or spend. Triggers on deployment, shipping, Cloud management, environment setup, database provisioning, billing or usage queries, the `cloud` CLI, and Laravel Cloud deployment troubleshooting." +license: MIT +metadata: + author: laravel +--- + +# Deploying with Laravel Cloud + +Use the [Laravel Cloud documentation](https://cloud.laravel.com/docs/llms.txt) for detailed, current feature behavior. Use the Cloud CLI for Cloud operations rather than guessing dashboard or API workflows. Install it in the project and invoke it as `./vendor/bin/cloud` by default; use a globally installed `cloud` command only as a fallback. Commands below are written as `cloud` for brevity; run them as `./vendor/bin/cloud` unless falling back to the global installation. + +## Application Setup + +- Laravel Cloud deploys from GitHub, GitLab, or Bitbucket. A new Laravel application requires PHP 8.2 or later, Laravel 9 or later, a connected Git provider, and a deployment region. +- Cloud creates environments for applications. Use separate production, staging, and preview environments; each environment has its own compute, resources, and deployment settings. +- Keep application compute and attached resources in the same region where possible. +- Cloud builds a Docker image using the selected PHP version, runs the configured build and deploy commands, and switches traffic to a successful deployment with zero downtime. Push-to-deploy is enabled by default, and manual deployments and deploy hooks are also available. + +## Build and Deploy + +- A typical Laravel build command is `composer install --no-dev && npm run build`. +- Run optimization and cache-building commands during the build, not during deployment. A typical deploy command is `php artisan migrate --force`. +- Build and deploy commands have a 15-minute timeout. Deploy commands run immediately before the release becomes live, and filesystem changes made by deploy commands are not persisted. +- Do not add `php artisan queue:restart`, `php artisan horizon:terminate`, `php artisan optimize:clear`, or `php artisan storage:link` to deploy commands. Cloud handles worker restarts and process management; deploy filesystem changes are not persistent. +- After changing environment settings, attached resources, or linked secrets, redeploy the environment for the changes to take effect. + +## Configuration and Resources + +- Attached databases, caches, and object storage inject their connection variables automatically. Custom environment variables take precedence over injected values. +- Use Secrets Manager for encrypted organization-level values shared across environments. Secret values cannot be read after creation; redeploy affected environments after creating, updating, linking, unlinking, or deleting a secret. +- Environment filesystems are ephemeral and are not shared between replicas. Use a database or Laravel Valkey for persistent cache and sessions, and Laravel Cloud Object Storage for persistent files. Do not rely on local files surviving a deployment. + +### Object Storage and File Visibility + +- Laravel Cloud Object Storage is backed by Cloudflare R2. R2 applies visibility at the bucket level; a bucket cannot contain a mix of private and public objects. +- For private files, always use `Storage::disk()` with no arguments, such as `Storage::disk()->put(...)`, so Laravel uses the environment's default disk. Do not pass a disk name for private/default storage. Attach a private Cloud bucket as the environment's default disk. +- For public files, use the named public disk: `Storage::disk('public')->put(...)`. Attach a second Cloud bucket configured as public and give it the `public` disk name. +- Applications that use both private and public files therefore need two Cloud Object Storage buckets attached to the environment: a private default bucket and a public bucket named `public`. +- Do not set per-file or Flysystem `visibility: 'public'` configuration for Cloud Object Storage. R2 does not support per-object ACL headers and rejects those requests; select the bucket visibility when creating the bucket. +- Private buckets are not internet-accessible, but Laravel can generate temporary public URLs with `Storage::temporaryUrl(...)`. Public buckets expose all objects through their Cloud-provided public URL. +- Install the S3 Flysystem adapter before using Cloud Object Storage: `composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies`. + +## Queues and Scheduling + +- Managed queues are the recommended queue option. They provision dedicated workers and autoscale based on queued work. +- Managed queues require a supported recent Laravel version and `aws/aws-sdk-php`; check the current Cloud documentation before changing dependencies. +- Enable the scheduler on an App or Worker cluster to run `php artisan schedule:run` every minute. When an environment has multiple replicas, use Laravel's `onOneServer` for tasks that must run once. +- Only Flex compute sizes can scale to zero. Scale-to-zero environments wake for Laravel scheduled tasks and queued jobs, but long-running jobs may be interrupted when the sleep timeout is reached. Use managed queues for workloads that must not be interrupted. + +## Domains and Storage + +- Each environment receives a `laravel.cloud` domain after its first successful deployment. Cloud automatically verifies custom domains and provisions SSL after the required DNS records are configured. +- Do not use the ephemeral filesystem for persistent uploads, generated files, cache, or session data. Use the appropriate Cloud resource instead. + +## Cloud CLI + +### Setup + +```shell +composer require --dev laravel/cloud-cli +./vendor/bin/cloud auth -n +``` + +If a project-local installation is not available, install the CLI globally with `composer global require laravel/cloud-cli` and invoke it as `cloud`. + +`cloud auth` opens a browser. If a browser is unavailable, set `LARAVEL_CLOUD_TOKEN` in the environment. It overrides any saved token and writes nothing to disk. To save a token instead, run `cloud auth:token --add --token= -n` or pipe it with `echo "$TOKEN" | cloud auth:token --add -n`. + +### Commands + +Commands follow a CRUD pattern: `resource:list`, `resource:get`, `resource:create`, `resource:update`, `resource:delete`. + +Available resources: `application`, `environment`, `instance`, `database-cluster`, `database`, `cache`, `bucket`, `domain`, `websocket-cluster`, `background-process`, `secret`, `command`, `deployment`. + +Some resources have additional commands (e.g., `domain:verify`, `database:open`, `instance:sizes`, `cache:types`). Discover these via `cloud -h`. + +Never hardcode command signatures. Always run `cloud -h` to discover options at runtime. + +### CLI Flags + +- Always add `-n` to every command to prevent the CLI from hanging. +- Never use `-q` or `--silent`; they suppress all output. + +Flag combos per operation: +- Read (`:list`, `:get`) → `--json -n` +- Create (`:create`) → `--json -n` +- Update (`:update`) → `--json -n --force` +- Delete (`:delete`) → `-n --force` (no `--json`) +- Environment variables → `-n --force` +- Deploy or ship → `-n` with all options passed explicitly (no `--json`) + +### Deployment Workflow + +Determine the task and follow the matching path: + +- First deployment: inspect `cloud ship -h`, then run `cloud ship -n` with all required values. + +- Existing application: + ```shell + cloud deploy {app_name} {environment} -n --open + cloud deploy:monitor -n + ``` + +- Environment variables: `cloud environment:variables -n --force` + +- Secrets: `echo "$VALUE" | cloud secret:create --name=NAME --json -n`, then `cloud environment-secret:attach` + +- Provision infrastructure: `cloud :create --json -n` + +- Monorepo (application in a subdirectory): add `--root-directory=` to `cloud ship` or `cloud application:create`. + +- Custom domain: `cloud domain:create --json -n`, then `cloud domain:verify -n`. + +- Repository defaults: `cloud repo:config {application} -n` sets repository-local application and organization defaults. Pass `--organization=` when the user has multiple organizations. + +For multi-step operations, see [reference/checklists.md](reference/checklists.md). + +- If the user's needs are unclear, ask before running anything. + +### When a Command Fails + +1. Read the error output. +2. Check the resource status with `:list --json -n` or `:get --json -n`. +3. For authentication errors, run `cloud auth -n`. +4. Fix the issue and rerun the command. +5. If the same error occurs after one fix, stop and ask the user rather than repeating the operation. + +Always run `cloud deploy:monitor -n` after every deploy. If it fails, inspect the deployment status and logs and show the user what went wrong before attempting a fix. + +### Subagent Delegation + +Delegate high-output operations to subagents using the Task tool to keep the main context window small. Only the summary returns; verbose output stays in the subagent's context. + +Delegate these to a subagent: +- `cloud deploy:monitor -n` — deployment logs can be very long +- `cloud deployment:get --json -n` — full deployment details +- `cloud :list --json -n` — listing many resources produces large JSON +- `cloud command:run` — when output may be long +- `cloud usage --detailed --json -n` — the payload includes details for every database, cache, bucket, websocket, and application +- Fetching docs from https://cloud.laravel.com/docs/llms.txt via `WebFetch` + +Keep in the main context: +- Short commands like `:create`, `:delete`, `:update` — output is small +- `cloud deploy -n` — you need the deployment ID immediately +- Any command where you need the result for the next step right away + +### Rules + +Follow these rules: +- Flag selection — always use the documented combos above +- Deploy sequence — deploy then monitor, never skip monitoring +- Destructive commands — always confirm with the user first, show the command and wait for approval. This includes deleting applications, environments, databases, caches, buckets, domains, or secrets. +- Error loop — diagnose, fix once, ask user if it fails again + +Use judgment for: +- Instance sizes, regions, cluster types — ask the user if not specified +- Which resources to provision — based on what the user describes +- Order of provisioning — no strict sequence required +- How to present output — summarize, show raw, or extract fields based on context + +### Secrets + +Secrets are encrypted values shared across the organization and attached to environments. The CLI encrypts values locally, so plaintext never reaches the API. + +```shell +cloud secret:list --json -n +echo "$VALUE" | cloud secret:create --name=STRIPE_KEY --json -n +cloud environment-secret:attach {environment} {secretId} -n +cloud environment-secret:list {environment} --json -n +``` + +Pipe the value in. `--value=` works, but leaves the plaintext in shell history and the process list. + +`secret:update`, `secret:delete`, and `environment-secret:attach` take secret IDs, not names — names are not unique. Read IDs from `cloud secret:list --json -n`. + +There is no `secret:get`, and no way to detach a secret from a single environment. `secret:delete` removes it and detaches it everywhere. + +Secrets need the `sodium` PHP extension. No other command does. + +Redeploy affected environments after creating, updating, attaching, or deleting a secret. + +### Remote Access + +#### Tinker (v0.2.0 or later) + +Run PHP code directly in a Cloud environment: + +```shell +cloud tinker {environment} --code='Your PHP code here' --timeout=60 -n +``` + +- `--code` — PHP code to execute (required in non-interactive mode) +- `--timeout` — maximum number of seconds to wait for output (default: 60) + +The code must explicitly output results using `echo`, `dump`, or similar; expressions alone produce no output. + +Always pass `--code` and `-n` to avoid interactive prompts. + +#### Remote Commands + +Run shell commands on a Cloud environment: + +```shell +cloud command:run {environment} --cmd='your command here' -n +``` + +- `--cmd` — the command to run (required in non-interactive mode) +- `--no-monitor` — skip real-time output streaming +- `--copy-output` — copy output to clipboard + +Review past commands: + +- `cloud command:list {environment} --json -n` — list command history +- `cloud command:get {commandId} --json -n` — get details and output of a specific command + +### Billing and Usage + +View billing and usage for the current organization: + +```shell +cloud usage --json -n +``` + +- `--period=current|previous|1|2|3` — Billing period (default: `current`; `1`, `2`, and `3` are one, two, and three periods back, respectively). Other values return an error. +- `--environment=` — filter usage to a single environment +- `--detailed` — include per-application, per-resource, and per-add-on breakdowns +- `--json` — machine-readable output (always pair with `-n`) + +Common queries: + +- Current spend: `cloud usage --json -n | jq '.currentSpendCents'` +- Last month's bill: `cloud usage --period=previous --json -n` +- One environment, full breakdown: `cloud usage --environment= --detailed --json -n` + +All amounts are in cents. Keys are camelCase at every level (e.g., `currentSpendCents`, `bandwidth.allowanceBytes`, `databases[].totalCents`, `applications[].totalCostCents`). + +### Config + +1. Environment: `LARAVEL_CLOUD_TOKEN` — an API token, taking precedence over any saved one (empty counts as unset) +2. Global: `~/.config/cloud/config.json` — auth tokens and preferences +3. Repo-local: `.cloud/config.json` — app and environment defaults (set by `cloud repo:config {application} -n`) +4. CLI arguments override both config files + +Pass the application to `repo:config`. Without it, the command must prompt and fails under `-n` when the organization has more than one application. Deploy commands do not need these defaults; pass the application and environment to them directly. + +Using multiple organizations requires multiple stored API tokens. Commands read `organization_id` from `.cloud/config.json` to select one and fail if it is not set. Set it with `cloud repo:config {application} --organization= -n`. + +`LARAVEL_CLOUD_TOKEN` holds one token, so it picks the organization on its own. Naming a different one with `--organization` fails rather than falling back to the token's organization. + +## Documentation + +Laravel Cloud documentation: https://cloud.laravel.com/docs/llms.txt + +When the user asks how something works or needs an explanation of a Laravel Cloud feature, fetch the docs from the URL above using `WebFetch` and use it to provide accurate answers. + +## When Stuck + +- Fetch https://cloud.laravel.com/docs/llms.txt for official documentation +- Run `cloud -h` for any command's options +- Run `cloud -h` to discover commands + +## Verification + +1. Confirm the target application and environment before changing or deploying anything. +2. Check staged environment changes and required resources before deployment. +3. Monitor every deployment to completion with `cloud deploy:monitor -n`. +4. For production changes, verify the application URL, logs, queues, scheduled tasks, and relevant resource health. diff --git a/_api_app/.github/skills/deploying-to-cloud/reference/checklists.md b/_api_app/.github/skills/deploying-to-cloud/reference/checklists.md new file mode 100644 index 00000000..d3cb54bd --- /dev/null +++ b/_api_app/.github/skills/deploying-to-cloud/reference/checklists.md @@ -0,0 +1,55 @@ +# Checklists for Multi-Step Operations + +Use these checklists when performing complex setups. Copy the relevant checklist and track progress as you go. + +## Full environment setup (app + database + cache + domain) + +``` +- [ ] Create or select the application +- [ ] Create the environment +- [ ] Provision the database (`database-cluster:create` then `database:create`) +- [ ] Provision the cache (`cache:create`) +- [ ] Set environment variables (`environment:variables`) +- [ ] Create and attach any secrets (`secret:create` then `environment-secret:attach`) +- [ ] Attach custom domain (`domain:create` then `domain:verify`) +- [ ] Deploy (`deploy` then `deploy:monitor`) +- [ ] Verify the deployment is live +``` + +## New app from scratch + +``` +- [ ] Run `cloud ship -n` with required options +- [ ] Monitor deployment status +- [ ] Set environment variables if needed +- [ ] Verify the app is accessible +``` + +## Adding a database to an existing environment + +``` +- [ ] Check existing database clusters (`database-cluster:list --json -n`) +- [ ] Create a new cluster if needed (`database-cluster:create --json -n`) +- [ ] Create the database on the cluster (`database:create --json -n`) +- [ ] Update environment variables with connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Adding a cache to an existing environment + +``` +- [ ] Check existing caches (`cache:list --json -n`) +- [ ] Create the cache (`cache:create --json -n`) +- [ ] Update environment variables with cache connection details if needed +- [ ] Redeploy if the app needs to pick up new config +``` + +## Custom domain setup + +``` +- [ ] Create the domain (`domain:create --json -n`) +- [ ] Get DNS records to configure (`domain:get --json -n`) +- [ ] Tell the user to add the DNS records at their registrar +- [ ] Verify the domain (`domain:verify -n`) +- [ ] Confirm verification passed +``` diff --git a/_api_app/.github/skills/infer-conventions/SKILL.md b/_api_app/.github/skills/infer-conventions/SKILL.md new file mode 100644 index 00000000..eb3fc846 --- /dev/null +++ b/_api_app/.github/skills/infer-conventions/SKILL.md @@ -0,0 +1,105 @@ +--- +name: infer-conventions +description: "Use this skill to analyze how a Laravel application is actually written and record its conventions as shared rules. Trigger when the user wants to detect, infer, document, or standardize project conventions or coding style, set up or grow `.ai/rules`, resolve mixed or conflicting patterns (e.g. \"are we using Form Requests or inline validation?\"), or onboard agents and teammates to \"how we do things here\". Covers: a systematic sweep of ~49 Laravel convention dimensions (validation, models, architecture, testing, frontend, database, console), open-ended house-pattern discovery, conflict reporting, and recording rules scoped to the right paths via the Boost `record-rule` MCP tool. Only run this skill when the user explicitly asks for it; never start a sweep as part of another task. Do not use for one-off code review, enforcing formatting a linter already handles, or editing `.ai/rules` files by hand." +disable-model-invocation: true +license: MIT +metadata: + author: laravel +--- + +# Infer Conventions + +Learn how this application writes Laravel, then record what you learn as durable, path-scoped rules other agents will read. You are documenting reality, not improving it. + +## Ground Rules (read before you start) + +- Consistency first. The codebase's majority style is the convention. Never judge it, never propose a "better" pattern, never record what the code should do. If the app validates inline everywhere, that is the rule, even if Form Requests would be nicer. +- Skip what an active tool produces, keep what a tool would fight. Inspect the project's Pint and Rector configuration first; a Rector transformation is tooling-owned only when its package and relevant rule or set are installed and enabled. Active tools may rewrite code toward one canonical form: `$casts` to `casts()`, `$fillable` to attributes, magic accessors to the `Attribute` class, pipe-string rules to arrays, `$signature` to `#[Signature]`, named migrations to anonymous, and many more. When the app already sits at an active tool's target form, the tool owns it, so record nothing. But when the app deliberately holds a form an active tool would refactor away, such as legacy `getXxxAttribute()` accessors the `Attribute` class would replace, no tool can reproduce that choice and an agent defaults the other way. That against-the-grain hold is exactly what to record. +- Record decisions, not defaults. A consistent pattern earns a rule only when it reflects a choice: the app took one valid option where the framework or common practice offered others, or the pattern would surprise a competent agent. Framework defaults steer nothing, so skip them: anonymous migrations, `$signature` commands, `ShouldQueue` jobs, `casts()` on Laravel 11+, named routes, Rule objects in `app/Rules`, and `Mail::fake()` or `Bus::fake()` to isolate framework services. A real fork is not enough on its own. Weigh the side the app took, and record only the side an agent would not reach for by itself: inline closures everywhere, legacy accessors, a bespoke query layer. Watch for the false fork too. "No Mockery" next to facade fakes is not a choice against Mockery, because they double different things. The test for every candidate: without this rule, would the next agent plausibly write it differently? Only "yes" earns a rule. +- Architecture choices are the gold. Record presence and deliberate absence. The structural pattern the app commits to is the highest-signal convention and the one no tool can decide: Action classes and how they are invoked (`handle` / `execute` / `__invoke`), service objects, dedicated query objects exposing `builder()`, DTOs (spatie/laravel-data vs readonly classes), Form Request validation vs inline, an events and listeners spine vs direct calls, and domain or module folders. Also record a consistent non-pattern, such as "query Eloquent directly in controllers, no repository layer", so the next agent matches the app's altitude instead of over-engineering. +- Never duplicate `.ai/rules`. Read `.ai/rules/index.md` and the area files before the sweep. A dimension already covered there is marked done and skipped. +- Evidence or silence. A convention needs at least 3 consistent examples and no meaningful rival to become a candidate. Every Step 1 verdict applies this bar. +- The recorded rule states the convention, nothing else. One or two imperative lines: this project does X, so do X here. Keep detection evidence out. No counts, ratios, current usage, file lists, or example paths, because that is proof for the confirm step, not part of the rule. One short syntax fragment at most, and point to `search-docs` for API details. + +## Process + +Each step ends on a checkable completion criterion. Do not advance until it holds. + +Fan out when you can. The sweep is embarrassingly parallel. If your environment can spawn subagents (a Task, dispatch, or equivalent tool), do Step 0 yourself, then hand each checklist group (A to J) and the architecture map to its own subagent. Each subagent runs the greps, reads a few representative files, and returns structured verdicts (dimension, verdict, evidence, proposed glob / title / note). You aggregate, dedupe, then run Steps 3 to 5. It is far faster on a real app. No subagents available? Run the steps in sequence, with the same bar and the same output. + +### Step 0: Orient + +Read `composer.json` (installed packages tell you which checklist groups apply), the `pint.json` / PHPStan / Rector config, `.ai/rules/index.md` if present, and most important, map the `app/` tree. List every directory under `app/` (and any `Modules/`, `src/`, `packages/`, or domain root). Every folder beyond Laravel's default skeleton (`Http`, `Models`, `Providers`, `Console`, `Exceptions`) is a structural pattern the app committed to and a high-value rule waiting to be written: `Actions`, `Services`, `Data` or DTOs, `Queries`, `Repositories`, `ViewModels`, `Pipelines`, `Support`, `Enums`, `Contracts`, `Observers`, or `Domain` and module roots. Note each one. You will confirm how it is used in Step 2. + +This app has no Livewire/Inertia/Flux packages installed. Treat the frontend group as likely API-only: confirm from `resources/views` before spending time there, and skip the Livewire/Inertia/Flux dimensions. + +Done when: you have the applicable checklist groups, the dimensions already recorded in `.ai/rules`, and a list of every non-default `app/` directory mapped to the pattern it represents. + +### Step 1: Predefined sweep + +Open `references/checklist.md` and work every applicable dimension using its search hints. Give each exactly one verdict: + +- Pattern. Clears the bar, rival under ~20% of sites, and reflects a real choice (passes the decisions-not-defaults test). A recording candidate. Cite 2 to 3 example files. +- Conflict. Both styles present in meaningful numbers. Report the split with counts and example files. Never record a preferred winner while the code remains mixed, even in yolo, because that would describe an aspiration rather than reality. Record only if the user identifies a stable path or context boundary that explains both styles; otherwise defer until the code is reconciled. +- Default. Consistent, but a framework or common-practice default the agent already writes unprompted. Skip it as a no-op, not a convention. +- No signal. Under the bar: feature unused, or too few examples. Skip silently (one summary line at most). +- Tooling-owned or Already-recorded. Skip per the ground rules. + +Done when: every applicable dimension carries exactly one of those verdicts. + +### Step 2: Open-ended pass + +First, close out the architecture map from Step 0. For every non-default `app/` directory you listed, confirm how the pattern is used and apply the same evidence and decisions-not-defaults tests as Step 1. Generator-standard or sparsely used directories such as `Rules`, `Observers`, `Mail`, and `Notifications` are signals to inspect, not automatic conventions. Make genuine structural patterns candidates: Action classes invoked via `handle` / `execute` / `__invoke`, Services constructor-injected, `Queries` objects exposing `builder(): Builder`, DTOs as readonly classes or spatie/laravel-data, module or domain folders as the unit of organization. Scope each qualifying pattern to its own directory glob. Also record a consistent deliberate absence, such as "no repository layer, controllers query Eloquent directly", so the next agent matches the app's altitude. + +Then find what else makes this codebase itself: base or abstract classes most code extends, traits used everywhere, tenancy or authorization scoping woven through queries, naming schemes, and custom helpers. Same evidence bar, cite files. Record every genuine structural pattern, and cap the other house findings at ~5 so the pass stays high-signal. + +Done when: every non-default `app/` directory from Step 0 has a verdict, and the pass has produced its cited house findings (or concluded there are none). + +### Step 3: Confirm + +Present every candidate in one batch. Per item: dimension, verdict, evidence (counts and files), and the exact proposed `glob` or `globs` / `title` / `note`. Conflicts are presented as questions about an existing context boundary or deferred cleanup, not as a choice of future style. + +Default mode is confirm: record only what the user approves. Switch to yolo only when the invocation said so ("yolo", "don't ask", "just record them"), then record all pattern candidates without asking. Conflicts still go to the user in yolo. + +Done when: every candidate is approved, rejected, or (conflicts) decided. + +### Step 4: Record + +Make one `record-rule` call for each glob an approved convention applies to. Choose the most specific globs that cover the cited evidence from the mapping table below; if a convention spans models and migrations, record it under both domains so agents discover it from either path. The `note` is the bare convention: strip every trace of detection (see the ground rule). If `record-rule` is unavailable (rules disabled), report the full rule text so the user can enable `BOOST_RULES_ENABLED` or add it by hand. + +Record this: + +> Accessors and mutators: use the legacy magic-method style (`getXxxAttribute()` / `setXxxAttribute()`), not the `Attribute` class. Match it in models. + +Not this: + +> Accessors/mutators use the legacy magic-method style; the `Attribute`-class style is not used anywhere (13 legacy, 0 Attribute-class), e.g. `app/Models/Post.php`. Match the legacy style in existing models. + +Done when: every approved item has a successful tool response, and any failure is reported with its rule text. + +### Step 5: Summarize + +List recorded rules (file and title), conflicts the user deferred, notable no-signals, and remind the user to commit `.ai/rules` so their team and agents share the conventions. + +## Glob mapping + +Attach each rule to the most specific path that covers its evidence. Never a lazy `app/**` when a subtree fits. Match the glob to where the code actually lives, which is not the same in a default skeleton and in a modular or DDD layout. Use the Step 0 `app/` map to pick the real path. + +Examples: + +- Models: `app/Models/**` in a default app, or `app/Modules/Blog/Models/**` / `src/Domain/Blog/**` in a modular one. +- Controllers, routing, validation, responses: `app/Http/**`, or `app/Modules/*/Http/**` when each module owns its HTTP layer. +- Actions, Services, DTOs: `app/Actions/**`, `app/Services/**`, `app/Data/**`, or the module path the app actually uses. +- Tests: `tests/**`. +- Migrations and database: `database/migrations/**`. +- Truly app-wide (rare, e.g. auth retrieval): `app/**`. + +`record-rule` takes one glob. When a convention genuinely spans two domains (e.g. UUID keys touch models and migrations), call it once per domain with the same title and note; mentioning another path in the note does not make the rule discoverable there. + +## Edge cases + +- Rules disabled or `record-rule` missing: detection is read-only, so Steps 0 to 3 still run, and recording falls back to the manual path in Step 4. +- Tiny or fresh app: most dimensions land on no-signal. Say so honestly ("not enough code to infer conventions yet") and record nothing. +- Huge app: each dimension is a bounded grep plus a handful of file reads. Sample representative files, do not read everything. +- Re-runs: reading `.ai/rules` in Step 0 makes re-runs incremental, so only new or undecided dimensions surface. +- Non-standard layout (modules, DDD): the open-ended pass catches the layout itself as convention #1. Adapt the globs in the mapping table to the observed paths. diff --git a/_api_app/.github/skills/infer-conventions/references/checklist.md b/_api_app/.github/skills/infer-conventions/references/checklist.md new file mode 100644 index 00000000..80cef34b --- /dev/null +++ b/_api_app/.github/skills/infer-conventions/references/checklist.md @@ -0,0 +1,137 @@ +# Detection Checklist + +Every dimension here is a genuine fork: Laravel offers two or more valid approaches, the app's choice changes what the next agent writes, and no active project tool can pick for you. Left out on purpose: pure formatting (Pint owns it), any form an installed and enabled Rector rule rewrites to one canonical shape (`$casts` to `casts()`, `$fillable` to attributes, pipe-string rules to arrays, named to anonymous migrations, `$signature` to `#[Signature]`), and framework defaults any agent writes unprompted (`ShouldQueue` jobs, relation return types, `HasFactory`). + +Each item gives the fork, then a hint (a grep or dir to spot which side the app takes). Hints are only a start. Read the matched files, never record on a raw count. Apply the ground rules to every verdict: a consistent choice that is a default or a tool's target form is not a pattern. Rows tagged (architecture) are the highest-signal, so record presence and deliberate absence. + +--- + +## A. Validation & HTTP input + +1. Validation entry point: inline `$request->validate()` vs Form Request classes vs `Validator::make()`. + - Hint: `ls app/Http/Requests`; grep `->validate(` / `Validator::make(` in `app/Http/Controllers`. +2. Custom rule location: invokable rule objects in `app/Rules` vs inline closures vs `Validator::extend()` in a provider. Rule objects are the default `make:rule` path, so record only if the app leans on closures or `Validator::extend` instead. "No rule objects" alone is just no-signal. + - Hint: `ls app/Rules`; grep `Validator::extend` in `app/Providers`. +3. Typed input retrieval: typed getters (`$request->string()`, `->integer()`, `->enum()`, `->date()`) vs raw `$request->input()` / dynamic properties. + - Hint: grep `->string(` / `->integer(` / `->enum(` vs `->input(` in `app/Http`. +4. Custom messages/attributes: `lang/*/validation.php` vs Form Request `messages()` / `attributes()` methods. + - Hint: `ls lang`; grep `function messages`, `function attributes` in `app/Http/Requests`. + +## B. Controllers & routing + +5. Controller shape: invokable single-action (`__invoke`) vs resource controllers vs plain multi-method. + - Hint: grep `__invoke` in controllers; `Route::resource` / `apiResource` vs verb routes. +6. Business-logic location (architecture): fat controllers vs delegated to Actions / Services / Jobs. + - Hint: read a few controller methods; `ls app/Actions app/Services`. +7. Route handler style: closures in `routes/*.php` vs controller classes. + - Hint: count `function ()` vs `::class` in `routes/web.php`, `routes/api.php`. +8. Middleware assignment: route/group `->middleware()` vs controller `HasMiddleware::middleware()` vs `#[Middleware]` attribute. + - Hint: grep `implements HasMiddleware`, `#[Middleware(` in controllers vs `->middleware(` in routes. +9. Route model binding: implicit (type-hinted models) vs explicit `Route::bind` vs manual `findOrFail`. + - Hint: typed model params in signatures vs `findOrFail(` in controllers; grep `Route::bind`. +10. Rate limiting: named `RateLimiter::for()` + `throttle:name` vs inline `throttle:60,1`. + - Hint: grep `RateLimiter::for` in providers vs `throttle:` in route files. + +## C. Authorization + +11. Authorization home: Gates (`Gate::define`) vs Policy classes in `app/Policies`. + - Hint: `ls app/Policies`; grep `Gate::define` in `app/Providers`. +12. Authorization call site: `$this->authorize()` / `Gate::authorize()` vs `$user->can()` vs `can` middleware vs `#[Authorize]` vs `@can` in Blade. + - Hint: grep `authorize(`, `->can(`, `middleware('can:`, `#[Authorize(`, `@can(`. + +## D. Eloquent & models + +13. Mass assignment: `$fillable` allow-list vs `$guarded` block-list. + - Hint: grep `protected $fillable` / `protected $guarded` in `app/Models`. +14. Accessors/mutators: modern `Attribute` class vs legacy `getXxxAttribute()` / `setXxxAttribute()`. Record a legacy hold, it goes against the tool's grain. + - Hint: grep `: Attribute` / `Attribute::make` vs `function get[A-Z].*Attribute` in `app/Models`. +15. Primary keys: auto-increment vs `HasUuids` vs `HasUlids`. + - Hint: grep `HasUuids` / `HasUlids` in `app/Models`; migration `id()` vs `uuid('id')`. +16. Custom casts: dedicated `CastsAttributes` classes (`app/Casts`) vs inline `Attribute` vs built-in cast strings. + - Hint: `ls app/Casts`; grep `Cast::class`, `AsStringable::class` in models. +17. Data/query layer (architecture): Eloquent directly in controllers vs repositories vs dedicated query objects (e.g. classes exposing `builder(): Builder`). + - Hint: `ls app/Repositories app/Queries`; see where non-trivial queries are built. +18. Query scopes: local `scope`/`#[Scope]` methods vs dedicated builder classes. + - Hint: grep `function scope` / `#[Scope]` in models; `ls app/*/Builders`. +19. Model events: observers (`app/Observers`, `#[ObservedBy]`) vs `booted()` closures vs event classes. + - Hint: `ls app/Observers`; grep `booted`, `::observe`, `#[ObservedBy]`. +20. Eager-load posture: explicit per-query `->with()` vs model-level `$with` defaults. Treat `preventLazyLoading()` separately as a development guard because it can complement either posture. + - Hint: grep `protected $with`, `->with(`, and separately `preventLazyLoading` in `app/`. + +## E. Architecture & organization + +21. Action/Service structure (architecture): Action classes (invoked via `handle` / `execute` / `__invoke`) vs service objects vs neither. Cross-check the Step 0 `app/` map: any `Actions`/`Services`/`Pipelines`/`Jobs`-as-actions folder is this pattern, so record how it is invoked. + - Hint: `ls app/` (the whole tree, not just `Actions`/`Services`); grep the invocation method in the folder you find. +22. DTOs (architecture): spatie/laravel-data vs plain readonly classes vs arrays everywhere. + - Hint: `ls app/Data`; grep `extends Data`, `readonly class` in `app/`. +23. Dependency acquisition: constructor/method injection vs `app()` / `resolve()` / `App::make()` service location. + - Hint: grep `app(` / `resolve(` / `::make(` in `app/` vs promoted constructor deps. +24. Decoupling: events + listeners vs direct service calls. + - Hint: `ls app/Events app/Listeners`; grep `event(`, `::dispatch(`. +25. Helper vs facade idiom: global helpers (`config()`, `auth()`, `response()`) vs facades (`Config::`, `Auth::`, `Response::`). + - Hint: ratio of `config(` vs `Config::` (etc.) across `app/`. +26. Namespace layout (architecture): default `app/` skeleton vs domain/module folders (`app/Domain/**`, modules). + - Hint: `ls app/`, look for `Domain/`, `Modules/`, bounded-context folders. +27. Enums: backed vs pure; case naming; where they live. + - Hint: `ls app/Enums`; grep `enum .*: string`, `enum .*: int`. + +## F. Frontend & views + +No Livewire/Inertia/Flux package is installed. This app may be API-only. Confirm from `resources/views` before sweeping, and treat the Livewire/Flux dimensions as not applicable. + +28. Frontend stack: Blade+Livewire vs Inertia (Vue/React/Svelte) vs Blade-only / API + separate SPA. + - Hint: `composer.json` + `package.json`; `ls resources/js/pages`, `resources/views`. +29. Blade composition: class `` components vs anonymous components (`@props`) vs `@include` partials. + - Hint: `ls app/View/Components`; grep `constrained()` vs `foreignIdFor(Model::class)` vs manual `foreign()->references()->on()`. + - Hint: grep `foreignId(`, `foreignIdFor(`, `->foreign(` in `database/migrations`. +34. `down()` methods: real reverse logic vs omitted / one-way migrations. + - Hint: grep `function down` vs the migration count. +35. Enum storage: DB `enum()` column vs `string()` + PHP-enum cast on the model. + - Hint: grep `->enum(` in migrations vs string columns cast to enums. +36. Transactions: `DB::transaction(fn ...)` closure vs manual `beginTransaction` / `commit` / `rollBack`. + - Hint: grep `DB::transaction`, `beginTransaction` in `app/`. +37. Idempotent writes: `upsert` / `updateOrCreate` / `firstOrCreate` vs find-then-save. + - Hint: grep `upsert(`, `updateOrCreate(`, `firstOrCreate(` in `app/`. + +## H. Testing + +38. Framework: Pest (`it()` / `test()` / `expect()`) vs PHPUnit classes. + - Hint: `ls tests/Pest.php`; grep `it(` / `test(` vs `extends TestCase`. +39. DB reset: `RefreshDatabase` vs `DatabaseTruncation` vs `DatabaseMigrations`. + - Hint: grep those trait names in `tests/`. +40. Fixtures: compare how equivalent test-owned records are created, such as factories vs manual inserts. Track seeders separately for shared reference data because `$this->seed()` commonly and legitimately coexists with factories. + - Hint: grep `::factory(` and direct inserts in `tests/`; separately inspect `$this->seed(` calls and what those seeders provide. +41. Collaborator isolation: how the app doubles its own classes, Mockery `mock()` / `spy()` vs real integration. Ignore facade fakes like `Mail::fake()` here, they isolate framework services by default and are not a fork against Mockery. + - Hint: grep `->mock(`, `->spy(`, `Mockery::` in `tests/`. +42. Endpoint assertions: array `assertJson([...])` / `assertJsonFragment` vs fluent `AssertableJson`. + - Hint: grep `AssertableJson`, `assertJsonFragment` in `tests/`. + +## I. Responses & API resources + +43. Response shape: API Resource classes vs `response()->json()` vs returning models/arrays directly. + - Hint: `ls app/Http/Resources`; grep `JsonResource`, `->json(` in controllers. +44. Resource relationship inclusion: `whenLoaded()` guards vs unconditional relationship access. Do not count ordinary scalar attributes as rivals to conditional relationships, and evaluate general `when()` fields separately. + - Hint: compare relationship fields using `whenLoaded(` with unconditional relationship property access in `app/Http/Resources`. +45. Pagination contracts: within comparable endpoint categories, length-aware `paginate()` vs `simplePaginate()` vs `cursorPaginate()`. These have different totals, navigation, ordering, and performance contracts, so record only a stable path-scoped API policy, never a project-wide majority. + - Hint: grep those in `app/`, then group matches by endpoint type and client contract before comparing them. +46. Web redirects/URLs: `route('name')` vs `url('/path')` vs `action([...])`. + - Hint: grep `route('`, `url('/`, `action([` in `app/Http` and views. + +## J. Strings, collections & dates + +47. Iteration idiom: `collect()->map()->filter()` pipelines vs `array_map` / `foreach`. + - Hint: grep `collect(`, `->map(` vs `array_map`, `foreach` density in `app/`. +48. String API: fluent `Str::of()->...` (Stringable) vs static `Str::` vs native (`trim`, `strtoupper`). + - Hint: grep `Str::of(` vs `Str::` vs native string funcs. +49. Dates: compare equivalent construction call styles (`now()` / `today()` helpers vs `Carbon::`) separately from the application's mutable/immutable date policy. `Date::use(CarbonImmutable::class)` can make helpers return immutable dates, so those signals are complementary rather than conflicting. + - Hint: grep `now(` and `Carbon::` for call style; separately inspect `CarbonImmutable` and `Date::use` for mutability policy. + +--- + +Genuine forks only. Every row survived the "no tool can decide this, and it isn't the default" filter. Give each applicable dimension exactly one verdict: pattern, conflict, default, no-signal, tooling-owned, or already-recorded. The rows tagged (architecture) are where the highest-value rules come from. diff --git a/_api_app/.github/skills/laravel-best-practices/SKILL.md b/_api_app/.github/skills/laravel-best-practices/SKILL.md index 99018f3a..311ab844 100644 --- a/_api_app/.github/skills/laravel-best-practices/SKILL.md +++ b/_api_app/.github/skills/laravel-best-practices/SKILL.md @@ -8,183 +8,52 @@ metadata: # 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`. +Best practices for Laravel, organized as an index of rule files. Each rule file 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. +Before applying any rule, check what the application already does. Laravel offers multiple valid approaches, and 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 +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. ## 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 \ No newline at end of file +1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out. +2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files. +3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job. +4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable. +5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them. +6. Re-read the diff against every mapped rule before finishing. + +## Rule Index + +Cross-cutting changes often need more than one rule file. + +| Concern | Read | +| --- | --- | +| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) | +| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) | +| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) | +| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) | +| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) | +| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) | +| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) | +| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) | +| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) | +| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) | +| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) | +| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) | +| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) | +| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) | +| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) | +| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) | +| Environment values and application configuration | [`rules/config.md`](rules/config.md) | +| Tests: coverage, factories, fakes, and assertions | the `testing-best-practices` skill | +| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) | +| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) | + +## Decision Rules + +- Prefer framework features and existing application abstractions over new helpers or dependencies. +- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable. +- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/advanced-queries.md b/_api_app/.github/skills/laravel-best-practices/rules/advanced-queries.md index 920714a1..54dd783d 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/advanced-queries.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/advanced-queries.md @@ -1,8 +1,8 @@ -# Advanced Query Patterns +# Advanced Query Best Practices -## Use `addSelect()` Subqueries for Single Values from Has-Many +## Select Single Relationship Values with Subqueries -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. +When only one value from a has-many relationship is needed, consider a correlated subquery with `addSelect()` instead of loading the entire relationship. This selects the value as part of the main query without an additional relationship query. ```php public function scopeWithLastLoginAt($query): void @@ -16,14 +16,14 @@ public function scopeWithLastLoginAt($query): void } ``` -## Create Dynamic Relationships via Subquery FK +## Create Dynamic Relationships with a Subquery Foreign Key -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. +The same pattern can select a foreign key and expose the selected model through a `belongsTo` relationship. Eager loading that relationship still executes a separate query, but it avoids loading the full has-many collection. ```php public function lastLogin(): BelongsTo { - return $this->belongsTo(Login::class); + return $this->belongsTo(Login::class, 'last_login_id'); } public function scopeWithLastLogin($query): void @@ -37,9 +37,9 @@ public function scopeWithLastLogin($query): void } ``` -## Use Conditional Aggregates Instead of Multiple Count Queries +## Combine Related Counts with Conditional Aggregates -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. +Combine several counts over the same filtered data set into one query by using conditional aggregates. Use `toBase()` when only scalar values are needed and model hydration provides no benefit. Confirm the expression syntax against the application's database engine. ```php $statuses = Feature::toBase() @@ -49,50 +49,50 @@ $statuses = Feature::toBase() ->first(); ``` -## Use `setRelation()` to Prevent Circular N+1 +## Reuse Loaded Parent Models with `setRelation()` -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. +When a parent and its children are already loaded and code also accesses `$child->parent`, set the inverse relationship to the existing parent instance. This avoids an additional lazy-loading query for each child. ```php $feature->load('comments.user'); $feature->comments->each->setRelation('feature', $feature); ``` -## Prefer `whereIn` + Subquery Over `whereHas` +## Compare `whereHas()` with an `IN` Subquery -`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. +`whereHas()` typically produces an `EXISTS` subquery, while `whereIn()` can express the same filter with an `IN` subquery. Either form may be faster depending on the database engine, indexes, cardinality, and query plan. Measure both forms with representative data; neither subquery loads its result set into PHP memory. -Incorrect (correlated EXISTS re-executes per row): +Option using `EXISTS`: ```php $query->whereHas('company', fn ($q) => $q->where('name', 'like', $term)); ``` -Correct (index-friendly subquery, no PHP memory overhead): +Option using `IN`: ```php $query->whereIn('company_id', Company::where('name', 'like', $term)->select('id')); ``` -## Sometimes Two Simple Queries Beat One Complex Query +## Measure Two Simple Queries Against 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. +Two targeted queries can outperform one complex correlated subquery or join when the first query is highly selective. They also add a database round trip, can transfer a large identifier list, and do not provide a single-query consistency snapshot. Decide from query plans and production-like measurements. -## Use Compound Indexes Matching `orderBy` Column Order +## Design Composite Indexes for the Query -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. +For common multi-column sorts, consider a composite index whose column order supports the query's filters and ordering. Database engines may combine indexes or choose an explicit sort, so matching the `ORDER BY` list alone does not guarantee that an index will be used. Verify the query plan. ```php // Migration $table->index(['last_name', 'first_name']); -// Query — column order must match the index +// Query that this index may support User::query()->orderBy('last_name')->orderBy('first_name')->paginate(); ``` -## Use Correlated Subqueries for Has-Many Ordering +## Consider a Correlated Subquery 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. +When sorting by one value from a has-many relationship, a direct join can duplicate parent rows unless it first reduces the related table to one row per parent. A correlated subquery in `orderBy()` is often simpler, but its performance depends on the query plan and supporting indexes. ```php public function scopeOrderByLastLogin($query): void @@ -103,4 +103,4 @@ public function scopeOrderByLastLogin($query): void ->take(1) ); } -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/architecture.md b/_api_app/.github/skills/laravel-best-practices/rules/architecture.md index 16505642..5e7af23b 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/architecture.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/architecture.md @@ -1,15 +1,15 @@ # Architecture Best Practices -## Single-Purpose Action Classes +## Extract Focused Business Operations -Extract discrete business operations into invokable Action classes. +Extract a discrete business operation into an action class when doing so makes the operation easier to reuse or test. An action class has no special meaning to Laravel; follow the project's naming and invocation conventions. ```php class CreateOrderAction { public function __construct(private InventoryService $inventory) {} - public function execute(array $data): Order + public function handle(array $data): Order { $order = Order::create($data); $this->inventory->reserve($order); @@ -19,11 +19,12 @@ class CreateOrderAction } ``` -## Use Dependency Injection +## Inject Required Dependencies -Always use constructor injection. Avoid `app()` or `resolve()` inside classes. +Prefer constructor injection for dependencies required throughout an object's lifetime. Method injection is appropriate for dependencies needed by one controller action, listener, job handler, or other container-invoked method. Avoid `app()` and `resolve()` when normal injection can make a dependency explicit. + +Hidden dependency: -Incorrect: ```php class OrderController extends Controller { @@ -36,24 +37,24 @@ class OrderController extends Controller } ``` -Correct: +Injected dependency: + ```php class OrderController extends Controller { - public function __construct(private OrderService $service) {} - - public function store(StoreOrderRequest $request) + public function store(StoreOrderRequest $request, OrderService $service) { - return $this->service->create($request->validated()); + return $service->create($request->validated()); } } ``` -## Code to Interfaces +## Depend on Contracts at Boundaries + +Depend on contracts at system boundaries, such as payment gateways, notification channels, and external services, when testability or interchangeable implementations justify the abstraction. -Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability. +Concrete boundary dependency: -Incorrect (concrete dependency): ```php class OrderService { @@ -61,7 +62,8 @@ class OrderService } ``` -Correct (interface dependency): +Contract boundary dependency: + ```php interface PaymentGateway { @@ -80,86 +82,99 @@ Bind in a service provider: $this->app->bind(PaymentGateway::class, StripeGateway::class); ``` -## Default Sort by Descending +## Specify a Deterministic Sort Order -When no explicit order is specified, sort by `id` or `created_at` descending. Explicit ordering prevents cross-database inconsistencies between MySQL and Postgres. +Without an explicit `ORDER BY`, row order is undefined. Choose an order that matches the feature, and add a unique tie-breaker when stable pagination matters. + +Unspecified order: -Incorrect: ```php $posts = Post::paginate(); ``` -Correct: +Newest first with a stable tie-breaker: + ```php -$posts = Post::latest()->paginate(); +$posts = Post::query() + ->orderByDesc('created_at') + ->orderByDesc('id') + ->paginate(); ``` ## Use Atomic Locks for Race Conditions -Prevent race conditions with `Cache::lock()` or `lockForUpdate()`. +Use a lock when concurrent execution must be serialized. `Cache::lock()` provides an atomic lock when the configured cache store supports locks. `lockForUpdate()` locks selected database rows and must run inside a database transaction. These mechanisms solve different coordination problems. ```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(); +// Or at query level, inside a transaction +DB::transaction(function () use ($id) { + $product = Product::where('id', $id)->lockForUpdate()->first(); + + // Read and update the product while the database lock is held. +}); ``` ## 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. +When no Laravel helper exists, prefer multibyte-aware functions such as `mb_strlen()` and `mb_strtolower()` for UTF-8 text. For example, `strlen()` counts bytes, while `strtolower()` is not multibyte-aware. Incorrect: + ```php -strlen('José'); // 5 (bytes, not characters) -strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte +strlen('José'); // 5 bytes, not 4 characters +strtolower('MÜNCHEN'); // Does not lowercase Ü ``` Correct: + ```php -mb_strlen('José'); // 4 (characters) -mb_strtolower('MÜNCHEN'); // 'münchen' +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' +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. +For lightweight work that does not need retries or crash durability, consider `defer()` instead of dispatching a job. During an HTTP request, the callback normally runs after the response has been sent but remains in the same PHP process. + +Queued and durable: -Incorrect (job overhead for trivial work): ```php dispatch(new LogPageView($page)); ``` -Correct (runs after response, same process): +Deferred in the current 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 a queued job when the work needs retries, queue controls, or durability across process failures. ## Use `Context` for Request-Scoped Data -The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually. +The `Context` facade makes contextual data available across the current execution lifecycle without manually passing arguments through every layer. ```php // In middleware Context::add('tenant_id', $request->header('X-Tenant-ID')); -// Anywhere later — controllers, jobs, log context +// Later in the same execution lifecycle $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`. +Visible context is added to log context, and both visible and hidden context are captured and restored for queued jobs. Use `Context::addHidden()` for data that should propagate to queued jobs without appearing in logs. Do not place secrets in context unless that propagation is intended. ## Use `Concurrency::run()` for Parallel Execution -Run independent operations in parallel using child processes — no async libraries needed. +Run independent operations concurrently through Laravel's configured concurrency driver. ```php use Illuminate\Support\Facades\Concurrency; @@ -170,13 +185,14 @@ use Illuminate\Support\Facades\Concurrency; ]); ``` -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. +With a process-based driver, each closure runs in a separate PHP process that boots the application. Use concurrency when independent database queries, HTTP client calls, or computations benefit enough to offset process and serialization overhead. The `sync` driver executes closures sequentially and is useful primarily during testing. -## Convention Over Configuration +## Follow Framework Conventions -Follow Laravel conventions. Don't override defaults unnecessarily. +Follow Laravel conventions unless the domain or an existing schema requires an override. + +Customized schema: -Incorrect: ```php class Customer extends Model { @@ -190,7 +206,8 @@ class Customer extends Model } ``` -Correct: +Conventional schema: + ```php class Customer extends Model { @@ -199,4 +216,4 @@ class Customer extends Model return $this->belongsToMany(Role::class); } } -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/blade-views.md b/_api_app/.github/skills/laravel-best-practices/rules/blade-views.md index c6f8aaf1..4ea2c6c3 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/blade-views.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/blade-views.md @@ -1,8 +1,8 @@ -# Blade & Views Best Practices +# Blade and View Best Practices ## Use `$attributes->merge()` in Component Templates -Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly. +Use the component attribute bag so callers can add attributes. `merge()` combines default attributes with caller-provided values; class values receive special merging behavior. ```blade
merge(['class' => 'alert alert-'.$type]) }}> @@ -12,25 +12,25 @@ Hardcoding classes prevents consumers from adding their own. `merge()` combines ## 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. +If a component renders repeatedly, `@push` adds its script on every render. Use a consistently named `@pushOnce` block to add that content once per rendered response. -## Prefer Blade Components Over `@include` +## Prefer Components for Explicit Interfaces -`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots. +Use a Blade component when a reusable interface benefits from explicit props, an attribute bag, or slots. An include remains suitable for a small partial that intentionally uses the current view data; pass an explicit data array when implicit variable sharing would obscure its dependencies. -## Use View Composers for Shared View Data +## Share Compatible View Data with a View Composer -If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it. +Use a view composer to centralize data needed whenever one or more named Blade views are rendered. Keep the composer compatible with every view it targets, and avoid broad wildcards when views require different data shapes. A view composer runs when Laravel renders the matching view; it does not supply data to JSON, streamed, or other non-view responses. -## Use Blade Fragments for Partial Re-Renders (htmx/Turbo) +## Return Blade Fragments for Partial Rendering -A single view can return either the full page or just a fragment, keeping routing clean. +A route can return either a full view or a named fragment for clients such as htmx or Turbo. ```php return view('dashboard', compact('users')) ->fragmentIf($request->hasHeader('HX-Request'), 'user-list'); ``` -## Use `@aware` for Deeply Nested Component Props +## Share Parent Component Props with `@aware` -Avoids re-passing parent props through every level of nested components. \ No newline at end of file +Use `@aware` when a nested component needs a prop explicitly passed to an ancestor component. It does not expose an ancestor's default prop value unless that value was passed through the attribute bag. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/caching.md b/_api_app/.github/skills/laravel-best-practices/rules/caching.md index eb3ef3e6..cd2ffd60 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/caching.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/caching.md @@ -1,10 +1,13 @@ # Caching Best Practices -## Use `Cache::remember()` Instead of Manual Get/Put +## Use `Cache::remember()` for Cache-Aside Reads -Atomic pattern prevents race conditions and removes boilerplate. +`Cache::remember()` implements a cache-aside read without a separate truthiness check. It does not prevent concurrent requests from computing the same missing value; use an atomic lock when duplicate computation must be prevented. + +The manual version below incorrectly treats valid falsy values, such as `false` or `0`, as cache misses. Incorrect: + ```php $val = Cache::get('stats'); if (! $val) { @@ -14,27 +17,42 @@ if (! $val) { ``` Correct: + ```php $val = Cache::remember('stats', 60, fn () => $this->computeStats()); ``` -## Use `Cache::flexible()` for Stale-While-Revalidate +## Consider `Cache::flexible()` for Stale-While-Revalidate + +For frequently read keys, `Cache::flexible()` can serve stale data during a defined stale period and register a deferred refresh. During an HTTP request, that refresh normally runs after the response; it is not a durable background job. Once the stale period has elapsed, the request recomputes the value synchronously. + +Synchronous expiration: + +```php +Cache::remember('users', 300, fn () => User::all()); +``` + +Stale-while-revalidate tradeoff: -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. +```php +Cache::flexible('users', [300, 600], fn () => User::all()); +``` -Incorrect: `Cache::remember('users', 300, fn () => User::all());` +This value is fresh for five minutes and may be served stale until ten minutes after it was cached. -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 an Execution -## Use `Cache::memo()` to Avoid Redundant Hits Within a Request +If the same cache key is read repeatedly during one request or job, `memo()` decorates a cache store and retains resolved values in memory for that execution. -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. +```php +$settings = Cache::memo()->get('settings'); +``` -`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5. +Repeated reads through the same memoized store avoid additional store lookups. Writes through the memoized store update or invalidate its in-memory values as appropriate. ## 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`. +Tags group related entries for invalidation without tracking each key. Cache tags are not supported by the `file`, `dynamodb`, or `database` drivers; confirm support before choosing a store. ```php Cache::tags(['user-1'])->flush(); @@ -42,15 +60,27 @@ 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. +`add()` atomically writes a value only when the key does not already exist. + +Incorrect: + +```php +if (! Cache::has('lock')) { + Cache::put('lock', true, 10); +} +``` + +Correct: -Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }` +```php +Cache::add('lock', true, 10); +``` -Correct: `Cache::add('lock', true, 10);` +Use `Cache::lock()` rather than an ordinary cache key when lock ownership and safe release are required. -## Use `once()` for Per-Request Memoization +## Use `once()` for In-Process 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. +`once()` memoizes a callback's return value for the current request or job. Calls made from an object instance are scoped to that instance. Unlike `Cache::memo()`, `once()` does not read from an external cache store. ```php public function roles(): Collection @@ -59,12 +89,12 @@ public function roles(): Collection } ``` -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. +Repeated calls return the memoized result without rerunning the callback. Use `once()` for repeated computation within one execution. Use `Cache::memo()` to memoize access to an underlying store that can also persist values across executions. ## Configure Failover Cache Stores in Production -If Redis goes down, the app falls back to a secondary store automatically. +The failover driver tries each configured store in order when a store operation throws an exception. It does not consult later stores for an ordinary cache miss, and data is not replicated between stores. ```php 'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']], -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/collections.md b/_api_app/.github/skills/laravel-best-practices/rules/collections.md index 14f683d3..211fec11 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/collections.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/collections.md @@ -2,43 +2,71 @@ ## Use Higher-Order Messages for Simple Operations -Incorrect: +Explicit closure: + ```php $users->each(function (User $user) { $user->markAsVip(); }); ``` -Correct: `$users->each->markAsVip();` +Concise equivalent: + +```php +$users->each->markAsVip(); +``` -Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc. +Higher-order messages are available for supported collection methods such as `each`, `map`, `filter`, and `sum`. Use an explicit closure when arguments or nontrivial logic would be clearer. -## Choose `cursor()` vs. `lazy()` Correctly +## Choose Between `cursor()` and `lazy()` -- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk). -- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading. +`cursor()` executes one query and hydrates models individually, but it cannot eager load relationships. The database driver's result buffering can still consume substantial memory for very large results. Use it for low-memory, attribute-only iteration when one long-running query is acceptable. -Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored. +`lazy()` executes multiple chunked queries and returns a flat `LazyCollection`. It supports eager loading relationships for each chunk and avoids holding one database cursor open for the entire iteration. -Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work. +With relationships: + +```php +User::with('roles')->lazy()->each(function (User $user) { + // The roles for this chunk have been eager loaded. +}); +``` + +Without relationships: + +```php +User::cursor()->each(function (User $user) { + // Process model attributes. +}); +``` ## 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. +`lazy()` uses offset pagination, so updates to columns that affect the query can shift rows and cause records to be skipped or processed twice. `lazyById()` paginates by a monotonic key and is safer when updating other columns during iteration. Do not change the pagination key itself while iterating. ## Use `toQuery()` for Bulk Operations on Collections -Avoids manual `whereIn` construction. +Use `toQuery()` to build a query from the models in an Eloquent collection instead of manually constructing a `whereIn` clause. + +Manual query: + +```php +User::whereIn('id', $users->modelKeys())->update(['active' => false]); +``` + +Collection query: -Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);` +```php +$users->toQuery()->update(['active' => false]); +``` -Correct: `$users->toQuery()->update([...]);` +`toQuery()` requires a non-empty Eloquent collection whose models are of the same type. Like other bulk Eloquent updates, it does not dispatch per-model update events, so use it only when those events are not required. ## Use `#[CollectedBy]` for Custom Collection Classes -More declarative than overriding `newCollection()`. +The `#[CollectedBy]` attribute declares the custom collection class without requiring a `newCollection()` override. ```php #[CollectedBy(UserCollection::class)] class User extends Model {} -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/config.md b/_api_app/.github/skills/laravel-best-practices/rules/config.md index 8fd8f536..82691c42 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/config.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/config.md @@ -1,73 +1,85 @@ # Configuration Best Practices -## `env()` Only in Config Files +## Read Environment Variables in Configuration Files -Direct `env()` calls return `null` when config is cached. +Call `env()` only from configuration files. After configuration is cached, Laravel does not load the application's `.env` file, so application code should read configuration values through `config()`. Incorrect: + ```php $key = env('API_KEY'); ``` Correct: + ```php // config/services.php -'key' => env('API_KEY'), +return [ + 'key' => env('API_KEY'), +]; // Application code $key = config('services.key'); ``` -## Use Encrypted Env or External Secrets +## Protect Production Secrets -Never store production secrets in plain `.env` files in version control. +Do not commit plaintext production secrets. Laravel can encrypt an environment file so its encrypted form can be stored safely, while deployment platforms can supply secrets through their native secret stores. Incorrect: -```bash -# .env committed to repo or shared in Slack - -STRIPE_SECRET=sk_live_abc123 -AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI +```bash +# A plaintext .env file committed to the repository +STRIPE_SECRET= +AWS_SECRET_ACCESS_KEY= ``` -Correct: +Encrypted environment file: + ```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. +For hosted deployments, consider the platform's native secret store, such as AWS Secrets Manager or Vault, and inject secrets 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 +## Name Repeated Domain Values -Use class constants instead of hardcoded magic strings for model states, types, and statuses. +Use an enum or class constant when a domain value is repeated or represents a constrained set. A one-off string literal does not always need a named constant. ```php -// Incorrect +// Repeated literal return $this->type === 'normal'; -// Correct +// Named domain value 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. +If the application supports localization, put user-facing strings in language files and retrieve them with `__()`. Simple literals are reasonable for applications that intentionally do not support multiple languages. ```php -// Only when lang files already exist in the project +// In a localized application return back()->with('message', __('app.article_added')); -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/db-performance.md b/_api_app/.github/skills/laravel-best-practices/rules/db-performance.md index 8fb71937..3f339770 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/db-performance.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/db-performance.md @@ -1,10 +1,11 @@ # Database Performance Best Practices -## Always Eager Load Relationships +## Eager Load Relationships Before Iterating -Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront. +When a relationship will be accessed for many models, eager load it with `with()` to avoid running one initial query plus one relationship query per model, commonly called an N+1 query pattern. Lazy loading is reasonable when the relationship may not be needed or only one model is involved. + +Lazy-loaded version: -Incorrect (N+1 — executes 1 + N queries): ```php $posts = Post::all(); foreach ($posts as $post) { @@ -12,7 +13,8 @@ foreach ($posts as $post) { } ``` -Correct (2 queries total): +Eager-loaded version: + ```php $posts = Post::with('author')->get(); foreach ($posts as $post) { @@ -20,7 +22,7 @@ foreach ($posts as $post) { } ``` -Constrain eager loads to select only needed columns (always include the foreign key): +Constrain eager loads when large columns are unnecessary. Include the related model's primary key and every column Eloquent needs to match the relationship. In this example, `users.id` and `posts.user_id` match posts to users, while selecting `posts.id` preserves each related model's primary key: ```php $users = User::with(['posts' => function ($query) { @@ -42,31 +44,34 @@ public function boot(): void } ``` -Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded. +By default, accessing an unloaded relationship then throws a `LazyLoadingViolationException`. Applications can customize violation handling with `handleLazyLoadingViolationUsing()`. ## Select Only Needed Columns -Avoid `SELECT *` — especially when tables have large text or JSON columns. +Select only the columns the operation needs when omitting large text, binary, or JSON columns provides a meaningful benefit. + +All columns: -Incorrect: ```php $posts = Post::with('author')->get(); ``` -Correct: +Selected columns: + ```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. +When limiting selected columns, retain every key Eloquent needs for matching. A `belongsTo` relationship needs its foreign key on the parent query and the owner's key on the related query. A `hasMany` relationship needs the parent's local key and the related model's foreign key. -## Chunk Large Datasets +## Process Large Data Sets Incrementally -Never load thousands of records at once. Use chunking for batch processing. +Use chunking or lazy iteration when loading an entire result set would exceed the application's practical memory budget. + +Loads the complete result set: -Incorrect: ```php $users = User::all(); foreach ($users as $user) { @@ -74,7 +79,8 @@ foreach ($users as $user) { } ``` -Correct: +Processes bounded chunks: + ```php User::where('subscribed', true)->chunk(200, function ($users) { foreach ($users as $user) { @@ -83,7 +89,7 @@ User::where('subscribed', true)->chunk(200, function ($users) { }); ``` -Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change: +Use `chunkById()` when updates can change which rows match the query. Standard `chunk()` uses offset pagination, whose result positions can shift as rows change: ```php User::where('active', false)->chunkById(200, function ($users) { @@ -91,11 +97,14 @@ User::where('active', false)->chunkById(200, function ($users) { }); ``` -## Add Database Indexes +For read-only, attribute-only iteration, `cursor()` hydrates models individually from one query, although some database drivers still buffer raw results. Use `lazy()` when relationships must be eager loaded in chunks, and use `lazyById()` or `chunkById()` when updates can affect query membership. See the collection rules for detailed tradeoffs. + +## Add Indexes for Measured Query Patterns -Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses. +Design indexes around frequent, performance-sensitive query patterns. A column's presence in `WHERE`, `ORDER BY`, `JOIN`, or `GROUP BY` does not by itself justify an index; selectivity, write cost, existing indexes, and the database query plan all matter. + +Schema without an application-specific query index: -Incorrect: ```php Schema::create('orders', function (Blueprint $table) { $table->id(); @@ -105,24 +114,26 @@ Schema::create('orders', function (Blueprint $table) { }); ``` -Correct: +Schema optimized for `WHERE status = ? ORDER BY created_at`: + ```php Schema::create('orders', function (Blueprint $table) { $table->id(); - $table->foreignId('user_id')->index()->constrained(); - $table->string('status')->index(); + $table->foreignId('user_id')->constrained(); + $table->string('status'); $table->timestamps(); $table->index(['status', 'created_at']); }); ``` -Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`). +Confirm composite index column order and effectiveness with production-like data and the database's query-plan tools. Also check whether the database already created an index to support a foreign key before adding another one. + +## Count Relationships Without Loading Them -## Use `withCount()` for Counting Relations +Use `withCount()` when only relationship counts are needed; loading and hydrating every related model wastes memory. -Never load entire collections just to count them. +Loads related models: -Incorrect: ```php $posts = Post::all(); foreach ($posts as $post) { @@ -130,7 +141,8 @@ foreach ($posts as $post) { } ``` -Correct: +Selects relationship counts: + ```php $posts = Post::withCount('comments')->get(); foreach ($posts as $post) { @@ -149,39 +161,24 @@ $posts = Post::withCount([ ])->get(); ``` -## Use `cursor()` for Memory-Efficient Iteration +## Keep Queries Out of Blade Templates -For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator. +Prepare data before rendering a Blade template, such as in a controller, query service, or view composer. This keeps query behavior visible and testable. -Incorrect: -```php -$users = User::where('active', true)->get(); -``` - -Correct: -```php -foreach (User::where('active', true)->cursor() as $user) { - ProcessUser::dispatch($user->id); -} -``` +Query in the template: -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: +Data prepared before rendering: + ```php // Controller $users = User::with('profile')->get(); + return view('users.index', compact('users')); ``` @@ -189,4 +186,4 @@ return view('users.index', compact('users')); @foreach ($users as $user) {{ $user->profile->name }} @endforeach -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/eloquent.md b/_api_app/.github/skills/laravel-best-practices/rules/eloquent.md index 09cd66a0..5a89510d 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/eloquent.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/eloquent.md @@ -1,8 +1,8 @@ # Eloquent Best Practices -## Use Correct Relationship Types +## Define Precise Relationship Types -Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints. +Define the relationship that matches the database association, and declare its concrete return type. ```php public function comments(): HasMany @@ -20,7 +20,8 @@ public function author(): BelongsTo Extract reusable query constraints into local scopes to avoid duplication. -Incorrect: +Duplicated constraints: + ```php $active = User::where('verified', true)->whereNotNull('activated_at')->get(); $articles = Article::whereHas('user', function ($q) { @@ -28,9 +29,11 @@ $articles = Article::whereHas('user', function ($q) { })->get(); ``` -Correct: +Reusable local scope: + ```php -public function scopeActive(Builder $query): Builder +#[Scope] +protected function active(Builder $query): Builder { return $query->where('verified', true)->whereNotNull('activated_at'); } @@ -44,7 +47,8 @@ $articles = Article::whereHas('user', fn ($q) => $q->active())->get(); 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): +Global scope tradeoff: + ```php class PublishedScope implements Scope { @@ -53,12 +57,15 @@ class PublishedScope implements Scope $builder->where('published', true); } } -// Now admin panels, reports, and background jobs all silently skip drafts + +// Admin panels, reports, and jobs now omit drafts unless the scope is removed. ``` -Correct (local scope you opt into): +Explicit local scope: + ```php -public function scopePublished(Builder $query): Builder +#[Scope] +protected function published(Builder $query): Builder { return $query->where('published', true); } @@ -82,16 +89,18 @@ protected function casts(): array } ``` -## Cast Date Columns Properly +## Cast Date and Time Attributes + +Cast a date or timestamp attribute when application code should treat it as a Carbon instance. Eloquent already casts the conventional `created_at` and `updated_at` timestamps. -Always cast date columns. Use Carbon instances in templates instead of formatting strings manually. +Manual parsing in the template: -Incorrect: ```blade -{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }} +{{ Carbon::parse($order->ordered_at)->toDateString() }} ``` -Correct: +Model cast: + ```php protected function casts(): array { @@ -108,24 +117,27 @@ protected function casts(): array ## Use `whereBelongsTo()` for Relationship Queries -Cleaner than manually specifying foreign keys. +`whereBelongsTo()` expresses the relationship constraint without manually specifying its foreign key. + +Foreign key constraint: -Incorrect: ```php Post::where('user_id', $user->id)->get(); ``` -Correct: +Relationship-aware constraint: + ```php Post::whereBelongsTo($user)->get(); Post::whereBelongsTo($user, 'author')->get(); ``` -## Avoid Hardcoded Table Names in Queries +## Keep Application Queries Model-Aware -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). +Prefer Eloquent models and relationships for model-backed application queries. They preserve casts, scopes, and model table configuration. The query builder and raw SQL legitimately require table names, so use them when their lower-level behavior is intentional. + +Lower-level alternatives: -Incorrect: ```php DB::table('users')->where('active', true)->get(); @@ -134,15 +146,13 @@ $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(); +Model-aware queries: -// Even better — use Eloquent or the query builder instead of raw SQL +```php 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. +When a query builder operation should follow a model's configured table name, use `(new User)->getTable()`. For complex joins or raw SQL, explicit table names may be clearer; keep those references covered by tests when schema changes are possible. -**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. \ No newline at end of file +In migrations, use explicit table names rather than application models. Migrations are historical snapshots, while models and their scopes can change after a migration is deployed. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/error-handling.md b/_api_app/.github/skills/laravel-best-practices/rules/error-handling.md index bb8e7a38..afcdf4ac 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/error-handling.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/error-handling.md @@ -1,15 +1,18 @@ # Error Handling Best Practices -## Exception Reporting and Rendering +## Choose Where to Report and Render Exceptions -There are two valid approaches — choose one and apply it consistently across the project. +Laravel supports exception-specific methods and centralized handler callbacks. Follow the pattern already established by the project. -**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find: +Exception methods keep behavior beside the exception definition: ```php class InvalidOrderException extends Exception { - public function report(): void { /* custom reporting */ } + public function report(): void + { + // Send the exception to a custom reporter. + } public function render(Request $request): Response { @@ -18,38 +21,40 @@ class InvalidOrderException extends Exception } ``` -**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture: +Centralized callbacks in `bootstrap/app.php` keep the application's exception policy together: ```php ->withExceptions(function (Exceptions $exceptions) { - $exceptions->report(function (InvalidOrderException $e) { /* ... */ }); + $exceptions->report(function (InvalidOrderException $e) { + // Send the exception to a custom reporter. + }); $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. +An exception's `report()` method suppresses Laravel's default reporting unless it returns `false`. A report callback allows default reporting unless it returns `false` or is chained with `stop()`. Use `ShouldntReport` or `dontReport()` when the handler should not report an exception at all. By contrast, returning `false` from a `render()` method or render callback defers to Laravel's default rendering. -## Use `ShouldntReport` for Exceptions That Should Never Log +## Mark Exceptions the Handler Should Not Report -More discoverable than listing classes in `dontReport()`. +Implementing `ShouldntReport` prevents Laravel's exception handler from reporting that exception type and keeps the policy visible on the class. It does not prevent application code from logging the exception explicitly. ```php class PodcastProcessingException extends Exception implements ShouldntReport {} ``` -## Throttle High-Volume Exceptions +## Throttle High-Volume Exception Reports -A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type. +A failing integration can flood logs or error tracking. Configure `throttle()` with a `Lottery` or `Limit` result to sample or rate-limit matching exception reports. Choose keys deliberately when separate exception classes, tenants, or integrations need independent limits. -## Enable `dontReportDuplicates()` +## Prevent Duplicate Reports of One Exception Instance -Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks. +Enable `dontReportDuplicates()` when the same exception object may pass through multiple `report($exception)` calls. It deduplicates by object identity, not by exception class or message. -## Force JSON Error Rendering for API Routes +## Define JSON Rendering for API Routes -Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes. +Laravel normally uses request content negotiation to decide whether to render an exception as JSON. If the application's API contract requires JSON regardless of the `Accept` header, define that policy explicitly for the relevant routes. ```php $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { @@ -59,7 +64,7 @@ $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { ## 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. +Attach structured data to an exception through `context()`. Laravel merges that data into the exception's log context when the handler reports it. ```php class InvalidOrderException extends Exception @@ -69,4 +74,4 @@ class InvalidOrderException extends Exception return ['order_id' => $this->orderId]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/events-notifications.md b/_api_app/.github/skills/laravel-best-practices/rules/events-notifications.md index bc43f199..a8cce31a 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/events-notifications.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/events-notifications.md @@ -1,24 +1,24 @@ -# Events & Notifications Best Practices +# Events and Notifications Best Practices ## Rely on Event Discovery -Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`. +Laravel discovers listeners in the configured listener directories by inspecting type-hinted event arguments on `handle()` or `__invoke()` methods. Register listeners manually only when discovery is disabled, the listener is outside those directories, or explicit registration is clearer. -## Run `event:cache` in Production Deploy +## Cache Event Discovery During Production Deployment -Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`. +Cache discovered listeners during production deployment with `php artisan optimize` or `php artisan event:cache`. Rebuild the cache whenever listener definitions change. ## Use `ShouldDispatchAfterCommit` Inside Transactions -Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet. +When an event is dispatched inside a database transaction, `ShouldDispatchAfterCommit` delays dispatch until all open database transactions commit. If a transaction rolls back, Laravel discards the event. This affects synchronous and queued listeners; it is not limited to queue timing. ```php class OrderShipped implements ShouldDispatchAfterCommit {} ``` -## Always Queue Notifications +## Queue Slow Notifications -Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response. +Queue notifications that call external services, such as email, text messaging, or Slack, when they do not need to complete before the response. Keep a notification synchronous when immediate completion or failure feedback is part of the operation. ```php class InvoicePaid extends Notification implements ShouldQueue @@ -27,13 +27,17 @@ class InvoicePaid extends Notification implements ShouldQueue } ``` -## Use `afterCommit()` on Notifications in Transactions +## Dispatch Queued Notifications After Commit -Same race condition as events — the queued notification job may run before the transaction commits. +A queued notification sent inside a database transaction can run before the transaction commits. Call `afterCommit()` on the queued notification, or enable the queue connection's `after_commit` option, when its delivery depends on committed data. This setting has no scheduling effect on a synchronous notification. + +```php +$user->notify((new InvoicePaid($invoice))->afterCommit()); +``` ## Route Notification Channels to Dedicated Queues -Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues. +Different notification channels can have different latency and priority requirements. Implement `viaQueues()` when channels should use separate queues. ## Use On-Demand Notifications for Non-User Recipients @@ -45,4 +49,4 @@ 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. \ No newline at end of file +Implement `HasLocalePreference::preferredLocale()` on a notifiable model when notifications and mailables should use the recipient's locale. Laravel also preserves that locale for queued delivery. An explicit `locale()` call can still override the preference for an individual notification. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/http-client.md b/_api_app/.github/skills/laravel-best-practices/rules/http-client.md index 0a7876ed..cc8fdd6a 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/http-client.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/http-client.md @@ -1,86 +1,100 @@ # HTTP Client Best Practices -## Always Set Explicit Timeouts +## Set Explicit Timeouts -The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast. +Laravel's HTTP client has a 30-second response timeout by default. Choose response and connection timeouts that fit the service and the calling request or job. Remember that retries can multiply the total elapsed time. + +Less resilient: -Incorrect: ```php $response = Http::get('https://api.example.com/users'); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) - ->connectTimeout(3) +$response = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users'); ``` -For service-specific clients, define timeouts in a macro: +Define shared settings in a macro or a dedicated client: ```php Http::macro('github', function () { return Http::baseUrl('https://api.github.com') - ->timeout(10) ->connectTimeout(3) + ->timeout(10) ->withToken(config('services.github.token')); }); $response = Http::github()->get('/repos/laravel/framework'); ``` -## Use Retry with Backoff for External APIs +## Retry Only Safe Operations -External APIs have transient failures. Use `retry()` with increasing delays. +Retry transient connection failures, rate-limit responses, and server errors with an appropriate delay. Retry idempotent requests such as `GET` when the operation can safely run more than once. Retry a state-changing request only when the remote API supports an idempotency key or provides equivalent duplicate protection. -Incorrect: -```php -$response = Http::post('https://api.stripe.com/v1/charges', $data); +Unsafe without an idempotency guarantee: -if ($response->failed()) { - throw new PaymentFailedException('Charge failed'); -} +```php +$response = Http::retry([100, 500, 1000]) + ->post('https://api.example.com/v1/charges', $data); ``` -Correct: +Safe for an idempotent request: + ```php -$response = Http::retry([100, 500, 1000]) +$response = Http::connectTimeout(3) ->timeout(10) - ->post('https://api.stripe.com/v1/charges', $data); + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->get('https://api.example.com/data'); ``` -Only retry on specific errors: +For a supported state-changing API, send a stable idempotency key for every attempt: ```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'); +$response = Http::withHeaders(['Idempotency-Key' => $paymentAttempt->uuid]) + ->connectTimeout(3) + ->timeout(10) + ->retry([100, 500, 1000], 0, function (Throwable $exception) { + return $exception instanceof ConnectionException + || ($exception instanceof RequestException + && ($exception->response->serverError() || $exception->response->status() === 429)); + }) + ->post('https://api.example.com/v1/charges', $data); ``` ## Handle Errors Explicitly -The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`. +The HTTP client returns responses for `4xx` and `5xx` status codes instead of throwing by default. Inspect the expected statuses or call `throw()` before consuming a success payload. + +Unsafe when a success payload is expected: -Incorrect: ```php -$response = Http::get('https://api.example.com/users/1'); -$user = $response->json(); // Could be an error body +$user = Http::get('https://api.example.com/users/1')->json(); ``` -Correct: +Preferred: + ```php -$response = Http::timeout(5) +$user = Http::connectTimeout(3) + ->timeout(5) ->get('https://api.example.com/users/1') - ->throw(); - -$user = $response->json(); + ->throw() + ->json(); ``` -For graceful degradation: +Handle expected alternatives explicitly when graceful degradation is required: ```php -$response = Http::get('https://api.example.com/users/1'); +$response = Http::connectTimeout(3) + ->timeout(5) + ->get('https://api.example.com/users/1'); if ($response->successful()) { return $response->json(); @@ -93,46 +107,30 @@ if ($response->notFound()) { $response->throw(); ``` -## Use Request Pooling for Concurrent Requests - -When making multiple independent API calls, use `Http::pool()` instead of sequential calls. +## Pool Independent Requests -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(); -``` +Use `Http::pool()` when several independent requests can run concurrently. Pooling changes execution time, not error handling; inspect or throw for each response as needed. -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'), + $pool->as('users')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/users'), + $pool->as('posts')->connectTimeout(3)->timeout(5) + ->get('https://api.example.com/posts'), ]); -$users = $responses['users']->json(); -$posts = $responses['posts']->json(); +$users = $responses['users']->throw()->json(); +$posts = $responses['posts']->throw()->json(); ``` -## Fake HTTP Calls in Tests - -Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`. +## Fake HTTP Requests in Tests -Incorrect: -```php -it('syncs user from API', function () { - $service = new UserSyncService; - $service->sync(1); // Hits the real API -}); -``` +Use `Http::fake()` for external integrations, and use `Http::preventStrayRequests()` when an unexpected real request should fail the test. Also test timeouts, connection failures, and error responses that the application handles. -Correct: ```php -it('syncs user from API', function () { +it('syncs a user from the API', function () { Http::preventStrayRequests(); Http::fake([ @@ -142,19 +140,18 @@ it('syncs user from API', function () { ]), ]); - $service = new UserSyncService; - $service->sync(1); + (new UserSyncService)->sync(1); - Http::assertSent(function (Request $request) { - return $request->url() === 'https://api.example.com/users/1'; - }); + Http::assertSent(fn (Request $request) => + $request->url() === 'https://api.example.com/users/1' + ); }); ``` -Test failure scenarios too: +For example, fake a connection failure when testing the integration's failure path: ```php Http::fake([ 'api.example.com/*' => Http::failedConnection(), ]); -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/mail.md b/_api_app/.github/skills/laravel-best-practices/rules/mail.md index c7f67966..f6310437 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/mail.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/mail.md @@ -1,27 +1,54 @@ # Mail Best Practices -## Implement `ShouldQueue` on the Mailable Class +## Queue Slow Mail Delivery -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. +Implement `ShouldQueue` on a mailable when delivery should normally happen in the background. Laravel queues that mailable even when the call site uses `Mail::send()`. -## Use `afterCommit()` on Mailables Inside Transactions +```php +class OrderShipped extends Mailable implements ShouldQueue +{ + use Queueable, SerializesModels; +} +``` -A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor. +Keep mail synchronous when the caller must know immediately whether delivery was accepted, or when no queue worker is available. -## Use `assertQueued()` Not `assertSent()` for Queued Mailables +## Dispatch Queued Mail After Commit -`Mail::assertSent()` only catches synchronous mail. Queued mailables silently pass `assertSent`, giving false confidence. +A queued mailable dispatched during a database transaction can be processed before the transaction commits. Call `afterCommit()` on the mailable, or enable the queue connection's `after_commit` option, when the mail depends on committed records. -Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`. +```php +Mail::to($user)->send( + (new OrderShipped($order))->afterCommit() +); +``` -Correct: `Mail::assertQueued(OrderShipped::class);` +If the transaction rolls back, an after-commit mailable is not dispatched. This setting affects queued mail only; it does not defer synchronous delivery. -## Use Markdown Mailables for Transactional Emails +## Assert the Delivery Mode -Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag. +Use `Mail::assertQueued()` for queued mailables and `Mail::assertSent()` for synchronously sent mailables. -## Separate Content Tests from Sending Tests +Incorrect for a mailable that implements `ShouldQueue`: -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. \ No newline at end of file +```php +Mail::assertSent(OrderShipped::class); +``` + +Correct: + +```php +Mail::assertQueued(OrderShipped::class); +``` + +## Use Markdown Mailables When They Fit + +Markdown mailables render HTML and plain-text versions from Laravel's mail components and support publishable themes. They are useful for conventional transactional messages, but a custom HTML and text pair may be more appropriate for a specialized design. + +```bash +php artisan make:mail OrderShipped --markdown=mail.orders.shipped +``` + +## Separate Content and Delivery Tests + +Test rendered content by instantiating the mailable and using assertions such as `assertSeeInHtml()` and `assertSeeInText()`. Test delivery separately with `Mail::fake()` and `assertSent()` or `assertQueued()` so failures identify the affected behavior. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/migrations.md b/_api_app/.github/skills/laravel-best-practices/rules/migrations.md index de25aa39..3345402e 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/migrations.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/migrations.md @@ -2,76 +2,51 @@ ## Generate Migrations with Artisan -Always use `php artisan make:migration` for consistent naming and timestamps. +Use `php artisan make:migration` to generate the timestamped filename and migration structure. -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 +## Define Foreign-Key Constraints Deliberately -Automatic naming and referential integrity. +Use `constrained()` when its naming conventions and default actions match the relationship. Specify the table or delete behavior when they do not. ```php $table->foreignId('user_id')->constrained()->cascadeOnDelete(); - -// Non-standard names $table->foreignId('author_id')->constrained('users'); ``` -## Never Modify Deployed Migrations +Do not add a duplicate single-column index without checking the database driver's treatment of foreign-key indexes and the indexes already created by the migration. -Once a migration has run in production, treat it as immutable. Create a new migration to change the table. +## Treat Deployed Migrations as Immutable -Incorrect (editing a deployed migration): -```php -// 2024_01_01_create_posts_table.php — already in production -$table->string('slug')->unique(); // ← added after deployment -``` +After a migration has run in a shared or production environment, create a new migration for subsequent changes. Editing the old file makes fresh installations differ from upgraded installations. -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'); -}); -``` +For a local migration that has not been shared or deployed, editing and rerunning it may be simpler. -## Add Indexes in the Migration +## Design Indexes for Real Queries -Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes. +Add indexes based on query patterns, selectivity, write cost, and the database's ability to use composite indexes. A column appearing in `WHERE`, `ORDER BY`, or `JOIN` does not automatically need its own index. -Incorrect: -```php -Schema::create('orders', function (Blueprint $table) { - $table->id(); - $table->foreignId('user_id')->constrained(); - $table->string('status'); - $table->timestamps(); -}); -``` +Declare each selected index in the schema migration that creates or changes the relevant table. Confirm important indexes with representative data and the database's query plan, and avoid redundant indexes whose leading columns duplicate an existing index without serving a distinct query. See the database performance and advanced query rules for index selection and column-order guidance. + +## Stage Changes That Affect Existing Rows + +Adding a required or unique column to a populated table often needs multiple deployment-safe steps. Add a nullable column, deploy code that can handle both states, backfill existing rows in bounded chunks, then add the required constraint or index after the data is valid. + +Do not assume this migration is safe on a populated table: -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(); -}); +$table->string('slug')->unique(); ``` -## Mirror Defaults in Model `$attributes` +Large backfills are usually better implemented as an observable, restartable command or job than inside a schema migration. Small deterministic data changes may be reasonable in a migration when their locking, transaction, and deployment behavior is understood. + +## Mirror Defaults Only When Unsaved Models Need Them -When a column has a database default, mirror it in the model so new instances have correct values before saving. +A database default is applied when a row is inserted, not when a model is instantiated. Mirror the value in the model's `$attributes` only when application code must observe that default before persistence, and keep both definitions synchronized. ```php // Migration @@ -83,39 +58,10 @@ protected $attributes = [ ]; ``` -## 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'); - }); -} -``` +## Make Rollbacks Honest -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. +Implement `down()` when the change can be safely reversed. A rollback that drops populated columns or cannot restore transformed data is destructive even if it is syntactically reversible; document that limitation and prefer a forward-fix migration in production. ## 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']); -``` \ No newline at end of file +Keep each migration small enough to reason about, deploy, and reverse. Separate long-running backfills from schema changes when doing so reduces locks and supports phased deployment, but do not split related operations merely to enforce a blanket separation between data definition and data manipulation. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/queue-jobs.md b/_api_app/.github/skills/laravel-best-practices/rules/queue-jobs.md index 2f174dfc..011828a6 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/queue-jobs.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/queue-jobs.md @@ -1,82 +1,82 @@ -# Queue & Job Best Practices +# Queue and Job Best Practices -## Set `retry_after` Greater Than `timeout` +## Keep Reservation Time Longer Than Execution Time -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. +For queue drivers that use Laravel's `retry_after` setting, configure it to exceed the longest worker or job timeout by a safety margin. When a reservation expires, another worker can reserve the same job while the first process is still running. Keep the worker's `--timeout` several seconds shorter than `retry_after`. -Incorrect (`retry_after` ≤ `timeout`): ```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +// Job +public $timeout = 120; -// config/queue.php — retry_after: 90 ← job retried while still running! +// config/queue.php for the connection +'retry_after' => 150, ``` -Correct (`retry_after` > `timeout`): -```php -class ProcessReport implements ShouldQueue -{ - public $timeout = 120; -} +Amazon Simple Queue Service uses its visibility timeout instead of Laravel's `retry_after`; configure that timeout at the queue level. Because workers can also stop after side effects but before acknowledging a job, make important jobs idempotent even with correct timeout settings. -// config/queue.php — retry_after: 180 ← safely longer than any job timeout -``` - -## Use Exponential Backoff +## Back Off Transient Failures -Use progressively longer delays between retries to avoid hammering failing services. +Use progressively longer delays when a dependency needs time to recover. Do not retry permanent validation or business-rule failures. -Incorrect (fixed retry interval): ```php class SyncWithStripe implements ShouldQueue { - public $tries = 3; - // Default: retries immediately, overwhelming the API -} -``` + public $tries = 4; -Correct (exponential backoff): -```php -class SyncWithStripe implements ShouldQueue -{ - public $tries = 3; public $backoff = [1, 5, 10]; } ``` -## Implement `ShouldBeUnique` +Rate-limiting and exception-throttling middleware can release jobs back to the queue. Released attempts may still count toward the maximum attempt limit, so configure `$tries` or `retryUntil()` to allow the intended retry window. -Prevent duplicate job processing. +## Use Unique Jobs for Dispatch Deduplication + +Implement `ShouldBeUnique` when only one queued instance of a logical job should exist. Uniqueness uses a cache lock and is not a substitute for idempotent processing or a database constraint. ```php class GenerateInvoice implements ShouldQueue, ShouldBeUnique { + public $uniqueFor = 3600; + public function uniqueId(): string { - return $this->order->id; + return (string) $this->order->id; } +} +``` - public $uniqueFor = 3600; +All dispatching processes must use a shared cache that supports locks. Unique-job constraints do not apply to jobs within batches. + +Use `ShouldBeUniqueUntilProcessing` only when the lock should be released immediately before processing begins, allowing another instance to be dispatched while the first is running: + +```php +class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing +{ + // ... } ``` -## Always Implement `failed()` +## Handle Terminal Failure When Needed + +Implement `failed()` when the application must update state, alert an operator, or record domain-specific context after all attempts are exhausted. Logging every failure in each job may duplicate the queue system's failure reporting. -Handle errors explicitly — don't rely on silent failure. +Laravel invokes `failed()` on a new job instance, so mutations made to the job during `handle()` are not available there. ```php public function failed(?Throwable $exception): void { $this->podcast->update(['status' => 'failed']); - Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]); + + Log::error('Podcast processing failed', [ + 'podcast_id' => $this->podcast->id, + 'exception' => $exception, + ]); } ``` -## Rate Limit External API Calls in Jobs +## Rate Limit External Calls -Use `RateLimited` middleware to throttle jobs calling third-party APIs. +Use queue middleware such as `RateLimited` when jobs share a third-party API quota. Define the named limiter and choose release delays and attempt limits together. ```php public function middleware(): array @@ -85,62 +85,33 @@ public function middleware(): array } ``` -## Batch Related Jobs +## Batch Jobs for Group Coordination -Use `Bus::batch()` when jobs should succeed or fail together. +Use `Bus::batch()` to monitor a group of jobs and run callbacks when the batch completes or encounters failures. A batch is not a database transaction: completed jobs are not rolled back when another job fails. By default, one failed job cancels the batch; call `allowFailures()` only when partial failure is acceptable. ```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(); + ->then(fn (Batch $batch) => Notification::send($user, new ImportComplete)) + ->catch(fn (Batch $batch, Throwable $exception) => Log::error('Import batch failed', [ + 'exception' => $exception, + ])) + ->dispatch(); ``` -## `retryUntil()` Needs `$tries = 0` +## Configure Time-Based Retry Limits Deliberately -When using time-based retry limits, set `$tries = 0` to avoid premature failure. +Use `retryUntil()` as the time-based alternative to a maximum attempt count. Laravel may attempt the job any number of times until this deadline, subject to other failure conditions such as maximum exceptions. The method takes precedence over attempt-based limits, so setting `$tries = 0` is not required. ```php -public $tries = 0; - -public function retryUntil(): \DateTimeInterface +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 Horizon for Redis Queue Operations -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, - ], - ], -], -``` \ No newline at end of file +Laravel Horizon provides monitoring, balancing, metrics, and supervisor configuration for Redis queues. It does not support non-Redis queue drivers. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/routing.md b/_api_app/.github/skills/laravel-best-practices/rules/routing.md index e288375d..604a8d96 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/routing.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/routing.md @@ -1,98 +1,106 @@ -# Routing & Controllers Best Practices +# Routing and Controller Best Practices ## Use Implicit Route Model Binding -Let Laravel resolve models automatically from route parameters. +Let Laravel resolve models from route parameters when the default lookup and missing-model behavior fit the endpoint. + +Instead of manual lookup: -Incorrect: ```php -public function show(int $id) +public function show(int $id): View { $post = Post::findOrFail($id); + + return view('posts.show', ['post' => $post]); } ``` -Correct: +Use route model binding: + ```php -public function show(Post $post) +public function show(Post $post): View { return view('posts.show', ['post' => $post]); } ``` -## Use Scoped Bindings for Nested Resources +## Scope Nested Bindings -Enforce parent-child relationships automatically. +Use scoped bindings when a nested resource must belong to its parent. This constrains model resolution; it does not replace authorization. ```php Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) { - // $post is automatically scoped to $user + // The resolved post belongs to the resolved user. })->scopeBindings(); ``` -## Use Resource Controllers +## Use Resource Routes for Resourceful Actions -Use `Route::resource()` or `apiResource()` for RESTful endpoints. +Use `Route::resource()` or `Route::apiResource()` when the endpoint follows Laravel's resource-controller actions. Define explicit routes when the behavior does not fit that vocabulary. ```php Route::resource('posts', PostController::class); -Route::apiResource('api/posts', Api\PostController::class); + +// Alternatively, for an API-only resource: +Route::apiResource('posts', ApiPostController::class); ``` -## Keep Controllers Thin +`apiResource()` omits the HTML-oriented `create` and `edit` routes. It does not itself add an `/api` prefix; that prefix comes from the application's API route configuration. + +## Organize Controllers Around Resources + +As a general default, organize each controller around one resource and use Laravel's standard resource actions: `index`, `show`, `create`, `store`, `edit`, `update`, and `destroy`. This keeps routes predictable and prevents controllers from accumulating unrelated behavior. -Aim for under 10 lines per method. Extract business logic to action or service classes. +When a controller needs a custom action such as `publish`, `approve`, or `archive`, first consider whether that behavior represents a separate resource. A focused resource controller gives the behavior its own authorization, validation, and middleware boundary. + +Custom action on the primary controller: -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); -} +Route::post('/podcasts/{podcast}/publish', [PodcastController::class, 'publish']); ``` -Correct: +The published podcast modeled as a resource: + ```php -public function store(StorePostRequest $request, CreatePostAction $create) -{ - $post = $create->execute($request->validated()); +Route::post('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'store']) + ->name('published-podcasts.store'); - return redirect()->route('posts.show', $post); -} +Route::delete('/published-podcasts/{podcast}', [PublishedPodcastController::class, 'destroy']) + ->name('published-podcasts.destroy'); ``` -## 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 +class PublishedPodcastController extends Controller { - $validated = $request->validate([ - 'title' => ['required', 'max:255'], - 'body' => ['required'], - ]); + public function store(Podcast $podcast): RedirectResponse + { + $podcast->publish(); - Post::create($validated); + return back(); + } + + public function destroy(Podcast $podcast): RedirectResponse + { + $podcast->unpublish(); - return redirect()->route('posts.index'); + return back(); + } } ``` -Correct: +Treat a custom verb as a design signal, not proof that another controller is required. Use query parameters for simple filtering, and keep an explicit action route when modeling the operation as a resource would obscure the domain or conflict with established project conventions. + +## Keep Controllers Focused on HTTP Concerns + +Controllers should coordinate HTTP input, authorization, validation, an application operation, and the response. Extract substantial or reusable business logic, but do not introduce an action or service merely to satisfy an arbitrary line limit. + ```php -public function store(StorePostRequest $request): RedirectResponse +public function store(StorePostRequest $request, CreatePostAction $create): RedirectResponse { - Post::create($request->validated()); + $post = $create->handle($request->validated()); - return redirect()->route('posts.index'); + return redirect()->route('posts.show', $post); } -``` \ No newline at end of file +``` + +A form request can perform validation and authorization before the controller runs. Do not repeat its rules in the controller. Keep simple, endpoint-specific validation inline when extraction would not improve reuse or clarity; see the validation rules for detailed guidance. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/scheduling.md b/_api_app/.github/skills/laravel-best-practices/rules/scheduling.md index dfaefa26..4c25134a 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/scheduling.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/scheduling.md @@ -1,32 +1,50 @@ # Task Scheduling Best Practices -## Use `withoutOverlapping()` on Variable-Duration Tasks +## Prevent Unwanted Overlap -Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion. +Use `withoutOverlapping()` when a second run must not begin while the previous run holds the lock. This is appropriate for variable-duration tasks that are not safe to run concurrently. -## Use `onOneServer()` on Multi-Server Deployments +```php +Schedule::command('reports:generate') + ->everyFifteenMinutes() + ->withoutOverlapping(30); +``` -Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached). +The optional value is the lock expiration time in minutes, not the task timeout. Choose it carefully: the default is 24 hours, stale locks can be cleared with `php artisan schedule:clear-cache`, and an expiration that is too short can permit overlap while the first task still runs. The task itself should still tolerate retries and partial execution where practical. -## Use `runInBackground()` for Concurrent Long Tasks +## Run a Task on One Server -By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes. +Use `onOneServer()` when only one scheduler node should run an eligible task. Scheduler nodes must use the same default cache store, and that store must support atomic locks. Supported stores include `database`, `memcached`, `dynamodb`, and `redis`. -## Use `environments()` to Restrict Tasks +```php +Schedule::command('billing:charge')->daily()->onOneServer(); +``` -Prevent accidental execution of production-only tasks (billing, reporting) on staging. +Name scheduled closures before applying `onOneServer()`, especially when scheduling the same closure with different parameters, so each task has a distinct lock identity. + +## Run Eligible Commands in the Background + +Tasks due at the same time run sequentially by default. Use `runInBackground()` when an independent, long-running scheduled command should not delay later tasks. ```php -Schedule::command('billing:charge')->monthly()->environments(['production']); +Schedule::command('analytics:process')->hourly()->runInBackground(); ``` -## Use `takeUntilTimeout()` for Time-Bounded Processing +Laravel restricts `runInBackground()` to tasks scheduled with `command()` and `exec()`; it is not available for scheduled closures. Ensure background processes have appropriate logging and failure monitoring. -A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time. +## Restrict Tasks by Environment -## Use Schedule Groups for Shared Configuration +Use `environments()` when a task should run only in named application environments. Treat this as an operational safeguard, not an authorization control. -Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks. +```php +Schedule::command('billing:charge') + ->monthly() + ->environments(['production']); +``` + +## Group Shared Configuration + +Use schedule groups when several tasks genuinely share frequency or constraints. ```php Schedule::daily() @@ -36,4 +54,8 @@ Schedule::daily() Schedule::command('emails:send --force'); Schedule::command('emails:prune'); }); -``` \ No newline at end of file +``` + +## Bound Work Inside the Task + +The scheduler does not provide a `takeUntilTimeout()` event method or terminate arbitrary tasks at a deadline. Bound work in the command or job itself by processing finite chunks, checking a deadline, or dispatching queue jobs with suitable timeouts. Use operating-system or process controls when hard termination is required. diff --git a/_api_app/.github/skills/laravel-best-practices/rules/security.md b/_api_app/.github/skills/laravel-best-practices/rules/security.md index 524d47e6..57b57f0f 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/security.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/security.md @@ -1,18 +1,9 @@ # Security Best Practices -## Mass Assignment Protection +## Control Mass Assignment -Every model must define `$fillable` (whitelist) or `$guarded` (blacklist). +Define `$fillable` when a model is populated from request-derived arrays, or deliberately guard attributes by another consistent model convention. Laravel models guard all attributes by default; `$guarded = []` opts out of that protection. -Incorrect: -```php -class User extends Model -{ - protected $guarded = []; // All fields are mass assignable -} -``` - -Correct: ```php class User extends Model { @@ -24,82 +15,71 @@ class User extends Model } ``` -Never use `$guarded = []` on models that accept user input. +Do not pass untrusted request data to a model with `$guarded = []`. Mass-assignment protection controls which attributes `create()`, `fill()`, and `update()` may set; it does not validate values or authorize the operation. -## Authorize Every Action +## Authorize Protected Actions -Use policies or gates in controllers. Never skip authorization. +Use policies, gates, or form request authorization for actions that depend on the current user's permissions. Authentication alone does not establish permission, and validation is not authorization. -Incorrect: ```php -public function update(Request $request, Post $post) -{ - $post->update($request->validated()); -} -``` - -Correct: -```php -public function update(UpdatePostRequest $request, Post $post) +public function update(UpdatePostRequest $request, Post $post): RedirectResponse { Gate::authorize('update', $post); $post->update($request->validated()); + + return redirect()->route('posts.show', $post); } ``` -Or via Form Request: +Authorization may instead live in the form request: ```php public function authorize(): bool { - return $this->user()->can('update', $this->route('post')); + return $this->user()?->can('update', $this->route('post')) ?? false; } ``` -## Prevent SQL Injection +Public actions intentionally available to everyone do not need a redundant authorization check. -Always use parameter binding. Never interpolate user input into queries. +## Bind Query Parameters + +Use Eloquent, the query builder, or explicit bindings instead of interpolating untrusted values into Structured Query Language (SQL). Bindings protect values, not identifiers such as column names or sort directions; map user-selected identifiers to an allow-list. 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(); +User::whereRaw('LOWER(name) = ?', [$request->string('name')->lower()->toString()])->get(); ``` -## Escape Output to Prevent XSS +## Escape Output in Its Context -Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content. +Blade's `{{ }}` syntax HTML-escapes output. Use `{!! !!}` only for content that has been sanitized for the exact HTML context in which it is rendered. Escaping rules differ for HTML, URLs, JavaScript, and Cascading Style Sheets. + +Incorrect for untrusted content: -Incorrect: ```blade {!! $user->bio !!} ``` Correct: + ```blade {{ $user->bio }} ``` -## CSRF Protection +## Apply Cross-Site Request Forgery Protection -Include `@csrf` in all POST/PUT/DELETE Blade forms. Not needed in Inertia. +Include `@csrf` in state-changing Blade forms handled by Laravel's `web` middleware. Routes intentionally excluded from cross-site request forgery (CSRF) verification, such as validated third-party webhooks, need their own authenticity check. -Incorrect: -```blade -
- -
-``` - -Correct: ```blade
@csrf @@ -107,21 +87,27 @@ Correct:
``` -## Rate Limit Auth and API Routes +Inertia applications commonly use Axios, which returns the encrypted `XSRF-TOKEN` cookie in the `X-XSRF-TOKEN` header. Confirm equivalent configuration when using another HTTP client. Do not disable CSRF protection merely to fix a token mismatch. + +## Rate Limit Sensitive Endpoints -Apply `throttle` middleware to authentication and API routes. +Apply suitable rate limits to login attempts, password recovery, verification messages, and expensive or abuse-prone application programming interface (API) routes. Choose the limiter key deliberately; an Internet Protocol (IP) address alone can unfairly group users behind a shared network, while an account identifier alone can enable targeted denial of service. ```php RateLimiter::for('login', function (Request $request) { - return Limit::perMinute(5)->by($request->ip()); + return Limit::perMinute(5)->by(Str::transliterate( + Str::lower($request->string('email')).'|'.$request->ip() + )); }); Route::post('/login', LoginController::class)->middleware('throttle:login'); ``` -## Validate File Uploads +Rate limiting reduces abuse; it does not replace authentication, authorization, or upstream denial-of-service protection. -Validate MIME type, extension, and size. Never trust client-provided filenames. +## Validate and Store Uploads Safely + +Validate expected content type, dimensions where relevant, and size. Laravel's `mimes` rule reads the file contents and guesses a Multipurpose Internet Mail Extensions (MIME) type corresponding to the listed extensions; it does not validate the user-assigned filename extension. The `extensions` rule checks that extension and should not be used by itself. ```php public function rules(): array @@ -132,56 +118,28 @@ public function rules(): array } ``` -Store with generated filenames: - -```php -$path = $request->file('avatar')->store('avatars', 'public'); -``` - -## Keep Secrets Out of Code +Use Laravel's storage methods to generate a filename, and store untrusted files outside a publicly executable location. Public files can require additional controls, such as image re-encoding, content-disposition headers, and explicit blocking of active formats. -Never commit `.env`. Access secrets via `config()` only. - -Incorrect: ```php -$key = env('API_KEY'); +$path = $request->file('avatar')->store('avatars'); ``` -Correct: -```php -// config/services.php -'api_key' => env('API_KEY'), +## Keep Secrets Out of Application Code -// In application code -$key = config('services.api_key'); -``` +Do not commit populated environment files or hard-code credentials. Read environment variables in configuration files, then use `config()` in application code so configuration caching works correctly. See the configuration rules for encrypted environment files and external secret stores. ## Audit Dependencies -Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment. +Run `composer audit` regularly and in continuous integration. Review findings for exploitability and update or mitigate affected packages promptly. ```bash composer audit ``` -## Encrypt Sensitive Database Fields +## Encrypt Sensitive Attributes When Appropriate -Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`. +Use an `encrypted` cast for sensitive values that must be recoverable, and use `$hidden` to omit them from array and JavaScript Object Notation (JSON) serialization. Hidden attributes remain accessible in PHP, and encryption does not replace access control. Encrypted values cannot be meaningfully queried and should use a `TEXT` or larger column because ciphertext length is variable. -Incorrect: -```php -class Integration extends Model -{ - protected function casts(): array - { - return [ - 'api_key' => 'string', - ]; - } -} -``` - -Correct: ```php class Integration extends Model { @@ -195,4 +153,4 @@ class Integration extends Model ]; } } -``` \ No newline at end of file +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/style.md b/_api_app/.github/skills/laravel-best-practices/rules/style.md index 67af9891..3f3f14fb 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/style.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/style.md @@ -1,125 +1,110 @@ -# 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 | -|---------|---------| +# Convention and Style Best Practices + +## Follow Project Naming Conventions + +Prefer Laravel's conventions in new code, but preserve an established project convention unless a coordinated rename is worthwhile. + +| Element | Convention | Example | +| --- | --- | --- | +| Controller | Singular resource name | `ArticleController` | +| Model | Singular StudlyCase | `User` | +| Table | Plural snake_case | `article_comments` | +| Pivot table | Singular model names in alphabetical order, in snake_case | `article_user` | +| Column | snake_case | `meta_title` | +| Conventional foreign key | Singular model name plus `_id`, in snake_case | `article_id` | +| Resource URI | Plural resource | `articles/1` | +| Route name | Dotted segments; snake_case within a segment when needed | `users.show_active` | +| Method | camelCase | `getAll` | +| Variable | camelCase | `$articlesWithAuthor` | +| Collection | Descriptive and plural | `$activeUsers` | +| Object | Descriptive and singular | `$activeUser` | +| View | kebab-case | `show-filtered.blade.php` | +| Configuration file | snake_case | `google_calendar.php` | +| Enumeration | Singular StudlyCase | `UserType` | + +## Prefer Clear, Idiomatic Syntax + +Use Laravel helpers and query methods when they communicate intent more directly. Do not shorten code when the result is ambiguous or loses useful type information. + +| More verbose | Idiomatic alternative | +| --- | --- | | `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')` | +| `->first()?->name` | `->value('name')` when only that value is needed | -## Use Laravel String & Array Helpers +Use typed request accessors such as `$request->string()`, `$request->integer()`, and `$request->boolean()` when their coercion matches the operation. -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. +## Use Utilities When They Clarify Intent -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); +Laravel's `Str`, `Arr`, `Number`, and `Uri` utilities provide expressive operations and framework-consistent behavior. Prefer them when they are clearer or safer than an equivalent PHP operation, not as an unconditional replacement for every built-in function. -// Correct +```php $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 +$class = class_basename(User::class); $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()`. +Use `Arr` for dot notation and common transformations: -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'); +$public = Arr::only($attributes, ['name', 'email']); ``` -Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`. +Use `Number` for localized display formatting rather than values that will be stored or calculated: -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%" +Number::format(1000000); +Number::currency(1500, 'USD'); +Number::fileSize(1024 * 1024); ``` -URIs — use `Uri` for URL manipulation: +Use `Uri` when constructing or transforming a uniform resource identifier (URI) benefits from a structured API: + ```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. +Check the documentation for the Laravel version supported by the project before using newer utility classes or methods. -Use `search-docs` for the full list of available methods — these helpers are extensive. +## Keep Presentation Code Maintainable -## No Inline JS/CSS in Blade +Prefer the project's asset pipeline, components, and existing conventions for substantial JavaScript and Cascading Style Sheets (CSS). Small page-specific scripts or styles can be reasonable in Blade layouts or stacks; avoid mixing large behavior and style blocks into templates. -Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes. +Pass server data with an encoding mechanism appropriate to its context. For example, Blade's `Js::from()` safely formats data for JavaScript: -Incorrect: ```blade -let article = `{{ json_encode($article) }}`; + ``` -Correct: -```blade - -``` +Data attributes are useful for small scalar values, but serializing a large model into an attribute can expose unnecessary fields and complicate escaping. -Pass data to JS via data attributes or use a dedicated PHP-to-JS package. +## Write Comments That Explain Why -## No Unnecessary Comments +Prefer clear names and small units of code over comments that merely restate an operation. Add concise comments for non-obvious constraints, tradeoffs, workarounds, regular expressions, or external behavior that the code cannot express by itself. Keep comments accurate when behavior changes. -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. +Unhelpful: -Incorrect: ```php -// Check if there are any joins -if (count((array) $builder->getQuery()->joins) > 0) +// Check whether the query has joins. +if (count((array) $builder->getQuery()->joins) > 0) { + // ... +} ``` -Correct: +Clearer: + ```php -if ($this->hasJoins()) -``` \ No newline at end of file +if ($this->hasJoins()) { + // ... +} +``` diff --git a/_api_app/.github/skills/laravel-best-practices/rules/testing.md b/_api_app/.github/skills/laravel-best-practices/rules/testing.md deleted file mode 100644 index d39cc3ed..00000000 --- a/_api_app/.github/skills/laravel-best-practices/rules/testing.md +++ /dev/null @@ -1,43 +0,0 @@ -# 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(); -``` \ No newline at end of file diff --git a/_api_app/.github/skills/laravel-best-practices/rules/validation.md b/_api_app/.github/skills/laravel-best-practices/rules/validation.md index a20202ff..37ce6c5e 100644 --- a/_api_app/.github/skills/laravel-best-practices/rules/validation.md +++ b/_api_app/.github/skills/laravel-best-practices/rules/validation.md @@ -1,75 +1,89 @@ -# Validation & Forms Best Practices +# Validation and Forms Best Practices -## Use Form Request Classes +## Extract Validation When It Improves the Boundary -Extract validation from controllers into dedicated Form Request classes. +Use a form request when validation or authorization is substantial, reused, or clearer outside the controller. Inline `$request->validate()` remains appropriate for a small, endpoint-specific rule set. -Incorrect: ```php -public function store(Request $request) +public function store(StorePostRequest $request): RedirectResponse { - $request->validate([ - 'title' => 'required|max:255', - 'body' => 'required', - ]); -} -``` + $post = Post::create($request->validated()); -Correct: -```php -public function store(StorePostRequest $request) -{ - Post::create($request->validated()); + return redirect()->route('posts.show', $post); } ``` -## Array vs. String Notation for Rules +A form request's `authorize()` method can enforce access to the operation. Validation establishes the shape and values of input; it does not itself authorize the user. + +## Prefer Readable Rule Syntax -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. +Array syntax composes cleanly with rule objects and avoids delimiter issues. Prefer it in new code when it improves readability, while following a consistent local style. ```php -// Preferred for new code 'email' => ['required', 'email', Rule::unique('users')], +``` + +String syntax remains valid for simple rules: -// Follow existing convention if the project uses string notation +```php 'email' => 'required|email|unique:users', ``` -## Always Use `validated()` +## Use Only Intended Validated Data + +Use `validated()` or `safe()` instead of `$request->all()` when passing request data onward. Then select the fields intended for the operation when the validation rules also cover control fields or nested data. -Get only validated data. Never use `$request->all()` for mass operations. +Unsafe: -Incorrect: ```php Post::create($request->all()); ``` -Correct: +Preferred: + ```php -Post::create($request->validated()); +$post = Post::create($request->safe()->only(['title', 'body'])); ``` -## Use `Rule::when()` for Conditional Validation +Validated data is not automatically safe for mass assignment. Keep model `$fillable` or `$guarded` rules aligned with the operation, and never add a sensitive attribute to validation merely to make mass assignment convenient. + +## Express Conditional Rules Clearly + +Use conditional rules such as `Rule::when()`, `required_if`, or `exclude_unless` when they make the condition explicit. Choose the simplest form that remains easy to test. ```php 'company_name' => [ - Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']), + 'string', + 'max:255', + Rule::when( + $this->input('account_type') === 'business', + ['required'], + ['nullable'], + ), ], ``` -## Use the `after()` Method for Custom Validation +## Add Cross-Field Validation After Base Rules -Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields. +Use a form request's `after()` method for validation that depends on multiple fields or application state. Avoid expensive queries when prerequisite fields have already failed validation. ```php public function after(): array { return [ function (Validator $validator) { - if ($this->quantity > Product::find($this->product_id)?->stock) { + if ($validator->errors()->hasAny(['product_id', 'quantity'])) { + return; + } + + $stock = Product::find($this->integer('product_id'))?->stock; + + if ($stock !== null && $this->integer('quantity') > $stock) { $validator->errors()->add('quantity', 'Not enough stock.'); } }, ]; } -``` \ No newline at end of file +``` + +Validation against mutable state does not prevent a race between validation and persistence. Enforce inventory, uniqueness, and similar invariants with database constraints, atomic updates, or a database transaction as appropriate. diff --git a/_api_app/.github/skills/pest-testing/SKILL.md b/_api_app/.github/skills/pest-testing/SKILL.md deleted file mode 100644 index 0761b6e7..00000000 --- a/_api_app/.github/skills/pest-testing/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -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) or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 3 features. Do not use for editing factories, seeders, migrations, controllers, models, or non-test PHP code." -license: MIT -metadata: - author: laravel ---- - -# Pest Testing 3 - -## Documentation - -Use `search-docs` for detailed Pest 3 patterns and documentation. - -## Basic Usage - -### Creating Tests - -All tests must be written using Pest. Use `php artisan make:test --pest {name}`. - -### Test Organization - -- Tests live in the `tests/Feature` and `tests/Unit` directories. -- Do NOT remove tests without approval - these are core application code. -- Test happy paths, failure paths, and edge cases. - -### 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()`. - - -```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()`: - - -```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.): - - -```php -it('has emails', function (string $email) { - expect($email)->not->toBeEmpty(); -})->with([ - 'james' => 'james@laravel.com', - 'taylor' => 'taylor@laravel.com', -]); -``` - -## Pest 3 Features - -### Architecture Testing - -Pest 3 includes architecture testing to enforce code conventions: - - -```php -arch('controllers') - ->expect('App\Http\Controllers') - ->toExtendNothing() - ->toHaveSuffix('Controller'); - -arch('models') - ->expect('App\Models') - ->toExtend('Illuminate\Database\Eloquent\Model'); - -arch('no debugging') - ->expect(['dd', 'dump', 'ray']) - ->not->toBeUsed(); -``` - -### Type Coverage - -Pest 3 provides improved type coverage analysis. Run with `--type-coverage` flag. - -## 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 \ No newline at end of file diff --git a/_api_app/.github/skills/testing-best-practices/SKILL.md b/_api_app/.github/skills/testing-best-practices/SKILL.md new file mode 100644 index 00000000..35d02a1d --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/SKILL.md @@ -0,0 +1,58 @@ +--- +name: testing-best-practices +description: "Laravel test design and review. Use when selecting coverage, naming or structuring tests, choosing assertions or test data, isolating dependencies, testing HTTP or security boundaries, improving suite performance, or reviewing test value. Use framework guidance or search-docs for Pest and PHPUnit syntax." +license: MIT +metadata: + author: laravel +--- + +# Testing Best Practices + +This skill provides rules for designing Laravel tests. Each rule file explains what to do and why. Use `search-docs` for Laravel and Pest API syntax. +This project uses Pest. Follow the corresponding guidance in each rule. + +## Consistency First + +Read nearby tests before you choose syntax and organization. + +A pattern repeated throughout the project is a convention, and project conventions take precedence over this skill. Follow them and give new tests the same structure. + +These rules govern the tests you write now. An existing test that follows a project convention is not defective merely because it conflicts with this skill. Do not delete or rewrite it. If the convention has drawbacks, explain them and let the user decide. + +## What to Test + +Read this section before you write a test. + +- Test observable behavior and application contracts. A test must pass after an implementation change if the behavior stays the same. +- Cover every changed decision and each applicable high-value failure mode. A decision is a branch, a validation, a calculation, or an authorization. +- Exercise declarations through behavior instead of repeating their text. +- Leave framework behavior to framework tests. Testing project configuration is not testing the framework. A constrained relationship, cast, scope, or validation rule belongs to this project. +- Keep every test that can detect a distinct defect. When two tests detect the same defect, trim the higher-layer test to one case and report the duplication. Do not delete an existing test. +- Write a feature test first. Write a unit test only for logic that does not use the framework. +- Write a feature test for every behavior reachable through a request. Real-browser tests require `pestphp/pest-plugin-browser` and a browser download, neither of which this project installs. Mention the package only if the user asks for a real-browser test. +- Judge an architecture test by the convention it protects, not by the rules above. An `arch()` test declares a rule for an entire directory, such as the parent class of every model, the classes that may use an enum, or the methods every factory declares. It intentionally checks declarations and fails when a new file breaks the convention. +- Use the test tools that the project installs. Add a new test dependency, plugin, or browser only after the user asks for it. + +## How to Apply + +1. Read the code under test. Read the tests in the same directory. Identify every decision in the code. +2. Select every applicable branch in the rule index. Read every selected rule file. +3. Report each defect in the code before you write a test. Examples are a method with no body, a policy that no action calls, and a write action with no validation. Test the actual behavior. Report the defect to the user. +4. Write the tests. Run the smallest set of tests that covers the change. The tests must pass. +5. Check every applicable item in `rules/review.md` and every selected rule file. Resolve every mismatch before completion. + +## Rule Index + +Most changes need more than one rule file. + +| Subject | Rule File | +| --- | --- | +| Test framework features that may already do the work | [`rules/finding-features.md`](rules/finding-features.md) | +| File layout, test names, and groups | [`rules/naming.md`](rules/naming.md) | +| Arrange-act-assert and choosing the correct assertion | [`rules/assertions.md`](rules/assertions.md) | +| Endpoint coverage, authentication, authorization, tenant isolation, validation, and browser tests | [`rules/endpoint-tests.md`](rules/endpoint-tests.md) | +| Factories, test data ownership, and repeated input values | [`rules/test-data.md`](rules/test-data.md) | +| Fakes, mocks, outbound HTTP, time, randomness, and databases | [`rules/isolation.md`](rules/isolation.md) | +| Escaping, injection, cross-tenant access, and privilege checks | [`rules/security.md`](rules/security.md) | +| Environment and CI settings for a slow suite | [`rules/performance.md`](rules/performance.md) | +| Reviewing a test or suite | [`rules/review.md`](rules/review.md) | diff --git a/_api_app/.github/skills/testing-best-practices/rules/assertions.md b/_api_app/.github/skills/testing-best-practices/rules/assertions.md new file mode 100644 index 00000000..ba55fb25 --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/assertions.md @@ -0,0 +1,60 @@ +# Assertions + +## Arrange, Act, Assert + +Write each test in three parts: setup, one action, and assertions. Put one blank line between them so readers can identify each part without comments. + +Keep each test self-contained. Do not use values created by another test. + +## How to Find the Correct Assertion + +First identify the subject of the check, then find an assertion designed for it. A subject-specific assertion identifies the incorrect value when the test fails. + +1. Search Laravel's assertions for framework subjects such as responses, the database, sessions, models, queues, events, mail, and notifications. +2. Fetch `https://pestphp.com/docs/expectations.md` for the expectations of Pest for a plain value, a type, a format, or a shape. +3. Build the check by hand only if no assertion exists for the subject. +4. Confirm the name in the documentation before you use it. Do not write an assertion that you did not confirm. + +Use the assertion in this table for each subject. + +| Subject | Assertion to use | +| --- | --- | +| A return value, the state of an object, or a transformation of a value | an `expect()` chain | +| An HTTP status, JSON, a session, or Inertia | a Laravel response assertion | +| The state in the database | a Laravel database assertion | +| The existence of a model | `assertModelExists($model)` rather than `assertDatabaseHas('users', ['id' => $user->id])` | + +Use a PHPUnit assertion only if no Pest expectation and no Laravel assertion exists for the subject. + +Assert each fact once. Do not assert a 200 status before `assertSee`, because `assertSee` already shows that the page rendered. + +## Named Response Assertions + +Use a named response assertion, such as `assertNotFound()`, rather than `assertStatus(404)`. A failure then identifies the broken contract. Laravel provides named assertions for commonly tested status codes. + +Keep one `expect()` chain on one subject. Start a new chain when the subject changes, or when the chain is difficult to read. + +## Assert a Known Value + +Write the expected value in the test, or calculate the expected value by a different method. Do not calculate the expected value with the logic of the implementation, because the test then passes when that logic is wrong. + +```php +// The test calculates the value with the logic of the implementation... +$expected = now()->subHours(24)->floorSeconds(30)->toJson(); +expect($from)->toBe($expected); + +// The test sets a fixed input and asserts a known value... +travelTo('2025-01-01 00:00:00'); +expect($from)->toBe('2024-12-31T00:00:00.000000Z'); +``` + +## Assert the Complete Result + +A status code is not the complete result of a write operation. Assert each of the following if the operation changes it: + +- The response or the return value. +- The state in the database. +- The jobs and the events that the operation dispatches. +- The notifications and the mail that the operation sends. + +On the failure path, assert that the operation makes none of these changes. A test that asserts only `assertOk()` passes even when the application saves no record. diff --git a/_api_app/.github/skills/testing-best-practices/rules/endpoint-tests.md b/_api_app/.github/skills/testing-best-practices/rules/endpoint-tests.md new file mode 100644 index 00000000..32e32e64 --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/endpoint-tests.md @@ -0,0 +1,47 @@ +# Endpoint Tests + +## How to Write the Test + +Fetch `https://laravel.com/framework/docs/http-tests` for the request helpers, the authentication helpers, and the response assertions. Confirm the name before you use it, and do not guess an assertion. + +Choose an assertion based on the subject of the check: the status, a header, a redirect, the JSON body, the session, a validation error, or the view. Laravel provides a named assertion for each subject that identifies the incorrect value. + +## Endpoint Coverage + +Write a test for each applicable case: + +- The request has missing or invalid authentication. +- The request comes from a different tenant, team, or organization. +- The user has an insufficient role or permission. +- The request does not satisfy a route or scope constraint. +- The request fails the validation. +- The request is valid. Assert both the response and the persisted state. + +Assert the application's actual behavior rather than a generic status code. An API returns `401` for a missing or invalid token, while a browser endpoint redirects to the sign-in route. + +## Tenant Isolation + +Assert the status code returned for a cross-tenant request. Use `404` rather than `403` when one tenant must not learn that another tenant's record exists, because `403` confirms its existence. + +## Test Authorization at the Policy Level + +An HTTP test shows that the endpoint performs authorization. It cannot identify which mechanism refused the request because middleware, a policy, and a call to `abort()` can all return `403`. + +- Assert the complete matrix of the permissions against the policy or the gate. A failure then names the rule that is not correct. +- Write one HTTP test for one refused role, which shows that the endpoint calls the authorization. +- Use the helper of the project that asserts the ability and the arguments of the gate, if such a helper exists. + +## Testing Validation + +- Write one test for each validation rule when each failure represents a separate contract. +- Write one test with an empty payload to assert several required fields together. +- Assert the text of the message that the user gets. A message that is present but wrong is a defect. +- Use a dataset for input values that need the same setup and the same assertions. + +Send an input value that is not valid through the application, and assert the error. Do not assert that an array of rules contains a string, because that assertion tests the declaration and not the behavior. Use such an assertion only for a rule that no request can reach, and write the reason in the test. + +### Which Layer Owns Which Case + +The rule-class test owns the matrix of values that pass and fail. The endpoint test proves that the endpoint applies the rule and that the user receives the message. + +When both tests contain the matrix, move it to the rule-class test and retain one case in the endpoint test. Never remove the last case, because the rule-class test still passes if the request omits the rule. The same division applies to policies, scopes, and other classes called by a request. diff --git a/_api_app/.github/skills/testing-best-practices/rules/finding-features.md b/_api_app/.github/skills/testing-best-practices/rules/finding-features.md new file mode 100644 index 00000000..83ce650b --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/finding-features.md @@ -0,0 +1,34 @@ +# How to Find Test Framework Features + +Pest adds features faster than this skill can list them. Find an existing feature before implementing the behavior by hand. + +- Give `search-docs` the capability you need rather than the name of a function you remember. It returns features available in the installed version. +- Fetch `https://pestphp.com/llms.txt` for the complete feature list and additions in each release. +- If a search returns no results, tell the user that the installed version does not provide the feature. Do not write an API that you have not confirmed. + +Search for a feature in this table before you write the code by hand. + +| Work that you need | Term to search for | +| --- | --- | +| Run one test with many input values | datasets, bound datasets | +| Assert over many values or over a collection | higher-order expectations | +| Remove the same setup from each test in a file | hooks, higher-order tests | +| Apply a convention to the complete codebase | architecture testing | +| Measure if the suite finds a defect | mutation testing | +| Find code with no types | type coverage | +| Reduce the time of a slow suite | parallel, profiling | +| Run one test while you debug | filtering, `--bail`, `--dirty` | + +## Built-in Laravel Assertion Methods + +Laravel provides assertions for each part of the framework. Fetch `https://laravel.com/framework/docs/testing` for the complete list, and search for an assertion before building a check by hand. Examples include `assertDatabaseHas()`, `assertModelExists()`, `assertSoftDeleted()`, response assertions such as `assertRedirectToRoute()` and `assertJsonPath()`, and fake assertions such as `Queue::assertPushed()` and `Notification::assertSentTo()`. + +A hand-built check fails with `false is not true`, which identifies nothing. A framework assertion names the incorrect table, value, or response, so the failure indicates what to fix. + +```php +// The failure says that false is not true. Instead of this... +expect(User::where('email', 'taylor@laravel.com')->exists())->toBeTrue(); + +// Use this... the failure names the table and the attributes that it did not find... +$this->assertDatabaseHas('users', ['email' => 'taylor@laravel.com']); +``` diff --git a/_api_app/.github/skills/testing-best-practices/rules/isolation.md b/_api_app/.github/skills/testing-best-practices/rules/isolation.md new file mode 100644 index 00000000..e1a21fdb --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/isolation.md @@ -0,0 +1,52 @@ +# Fakes, Mocks, and Determinism + +Tests that depend on actual time, randomness, sleeping, or network calls can fail for reasons unrelated to the code under test. Control all four. + +## How to Isolate a Dependency + +Fetch `https://laravel.com/framework/docs/mocking` for Laravel's fakes, facade doubles, and fake assertions. Confirm each name before using it. + +Identify the dependency, then choose the first applicable option. A framework fake preserves the real code path, while a mock replaces the dependency. + +1. Always use framework fakes for facades such as events, queues, mail, notifications, storage, the HTTP client, time, and sleep. +2. Use a developer-defined fake implementation of a service if the application provides one. +3. Use a mock for a container-resolved contract only when the real implementation leaves the process or is nondeterministic. +4. Use the real implementation for everything else, including the database. + +## Framework Fakes + +- Create each fake inside the test that needs it. Do not create fakes in a file-level `beforeEach()`. +- Pass class names to `Event::fake()` and `Queue::fake()` when you know which classes the code dispatches. A fake without class names can hide an unexpected dispatch. +- Use a fake without class names only when the test asserts the complete result, including a call to `assertNothingPushed()`. +- Write one assertion for each fake. The assertion states that the code dispatches the item, or that the code does not dispatch the item. +- Assert the data of a job or of an event if that data is part of the behavior. +- Use `Exceptions::fake()` to assert that the application reports the correct exception. Do not use `withoutExceptionHandling()`, because it changes the response under test. + +Create prerequisite factory records before calling `Event::fake()`. Factories use model events, such as a `creating` hook that generates a UUID, and a fake without class names suppresses those events and can produce an invalid model. Call the fake first only when a factory event is under test, and pass that event's class name. + +## Mocking + +Use `shouldReceive()` before the action to declare an expectation. Use `shouldHaveReceived()` after the action for a spy. Use `Mockery::on()` or `withArgs()` if an equality check cannot state the expected argument, such as a check of one field of a value object. + +Import the mock function before you use it: `use function Pest\Laravel\mock;`. + +## Outbound HTTP Testing + +Call `Http::preventStrayRequests()`. Any request without a matching fake then fails without reaching the network. + +Fake the exact endpoint used by each test. Do not call `Http::fake()` without an endpoint because it accepts unexpected requests and can hide defects. + +## Time and Randomness + +- Freeze the time or move the time in each test that depends on a date, a period, or a timestamp. +- Use the framework helpers `freezeTime()`, `travelTo()`, `travel()`, and `travelBack()`. Do not call `Carbon::setTestNow()`. +- Use `Str::createRandomStringsUsing()` to fix a generated string, if the test asserts an identifier or a slug. +- Use `Sleep::fake()` instead of a real sleep, and assert the sleeps that the code requests. +- Restore the time and the randomness after each test, if the suite does not restore them for every test. + +## Database + +- Run real queries against the real records in the test database. Do not mock the query builder, because the test then asserts the mock. +- Assert the exact keys of `toArray()` if the shape of the serialized model is a contract. The test then fails when the model exposes a new attribute. +- Test application behavior caused by the schema, such as deleting dependent records through a cascade. Do not test the database engine's cascade implementation. +- Use `LazilyRefreshDatabase` instead of `RefreshDatabase`. A test that does not use the database then does not run the migrations. diff --git a/_api_app/.github/skills/testing-best-practices/rules/naming.md b/_api_app/.github/skills/testing-best-practices/rules/naming.md new file mode 100644 index 00000000..4f3f5b1d --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/naming.md @@ -0,0 +1,45 @@ +# Naming and Structure + +## File Layout + +- Name each test file `{ClassName}Test.php`. +- Place each test file at the same relative path as the class under test. The class `app/Actions/DeleteTeam.php` gets the test `tests/Unit/Actions/DeleteTeamTest.php`. +- Follow the project's convention for fixture files. If none exists, put fixtures in `tests/Fixtures/` and load them by path. +- Move large literal values out of the test body and into fixture files. + +## Test Function + +Use the test function used by other files in the same directory. If no neighboring test files exist: + +- Use `it()` for the behavior of the code, and write the name as a verb phrase. +- Use `test()` for a declarative fact, such as a grant in a policy, the labels of an enum, or the shape of a serialized model. + +Use one Pest declaration style in each file. Use either `it()` or `test()` consistently. + +## Naming Tests + +The name of a test is a specification. State the user-visible result and the condition that causes it. + +- Name the behavior, and not the method under test. The file name already gives the class. +- Give the exact status code in the name of a test for an API error. +- Do not write `Given`, `When`, or `Then` in the name. + +```php +it('returns 401 when no token is provided', function () { ... }); +it('does not include deployments from deleted environments', function () { ... }); +it('falls back to the default region when none is configured', function () { ... }); +``` + +Use a verb that describes a result, such as `returns`, `renders`, `creates`, `dispatches`, `rejects`, `forbids`, `falls back`, or `does not`. + +Do not write `it('works correctly')` or `it('returns data')`, because neither specifies a meaningful result. Do not write `it('handleMethod creates record')`, because it names a method rather than behavior. + +## Grouping + +Use `describe()` if one file covers separate actions in a lifecycle. An example is a controller with the actions `index`, `show`, `store`, `update`, and `destroy`. + +Do not use `describe()` in these cases: + +- The file covers one action or one flow. +- The tests are different only in the input value. Use a dataset instead. +- The group adds a level but does not make the file easier to read. diff --git a/_api_app/.github/skills/testing-best-practices/rules/performance.md b/_api_app/.github/skills/testing-best-practices/rules/performance.md new file mode 100644 index 00000000..ee993fbb --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/performance.md @@ -0,0 +1,46 @@ +# Test Suite Performance + +These settings apply to the project and CI, not to individual tests. Read `rules/isolation.md` for choices within a test. + +Fetch `https://pestphp.com/docs/optimizing-tests` for Pest options that make test runs faster. +Verify each flag in the documentation before adding it to CI. + +Measure before changing a setting. Find the slow test first, and apply a project-wide setting only after identifying the costly work. + +## Test Environment + +- Set `BCRYPT_ROUNDS=4` in `.env.testing` or in `phpunit.xml`. The default value is 12, and the hash then takes most of the time of each test that signs a user in. +- Disable XDebug. Disable pcov also, unless the run needs the coverage. +- Disable packages that perform work on every request in the test environment. Examples are Pulse, Telescope, and Nightwatch. +- Use the `WithCachedConfig` and `WithCachedRoutes` traits, so the run does not parse the configuration and the routes for every test. +- Call `withoutVite()`, or `withoutMix()`, so the framework does not resolve a built asset. + +## Global Fakes + +Put these three calls in the base `Pest.php` of the project: + +- `Http::preventStrayRequests()`, because one request that reaches the network can slow the suite. This catches requests made through Laravel's HTTP client. Check direct Guzzle and cURL usage separately. +- `Sleep::fake(syncWithCarbon: true)`, so a retry and a backoff do not sleep. +- `Exceptions::fake()`, so the suite does not report an exception to an external service. + +## How to Run the Suite in Parallel + +Run `vendor/bin/pest --parallel` to spread tests across the machine's CPU cores. Add `--processes=N` if the default count is unsuitable for the machine or CI. + +A parallel run gives each process a separate database. Tests must meet these conditions; a test that fails only in parallel breaks one of them: + +- The test creates each record that it reads. It does not read a record that another test creates. +- The test does not depend on the order of the run. +- The test does not share a file, a cache key, or a queue with another test. Give each process a separate name for such a resource. + +## How to Find a Slow Test + +Run `vendor/bin/pest --profile` to list the slowest tests. Start with the ten slowest tests, because the same cause often applies to the complete suite. + +If the cause of a slow test is unclear, add an event listener or temporary log entry to identify its work. + +## Common Errors + +- The run loads XDebug for a test that does not need it. +- `BCRYPT_ROUNDS` keeps the default value, because the project has no `.env.testing`. +- The code under test calls the real `sleep()`, and `Sleep::fake()` then does not help. diff --git a/_api_app/.github/skills/testing-best-practices/rules/review.md b/_api_app/.github/skills/testing-best-practices/rules/review.md new file mode 100644 index 00000000..e0cfca14 --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/review.md @@ -0,0 +1,44 @@ +# Reviewing Tests + +Check every item in this file. A passing test may still provide no value. For each test, identify the defect it would catch. + +Report each finding. Do not delete or rewrite a test without the user's approval. When an issue appears throughout the suite as a convention, report the pattern once rather than every affected file. + +## Test Value + +Apply this section to behavioral tests. An architecture test states a convention for a directory, so these items do not apply to it. + +- [ ] Each test covers observable behavior or an application contract, and passes after a change to the implementation that keeps the behavior. +- [ ] Each tested declaration is exercised through behavior, and no test asserts the behavior of the framework. A test of what this project configures, such as a relation with a constraint, a cast, or a scope, belongs to this project. +- [ ] Each test detects a distinct defect that no other test covers. A duplicate shrinks at the higher layer to the one case that proves the wiring. +- [ ] Every changed decision and each applicable high-value failure mode has coverage. + +## Names and Structure + +- [ ] Each file has the name `{ClassName}Test.php` and the relative path of the class under test. +- [ ] Each name states a result, the condition that causes it, and the status code for an API error. +- [ ] Each file uses one declaration style consistently, and each `describe()` group holds separate behavior. + +## Coverage + +- [ ] HTTP tests cover authentication, authorization, role, scope, and validation when applicable. +- [ ] A request for a record of a different tenant gets a status code that does not confirm that the record exists. +- [ ] The complete permission matrix belongs in policy tests, not controller tests. +- [ ] Each validation rule has one test that asserts the user-visible message. When a unit test owns a matrix, reduce duplicate higher-level coverage to one case rather than deleting it. +- [ ] Rendered user input and each dynamic part of a query have a security test. + +## Data and Determinism + +- [ ] Each test creates its mutable records directly or through a helper that it calls, and every created record arranges the behavior or supports an assertion. +- [ ] Each `beforeEach()` holds configuration only. +- [ ] Each factory state and each relationship gives the meaning of the data. +- [ ] Each call to `make()` is in a test that does not need the database. +- [ ] Time, randomness, sleep, and outbound HTTP are controlled. +- [ ] Each test passes alone, and passes in the complete suite in any order. + +## Assertions + +- [ ] Each expected value is a known value, and the test does not calculate the value with the logic of the implementation. +- [ ] Each test of a write operation asserts the response, the state in the database, and the side effects. +- [ ] Each fake has one assertion, and gives the class names unless the test asserts the complete result. +- [ ] Each `expect()` chain stays on one subject. diff --git a/_api_app/.github/skills/testing-best-practices/rules/security.md b/_api_app/.github/skills/testing-best-practices/rules/security.md new file mode 100644 index 00000000..234f5b00 --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/security.md @@ -0,0 +1,27 @@ +# Security Tests + +Test each security boundary where user input affects authorization, rendered output, or query construction. A defect at such a boundary can be difficult to detect because the feature may continue to work. + +Write a test for each of these cases: + +- **Cross-tenant access.** Request a record of a different tenant, team, or organization. Read `rules/endpoint-tests.md` for why the response should possibly be `404` rather than `403`. +- **Each unprivileged role.** Use a dataset over the roles that the endpoint must refuse. +- **Escaping user-provided content.** Test escaping in HTML and mail. Include names and every free-text field a template renders. Assert that dangerous characters are escaped and the raw value is absent. Do not assert an exact entity for a quote, because Markdown and mail CSS inliners may decode it. +- **Injection into dynamic query components.** Examples include sort columns, filter fields, and sort directions. +- **An unexpected key** in a payload or configuration array. A merge that accepts every key can set an attribute the user must not control. + +```php +it('escapes dangerous content in the notification', function () { + $organization = Organization::factory()->make([ + 'name' => "O'Reilly ", + ]); + + $content = (new QuotaApproaching($organization, 80))->toMail()->render(); + + expect($content) + ->toContain('<script>') + ->not->toContain(""); +}); +``` + +Laravel provides defenses against mass assignment, unauthorized access, and unescaped output. Test that the application applies the appropriate defense to each attribute, route, and template. diff --git a/_api_app/.github/skills/testing-best-practices/rules/test-data.md b/_api_app/.github/skills/testing-best-practices/rules/test-data.md new file mode 100644 index 00000000..65124683 --- /dev/null +++ b/_api_app/.github/skills/testing-best-practices/rules/test-data.md @@ -0,0 +1,56 @@ +# Factories and Test Data + +## Each Test Makes Its Own Data + +Create mutable records inside the test that uses them. This keeps setup visible and lets each test select its factory state. + +Use `beforeEach()` only for configuration that applies to every test in the file. Do not create records in it. + +## Record Construction + +- Use `create()` if the test needs the record in the database. +- Use `make()` only if the test does not need the database. Examples include rendering a notification and testing a value object's behavior. +- Use a named factory state instead of a raw attribute. `User::factory()->unverified()->create()` gives the state meaning; `create(['email_verified_at' => null])` gives only its value. +- Use `for()` or the relationship helper of the project to declare the owner of a record. +- Use `recycle()` if several records must share one parent record. +- Use `sequence()` if several records need different attributes. + +```php +$organization = Organization::factory()->onPlan(BillingPlan::PRO)->create(); + +$environment = Environment::factory()->recycle($organization)->create(); + +$organizations = Organization::factory() + ->count(3) + ->sequence( + ['created_at' => now()->setSeconds(30)], + ['created_at' => now()->setSeconds(1)], + ) + ->create(); +``` + +Create only the records required to arrange the behavior or support an assertion. + +## Datasets + +Use a dataset when the setup, test body, and assertions remain the same across input values. + +```php +it('forbids roles other than admin', function (Role $role) { + actingAs(User::factory()->hasOrganization($role)->create()) + ->post('/settings') + ->assertForbidden(); +})->with(collect(Role::cases())->reject(fn (Role $role) => $role === Role::ADMIN)); +``` + +Use parameterized tests for: + +- enum cases +- roles and plans +- boundary values +- input values that are invalid in the same way +- input and output value pairs + +Write separate tests if the cases need a different setup, a different behavior, or different assertions. One test function with a branch in the body is two tests in one function. + +Give each dataset case a name that states the difference. A failure then identifies the case without requiring you to count positions. diff --git a/_api_app/AGENTS.md b/_api_app/AGENTS.md index 29ac7c22..66965b51 100644 --- a/_api_app/AGENTS.md +++ b/_api_app/AGENTS.md @@ -7,27 +7,15 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## 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 -- laravel/ai (AI) - v0 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- laravel/sanctum (SANCTUM) - v4 -- laravel/boost (BOOST) - v2 -- laravel/mcp (MCP) - v0 -- laravel/pint (PINT) - v1 -- laravel/sail (SAIL) - v1 -- pestphp/pest (PEST) - v3 -- phpunit/phpunit (PHPUNIT) - v11 +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. -## Skills Activation +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 ` for a single package. +- JS packages: check `package.json` for the installed versions. -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. +## Skills Activation -- `ai-sdk-development` — Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). -- `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. -- `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) or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 3 features. Do not use for editing factories, seeders, migrations, controllers, models, or non-test PHP code. +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 @@ -70,7 +58,7 @@ This project has domain-specific skills available. You MUST activate the relevan ## 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. +- Use `search-docs` before changes that depend on Laravel ecosystem APIs, behavior, configuration, or version-specific syntax. Skip it for copy-only edits and other changes where package documentation is irrelevant. Reuse sufficient results already in context instead of searching again. - 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`. @@ -82,12 +70,16 @@ This project has domain-specific skills available. You MUST activate the relevan 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 a rule with `record-rule` only when the user explicitly asks for one. Instructions for the work at hand are not rules, no matter how emphatic: "remove this typo", "use X here" are work to do, not rules to record. Never record a rule on your own initiative, as a byproduct of a change, or to summarize what you just did. When the user does ask, pass a `glob` (e.g. `app/Http/Controllers/**`), a short `title`, and a few-line `note`. Use `record-rule` rather than your native memory or notes tool, because native memory is personal and session-scoped, while 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. -- To check environment variables, read the `.env` file directly. ## Tinker @@ -106,14 +98,24 @@ This project has domain-specific skills available. You MUST activate the relevan - 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. +- Activate the `deploying-to-cloud` skill whenever deploying to Laravel Cloud, configuring Cloud environments or resources, using the Cloud CLI, or troubleshooting Cloud deployments. + === 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. +- Add or update tests for behavior and logic changes when a test provides meaningful regression coverage. +- Pure copy, styling, and layout-only changes do not require new or updated tests. +- When test coverage applies, run the affected tests and ensure they pass. +- Test the changed behavior and its important failure modes, but do not add tests beyond them. +- Read the `testing-best-practices` skill before writing tests. -=== ai/core rules === +=== laravel-ai/core rules === ## Laravel AI SDK @@ -150,10 +152,6 @@ This project has domain-specific skills available. You MUST activate the relevan - 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. - === laravel/v12 rules === # Laravel 12 @@ -173,6 +171,7 @@ This project has domain-specific skills available. You MUST activate the relevan ## Database - When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. + - Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. ### Models @@ -188,10 +187,18 @@ This project has domain-specific skills available. You MUST activate the relevan === pest/core rules === -## Pest +# Pest + +- This project uses Pest. Create tests with `php artisan make:test --pest {name}`. +- Do not include the test suite directory in `{name}`. Use `SomeFeatureTest`, not `Feature/SomeFeatureTest`. +- Read the `testing-best-practices` skill for guidance on coverage, naming, structure, dependency isolation, and review. +- Do not delete tests or test files without approval. They are part of the application. + +## Running Tests -- 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. +- Run the narrowest set of tests that covers the change. Pass a file path or `--filter=testName` to `php artisan test --compact`. +- Rerun a test after each change to it. +- Run `vendor/bin/pest` to call the test runner directly. It accepts the same file path and `--filter=testName` arguments. +- After the feature tests pass, ask the user to run the complete suite with `php artisan test --compact`. diff --git a/_api_app/CLAUDE.md b/_api_app/CLAUDE.md index 29ac7c22..43c994c2 100644 --- a/_api_app/CLAUDE.md +++ b/_api_app/CLAUDE.md @@ -1,197 +1 @@ - -=== 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 -- laravel/ai (AI) - v0 -- laravel/framework (LARAVEL) - v12 -- laravel/prompts (PROMPTS) - v0 -- laravel/sanctum (SANCTUM) - v4 -- laravel/boost (BOOST) - v2 -- laravel/mcp (MCP) - v0 -- laravel/pint (PINT) - v1 -- laravel/sail (SAIL) - v1 -- pestphp/pest (PEST) - v3 -- phpunit/phpunit (PHPUNIT) - v11 - -## 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. - -- `ai-sdk-development` — Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). -- `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. -- `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) or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 3 features. Do not use for editing factories, seeders, migrations, controllers, models, or non-test PHP code. - -## 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. - -=== ai/core rules === - -## Laravel AI SDK - -- This application uses the Laravel AI SDK (`laravel/ai`) for all AI functionality. -- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). - -=== 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. - -=== laravel/v12 rules === - -# Laravel 12 - -- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples. -- Since Laravel 11, Laravel has a new streamlined file structure which this project uses. - -## Laravel 12 Structure - -- In Laravel 12, middleware are no longer registered in `app/Http/Kernel.php`. -- Middleware are configured declaratively in `bootstrap/app.php` using `Application::configure()->withMiddleware()`. -- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files. -- `bootstrap/providers.php` contains application specific service providers. -- The `app/Console/Kernel.php` file no longer exists; use `bootstrap/app.php` or `routes/console.php` for console configuration. -- Console commands in `app/Console/Commands/` are automatically available and do not require manual registration. - -## Database - -- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. -- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. - -### Models - -- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. - -=== 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. - - +@AGENTS.md diff --git a/_api_app/boost.json b/_api_app/boost.json index 1ef53754..e8fb4542 100644 --- a/_api_app/boost.json +++ b/_api_app/boost.json @@ -6,14 +6,17 @@ "copilot", "opencode" ], + "cloud": true, "guidelines": true, "herd_mcp": true, "mcp": true, "nightwatch_mcp": false, "sail": false, "skills": [ + "infer-conventions", + "deploying-to-cloud", "ai-sdk-development", "laravel-best-practices", - "pest-testing" + "testing-best-practices" ] } diff --git a/_api_app/composer.json b/_api_app/composer.json index 1ca99742..c5941d6b 100644 --- a/_api_app/composer.json +++ b/_api_app/composer.json @@ -31,7 +31,7 @@ }, "require-dev": { "fakerphp/faker": "^1.23", - "laravel/boost": "^2.0", + "laravel/boost": "^2.10", "laravel/pint": "^1.29", "laravel/sail": "^1.41", "mockery/mockery": "^1.6", diff --git a/_api_app/composer.lock b/_api_app/composer.lock index f51c4371..3932ba0d 100644 --- a/_api_app/composer.lock +++ b/_api_app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e43790057f21f165924c93d4e14c8ce0", + "content-hash": "3115fbef0899b2de486fea278988865e", "packages": [ { "name": "brick/math", @@ -307,27 +307,25 @@ }, { "name": "doctrine/lexer", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + "reference": "e96fe45e92a54233726014a7cc7340abf29bb14c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e96fe45e92a54233726014a7cc7340abf29bb14c", + "reference": "e96fe45e92a54233726014a7cc7340abf29bb14c", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5", - "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^5.21" + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.58 || ^12.5.4" }, "type": "library", "autoload": { @@ -364,7 +362,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/3.0.1" + "source": "https://github.com/doctrine/lexer/tree/3.0.2" }, "funding": [ { @@ -380,7 +378,7 @@ "type": "tidelift" } ], - "time": "2024-02-05T11:56:58+00:00" + "time": "2026-06-14T20:44:06+00:00" }, { "name": "dragonmantank/cron-expression", @@ -1399,16 +1397,16 @@ }, { "name": "laravel/framework", - "version": "v12.69.1", + "version": "v12.69.3", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "0c07b0b1f88af44d8558ffadf66900a860f93c23" + "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/0c07b0b1f88af44d8558ffadf66900a860f93c23", - "reference": "0c07b0b1f88af44d8558ffadf66900a860f93c23", + "url": "https://api.github.com/repos/laravel/framework/zipball/58ea544a2a80dc03c168e13a5dc9a1d176a88717", + "reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717", "shasum": "" }, "require": { @@ -1617,7 +1615,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-09-01T21:34:37+00:00" + "time": "2026-09-29T12:53:25+00:00" }, { "name": "laravel/prompts", @@ -1743,16 +1741,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.16", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed" + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", - "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5", "shasum": "" }, "require": { @@ -1800,7 +1798,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-08-18T20:28:54+00:00" + "time": "2026-09-22T14:32:34+00:00" }, { "name": "laravel/tinker", @@ -1870,16 +1868,16 @@ }, { "name": "league/commonmark", - "version": "2.10.0", + "version": "2.10.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4" + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d2d1aa8b35e072966c89bc0c66cf926e56767dc4", - "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f", "shasum": "" }, "require": { @@ -1973,7 +1971,7 @@ "type": "tidelift" } ], - "time": "2026-08-11T16:06:25+00:00" + "time": "2026-09-21T13:07:34+00:00" }, { "name": "league/config", @@ -2493,16 +2491,16 @@ }, { "name": "monolog/monolog", - "version": "3.11.0", + "version": "3.12.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "147f303310f06334f03f409e49d7ad1e275ff05a" + "reference": "72c534fc0ab181ef52d92a68382318631e301608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/147f303310f06334f03f409e49d7ad1e275ff05a", - "reference": "147f303310f06334f03f409e49d7ad1e275ff05a", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/72c534fc0ab181ef52d92a68382318631e301608", + "reference": "72c534fc0ab181ef52d92a68382318631e301608", "shasum": "" }, "require": { @@ -2528,6 +2526,7 @@ "phpstan/phpstan-strict-rules": "^2", "phpunit/phpunit": "^10.5.17 || ^11.0.7", "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", "rollbar/rollbar": "^4.0", "ruflin/elastica": "^7 || ^8", "symfony/mailer": "^5.4 || ^6", @@ -2546,6 +2545,7 @@ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", "rollbar/rollbar": "Allow sending log messages to Rollbar", "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, @@ -2580,7 +2580,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.11.0" + "source": "https://github.com/Seldaek/monolog/tree/3.12.0" }, "funding": [ { @@ -2592,20 +2592,20 @@ "type": "tidelift" } ], - "time": "2026-09-02T12:39:56+00:00" + "time": "2026-09-09T08:34:20+00:00" }, { "name": "nesbot/carbon", - "version": "3.13.2", + "version": "3.14.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb" + "reference": "34e9109535165bf1d7ff17a610492372ae73a896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/a1c54919f5fff9800cd03c32bd01defd5a4061cb", - "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/34e9109535165bf1d7ff17a610492372ae73a896", + "reference": "34e9109535165bf1d7ff17a610492372ae73a896", "shasum": "" }, "require": { @@ -2697,7 +2697,7 @@ "type": "tidelift" } ], - "time": "2026-08-08T11:40:35+00:00" + "time": "2026-09-27T12:07:26+00:00" }, { "name": "nette/schema", @@ -3895,20 +3895,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.3", + "version": "4.9.4", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", - "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2", + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2", "shasum": "" }, "require": { - "brick/math": ">=0.8.16 <=0.18", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -3967,9 +3967,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.3" + "source": "https://github.com/ramsey/uuid/tree/4.9.4" }, - "time": "2026-06-18T03:57:49+00:00" + "time": "2026-09-16T11:39:30+00:00" }, { "name": "rcrowe/twigbridge", @@ -4399,16 +4399,16 @@ }, { "name": "symfony/console", - "version": "v7.4.18", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0" + "reference": "3a1973458b153f566a8d7499bc06ece323d60063" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", - "reference": "23d6f88a29f6d0eac45bd77d70307adf83ba7ab0", + "url": "https://api.github.com/repos/symfony/console/zipball/3a1973458b153f566a8d7499bc06ece323d60063", + "reference": "3a1973458b153f566a8d7499bc06ece323d60063", "shasum": "" }, "require": { @@ -4473,7 +4473,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.18" + "source": "https://github.com/symfony/console/tree/v7.4.19" }, "funding": [ { @@ -4493,7 +4493,7 @@ "type": "tidelift" } ], - "time": "2026-08-25T14:18:37+00:00" + "time": "2026-09-13T10:38:41+00:00" }, { "name": "symfony/css-selector", @@ -4885,16 +4885,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.17", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6" + "reference": "1b900bc6ae5ba60c5eee69c11ba44566190576fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", - "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "url": "https://api.github.com/repos/symfony/finder/zipball/1b900bc6ae5ba60c5eee69c11ba44566190576fe", + "reference": "1b900bc6ae5ba60c5eee69c11ba44566190576fe", "shasum": "" }, "require": { @@ -4929,7 +4929,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.17" + "source": "https://github.com/symfony/finder/tree/v7.4.19" }, "funding": [ { @@ -4949,20 +4949,20 @@ "type": "tidelift" } ], - "time": "2026-08-21T12:09:28+00:00" + "time": "2026-09-10T19:14:10+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.18", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "d070b716a32fbe3bf04204db0f58ace73b86d133" + "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/d070b716a32fbe3bf04204db0f58ace73b86d133", - "reference": "d070b716a32fbe3bf04204db0f58ace73b86d133", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f7b953be7801974a5a0685e1228bbc3b12b35936", + "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936", "shasum": "" }, "require": { @@ -5011,7 +5011,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.18" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.19" }, "funding": [ { @@ -5031,20 +5031,20 @@ "type": "tidelift" } ], - "time": "2026-08-30T20:10:52+00:00" + "time": "2026-09-14T17:41:17+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.18", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "275d2d2d24530f2a0eaf17704a3a93860a036351" + "reference": "683507424cc84ca85ac1551adb609b6b98216667" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/275d2d2d24530f2a0eaf17704a3a93860a036351", - "reference": "275d2d2d24530f2a0eaf17704a3a93860a036351", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/683507424cc84ca85ac1551adb609b6b98216667", + "reference": "683507424cc84ca85ac1551adb609b6b98216667", "shasum": "" }, "require": { @@ -5130,7 +5130,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.18" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.19" }, "funding": [ { @@ -5150,20 +5150,20 @@ "type": "tidelift" } ], - "time": "2026-08-30T21:24:29+00:00" + "time": "2026-09-15T07:09:35+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.17", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584" + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/b17c9bf3a551d5f635638a3b6c05f06c4dc87584", - "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584", + "url": "https://api.github.com/repos/symfony/mailer/zipball/6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514", "shasum": "" }, "require": { @@ -5214,7 +5214,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.17" + "source": "https://github.com/symfony/mailer/tree/v7.4.19" }, "funding": [ { @@ -5234,20 +5234,20 @@ "type": "tidelift" } ], - "time": "2026-08-21T17:40:08+00:00" + "time": "2026-09-15T06:01:15+00:00" }, { "name": "symfony/mime", - "version": "v7.4.18", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "bf328d82105831db3e409195db0540ff57f27c80" + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/bf328d82105831db3e409195db0540ff57f27c80", - "reference": "bf328d82105831db3e409195db0540ff57f27c80", + "url": "https://api.github.com/repos/symfony/mime/zipball/f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", "shasum": "" }, "require": { @@ -5303,7 +5303,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.18" + "source": "https://github.com/symfony/mime/tree/v7.4.19" }, "funding": [ { @@ -5323,7 +5323,7 @@ "type": "tidelift" } ], - "time": "2026-08-22T09:04:42+00:00" + "time": "2026-09-04T10:45:44+00:00" }, { "name": "symfony/options-resolver", @@ -6227,16 +6227,16 @@ }, { "name": "symfony/process", - "version": "v7.4.18", + "version": "v7.4.19", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "058d17fc284cce14efb2385783b55014a461b176" + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/058d17fc284cce14efb2385783b55014a461b176", - "reference": "058d17fc284cce14efb2385783b55014a461b176", + "url": "https://api.github.com/repos/symfony/process/zipball/ed0ae095b86994d370d5791612e55984f15aa30e", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e", "shasum": "" }, "require": { @@ -6268,7 +6268,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.18" + "source": "https://github.com/symfony/process/tree/v7.4.19" }, "funding": [ { @@ -6288,7 +6288,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T17:40:08+00:00" + "time": "2026-09-02T12:38:34+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -6552,16 +6552,16 @@ }, { "name": "symfony/string", - "version": "v8.1.2", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c", "shasum": "" }, "require": { @@ -6618,7 +6618,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.2" + "source": "https://github.com/symfony/string/tree/v8.1.7" }, "funding": [ { @@ -6638,7 +6638,7 @@ "type": "tidelift" } ], - "time": "2026-07-28T07:35:25+00:00" + "time": "2026-09-11T14:51:16+00:00" }, { "name": "symfony/translation", @@ -7368,6 +7368,83 @@ ], "time": "2026-01-08T08:02:38+00:00" }, + { + "name": "composer/semver", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Version comparison library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.5.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-09-24T14:38:51+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.6", @@ -7664,27 +7741,27 @@ }, { "name": "laravel/boost", - "version": "v2.4.3", + "version": "v2.10.0", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "841d52905728cfac9f93c778a1758e740ce9a367" + "reference": "76c236d4eb7d51698f882d149bc367c1ebc0d078" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/841d52905728cfac9f93c778a1758e740ce9a367", - "reference": "841d52905728cfac9f93c778a1758e740ce9a367", + "url": "https://api.github.com/repos/laravel/boost/zipball/76c236d4eb7d51698f882d149bc367c1ebc0d078", + "reference": "76c236d4eb7d51698f882d149bc367c1ebc0d078", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.9", + "guzzlehttp/guzzle": "^7.9|^8.0", "illuminate/console": "^11.45.3|^12.41.1|^13.0", "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", "illuminate/routing": "^11.45.3|^12.41.1|^13.0", "illuminate/support": "^11.45.3|^12.41.1|^13.0", - "laravel/mcp": "^0.5.1|^0.6.0", + "laravel/mcp": "^0.7.1|^0.8.0|^0.9.0|^1.0", "laravel/prompts": "^0.3.10", - "laravel/roster": "^0.5.0", + "laravel/roster": "^1.0.0", "php": "^8.2" }, "require-dev": { @@ -7726,20 +7803,20 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-04-10T15:59:10+00:00" + "time": "2026-09-23T09:33:31+00:00" }, { "name": "laravel/mcp", - "version": "v0.6.5", + "version": "v1.0.1", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "583a6282bf0f074d754f7ff5cd1fff9d34244691" + "reference": "92987a9d03847801299ff4abe909ba7686147dde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/583a6282bf0f074d754f7ff5cd1fff9d34244691", - "reference": "583a6282bf0f074d754f7ff5cd1fff9d34244691", + "url": "https://api.github.com/repos/laravel/mcp/zipball/92987a9d03847801299ff4abe909ba7686147dde", + "reference": "92987a9d03847801299ff4abe909ba7686147dde", "shasum": "" }, "require": { @@ -7753,7 +7830,8 @@ "illuminate/routing": "^11.45.3|^12.41.1|^13.0", "illuminate/support": "^11.45.3|^12.41.1|^13.0", "illuminate/validation": "^11.45.3|^12.41.1|^13.0", - "php": "^8.2" + "php": "^8.2", + "symfony/process": "^7.4.5|^8.0.5" }, "require-dev": { "laravel/pint": "^1.20", @@ -7766,7 +7844,7 @@ "extra": { "laravel": { "aliases": { - "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + "Mcp": "Laravel\\Mcp\\Facades\\Mcp" }, "providers": [ "Laravel\\Mcp\\Server\\McpServiceProvider" @@ -7799,7 +7877,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2026-03-30T19:17:10+00:00" + "time": "2026-09-24T10:45:09+00:00" }, { "name": "laravel/pint", @@ -7871,32 +7949,33 @@ }, { "name": "laravel/roster", - "version": "v0.5.1", + "version": "v1.0.0", "source": { "type": "git", "url": "https://github.com/laravel/roster.git", - "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" + "reference": "89e518bd88ae98ff50f6082f6b517c8d8e8245fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", - "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", + "url": "https://api.github.com/repos/laravel/roster/zipball/89e518bd88ae98ff50f6082f6b517c8d8e8245fa", + "reference": "89e518bd88ae98ff50f6082f6b517c8d8e8245fa", "shasum": "" }, "require": { + "composer/semver": "^3.0", "illuminate/console": "^11.0|^12.0|^13.0", "illuminate/contracts": "^11.0|^12.0|^13.0", - "illuminate/routing": "^11.0|^12.0|^13.0", "illuminate/support": "^11.0|^12.0|^13.0", "php": "^8.2", "symfony/yaml": "^7.2|^8.0" }, "require-dev": { - "laravel/pint": "^1.14", + "laravel/pint": "^1.29", "mockery/mockery": "^1.6", "orchestra/testbench": "^9.0|^10.0|^11.0", "pestphp/pest": "^3.0|^4.1", - "phpstan/phpstan": "^2.0" + "phpstan/phpstan": "^2.0", + "rector/rector": "^2.0" }, "type": "library", "extra": { @@ -7928,7 +8007,7 @@ "issues": "https://github.com/laravel/roster/issues", "source": "https://github.com/laravel/roster" }, - "time": "2026-03-05T07:58:43+00:00" + "time": "2026-07-18T17:53:15+00:00" }, { "name": "laravel/sail", From dd64cc62ce22bbdb2583e91ede3fb3998cd6de0a Mon Sep 17 00:00:00 2001 From: uldisrudzitis Date: Tue, 29 Sep 2026 17:37:43 +0300 Subject: [PATCH 2/2] Upgrade laravel/ai --- .../skills/ai-sdk-development/SKILL.md | 154 ++++++++++++-- .../skills/ai-sdk-development/SKILL.md | 154 ++++++++++++-- .../skills/ai-sdk-development/SKILL.md | 154 ++++++++++++-- .../skills/ai-sdk-development/SKILL.md | 154 ++++++++++++-- _api_app/AGENTS.md | 7 - _api_app/composer.json | 2 +- _api_app/composer.lock | 198 ++++++------------ 7 files changed, 612 insertions(+), 211 deletions(-) diff --git a/_api_app/.agents/skills/ai-sdk-development/SKILL.md b/_api_app/.agents/skills/ai-sdk-development/SKILL.md index ef6d72a2..a8d0b772 100644 --- a/_api_app/.agents/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.agents/skills/ai-sdk-development/SKILL.md @@ -1,6 +1,9 @@ --- name: ai-sdk-development -description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +description: TRIGGER when working with ai-sdk, Laravel's official first-party AI SDK. Activate when building or editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, MCP servers, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly. +license: MIT +metadata: + author: laravel --- # Developing with the Laravel AI SDK @@ -53,6 +56,7 @@ Vector stores? → `Stores::create()` ```php use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Enums\Lab; use Laravel\Ai\Promptable; class SalesCoach implements Agent @@ -69,6 +73,17 @@ class SalesCoach implements Agent $response = (new SalesCoach)->prompt('Analyze this transcript...'); echo $response->text; +// Container resolution with dependency injection +$agent = SalesCoach::make(user: $user); + +// Override provider, model, or timeout per-prompt +$response = (new SalesCoach)->prompt( + 'Analyze this transcript...', + provider: Lab::Anthropic, + model: 'claude-haiku-4-5-20251001', + timeout: 120, +); + // Streaming (returns SSE response from a route) return (new SalesCoach)->stream('Analyze this transcript...'); @@ -245,9 +260,11 @@ $store->add(Document::fromStorage('manual.pdf')); // Store + add in one step ### PHP Attributes ```php -use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Enums\Lab; -#[Provider('anthropic')] +#[Provider(Lab::Anthropic)] +#[Model('claude-haiku-4-5-20251001')] #[MaxSteps(10)] #[MaxTokens(4096)] #[Temperature(0.7)] @@ -261,6 +278,34 @@ class MyAgent implements Agent The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. +Use `#[RepairToolCalls]` to let an agent recover when a model calls an unknown local tool. The failed call is returned to the model with the available local tool names, and the implicit step budget includes one repair step. Explicit `#[MaxSteps]` limits remain unchanged. + +```php +use Laravel\Ai\Attributes\RepairToolCalls; + +#[RepairToolCalls] +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + // ... +} +``` + +The `#[WithoutBroadcasting]` attribute stops the given stream event types from broadcasting (e.g. data-heavy `ToolResult` payloads that exceed the WebSocket frame limit). The events are still streamed and persisted; they just never hit the channel: + +```php +use Laravel\Ai\Attributes\WithoutBroadcasting; +use Laravel\Ai\Streaming\Events\{ToolCall, ToolResult}; + +#[WithoutBroadcasting(ToolResult::class, ToolCall::class)] +class SearchAgent implements Agent, HasTools +{ + use Promptable; + // ... +} +``` + ### Tools Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: @@ -294,6 +339,31 @@ public function tools(): iterable } ``` +### MCP Servers + +Register the server once, then return its tools from `tools()`. The SDK automatically wraps each `Laravel\Mcp\Client\Primitives\Tool` and presents it to the model as `mcp_tools_`. Return your own `Laravel\Mcp\Server\Tool` instances in the same way and they retain their names and run in-process. + +```php +use Laravel\Mcp\Client; +use Laravel\Mcp\Facades\Mcp; + +// In a service provider or routes/ai.php +Mcp::registerClient('linear', fn () => Client::web('https://mcp.linear.app/mcp') + ->withToken(config('services.linear.token'))); + +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return Mcp::client('linear')->tools(); + } +} +``` + +The client connects on its first call, so call `connect()` only when you need to control the timing. Use `Client::local('npx', ['-y', 'some-server'])` for servers that run over stdio. + ### Conversation Memory ```php @@ -313,7 +383,7 @@ $response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More... ### Failover ```php -$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]); ``` ## Testing and Faking @@ -370,9 +440,51 @@ $store->assertAdded('file_id'); - Agent pattern: Implement the `Agent` interface and use the `Promptable` trait - Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` - Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings) - Artisan commands: `php artisan make:agent`, `php artisan make:tool` - Global helper: `agent()` for anonymous agents +## OpenAI-Compatible Provider + +Point the SDK at any OpenAI-compatible endpoint (LM Studio, vLLM, Together, etc.) with the config-driven `openai-compatible` driver. Define named instances in `config/ai.php`, no code required: + +```php +'my-llm' => [ + 'driver' => 'openai-compatible', + 'url' => env('MY_LLM_URL'), // required + 'key' => env('MY_LLM_API_KEY'), // optional Bearer token + 'models' => [ + 'text' => ['default' => 'some-chat-model'], + 'embeddings' => [ + 'default' => 'some-embedding-model', + 'dimensions' => 1024, // optional; omit to use native dimensions + ], + 'transcription' => ['default' => 'some-transcription-model'], + ], +], +``` + +Reference it by config key (or `Lab::OpenAiCompatible`). A model is required via the corresponding `models` configuration or per-call `model:`: + +```php +agent()->prompt('Hello', provider: 'my-llm', model: 'some-model'); + +Embeddings::for(['Hello'])->generate( + provider: 'my-llm', + model: 'some-embedding-model', +); +``` + +It uses OpenAI-standard shapes and supports text, streaming, tools, structured output, image attachments, text embeddings, and audio transcription. Embedding dimensions are optional; omit them to use the model's native dimensions. For extra request-body fields, implement `HasProviderOptions` — the returned array is merged into the body. + +Transcription uploads standard multipart (`file` + `model` + optional `language`) and defaults to `response_format: json`. Because endpoints vary, provider options override the defaults — pass `response_format: 'verbose_json'` for segments, or use `diarize()` on servers that implement `diarized_json`: + +```php +Transcription::fromDisk('recordings', $path) + ->withProviderOptions(['response_format' => 'verbose_json']) + ->generate(provider: 'my-llm'); +``` + ## Common Pitfalls ### Wrong Namespace @@ -394,20 +506,26 @@ use Laravel\AI\Agent; Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. -### Never Use Prism Directly +## Provider Support -Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. +| Feature | Providers | +| ---------- | --------------------------------------------------------------- | +| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, OpenAI-compatible | +| Images | OpenAI, Gemini, xAI | +| TTS | OpenAI, ElevenLabs, Mistral | +| STT | OpenAI, ElevenLabs, Mistral, Groq, OpenAI-compatible | +| Embeddings | OpenAI, OpenAI-compatible, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI | +| Reranking | Cohere, Jina | +| Files | OpenAI, Anthropic, Gemini | -## Provider Support +Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings: -| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | -| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | -| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | -| Anthropic | Y | - | - | - | - | - | Y | - | -| Gemini | Y | Y | - | - | Y | - | Y | Y | -| xAI | Y | Y | - | - | - | - | - | - | -| Groq | Y | - | - | - | - | - | - | - | -| OpenRouter | Y | - | - | - | - | - | - | - | -| ElevenLabs | - | - | Y | Y | - | - | - | - | -| Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | +```php +use Laravel\Ai\Enums\Lab; + +Lab::Anthropic; +Lab::OpenAI; +Lab::Gemini; +Lab::OpenAiCompatible; // configurable OpenAI-compatible endpoint +// ... +``` diff --git a/_api_app/.claude/skills/ai-sdk-development/SKILL.md b/_api_app/.claude/skills/ai-sdk-development/SKILL.md index ef6d72a2..a8d0b772 100644 --- a/_api_app/.claude/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.claude/skills/ai-sdk-development/SKILL.md @@ -1,6 +1,9 @@ --- name: ai-sdk-development -description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +description: TRIGGER when working with ai-sdk, Laravel's official first-party AI SDK. Activate when building or editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, MCP servers, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly. +license: MIT +metadata: + author: laravel --- # Developing with the Laravel AI SDK @@ -53,6 +56,7 @@ Vector stores? → `Stores::create()` ```php use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Enums\Lab; use Laravel\Ai\Promptable; class SalesCoach implements Agent @@ -69,6 +73,17 @@ class SalesCoach implements Agent $response = (new SalesCoach)->prompt('Analyze this transcript...'); echo $response->text; +// Container resolution with dependency injection +$agent = SalesCoach::make(user: $user); + +// Override provider, model, or timeout per-prompt +$response = (new SalesCoach)->prompt( + 'Analyze this transcript...', + provider: Lab::Anthropic, + model: 'claude-haiku-4-5-20251001', + timeout: 120, +); + // Streaming (returns SSE response from a route) return (new SalesCoach)->stream('Analyze this transcript...'); @@ -245,9 +260,11 @@ $store->add(Document::fromStorage('manual.pdf')); // Store + add in one step ### PHP Attributes ```php -use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Enums\Lab; -#[Provider('anthropic')] +#[Provider(Lab::Anthropic)] +#[Model('claude-haiku-4-5-20251001')] #[MaxSteps(10)] #[MaxTokens(4096)] #[Temperature(0.7)] @@ -261,6 +278,34 @@ class MyAgent implements Agent The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. +Use `#[RepairToolCalls]` to let an agent recover when a model calls an unknown local tool. The failed call is returned to the model with the available local tool names, and the implicit step budget includes one repair step. Explicit `#[MaxSteps]` limits remain unchanged. + +```php +use Laravel\Ai\Attributes\RepairToolCalls; + +#[RepairToolCalls] +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + // ... +} +``` + +The `#[WithoutBroadcasting]` attribute stops the given stream event types from broadcasting (e.g. data-heavy `ToolResult` payloads that exceed the WebSocket frame limit). The events are still streamed and persisted; they just never hit the channel: + +```php +use Laravel\Ai\Attributes\WithoutBroadcasting; +use Laravel\Ai\Streaming\Events\{ToolCall, ToolResult}; + +#[WithoutBroadcasting(ToolResult::class, ToolCall::class)] +class SearchAgent implements Agent, HasTools +{ + use Promptable; + // ... +} +``` + ### Tools Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: @@ -294,6 +339,31 @@ public function tools(): iterable } ``` +### MCP Servers + +Register the server once, then return its tools from `tools()`. The SDK automatically wraps each `Laravel\Mcp\Client\Primitives\Tool` and presents it to the model as `mcp_tools_`. Return your own `Laravel\Mcp\Server\Tool` instances in the same way and they retain their names and run in-process. + +```php +use Laravel\Mcp\Client; +use Laravel\Mcp\Facades\Mcp; + +// In a service provider or routes/ai.php +Mcp::registerClient('linear', fn () => Client::web('https://mcp.linear.app/mcp') + ->withToken(config('services.linear.token'))); + +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return Mcp::client('linear')->tools(); + } +} +``` + +The client connects on its first call, so call `connect()` only when you need to control the timing. Use `Client::local('npx', ['-y', 'some-server'])` for servers that run over stdio. + ### Conversation Memory ```php @@ -313,7 +383,7 @@ $response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More... ### Failover ```php -$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]); ``` ## Testing and Faking @@ -370,9 +440,51 @@ $store->assertAdded('file_id'); - Agent pattern: Implement the `Agent` interface and use the `Promptable` trait - Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` - Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings) - Artisan commands: `php artisan make:agent`, `php artisan make:tool` - Global helper: `agent()` for anonymous agents +## OpenAI-Compatible Provider + +Point the SDK at any OpenAI-compatible endpoint (LM Studio, vLLM, Together, etc.) with the config-driven `openai-compatible` driver. Define named instances in `config/ai.php`, no code required: + +```php +'my-llm' => [ + 'driver' => 'openai-compatible', + 'url' => env('MY_LLM_URL'), // required + 'key' => env('MY_LLM_API_KEY'), // optional Bearer token + 'models' => [ + 'text' => ['default' => 'some-chat-model'], + 'embeddings' => [ + 'default' => 'some-embedding-model', + 'dimensions' => 1024, // optional; omit to use native dimensions + ], + 'transcription' => ['default' => 'some-transcription-model'], + ], +], +``` + +Reference it by config key (or `Lab::OpenAiCompatible`). A model is required via the corresponding `models` configuration or per-call `model:`: + +```php +agent()->prompt('Hello', provider: 'my-llm', model: 'some-model'); + +Embeddings::for(['Hello'])->generate( + provider: 'my-llm', + model: 'some-embedding-model', +); +``` + +It uses OpenAI-standard shapes and supports text, streaming, tools, structured output, image attachments, text embeddings, and audio transcription. Embedding dimensions are optional; omit them to use the model's native dimensions. For extra request-body fields, implement `HasProviderOptions` — the returned array is merged into the body. + +Transcription uploads standard multipart (`file` + `model` + optional `language`) and defaults to `response_format: json`. Because endpoints vary, provider options override the defaults — pass `response_format: 'verbose_json'` for segments, or use `diarize()` on servers that implement `diarized_json`: + +```php +Transcription::fromDisk('recordings', $path) + ->withProviderOptions(['response_format' => 'verbose_json']) + ->generate(provider: 'my-llm'); +``` + ## Common Pitfalls ### Wrong Namespace @@ -394,20 +506,26 @@ use Laravel\AI\Agent; Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. -### Never Use Prism Directly +## Provider Support -Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. +| Feature | Providers | +| ---------- | --------------------------------------------------------------- | +| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, OpenAI-compatible | +| Images | OpenAI, Gemini, xAI | +| TTS | OpenAI, ElevenLabs, Mistral | +| STT | OpenAI, ElevenLabs, Mistral, Groq, OpenAI-compatible | +| Embeddings | OpenAI, OpenAI-compatible, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI | +| Reranking | Cohere, Jina | +| Files | OpenAI, Anthropic, Gemini | -## Provider Support +Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings: -| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | -| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | -| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | -| Anthropic | Y | - | - | - | - | - | Y | - | -| Gemini | Y | Y | - | - | Y | - | Y | Y | -| xAI | Y | Y | - | - | - | - | - | - | -| Groq | Y | - | - | - | - | - | - | - | -| OpenRouter | Y | - | - | - | - | - | - | - | -| ElevenLabs | - | - | Y | Y | - | - | - | - | -| Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | +```php +use Laravel\Ai\Enums\Lab; + +Lab::Anthropic; +Lab::OpenAI; +Lab::Gemini; +Lab::OpenAiCompatible; // configurable OpenAI-compatible endpoint +// ... +``` diff --git a/_api_app/.cursor/skills/ai-sdk-development/SKILL.md b/_api_app/.cursor/skills/ai-sdk-development/SKILL.md index ef6d72a2..a8d0b772 100644 --- a/_api_app/.cursor/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.cursor/skills/ai-sdk-development/SKILL.md @@ -1,6 +1,9 @@ --- name: ai-sdk-development -description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +description: TRIGGER when working with ai-sdk, Laravel's official first-party AI SDK. Activate when building or editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, MCP servers, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly. +license: MIT +metadata: + author: laravel --- # Developing with the Laravel AI SDK @@ -53,6 +56,7 @@ Vector stores? → `Stores::create()` ```php use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Enums\Lab; use Laravel\Ai\Promptable; class SalesCoach implements Agent @@ -69,6 +73,17 @@ class SalesCoach implements Agent $response = (new SalesCoach)->prompt('Analyze this transcript...'); echo $response->text; +// Container resolution with dependency injection +$agent = SalesCoach::make(user: $user); + +// Override provider, model, or timeout per-prompt +$response = (new SalesCoach)->prompt( + 'Analyze this transcript...', + provider: Lab::Anthropic, + model: 'claude-haiku-4-5-20251001', + timeout: 120, +); + // Streaming (returns SSE response from a route) return (new SalesCoach)->stream('Analyze this transcript...'); @@ -245,9 +260,11 @@ $store->add(Document::fromStorage('manual.pdf')); // Store + add in one step ### PHP Attributes ```php -use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Enums\Lab; -#[Provider('anthropic')] +#[Provider(Lab::Anthropic)] +#[Model('claude-haiku-4-5-20251001')] #[MaxSteps(10)] #[MaxTokens(4096)] #[Temperature(0.7)] @@ -261,6 +278,34 @@ class MyAgent implements Agent The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. +Use `#[RepairToolCalls]` to let an agent recover when a model calls an unknown local tool. The failed call is returned to the model with the available local tool names, and the implicit step budget includes one repair step. Explicit `#[MaxSteps]` limits remain unchanged. + +```php +use Laravel\Ai\Attributes\RepairToolCalls; + +#[RepairToolCalls] +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + // ... +} +``` + +The `#[WithoutBroadcasting]` attribute stops the given stream event types from broadcasting (e.g. data-heavy `ToolResult` payloads that exceed the WebSocket frame limit). The events are still streamed and persisted; they just never hit the channel: + +```php +use Laravel\Ai\Attributes\WithoutBroadcasting; +use Laravel\Ai\Streaming\Events\{ToolCall, ToolResult}; + +#[WithoutBroadcasting(ToolResult::class, ToolCall::class)] +class SearchAgent implements Agent, HasTools +{ + use Promptable; + // ... +} +``` + ### Tools Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: @@ -294,6 +339,31 @@ public function tools(): iterable } ``` +### MCP Servers + +Register the server once, then return its tools from `tools()`. The SDK automatically wraps each `Laravel\Mcp\Client\Primitives\Tool` and presents it to the model as `mcp_tools_`. Return your own `Laravel\Mcp\Server\Tool` instances in the same way and they retain their names and run in-process. + +```php +use Laravel\Mcp\Client; +use Laravel\Mcp\Facades\Mcp; + +// In a service provider or routes/ai.php +Mcp::registerClient('linear', fn () => Client::web('https://mcp.linear.app/mcp') + ->withToken(config('services.linear.token'))); + +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return Mcp::client('linear')->tools(); + } +} +``` + +The client connects on its first call, so call `connect()` only when you need to control the timing. Use `Client::local('npx', ['-y', 'some-server'])` for servers that run over stdio. + ### Conversation Memory ```php @@ -313,7 +383,7 @@ $response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More... ### Failover ```php -$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]); ``` ## Testing and Faking @@ -370,9 +440,51 @@ $store->assertAdded('file_id'); - Agent pattern: Implement the `Agent` interface and use the `Promptable` trait - Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` - Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings) - Artisan commands: `php artisan make:agent`, `php artisan make:tool` - Global helper: `agent()` for anonymous agents +## OpenAI-Compatible Provider + +Point the SDK at any OpenAI-compatible endpoint (LM Studio, vLLM, Together, etc.) with the config-driven `openai-compatible` driver. Define named instances in `config/ai.php`, no code required: + +```php +'my-llm' => [ + 'driver' => 'openai-compatible', + 'url' => env('MY_LLM_URL'), // required + 'key' => env('MY_LLM_API_KEY'), // optional Bearer token + 'models' => [ + 'text' => ['default' => 'some-chat-model'], + 'embeddings' => [ + 'default' => 'some-embedding-model', + 'dimensions' => 1024, // optional; omit to use native dimensions + ], + 'transcription' => ['default' => 'some-transcription-model'], + ], +], +``` + +Reference it by config key (or `Lab::OpenAiCompatible`). A model is required via the corresponding `models` configuration or per-call `model:`: + +```php +agent()->prompt('Hello', provider: 'my-llm', model: 'some-model'); + +Embeddings::for(['Hello'])->generate( + provider: 'my-llm', + model: 'some-embedding-model', +); +``` + +It uses OpenAI-standard shapes and supports text, streaming, tools, structured output, image attachments, text embeddings, and audio transcription. Embedding dimensions are optional; omit them to use the model's native dimensions. For extra request-body fields, implement `HasProviderOptions` — the returned array is merged into the body. + +Transcription uploads standard multipart (`file` + `model` + optional `language`) and defaults to `response_format: json`. Because endpoints vary, provider options override the defaults — pass `response_format: 'verbose_json'` for segments, or use `diarize()` on servers that implement `diarized_json`: + +```php +Transcription::fromDisk('recordings', $path) + ->withProviderOptions(['response_format' => 'verbose_json']) + ->generate(provider: 'my-llm'); +``` + ## Common Pitfalls ### Wrong Namespace @@ -394,20 +506,26 @@ use Laravel\AI\Agent; Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. -### Never Use Prism Directly +## Provider Support -Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. +| Feature | Providers | +| ---------- | --------------------------------------------------------------- | +| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, OpenAI-compatible | +| Images | OpenAI, Gemini, xAI | +| TTS | OpenAI, ElevenLabs, Mistral | +| STT | OpenAI, ElevenLabs, Mistral, Groq, OpenAI-compatible | +| Embeddings | OpenAI, OpenAI-compatible, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI | +| Reranking | Cohere, Jina | +| Files | OpenAI, Anthropic, Gemini | -## Provider Support +Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings: -| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | -| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | -| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | -| Anthropic | Y | - | - | - | - | - | Y | - | -| Gemini | Y | Y | - | - | Y | - | Y | Y | -| xAI | Y | Y | - | - | - | - | - | - | -| Groq | Y | - | - | - | - | - | - | - | -| OpenRouter | Y | - | - | - | - | - | - | - | -| ElevenLabs | - | - | Y | Y | - | - | - | - | -| Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | +```php +use Laravel\Ai\Enums\Lab; + +Lab::Anthropic; +Lab::OpenAI; +Lab::Gemini; +Lab::OpenAiCompatible; // configurable OpenAI-compatible endpoint +// ... +``` diff --git a/_api_app/.github/skills/ai-sdk-development/SKILL.md b/_api_app/.github/skills/ai-sdk-development/SKILL.md index ef6d72a2..a8d0b772 100644 --- a/_api_app/.github/skills/ai-sdk-development/SKILL.md +++ b/_api_app/.github/skills/ai-sdk-development/SKILL.md @@ -1,6 +1,9 @@ --- name: ai-sdk-development -description: Builds AI agents, generates text and chat responses, produces images, synthesizes audio, transcribes speech, generates vector embeddings, reranks documents, and manages files and vector stores using the Laravel AI SDK (laravel/ai). Supports structured output, streaming, tools, conversation memory, middleware, queueing, broadcasting, and provider failover. Use when building, editing, updating, debugging, or testing any AI functionality, including agents, LLMs, chatbots, text generation, image generation, audio, transcription, embeddings, RAG, similarity search, vector stores, prompting, structured output, or any AI provider (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). +description: TRIGGER when working with ai-sdk, Laravel's official first-party AI SDK. Activate when building or editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, MCP servers, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly. +license: MIT +metadata: + author: laravel --- # Developing with the Laravel AI SDK @@ -53,6 +56,7 @@ Vector stores? → `Stores::create()` ```php use Laravel\Ai\Contracts\Agent; +use Laravel\Ai\Enums\Lab; use Laravel\Ai\Promptable; class SalesCoach implements Agent @@ -69,6 +73,17 @@ class SalesCoach implements Agent $response = (new SalesCoach)->prompt('Analyze this transcript...'); echo $response->text; +// Container resolution with dependency injection +$agent = SalesCoach::make(user: $user); + +// Override provider, model, or timeout per-prompt +$response = (new SalesCoach)->prompt( + 'Analyze this transcript...', + provider: Lab::Anthropic, + model: 'claude-haiku-4-5-20251001', + timeout: 120, +); + // Streaming (returns SSE response from a route) return (new SalesCoach)->stream('Analyze this transcript...'); @@ -245,9 +260,11 @@ $store->add(Document::fromStorage('manual.pdf')); // Store + add in one step ### PHP Attributes ```php -use Laravel\Ai\Attributes\{Provider, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout}; +use Laravel\Ai\Enums\Lab; -#[Provider('anthropic')] +#[Provider(Lab::Anthropic)] +#[Model('claude-haiku-4-5-20251001')] #[MaxSteps(10)] #[MaxTokens(4096)] #[Temperature(0.7)] @@ -261,6 +278,34 @@ class MyAgent implements Agent The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection. +Use `#[RepairToolCalls]` to let an agent recover when a model calls an unknown local tool. The failed call is returned to the model with the available local tool names, and the implicit step budget includes one repair step. Explicit `#[MaxSteps]` limits remain unchanged. + +```php +use Laravel\Ai\Attributes\RepairToolCalls; + +#[RepairToolCalls] +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + // ... +} +``` + +The `#[WithoutBroadcasting]` attribute stops the given stream event types from broadcasting (e.g. data-heavy `ToolResult` payloads that exceed the WebSocket frame limit). The events are still streamed and persisted; they just never hit the channel: + +```php +use Laravel\Ai\Attributes\WithoutBroadcasting; +use Laravel\Ai\Streaming\Events\{ToolCall, ToolResult}; + +#[WithoutBroadcasting(ToolResult::class, ToolCall::class)] +class SearchAgent implements Agent, HasTools +{ + use Promptable; + // ... +} +``` + ### Tools Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`: @@ -294,6 +339,31 @@ public function tools(): iterable } ``` +### MCP Servers + +Register the server once, then return its tools from `tools()`. The SDK automatically wraps each `Laravel\Mcp\Client\Primitives\Tool` and presents it to the model as `mcp_tools_`. Return your own `Laravel\Mcp\Server\Tool` instances in the same way and they retain their names and run in-process. + +```php +use Laravel\Mcp\Client; +use Laravel\Mcp\Facades\Mcp; + +// In a service provider or routes/ai.php +Mcp::registerClient('linear', fn () => Client::web('https://mcp.linear.app/mcp') + ->withToken(config('services.linear.token'))); + +class SupportAgent implements Agent, HasTools +{ + use Promptable; + + public function tools(): iterable + { + return Mcp::client('linear')->tools(); + } +} +``` + +The client connects on its first call, so call `connect()` only when you need to control the timing. Use `Client::local('npx', ['-y', 'some-server'])` for servers that run over stdio. + ### Conversation Memory ```php @@ -313,7 +383,7 @@ $response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More... ### Failover ```php -$response = (new MyAgent)->prompt('Hello', provider: ['openai', 'anthropic']); +$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]); ``` ## Testing and Faking @@ -370,9 +440,51 @@ $store->assertAdded('file_id'); - Agent pattern: Implement the `Agent` interface and use the `Promptable` trait - Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational` - Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores` +- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings) - Artisan commands: `php artisan make:agent`, `php artisan make:tool` - Global helper: `agent()` for anonymous agents +## OpenAI-Compatible Provider + +Point the SDK at any OpenAI-compatible endpoint (LM Studio, vLLM, Together, etc.) with the config-driven `openai-compatible` driver. Define named instances in `config/ai.php`, no code required: + +```php +'my-llm' => [ + 'driver' => 'openai-compatible', + 'url' => env('MY_LLM_URL'), // required + 'key' => env('MY_LLM_API_KEY'), // optional Bearer token + 'models' => [ + 'text' => ['default' => 'some-chat-model'], + 'embeddings' => [ + 'default' => 'some-embedding-model', + 'dimensions' => 1024, // optional; omit to use native dimensions + ], + 'transcription' => ['default' => 'some-transcription-model'], + ], +], +``` + +Reference it by config key (or `Lab::OpenAiCompatible`). A model is required via the corresponding `models` configuration or per-call `model:`: + +```php +agent()->prompt('Hello', provider: 'my-llm', model: 'some-model'); + +Embeddings::for(['Hello'])->generate( + provider: 'my-llm', + model: 'some-embedding-model', +); +``` + +It uses OpenAI-standard shapes and supports text, streaming, tools, structured output, image attachments, text embeddings, and audio transcription. Embedding dimensions are optional; omit them to use the model's native dimensions. For extra request-body fields, implement `HasProviderOptions` — the returned array is merged into the body. + +Transcription uploads standard multipart (`file` + `model` + optional `language`) and defaults to `response_format: json`. Because endpoints vary, provider options override the defaults — pass `response_format: 'verbose_json'` for segments, or use `diarize()` on servers that implement `diarized_json`: + +```php +Transcription::fromDisk('recordings', $path) + ->withProviderOptions(['response_format' => 'verbose_json']) + ->generate(provider: 'my-llm'); +``` + ## Common Pitfalls ### Wrong Namespace @@ -394,20 +506,26 @@ use Laravel\AI\Agent; Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below. -### Never Use Prism Directly +## Provider Support -Use agents and entry-point classes (`Image`, `Audio`, etc.) — not `Prism::text()` directly. The AI SDK wraps Prism internally. +| Feature | Providers | +| ---------- | --------------------------------------------------------------- | +| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, OpenRouter, OpenAI-compatible | +| Images | OpenAI, Gemini, xAI | +| TTS | OpenAI, ElevenLabs, Mistral | +| STT | OpenAI, ElevenLabs, Mistral, Groq, OpenAI-compatible | +| Embeddings | OpenAI, OpenAI-compatible, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI | +| Reranking | Cohere, Jina | +| Files | OpenAI, Anthropic, Gemini | -## Provider Support +Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings: -| Provider | Text | Image | Audio | STT | Embeddings | Reranking | Files | Stores | -| ---------- | ---- | ----- | ----- | --- | ---------- | --------- | ----- | ------ | -| OpenAI | Y | Y | Y | Y | Y | - | Y | Y | -| Anthropic | Y | - | - | - | - | - | Y | - | -| Gemini | Y | Y | - | - | Y | - | Y | Y | -| xAI | Y | Y | - | - | - | - | - | - | -| Groq | Y | - | - | - | - | - | - | - | -| OpenRouter | Y | - | - | - | - | - | - | - | -| ElevenLabs | - | - | Y | Y | - | - | - | - | -| Cohere | - | - | - | - | Y | Y | - | - | -| Jina | - | - | - | - | Y | Y | - | - | +```php +use Laravel\Ai\Enums\Lab; + +Lab::Anthropic; +Lab::OpenAI; +Lab::Gemini; +Lab::OpenAiCompatible; // configurable OpenAI-compatible endpoint +// ... +``` diff --git a/_api_app/AGENTS.md b/_api_app/AGENTS.md index 66965b51..cb3861ed 100644 --- a/_api_app/AGENTS.md +++ b/_api_app/AGENTS.md @@ -115,13 +115,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac - Test the changed behavior and its important failure modes, but do not add tests beyond them. - Read the `testing-best-practices` skill before writing tests. -=== laravel-ai/core rules === - -## Laravel AI SDK - -- This application uses the Laravel AI SDK (`laravel/ai`) for all AI functionality. -- Activate the `developing-with-ai-sdk` skill when building, editing, updating, debugging, or testing AI agents, text generation, chat, streaming, structured output, tools, image generation, audio, transcription, embeddings, reranking, vector stores, files, conversation memory, or any AI provider integration (OpenAI, Anthropic, Gemini, Cohere, Groq, xAI, ElevenLabs, Jina, OpenRouter). - === laravel/core rules === # Do Things the Laravel Way diff --git a/_api_app/composer.json b/_api_app/composer.json index c5941d6b..cc76bae3 100644 --- a/_api_app/composer.json +++ b/_api_app/composer.json @@ -20,7 +20,7 @@ "ext-openssl": "*", "firebase/php-jwt": "^7.0", "intervention/image": "^3.11", - "laravel/ai": "^0.3.0", + "laravel/ai": "^1.0", "laravel/framework": "^12.0", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.10", diff --git a/_api_app/composer.lock b/_api_app/composer.lock index 3932ba0d..24eb321b 100644 --- a/_api_app/composer.lock +++ b/_api_app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3115fbef0899b2de486fea278988865e", + "content-hash": "08d689940f7db121d8e73766cfbb441a", "packages": [ { "name": "brick/math", @@ -1331,34 +1331,49 @@ }, { "name": "laravel/ai", - "version": "v0.3.2", + "version": "v1.0.0", "source": { "type": "git", "url": "https://github.com/laravel/ai.git", - "reference": "dfdf853427eb9fb8d763f8d9e2ed62cfcc03fb9c" + "reference": "101c7ea33cd8569d82570f753fbf38e48b7d3d95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/ai/zipball/dfdf853427eb9fb8d763f8d9e2ed62cfcc03fb9c", - "reference": "dfdf853427eb9fb8d763f8d9e2ed62cfcc03fb9c", + "url": "https://api.github.com/repos/laravel/ai/zipball/101c7ea33cd8569d82570f753fbf38e48b7d3d95", + "reference": "101c7ea33cd8569d82570f753fbf38e48b7d3d95", "shasum": "" }, "require": { "illuminate/console": "^12.0|^13.0", "illuminate/container": "^12.0|^13.0", "illuminate/contracts": "^12.0|^13.0", + "illuminate/database": "^12.0|^13.0", "illuminate/filesystem": "^12.0|^13.0", - "illuminate/json-schema": "^12.0|^13.0", + "illuminate/json-schema": "^12.62|^13.15", "illuminate/support": "^12.0|^13.0", "laravel/prompts": "^0.3.6", "laravel/serializable-closure": "^2.0", - "php": "^8.3", - "prism-php/prism": "^0.99.0" + "php": "^8.3" + }, + "conflict": { + "aws/aws-sdk-php": "<3.369.1", + "laravel/mcp": "<1.0" }, "require-dev": { + "aws/aws-sdk-php": "^3.369.1", + "driftingly/rector-laravel": "^2.5", + "laravel/mcp": "^1.0", "laravel/pint": "^1.26", "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^10.6|^11.0" + "orchestra/testbench": "^10.6|^11.0", + "pestphp/pest": "^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.0", + "phpstan/phpstan": "^2.1", + "rector/rector": "^2.5" + }, + "suggest": { + "aws/aws-sdk-php": "Required to use the Bedrock provider.", + "laravel/mcp": "Required to use MCP client tools or MCP server tools with Laravel AI agents." }, "type": "library", "extra": { @@ -1393,7 +1408,7 @@ "issues": "https://github.com/laravel/ai/issues", "source": "https://github.com/laravel/ai" }, - "time": "2026-03-18T14:44:36+00:00" + "time": "2026-09-23T06:31:17+00:00" }, { "name": "laravel/framework", @@ -3203,85 +3218,6 @@ ], "time": "2026-08-24T00:54:40+00:00" }, - { - "name": "prism-php/prism", - "version": "v0.99.22", - "source": { - "type": "git", - "url": "https://github.com/prism-php/prism.git", - "reference": "989f67567aef69c613eae6e932d615fb96e2f5d7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/prism-php/prism/zipball/989f67567aef69c613eae6e932d615fb96e2f5d7", - "reference": "989f67567aef69c613eae6e932d615fb96e2f5d7", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "laravel/framework": "^11.0|^12.0|^13.0", - "php": "^8.2" - }, - "require-dev": { - "brianium/paratest": "^7.8.4", - "laravel/mcp": "^0.6.0", - "laravel/pint": "^1.14", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^10", - "pestphp/pest": "^3.0", - "pestphp/pest-plugin-arch": "^3.0", - "pestphp/pest-plugin-laravel": "^3.0", - "phpstan/extension-installer": "^1.3", - "phpstan/phpdoc-parser": "^2.0", - "phpstan/phpstan": "2.1.34", - "phpstan/phpstan-deprecation-rules": "^2.0", - "projektgopher/whisky": "^0.7.0", - "rector/rector": "2.3.3", - "spatie/laravel-ray": "^1.39", - "symplify/rule-doc-generator-contracts": "^11.2" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "PrismServer": "Prism\\Prism\\Facades\\PrismServer" - }, - "providers": [ - "Prism\\Prism\\PrismServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "Prism\\Prism\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "TJ Miller", - "email": "hello@echolabs.dev" - } - ], - "description": "A powerful Laravel package for integrating Large Language Models (LLMs) into your applications.", - "support": { - "issues": "https://github.com/prism-php/prism/issues", - "source": "https://github.com/prism-php/prism/tree/v0.99.22" - }, - "funding": [ - { - "url": "https://github.com/sixlive", - "type": "github" - } - ], - "time": "2026-03-12T17:55:23+00:00" - }, { "name": "psr/clock", "version": "1.0.0", @@ -4399,16 +4335,16 @@ }, { "name": "symfony/console", - "version": "v7.4.19", + "version": "v7.4.20", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3a1973458b153f566a8d7499bc06ece323d60063" + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3a1973458b153f566a8d7499bc06ece323d60063", - "reference": "3a1973458b153f566a8d7499bc06ece323d60063", + "url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", "shasum": "" }, "require": { @@ -4473,7 +4409,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.19" + "source": "https://github.com/symfony/console/tree/v7.4.20" }, "funding": [ { @@ -4493,7 +4429,7 @@ "type": "tidelift" } ], - "time": "2026-09-13T10:38:41+00:00" + "time": "2026-09-18T15:55:00+00:00" }, { "name": "symfony/css-selector", @@ -4637,16 +4573,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.4.17", + "version": "v7.4.20", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "8373921e231e190a88e2ad526951bbaa791576fa" + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/8373921e231e190a88e2ad526951bbaa791576fa", - "reference": "8373921e231e190a88e2ad526951bbaa791576fa", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", "shasum": "" }, "require": { @@ -4695,7 +4631,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.17" + "source": "https://github.com/symfony/error-handler/tree/v7.4.20" }, "funding": [ { @@ -4715,7 +4651,7 @@ "type": "tidelift" } ], - "time": "2026-08-21T17:40:08+00:00" + "time": "2026-09-22T18:56:40+00:00" }, { "name": "symfony/event-dispatcher", @@ -4885,16 +4821,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.19", + "version": "v7.4.20", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "1b900bc6ae5ba60c5eee69c11ba44566190576fe" + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/1b900bc6ae5ba60c5eee69c11ba44566190576fe", - "reference": "1b900bc6ae5ba60c5eee69c11ba44566190576fe", + "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", "shasum": "" }, "require": { @@ -4929,7 +4865,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.19" + "source": "https://github.com/symfony/finder/tree/v7.4.20" }, "funding": [ { @@ -4949,20 +4885,20 @@ "type": "tidelift" } ], - "time": "2026-09-10T19:14:10+00:00" + "time": "2026-09-26T06:31:10+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.19", + "version": "v7.4.20", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936" + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f7b953be7801974a5a0685e1228bbc3b12b35936", - "reference": "f7b953be7801974a5a0685e1228bbc3b12b35936", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9d0761c0da6bd5b801b401666834901a9a1ffc33", + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33", "shasum": "" }, "require": { @@ -5011,7 +4947,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.19" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.20" }, "funding": [ { @@ -5031,20 +4967,20 @@ "type": "tidelift" } ], - "time": "2026-09-14T17:41:17+00:00" + "time": "2026-09-24T07:12:44+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.19", + "version": "v7.4.20", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "683507424cc84ca85ac1551adb609b6b98216667" + "reference": "392784b5d7b959b8d872542a2530c50c540a788f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/683507424cc84ca85ac1551adb609b6b98216667", - "reference": "683507424cc84ca85ac1551adb609b6b98216667", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/392784b5d7b959b8d872542a2530c50c540a788f", + "reference": "392784b5d7b959b8d872542a2530c50c540a788f", "shasum": "" }, "require": { @@ -5130,7 +5066,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.19" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.20" }, "funding": [ { @@ -5150,7 +5086,7 @@ "type": "tidelift" } ], - "time": "2026-09-15T07:09:35+00:00" + "time": "2026-09-29T14:05:16+00:00" }, { "name": "symfony/mailer", @@ -6380,16 +6316,16 @@ }, { "name": "symfony/routing", - "version": "v7.4.18", + "version": "v7.4.20", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3" + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/ddd558991e98f693ae6bf5063cc1b0362c6bbec3", - "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3", + "url": "https://api.github.com/repos/symfony/routing/zipball/2bacb72be5eadeaf9874d5accb75bfb33d21c74b", + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b", "shasum": "" }, "require": { @@ -6441,7 +6377,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.18" + "source": "https://github.com/symfony/routing/tree/v7.4.20" }, "funding": [ { @@ -6461,7 +6397,7 @@ "type": "tidelift" } ], - "time": "2026-08-17T13:12:36+00:00" + "time": "2026-09-28T19:09:50+00:00" }, { "name": "symfony/service-contracts", @@ -6817,16 +6753,16 @@ }, { "name": "symfony/uid", - "version": "v7.4.17", + "version": "v7.4.20", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "69d732355a139c6f8881337d28515aa01f12b8be" + "reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/69d732355a139c6f8881337d28515aa01f12b8be", - "reference": "69d732355a139c6f8881337d28515aa01f12b8be", + "url": "https://api.github.com/repos/symfony/uid/zipball/a96bbdb02a04ff3b5592cee6741b03edc78b56aa", + "reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa", "shasum": "" }, "require": { @@ -6871,7 +6807,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.17" + "source": "https://github.com/symfony/uid/tree/v7.4.20" }, "funding": [ { @@ -6891,7 +6827,7 @@ "type": "tidelift" } ], - "time": "2026-08-11T07:38:58+00:00" + "time": "2026-09-23T16:18:28+00:00" }, { "name": "symfony/var-dumper",