Skip to content

Commit 928a900

Browse files
authored
PHPLARA-100: Create agent skills (#3546)
* Add skill validator workflow The same skill validator that's used for the MongoDB agent-skills repo * Create agent skills
1 parent f8ce003 commit 928a900

23 files changed

Lines changed: 2287 additions & 8 deletions

.github/scripts/validate-skills.sh

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/bin/bash
2+
# Validates skill directories changed in a pull request.
3+
#
4+
# This script handles only the changed-skill check. A separate workflow step
5+
# validates all skills (see .github/workflows/validate-skills.yml).
6+
#
7+
# NOTE: The validator flags used here (e.g. --strict) should stay aligned with
8+
# tools/validate-skills.sh (the local version). If you change which checks are
9+
# run or how strictly they're enforced, update both scripts. The scripts otherwise
10+
# differ by design:
11+
# - CI script: diffs changed skills, uses --emit-annotations and -o markdown
12+
# - Local script: accepts an optional path or validates all skills, uses default
13+
# terminal output
14+
#
15+
# Usage: validate-skills.sh <base-ref>
16+
# base-ref The base branch name to diff against (e.g. "main").
17+
#
18+
# Exit codes:
19+
# 0 All validated skills passed (or no changed skills found).
20+
# 1 One or more skills failed validation.
21+
22+
# -e is intentionally omitted: all error paths are handled explicitly,
23+
# so abort-on-error would conflict with the || FAILED=1 accumulator pattern.
24+
set -uo pipefail
25+
26+
BASE_REF="${1:-}"
27+
28+
if [ -z "$BASE_REF" ]; then
29+
echo "Usage: validate-skills.sh <base-ref>"
30+
exit 1
31+
fi
32+
33+
# Find unique skill directories containing files changed in this PR.
34+
# The three-dot diff requires fetch-depth: 0 and a properly configured remote,
35+
# which is always the case on GitHub Actions.
36+
diff_output="$(git diff --name-only "origin/${BASE_REF}...HEAD" -- skills/)" || {
37+
echo "Error: git diff against origin/${BASE_REF} failed."
38+
echo "Ensure the base branch has been fetched (fetch-depth: 0 in the workflow)."
39+
exit 1
40+
}
41+
42+
changed_skills=()
43+
mapfile -t changed_skills < <(echo "$diff_output" \
44+
| cut -d'/' -f2 \
45+
| sort -u \
46+
| grep -v '^$')
47+
48+
if [ "${#changed_skills[@]}" -eq 0 ]; then
49+
echo "No changed skill directories found, skipping validation."
50+
exit 0
51+
fi
52+
53+
FAILED=0
54+
for skill in "${changed_skills[@]}"; do
55+
# Skip skills whose directories were deleted in this PR.
56+
if [ ! -d "skills/$skill" ]; then
57+
echo "Skipping deleted skill: $skill"
58+
continue
59+
fi
60+
61+
# Run validation with markdown output so the result is written to the job
62+
# summary in one pass. --emit-annotations works with any output format, so
63+
# inline PR annotations are still emitted alongside the markdown report.
64+
# We use process substitution to:
65+
# 1. Send all output (including ::error commands) to stdout for GitHub Actions
66+
# 2. Filter out ::error/::warning/::notice lines before writing to the summary
67+
skill-validator check --strict --emit-annotations -o markdown "skills/$skill/" \
68+
> >(tee >(grep -v '^::' >> "${GITHUB_STEP_SUMMARY:-/dev/null}")) 2>&1 || FAILED=1
69+
done
70+
71+
if [ $FAILED -ne 0 ]; then
72+
echo ""
73+
echo "❌ Skill validation failed!"
74+
echo ""
75+
echo "📋 See the Job Summary for detailed validation results:"
76+
echo " https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
77+
echo ""
78+
fi
79+
80+
exit $FAILED
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: Validate Skills
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "skills/**"
7+
- ".github/workflows/validate-skills.yml"
8+
- ".github/scripts/validate-skills.sh"
9+
- "tests/skills/**"
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
validate:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v7
19+
with:
20+
# Full history is needed so git diff can compare against the base branch
21+
fetch-depth: 0
22+
23+
- name: Set up Go
24+
uses: actions/setup-go@v6
25+
with:
26+
go-version: stable
27+
cache: false
28+
29+
- name: Install skill-validator
30+
run: go install github.com/agent-ecosystem/skill-validator/cmd/skill-validator@latest
31+
32+
- name: Validate changed skills
33+
env:
34+
BASE_REF: ${{ github.base_ref }}
35+
run: |
36+
echo "## Changed Skills" >> "$GITHUB_STEP_SUMMARY"
37+
bash .github/scripts/validate-skills.sh "$BASE_REF"
38+
39+
- name: Validate all skills
40+
run: |
41+
echo "## All Skills" >> "$GITHUB_STEP_SUMMARY"
42+
skill-validator check --strict --emit-annotations -o markdown skills/ \
43+
2>&1 | tee >(grep -v '^::' >> "$GITHUB_STEP_SUMMARY")
44+
45+
- name: Setup PHP
46+
uses: shivammathur/setup-php@v2
47+
with:
48+
php-version: "8.2"
49+
tools: none
50+
coverage: none
51+
52+
- name: Validate PHP examples in skill docs
53+
run: |
54+
echo "## PHP Examples" >> "$GITHUB_STEP_SUMMARY"
55+
php tests/skills/laravel-mongodb/validate-php-examples.php \
56+
2>&1 | tee -a "$GITHUB_STEP_SUMMARY"

AGENTS.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
## MongoDB-related coding assistance
1+
# Contributing to `mongodb/laravel-mongodb`
22

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

5-
- Inspecting Atlas system resources
6-
- Managing an Atlas cluster
7-
- Analysing MongoDB schema anti-patterns
8-
- Migrating an SQL schema to MongoDB
9-
- Setting up the official MongoDB MCP server
7+
## Keeping the skill in sync
108

11-
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).
9+
When a change adds, removes, or alters user-facing behaviour covered by the
10+
[`laravel-mongodb` skill](skills/laravel-mongodb/SKILL.md), update the relevant
11+
skill reference file in the same PR so the skill stays accurate.
12+
13+
To identify what needs to be changed, refer to the
14+
[change logs](https://github.com/mongodb/laravel-mongodb/releases/).

skills/laravel-mongodb/SKILL.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
name: laravel-mongodb
3+
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.
4+
license: Apache-2.0
5+
metadata:
6+
version: "1.0.0"
7+
---
8+
9+
# Laravel MongoDB
10+
11+
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.
12+
13+
## Core Workflow
14+
15+
1. Identify layer: model, query builder, relationship, schema/index, queue/cache/session, search, or transaction.
16+
2. Confirm model extends `MongoDB\Laravel\Eloquent\Model` (or uses `DocumentModel` trait).
17+
3. Map every FK and `_id`: ObjectId in DB, **string** in Eloquent — cast accordingly.
18+
4. Replace unsupported helpers (`withCount`, `toSql`, `groupByRaw`, `whereFulltext`, `union`, `inRandomOrder`, `whereColumn`) with MongoDB alternatives.
19+
5. Validate: `php artisan migrate`, Pest tests, `phpcs`/`phpstan`.
20+
21+
## Related MongoDB tooling
22+
23+
If the user needs MongoDB-related skills that aren't otherwise available through e.g. Boost or the skills directory, such as:
24+
25+
- Inspecting Atlas system resources
26+
- Managing an Atlas cluster
27+
- Analysing MongoDB schema anti-patterns
28+
- Migrating an SQL schema to MongoDB
29+
- Setting up the official MongoDB MCP server
30+
31+
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).
32+
33+
## Reference Guide
34+
35+
| Topic | Reference file | Load When |
36+
|---|---|---|
37+
| Models, casts, `_id` mapping | `references/eloquent-models.md` | Defining or modifying a model |
38+
| Query builder gotchas, aggregation | `references/query-builder.md` | Writing queries, `withCount`, `distinct`, grouping |
39+
| Embedded, hybrid, cross-database relations | `references/relationships.md` | `belongsTo`, `hasMany`, `embedsMany`, `hasManyIn` |
40+
| Connection setup | `references/connection.md` | `config/database.php`, multiple connections |
41+
| Indexes, migrations | `references/schema.md` | Creating indexes, migrations, collections |
42+
| Queue driver | `references/queues.md` | Dispatching jobs, queue config |
43+
| Transactions | `references/transactions.md` | Multi-document atomic writes |
44+
| Cache & sessions | `references/cache-sessions.md` | Configuring cache / session stores |
45+
| Atlas Search / Scout | `references/search-engine.md` | Full-text search, Scout integration |
46+
| Vector search, auto-embedding | `references/vector-search.md` | Semantic search, embedding pipelines, hybrid search |
47+
| Installation | `references/installation.md` | Setting up ext-mongodb and the package |
48+
| Support & issue reporting | `references/support.md` | Reporting bugs, finding the right repo |
49+
50+
## Constraints
51+
52+
### MUST DO
53+
54+
- Extend `MongoDB\Laravel\Eloquent\Model` (or apply `DocumentModel` trait to base classes you cannot change).
55+
- Cast `_id` to string in every API resource: `'id' => (string) $this->_id`.
56+
- 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.
57+
- Eager-load with `::with()` — MongoDB does no server-side joins for Eloquent relations.
58+
- Use aggregation pipeline for grouping, counting per group, `$lookup`, and `$sample`.
59+
- Create indexes in migrations: `Schema::connection('mongodb')->create('posts', fn (Blueprint $c) => $c->index('user_id'))`.
60+
- Use `DB::connection('mongodb')->transaction(...)` only on replica set / sharded cluster.
61+
62+
### MUST NOT DO
63+
64+
- `withCount()` / `withAvg()` / `withSum()` — silently wrong or throws. Use `$lookup` + `$size`/`$avg`/`$sum` aggregation.
65+
- `toSql()` / `toRawSql()` — no SQL. Use `->dump()` / `->dd()`.
66+
- `distinct('field')->get()` expecting scalars — returns a Collection. Use `->distinct()->pluck('field')`.
67+
- `groupByRaw()`, `orderByRaw()`, `havingRaw()`, `whereFulltext()`, `union()`, `whereColumn()` — use aggregation.
68+
- `inRandomOrder()` — use `Model::raw(fn($c) => $c->aggregate([['$sample' => ['size' => N]]]))`.
69+
- Auto-increment IDs — primary keys are ObjectIds.
70+
- `protected $collection` — removed. Use `protected $table` instead.
71+
- `$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.
72+
- Unencrypted PII — use Laravel encrypted casts or Queryable Encryption.
73+
74+
## Code Templates
75+
76+
### 1. Eloquent model
77+
78+
```php
79+
<?php
80+
81+
namespace App\Models;
82+
83+
use MongoDB\Laravel\Eloquent\Model;
84+
85+
final class Post extends Model
86+
{
87+
protected $connection = 'mongodb';
88+
protected $table = 'posts'; // $table not $collection
89+
90+
protected $fillable = ['title', 'body', 'author_id', 'published_at'];
91+
92+
protected $casts = [
93+
'author_id' => 'string', // FK as string for Eloquent relationship matching
94+
'published_at' => 'datetime',
95+
];
96+
}
97+
```
98+
99+
### 2. Relationship with ObjectId/string casting
100+
101+
```php
102+
<?php
103+
104+
namespace App\Models;
105+
106+
use MongoDB\Laravel\Eloquent\Model;
107+
use MongoDB\Laravel\Relations\BelongsTo;
108+
use MongoDB\Laravel\Relations\EmbedsMany;
109+
110+
final class Post extends Model
111+
{
112+
protected $casts = ['author_id' => 'string']; // cast FK to string for relation matching
113+
114+
public function author(): BelongsTo
115+
{
116+
return $this->belongsTo(User::class, 'author_id');
117+
}
118+
119+
public function comments(): EmbedsMany
120+
{
121+
return $this->embedsMany(Comment::class);
122+
}
123+
}
124+
125+
final class User extends Model
126+
{
127+
protected $keyType = 'string'; // expose primary key as string so Post.author_id matches
128+
}
129+
```
130+
131+
### 3. Aggregation replacing `withCount`
132+
133+
```php
134+
<?php
135+
136+
use App\Models\Post;
137+
138+
// WRONG: Post::withCount('comments')->get();
139+
$posts = Post::raw(fn ($collection) => $collection->aggregate([
140+
['$lookup' => [
141+
'from' => 'comments',
142+
'localField' => '_id',
143+
'foreignField' => 'post_id',
144+
'as' => 'comments',
145+
]],
146+
['$addFields' => ['comments_count' => ['$size' => '$comments']]],
147+
['$project' => ['comments' => 0]],
148+
]));
149+
```
150+
151+
### 4. Queue job
152+
153+
```php
154+
<?php
155+
156+
namespace App\Jobs;
157+
158+
use Illuminate\Bus\Queueable;
159+
use Illuminate\Contracts\Queue\ShouldQueue;
160+
use Illuminate\Foundation\Bus\Dispatchable;
161+
use Illuminate\Queue\InteractsWithQueue;
162+
use Illuminate\Queue\SerializesModels;
163+
164+
final class IndexPostJob implements ShouldQueue
165+
{
166+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
167+
168+
public function __construct(public string $postId) {}
169+
170+
public function handle(): void {}
171+
}
172+
173+
IndexPostJob::dispatch((string) $post->_id)->onConnection('mongodb');
174+
```
175+
176+
### 5. Feature test (Pest)
177+
178+
```php
179+
<?php
180+
181+
use App\Models\Post;
182+
183+
it('creates a post with an ObjectId primary key', function (): void {
184+
$post = Post::create(['title' => 'Hello Mongo', 'body' => 'first', 'tags' => ['mongo', 'laravel']]);
185+
186+
expect($post->id)->toBeString()
187+
->and(Post::query()->where('_id', $post->id)->exists())->toBeTrue();
188+
});
189+
```
190+
191+
## Validation Checkpoints
192+
193+
| Stage | Command | Expected Result |
194+
|---|---|---|
195+
| Style | `vendor/bin/phpcbf && vendor/bin/phpcs` | No violations |
196+
| Static analysis | `vendor/bin/phpstan analyse` | Level 8 clean |
197+
| Indexes / migration | `php artisan migrate --database=mongodb` | Migrations run; indexes created |
198+
| Tests | `vendor/bin/pest` | All green |
199+
| Query inspection | `Model::query()->where(...)->dump()` | Prints MongoDB filter array (no SQL) |

0 commit comments

Comments
 (0)