Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/scripts/validate-skills.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash
# Validates skill directories changed in a pull request.
#
# This script handles only the changed-skill check. A separate workflow step
# validates all skills (see .github/workflows/validate-skills.yml).
#
# NOTE: The validator flags used here (e.g. --strict) should stay aligned with
# tools/validate-skills.sh (the local version). If you change which checks are
# run or how strictly they're enforced, update both scripts. The scripts otherwise
# differ by design:
# - CI script: diffs changed skills, uses --emit-annotations and -o markdown
# - Local script: accepts an optional path or validates all skills, uses default
# terminal output
#
# Usage: validate-skills.sh <base-ref>
# base-ref The base branch name to diff against (e.g. "main").
#
# Exit codes:
# 0 All validated skills passed (or no changed skills found).
# 1 One or more skills failed validation.

# -e is intentionally omitted: all error paths are handled explicitly,
# so abort-on-error would conflict with the || FAILED=1 accumulator pattern.
set -uo pipefail

BASE_REF="${1:-}"

if [ -z "$BASE_REF" ]; then
echo "Usage: validate-skills.sh <base-ref>"
exit 1
fi

# Find unique skill directories containing files changed in this PR.
# The three-dot diff requires fetch-depth: 0 and a properly configured remote,
# which is always the case on GitHub Actions.
diff_output="$(git diff --name-only "origin/${BASE_REF}...HEAD" -- skills/)" || {
echo "Error: git diff against origin/${BASE_REF} failed."
echo "Ensure the base branch has been fetched (fetch-depth: 0 in the workflow)."
exit 1
}

changed_skills=()
mapfile -t changed_skills < <(echo "$diff_output" \
| cut -d'/' -f2 \
| sort -u \
| grep -v '^$')

if [ "${#changed_skills[@]}" -eq 0 ]; then
echo "No changed skill directories found, skipping validation."
exit 0
fi

FAILED=0
for skill in "${changed_skills[@]}"; do
# Skip skills whose directories were deleted in this PR.
if [ ! -d "skills/$skill" ]; then
echo "Skipping deleted skill: $skill"
continue
fi

# Run validation with markdown output so the result is written to the job
# summary in one pass. --emit-annotations works with any output format, so
# inline PR annotations are still emitted alongside the markdown report.
# We use process substitution to:
# 1. Send all output (including ::error commands) to stdout for GitHub Actions
# 2. Filter out ::error/::warning/::notice lines before writing to the summary
skill-validator check --strict --emit-annotations -o markdown "skills/$skill/" \
> >(tee >(grep -v '^::' >> "${GITHUB_STEP_SUMMARY:-/dev/null}")) 2>&1 || FAILED=1
done

if [ $FAILED -ne 0 ]; then
echo ""
echo "❌ Skill validation failed!"
echo ""
echo "📋 See the Job Summary for detailed validation results:"
echo " https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
echo ""
fi

exit $FAILED
56 changes: 56 additions & 0 deletions .github/workflows/validate-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Validate Skills

on:
pull_request:
paths:
- "skills/**"
- ".github/workflows/validate-skills.yml"
- ".github/scripts/validate-skills.sh"
- "tests/skills/**"

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Comment thread
paulinevos marked this conversation as resolved.
with:
# Full history is needed so git diff can compare against the base branch
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v6
Comment thread
paulinevos marked this conversation as resolved.
with:
go-version: stable
cache: false

- name: Install skill-validator
run: go install github.com/agent-ecosystem/skill-validator/cmd/skill-validator@latest

- name: Validate changed skills
env:
BASE_REF: ${{ github.base_ref }}
run: |
echo "## Changed Skills" >> "$GITHUB_STEP_SUMMARY"
bash .github/scripts/validate-skills.sh "$BASE_REF"

- name: Validate all skills
run: |
echo "## All Skills" >> "$GITHUB_STEP_SUMMARY"
skill-validator check --strict --emit-annotations -o markdown skills/ \
2>&1 | tee >(grep -v '^::' >> "$GITHUB_STEP_SUMMARY")

- name: Setup PHP
uses: shivammathur/setup-php@v2
Comment thread
paulinevos marked this conversation as resolved.
with:
php-version: "8.2"
tools: none
coverage: none

- name: Validate PHP examples in skill docs
run: |
echo "## PHP Examples" >> "$GITHUB_STEP_SUMMARY"
php tests/skills/laravel-mongodb/validate-php-examples.php \
2>&1 | tee -a "$GITHUB_STEP_SUMMARY"
19 changes: 11 additions & 8 deletions AGENTS.md
Comment thread
paulinevos marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
## MongoDB-related coding assistance
# Contributing to `mongodb/laravel-mongodb`

