@cristiancorreau/forge 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +265 -109
- package/assets/adapters/claude-code/commands/laravel-eloquent.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-mcp.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-pest.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-security.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-verify.md +7 -0
- package/assets/core/hooks/pre-bash-check.js +46 -0
- package/assets/core/hooks/pre-edit-check.js +14 -0
- package/assets/core/skills/laravel-eloquent/SKILL.md +453 -0
- package/assets/core/skills/laravel-mcp/SKILL.md +468 -0
- package/assets/core/skills/laravel-pest/SKILL.md +686 -0
- package/assets/core/skills/laravel-security/SKILL.md +658 -0
- package/assets/core/skills/laravel-verify/SKILL.md +462 -0
- package/assets/manifest.json +27 -2
- package/assets/profiles/astro/agents/frontend-engineer.md +2 -0
- package/assets/profiles/django/agents/api-engineer.md +2 -0
- package/assets/profiles/expo/agents/mobile-engineer.md +2 -0
- package/assets/profiles/express/agents/api-engineer.md +2 -0
- package/assets/profiles/fastapi/agents/api-engineer.md +2 -0
- package/assets/profiles/flask/agents/api-engineer.md +2 -0
- package/assets/profiles/flutter/agents/mobile-engineer.md +12 -10
- package/assets/profiles/go-gin/agents/api-engineer.md +3 -1
- package/assets/profiles/hono-drizzle/agents/api-engineer.md +2 -0
- package/assets/profiles/laravel/README.md +16 -2
- package/assets/profiles/laravel/agents/api-engineer.md +2 -0
- package/assets/profiles/laravel/agents/fullstack-engineer.md +4 -2
- package/assets/profiles/laravel/agents/laravel-specialist.md +607 -0
- package/assets/profiles/laravel/agents/laravel-test-engineer.md +448 -0
- package/assets/profiles/nestjs/agents/api-engineer.md +3 -1
- package/assets/profiles/nextjs-admin/agents/admin-engineer.md +2 -0
- package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +2 -0
- package/assets/profiles/rails/agents/fullstack-engineer.md +2 -0
- package/assets/profiles/rust/agents/api-engineer.md +2 -0
- package/assets/profiles/springboot/agents/api-engineer.md +11 -9
- package/assets/profiles/sveltekit/agents/frontend-engineer.md +4 -2
- package/assets/profiles/vuenuxt/agents/frontend-engineer.md +12 -10
- package/assets/profiles/wordpress/agents/divi-engineer.md +2 -0
- package/assets/profiles/wordpress/agents/elementor-engineer.md +2 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +187 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/mcp.d.ts +42 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +141 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/lib/catalog.d.ts.map +1 -1
- package/dist/lib/catalog.js +5 -0
- package/dist/lib/catalog.js.map +1 -1
- package/dist/lib/mcp-tools.d.ts +37 -0
- package/dist/lib/mcp-tools.d.ts.map +1 -0
- package/dist/lib/mcp-tools.js +124 -0
- package/dist/lib/mcp-tools.js.map +1 -0
- package/dist/lib/skill-security.d.ts +66 -0
- package/dist/lib/skill-security.d.ts.map +1 -0
- package/dist/lib/skill-security.js +225 -0
- package/dist/lib/skill-security.js.map +1 -0
- package/dist/lib/skill-source.d.ts +29 -0
- package/dist/lib/skill-source.d.ts.map +1 -0
- package/dist/lib/skill-source.js +94 -0
- package/dist/lib/skill-source.js.map +1 -0
- package/dist/tui/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +3 -6
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/tui/panel.d.ts.map +1 -1
- package/dist/tui/panel.js +3 -6
- package/dist/tui/panel.js.map +1 -1
- package/dist/tui/wizard.d.ts.map +1 -1
- package/dist/tui/wizard.js +3 -13
- package/dist/tui/wizard.js.map +1 -1
- package/dist/ui/colors.d.ts +3 -1
- package/dist/ui/colors.d.ts.map +1 -1
- package/dist/ui/colors.js +11 -2
- package/dist/ui/colors.js.map +1 -1
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +4 -3
- package/dist/ui/header.js.map +1 -1
- package/dist/ui/theme.d.ts +24 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +32 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# Skill: laravel-eloquent
|
|
2
|
+
|
|
3
|
+
Eloquent productivo en Laravel: modelar relaciones, eager loading para evitar N+1, casts y
|
|
4
|
+
accessors modernos, scopes, recorridos eficientes de datasets grandes y búsqueda vectorial con
|
|
5
|
+
pgvector. Activar al escribir o revisar modelos, queries o migraciones de Eloquent.
|
|
6
|
+
|
|
7
|
+
Triggers: /laravel-eloquent, "modelo eloquent", "relaciones eloquent", "evitar N+1", "eager loading",
|
|
8
|
+
"query lenta", "scope eloquent", "cast eloquent", "accessor mutator", "pgvector laravel",
|
|
9
|
+
"búsqueda semántica eloquent", "withCount whereHas".
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Cuándo usar este skill
|
|
14
|
+
|
|
15
|
+
- Al definir o modificar modelos Eloquent (relaciones, casts, accessors/mutators, scopes).
|
|
16
|
+
- Al detectar o prevenir queries N+1 en controllers, resources o vistas.
|
|
17
|
+
- Al recorrer datasets grandes sin reventar la memoria (`chunk`, `cursor`, `lazy`).
|
|
18
|
+
- Al agregar o auditar índices y analizar planes de ejecución con `EXPLAIN`.
|
|
19
|
+
- Al implementar búsqueda semántica con columnas vector / pgvector; verifica que tu versión de Laravel lo soporte de forma nativa.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Modelos y relaciones
|
|
24
|
+
|
|
25
|
+
Un modelo es una clase en `app/Models`. Genera el modelo con su migración, factory y seeder de una vez:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
php artisan make:model Post -mfs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Define las relaciones como métodos tipados. En Laravel declara el tipo de retorno de la relación;
|
|
32
|
+
mejora el autocompletado y el análisis estático.
|
|
33
|
+
|
|
34
|
+
```php
|
|
35
|
+
<?php
|
|
36
|
+
|
|
37
|
+
namespace App\Models;
|
|
38
|
+
|
|
39
|
+
use Illuminate\Database\Eloquent\Model;
|
|
40
|
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
41
|
+
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
42
|
+
|
|
43
|
+
class Post extends Model
|
|
44
|
+
{
|
|
45
|
+
public function author(): BelongsTo
|
|
46
|
+
{
|
|
47
|
+
return $this->belongsTo(User::class, 'user_id');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public function comments(): HasMany
|
|
51
|
+
{
|
|
52
|
+
return $this->hasMany(Comment::class);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Eager loading: evitar N+1
|
|
60
|
+
|
|
61
|
+
El problema N+1 ocurre cuando recorres una colección y accedes a una relación dentro del loop: por cada
|
|
62
|
+
modelo se dispara una query extra. La solución es cargar la relación por adelantado.
|
|
63
|
+
|
|
64
|
+
**`with()`** — eager loading en tiempo de query (lo más común):
|
|
65
|
+
|
|
66
|
+
```php
|
|
67
|
+
// MAL: 1 query para los posts + N queries (una por post) para el autor
|
|
68
|
+
$posts = Post::all();
|
|
69
|
+
foreach ($posts as $post) {
|
|
70
|
+
echo $post->author->name; // query por iteración
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// BIEN: 2 queries en total
|
|
74
|
+
$posts = Post::with('author')->get();
|
|
75
|
+
|
|
76
|
+
// Anidado y con varias relaciones a la vez
|
|
77
|
+
$posts = Post::with(['author', 'comments.author'])->get();
|
|
78
|
+
|
|
79
|
+
// Acotando columnas y filtrando la relación cargada
|
|
80
|
+
$posts = Post::with(['comments' => fn ($q) => $q->where('approved', true)->latest()])->get();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**`load()` / `loadMissing()`** — eager loading sobre modelos ya recuperados:
|
|
84
|
+
|
|
85
|
+
```php
|
|
86
|
+
$post = Post::find($id);
|
|
87
|
+
$post->load('comments'); // carga siempre
|
|
88
|
+
|
|
89
|
+
// loadMissing solo consulta si la relación aún no está cargada (idempotente)
|
|
90
|
+
$post->loadMissing(['author', 'comments']);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**`$with`** — relaciones que SIEMPRE se cargan con el modelo. Úsalo con criterio: si la relación
|
|
94
|
+
no se necesita en todos los flujos, termina haciendo trabajo de más.
|
|
95
|
+
|
|
96
|
+
```php
|
|
97
|
+
class Post extends Model
|
|
98
|
+
{
|
|
99
|
+
// Se cargan automáticamente en toda query del modelo
|
|
100
|
+
protected $with = ['author'];
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Strictness en desarrollo: `preventLazyLoading`
|
|
107
|
+
|
|
108
|
+
Configura Eloquent para que falle ruidosamente ante un N+1 en dev/test, pero nunca en producción
|
|
109
|
+
(donde lanzaría una excepción fatal al usuario final). Gátealo con `! $this->app->isProduction()`.
|
|
110
|
+
|
|
111
|
+
```php
|
|
112
|
+
<?php
|
|
113
|
+
// app/Providers/AppServiceProvider.php
|
|
114
|
+
|
|
115
|
+
namespace App\Providers;
|
|
116
|
+
|
|
117
|
+
use Illuminate\Database\Eloquent\Model;
|
|
118
|
+
use Illuminate\Support\ServiceProvider;
|
|
119
|
+
|
|
120
|
+
class AppServiceProvider extends ServiceProvider
|
|
121
|
+
{
|
|
122
|
+
public function boot(): void
|
|
123
|
+
{
|
|
124
|
+
// Solo dispara violaciones en dev/test, no en producción
|
|
125
|
+
Model::preventLazyLoading(! $this->app->isProduction());
|
|
126
|
+
|
|
127
|
+
// Falla si asignas (fill/create) un atributo que no existe en la tabla
|
|
128
|
+
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
|
|
129
|
+
|
|
130
|
+
// Falla si lees un atributo que no se hidrató (típico al hacer select parcial)
|
|
131
|
+
Model::preventAccessingMissingAttributes(! $this->app->isProduction());
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Al acceder a una relación no cargada en dev, Eloquent lanza
|
|
137
|
+
`Illuminate\Database\LazyLoadingViolationException`, lo que obliga a agregar el `with()` correcto.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Casts y Attribute accessors/mutators
|
|
142
|
+
|
|
143
|
+
**Casts** — define el método `casts()`. NO uses la propiedad legacy `protected $casts`.
|
|
144
|
+
|
|
145
|
+
```php
|
|
146
|
+
use Illuminate\Database\Eloquent\Casts\AsCollection;
|
|
147
|
+
|
|
148
|
+
class User extends Model
|
|
149
|
+
{
|
|
150
|
+
protected function casts(): array
|
|
151
|
+
{
|
|
152
|
+
return [
|
|
153
|
+
'is_admin' => 'boolean',
|
|
154
|
+
'options' => 'array',
|
|
155
|
+
'meta' => AsCollection::class,
|
|
156
|
+
'published_at' => 'datetime',
|
|
157
|
+
'price' => 'decimal:2',
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> Las columnas `vector` (pgvector) NO se castean a `array`: ese cast pasaría el valor por
|
|
164
|
+
> `json_encode`/`json_decode` y corrompería el literal del vector. Se leen y escriben con los
|
|
165
|
+
> helpers de embeddings y los operadores vectoriales (ver más abajo), no con un cast de modelo.
|
|
166
|
+
|
|
167
|
+
**Accessors / mutators** — un único método que retorna un `Attribute`. NO escribas los pares
|
|
168
|
+
mágicos legacy `getXxxAttribute()` / `setXxxAttribute()`.
|
|
169
|
+
|
|
170
|
+
```php
|
|
171
|
+
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
172
|
+
|
|
173
|
+
class User extends Model
|
|
174
|
+
{
|
|
175
|
+
protected function firstName(): Attribute
|
|
176
|
+
{
|
|
177
|
+
return Attribute::make(
|
|
178
|
+
get: fn (string $value) => ucfirst($value),
|
|
179
|
+
set: fn (string $value) => strtolower($value),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Atributo computado a partir de otras columnas; cachea el resultado primitivo
|
|
184
|
+
protected function fullName(): Attribute
|
|
185
|
+
{
|
|
186
|
+
return Attribute::make(
|
|
187
|
+
get: fn (mixed $value, array $attributes) =>
|
|
188
|
+
"{$attributes['first_name']} {$attributes['last_name']}",
|
|
189
|
+
)->shouldCache();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Scopes locales y globales
|
|
197
|
+
|
|
198
|
+
**Scope local** — un método reutilizable para encadenar en queries:
|
|
199
|
+
|
|
200
|
+
```php
|
|
201
|
+
use Illuminate\Database\Eloquent\Builder;
|
|
202
|
+
|
|
203
|
+
class Post extends Model
|
|
204
|
+
{
|
|
205
|
+
public function scopePublished(Builder $query): void
|
|
206
|
+
{
|
|
207
|
+
$query->whereNotNull('published_at')->where('published_at', '<=', now());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public function scopeOfAuthor(Builder $query, User $author): void
|
|
211
|
+
{
|
|
212
|
+
$query->where('user_id', $author->id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Uso: el prefijo "scope" se omite al llamar
|
|
217
|
+
$posts = Post::published()->ofAuthor($author)->latest()->get();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Scope global** — se aplica automáticamente a toda query del modelo (ideal para soft-tenancy o
|
|
221
|
+
filtros por defecto). Recuérdalo: para saltártelo usa `withoutGlobalScope()`.
|
|
222
|
+
|
|
223
|
+
```php
|
|
224
|
+
use Illuminate\Database\Eloquent\Builder;
|
|
225
|
+
use Illuminate\Database\Eloquent\Model;
|
|
226
|
+
use Illuminate\Database\Eloquent\Scope;
|
|
227
|
+
|
|
228
|
+
class PublishedScope implements Scope
|
|
229
|
+
{
|
|
230
|
+
public function apply(Builder $builder, Model $model): void
|
|
231
|
+
{
|
|
232
|
+
$builder->whereNotNull('published_at');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class Post extends Model
|
|
237
|
+
{
|
|
238
|
+
protected static function booted(): void
|
|
239
|
+
{
|
|
240
|
+
static::addGlobalScope(new PublishedScope);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Saltar el scope global cuando haga falta
|
|
245
|
+
$all = Post::withoutGlobalScope(PublishedScope::class)->get();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Agregados de relación: `withCount`, `whereHas`, `has`
|
|
251
|
+
|
|
252
|
+
Evita contar relaciones en un loop (otro N+1). Eloquent agrega la cuenta en la misma query.
|
|
253
|
+
|
|
254
|
+
```php
|
|
255
|
+
// Agrega columnas {relacion}_count sin cargar la relación entera
|
|
256
|
+
$posts = Post::withCount('comments')->get();
|
|
257
|
+
echo $posts->first()->comments_count;
|
|
258
|
+
|
|
259
|
+
// Conteo condicional y con alias
|
|
260
|
+
$posts = Post::withCount([
|
|
261
|
+
'comments',
|
|
262
|
+
'comments as approved_comments_count' => fn ($q) => $q->where('approved', true),
|
|
263
|
+
])->get();
|
|
264
|
+
|
|
265
|
+
// Filtrar por existencia de relación
|
|
266
|
+
$active = Post::has('comments')->get(); // tiene al menos 1 comentario
|
|
267
|
+
$popular = Post::has('comments', '>=', 10)->get(); // 10 o más
|
|
268
|
+
|
|
269
|
+
// whereHas: filtra por una condición DENTRO de la relación
|
|
270
|
+
$posts = Post::whereHas('comments', fn ($q) => $q->where('approved', true))->get();
|
|
271
|
+
|
|
272
|
+
// Negación
|
|
273
|
+
$silent = Post::whereDoesntHave('comments')->get();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Datasets grandes: `chunk`, `chunkById`, `cursor`, `lazy`
|
|
279
|
+
|
|
280
|
+
Nunca hagas `Model::all()` sobre tablas grandes: carga todo en memoria. Recorre por lotes o en streaming.
|
|
281
|
+
|
|
282
|
+
```php
|
|
283
|
+
// chunk: procesa en lotes de N; cada lote es una query
|
|
284
|
+
Post::where('active', true)->chunk(500, function ($posts) {
|
|
285
|
+
foreach ($posts as $post) {
|
|
286
|
+
// ...
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// chunkById: estable cuando MODIFICAS filas dentro del loop (evita saltarte registros)
|
|
291
|
+
Post::where('active', true)->chunkById(500, function ($posts) {
|
|
292
|
+
$posts->each->update(['processed' => true]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// lazy: API de colección con bajo uso de memoria (lotes por debajo, LazyCollection arriba)
|
|
296
|
+
Post::where('active', true)->lazy()->each(function ($post) {
|
|
297
|
+
// ...
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// cursor: UNA query, hidrata un modelo a la vez (mínima RAM, pero sin eager loading útil)
|
|
301
|
+
foreach (Post::where('active', true)->cursor() as $post) {
|
|
302
|
+
// ...
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Regla práctica: `chunkById` para mutaciones, `lazy`/`cursor` para lecturas pesadas de solo lectura.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Transacciones
|
|
311
|
+
|
|
312
|
+
Agrupa escrituras relacionadas para que sean atómicas. El closure hace commit al terminar y rollback
|
|
313
|
+
automático ante cualquier excepción.
|
|
314
|
+
|
|
315
|
+
```php
|
|
316
|
+
use Illuminate\Support\Facades\DB;
|
|
317
|
+
|
|
318
|
+
DB::transaction(function () use ($data) {
|
|
319
|
+
$order = Order::create($data['order']);
|
|
320
|
+
$order->items()->createMany($data['items']);
|
|
321
|
+
$order->user->decrement('credit', $order->total);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Reintentos ante deadlock (segundo argumento = número de intentos)
|
|
325
|
+
DB::transaction(fn () => $order->process(), attempts: 3);
|
|
326
|
+
|
|
327
|
+
// Control manual cuando necesitas lógica entre pasos
|
|
328
|
+
DB::beginTransaction();
|
|
329
|
+
try {
|
|
330
|
+
// ...
|
|
331
|
+
DB::commit();
|
|
332
|
+
} catch (\Throwable $e) {
|
|
333
|
+
DB::rollBack();
|
|
334
|
+
throw $e;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Índices y `EXPLAIN`
|
|
341
|
+
|
|
342
|
+
Una query lenta casi siempre es un índice faltante. Agrega índices en migraciones sobre columnas que
|
|
343
|
+
aparecen en `where`, `join`, `order by` y foreign keys.
|
|
344
|
+
|
|
345
|
+
```php
|
|
346
|
+
// database/migrations/xxxx_create_posts_table.php
|
|
347
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
348
|
+
use Illuminate\Support\Facades\Schema;
|
|
349
|
+
|
|
350
|
+
Schema::create('posts', function (Blueprint $table) {
|
|
351
|
+
$table->id();
|
|
352
|
+
$table->foreignId('user_id')->constrained()->index();
|
|
353
|
+
$table->string('slug')->unique();
|
|
354
|
+
$table->timestamp('published_at')->nullable();
|
|
355
|
+
$table->timestamps();
|
|
356
|
+
|
|
357
|
+
// Índice compuesto: el orden importa (más selectivo / más usado primero)
|
|
358
|
+
$table->index(['user_id', 'published_at']);
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Analiza el plan de ejecución directo desde el query builder con `->explain()`:
|
|
363
|
+
|
|
364
|
+
```php
|
|
365
|
+
// Devuelve el plan; revisa filas escaneadas y uso de índice
|
|
366
|
+
$plan = Post::where('user_id', 1)->where('published_at', '<=', now())->explain();
|
|
367
|
+
dump($plan->toArray());
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# Inspección directa en la consola SQL (PostgreSQL/MySQL)
|
|
372
|
+
# PostgreSQL: EXPLAIN ANALYZE SELECT ... ; busca "Seq Scan" (mala señal en tablas grandes)
|
|
373
|
+
# MySQL: EXPLAIN SELECT ... ; busca type=ALL y key=NULL (sin índice)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Búsqueda semántica con vector columns / pgvector
|
|
379
|
+
|
|
380
|
+
Las versiones recientes de Laravel integran columnas vectoriales sobre `pgvector` para búsqueda por
|
|
381
|
+
similitud, alimentadas por un AI SDK first-party. Útil para RAG y búsqueda semántica; verifica que
|
|
382
|
+
estas capacidades (columnas vector, AI SDK, embeddings) estén disponibles en tu versión instalada.
|
|
383
|
+
|
|
384
|
+
**Migración** — habilita la extensión y declara la columna vector con su dimensión:
|
|
385
|
+
|
|
386
|
+
```php
|
|
387
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
388
|
+
use Illuminate\Support\Facades\Schema;
|
|
389
|
+
|
|
390
|
+
Schema::ensureVectorExtensionExists();
|
|
391
|
+
|
|
392
|
+
Schema::create('documents', function (Blueprint $table) {
|
|
393
|
+
$table->id();
|
|
394
|
+
$table->text('content');
|
|
395
|
+
// La dimensión depende del modelo de embeddings (p. ej. 1536 para OpenAI)
|
|
396
|
+
$table->vector('embedding', dimensions: 1536)->index();
|
|
397
|
+
$table->timestamps();
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Generar embeddings** y guardarlos:
|
|
402
|
+
|
|
403
|
+
```php
|
|
404
|
+
use Illuminate\Support\Str;
|
|
405
|
+
|
|
406
|
+
$document = Document::create([
|
|
407
|
+
'content' => $text,
|
|
408
|
+
'embedding' => Str::of($text)->toEmbeddings(),
|
|
409
|
+
]);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Consultar por similitud** — pasar un string auto-embebe la consulta:
|
|
413
|
+
|
|
414
|
+
```php
|
|
415
|
+
$similar = Document::query()
|
|
416
|
+
->whereVectorSimilarTo('embedding', $queryString, minSimilarity: 0.4)
|
|
417
|
+
->orderByVectorDistance('embedding', $queryString)
|
|
418
|
+
->limit(5)
|
|
419
|
+
->get();
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
> Para RAG dentro de un agente del AI SDK existe la tool `SimilaritySearch::usingModel(Document::class, 'embedding')`.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Comandos útiles
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
php artisan tinker # REPL para inspeccionar modelos y queries en vivo
|
|
430
|
+
php artisan db:show # resumen de la conexión: tablas, tamaño, número de filas
|
|
431
|
+
php artisan db:table posts # detalle de una tabla concreta (columnas e índices)
|
|
432
|
+
php artisan model:show Post # relaciones, casts, atributos y eventos del modelo
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
En `tinker` puedes ver el SQL crudo de cualquier query sin ejecutarla:
|
|
436
|
+
|
|
437
|
+
```php
|
|
438
|
+
Post::with('author')->where('active', true)->toRawSql();
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Checklist antes de cerrar
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
✓ Toda query que se recorre carga sus relaciones con with()/load() (sin N+1)
|
|
447
|
+
✓ preventLazyLoading() activo en dev/test, gateado con ! isProduction()
|
|
448
|
+
✓ Casts vía método casts(); accessors/mutators vía Attribute::make (nada legacy)
|
|
449
|
+
✓ Conteos y filtros de relación con withCount/whereHas, no dentro de loops
|
|
450
|
+
✓ Datasets grandes recorridos con chunkById/lazy/cursor, nunca all()
|
|
451
|
+
✓ Escrituras relacionadas envueltas en DB::transaction()
|
|
452
|
+
✓ Índices presentes en columnas de where/join/order; EXPLAIN sin Seq Scan en tablas grandes
|
|
453
|
+
```
|