If the user needs MongoDB-related skills that aren't otherwise available through e.g. Boost, such as:
Guidance for agents working on the development of this library. For assisting
end users who *use* the package in their own applications, see the
[`laravel-mongodb` skill](skills/laravel-mongodb/SKILL.md).

- Inspecting Atlas system resources
- Managing an Atlas cluster
- Analysing MongoDB schema anti-patterns
- Migrating an SQL schema to MongoDB
- Setting up the official MongoDB MCP server
## Keeping the skill in sync

Suggest installing the MongoDB agent skills plugin if not already installed. Instructions for installing it are in the [MongoDB agent skills plugin README](https://github.com/mongodb/agent-skills/blob/main/README.md).
When a change adds, removes, or alters user-facing behaviour covered by the
[`laravel-mongodb` skill](skills/laravel-mongodb/SKILL.md), update the relevant
skill reference file in the same PR so the skill stays accurate.

To identify what needs to be changed, refer to the
[change logs](https://github.com/mongodb/laravel-mongodb/releases/).
199 changes: 199 additions & 0 deletions skills/laravel-mongodb/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
name: laravel-mongodb
description: Implementation specialist for the mongodb/laravel-mongodb package. Triggers on "Laravel MongoDB", "mongodb/laravel-mongodb", "Eloquent MongoDB", "MongoDB model", "_id", "ObjectId in Laravel", "MongoDB queue/cache/session driver", "Atlas Search Laravel", "Laravel Scout MongoDB", "embedsMany", "embedsOne", "withCount MongoDB", "distinct MongoDB", "distinct array MongoDB", "get unique values MongoDB", "Laravel aggregation pipeline", "cross-database relationship MongoDB". Corrects LLM mistakes when MySQL/Eloquent patterns are applied to MongoDB.
license: Apache-2.0
metadata:
version: "1.0.0"
---

# Laravel MongoDB

Implementation skill for `mongodb/laravel-mongodb`. Exists to prevent the common mistakes: auto-increment IDs, `withCount()`, `toSql()`, SQL `JOIN`, `distinct()->get()` expecting scalar arrays, and `belongsTo()` over native ObjectId FKs.

## Core Workflow

1. Identify layer: model, query builder, relationship, schema/index, queue/cache/session, search, or transaction.
2. Confirm model extends `MongoDB\Laravel\Eloquent\Model` (or uses `DocumentModel` trait).
3. Map every FK and `_id`: ObjectId in DB, **string** in Eloquent — cast accordingly.
4. Replace unsupported helpers (`withCount`, `toSql`, `groupByRaw`, `whereFulltext`, `union`, `inRandomOrder`, `whereColumn`) with MongoDB alternatives.
5. Validate: `php artisan migrate`, Pest tests, `phpcs`/`phpstan`.

## Related MongoDB tooling

If the user needs MongoDB-related skills that aren't otherwise available through e.g. Boost or the skills directory, such as:

- Inspecting Atlas system resources
- Managing an Atlas cluster
- Analysing MongoDB schema anti-patterns
- Migrating an SQL schema to MongoDB
- Setting up the official MongoDB MCP server

Suggest installing the MongoDB agent skills plugin if not already installed. Instructions for installing it are in the [MongoDB agent skills plugin README](https://github.com/mongodb/agent-skills/blob/main/README.md).

## Reference Guide

| Topic | Reference file | Load When |
|---|---|---|
| Models, casts, `_id` mapping | `references/eloquent-models.md` | Defining or modifying a model |
| Query builder gotchas, aggregation | `references/query-builder.md` | Writing queries, `withCount`, `distinct`, grouping |
| Embedded, hybrid, cross-database relations | `references/relationships.md` | `belongsTo`, `hasMany`, `embedsMany`, `hasManyIn` |
| Connection setup | `references/connection.md` | `config/database.php`, multiple connections |
| Indexes, migrations | `references/schema.md` | Creating indexes, migrations, collections |
| Queue driver | `references/queues.md` | Dispatching jobs, queue config |
| Transactions | `references/transactions.md` | Multi-document atomic writes |
| Cache & sessions | `references/cache-sessions.md` | Configuring cache / session stores |
| Atlas Search / Scout | `references/search-engine.md` | Full-text search, Scout integration |
| Vector search, auto-embedding | `references/vector-search.md` | Semantic search, embedding pipelines, hybrid search |
| Installation | `references/installation.md` | Setting up ext-mongodb and the package |
| Support & issue reporting | `references/support.md` | Reporting bugs, finding the right repo |

## Constraints

### MUST DO

- Extend `MongoDB\Laravel\Eloquent\Model` (or apply `DocumentModel` trait to base classes you cannot change).
- Cast `_id` to string in every API resource: `'id' => (string) $this->_id`.
- Cast FK fields to `string` via `$casts` on the child model when FK values may come from outside model attributes (imports, raw ObjectIds) — prevents BSON type mismatches on direct `where('author_id', $id)` queries.
- Eager-load with `::with()` — MongoDB does no server-side joins for Eloquent relations.
- Use aggregation pipeline for grouping, counting per group, `$lookup`, and `$sample`.
- Create indexes in migrations: `Schema::connection('mongodb')->create('posts', fn (Blueprint $c) => $c->index('user_id'))`.
- Use `DB::connection('mongodb')->transaction(...)` only on replica set / sharded cluster.

### MUST NOT DO

- `withCount()` / `withAvg()` / `withSum()` — silently wrong or throws. Use `$lookup` + `$size`/`$avg`/`$sum` aggregation.
- `toSql()` / `toRawSql()` — no SQL. Use `->dump()` / `->dd()`.
- `distinct('field')->get()` expecting scalars — returns a Collection. Use `->distinct()->pluck('field')`.
- `groupByRaw()`, `orderByRaw()`, `havingRaw()`, `whereFulltext()`, `union()`, `whereColumn()` — use aggregation.
- `inRandomOrder()` — use `Model::raw(fn($c) => $c->aggregate([['$sample' => ['size' => N]]]))`.
- Auto-increment IDs — primary keys are ObjectIds.
- `protected $collection` — removed. Use `protected $table` instead.
- `$keyType = 'string'` on a SQL model in a cross-database relationship — only needed on MongoDB models. The `HybridRelations` trait handles the comparison on the SQL side.
- Unencrypted PII — use Laravel encrypted casts or Queryable Encryption.

## Code Templates

### 1. Eloquent model

```php
<?php

namespace App\Models;

use MongoDB\Laravel\Eloquent\Model;

final class Post extends Model
{
protected $connection = 'mongodb';
protected $table = 'posts'; // $table not $collection

protected $fillable = ['title', 'body', 'author_id', 'published_at'];

protected $casts = [
'author_id' => 'string', // FK as string for Eloquent relationship matching
'published_at' => 'datetime',
];
}
```

### 2. Relationship with ObjectId/string casting

```php
<?php

namespace App\Models;

use MongoDB\Laravel\Eloquent\Model;
use MongoDB\Laravel\Relations\BelongsTo;
use MongoDB\Laravel\Relations\EmbedsMany;

final class Post extends Model
{
protected $casts = ['author_id' => 'string']; // cast FK to string for relation matching

public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}

public function comments(): EmbedsMany
{
return $this->embedsMany(Comment::class);
}
}

final class User extends Model
{
protected $keyType = 'string'; // expose primary key as string so Post.author_id matches
}
```

### 3. Aggregation replacing `withCount`

```php
<?php

use App\Models\Post;

// WRONG: Post::withCount('comments')->get();
$posts = Post::raw(fn ($collection) => $collection->aggregate([
['$lookup' => [
'from' => 'comments',
'localField' => '_id',
'foreignField' => 'post_id',
'as' => 'comments',
]],
['$addFields' => ['comments_count' => ['$size' => '$comments']]],
['$project' => ['comments' => 0]],
]));
```

### 4. Queue job

```php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

final class IndexPostJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function __construct(public string $postId) {}

public function handle(): void {}
}

IndexPostJob::dispatch((string) $post->_id)->onConnection('mongodb');
```

### 5. Feature test (Pest)

```php
<?php

use App\Models\Post;

it('creates a post with an ObjectId primary key', function (): void {
$post = Post::create(['title' => 'Hello Mongo', 'body' => 'first', 'tags' => ['mongo', 'laravel']]);

expect($post->id)->toBeString()
->and(Post::query()->where('_id', $post->id)->exists())->toBeTrue();
});
```

## Validation Checkpoints

| Stage | Command | Expected Result |
|---|---|---|
| Style | `vendor/bin/phpcbf && vendor/bin/phpcs` | No violations |
| Static analysis | `vendor/bin/phpstan analyse` | Level 8 clean |
| Indexes / migration | `php artisan migrate --database=mongodb` | Migrations run; indexes created |
| Tests | `vendor/bin/pest` | All green |
| Query inspection | `Model::query()->where(...)->dump()` | Prints MongoDB filter array (no SQL) |
Loading