Scaffold a complete Ptah CRUD from an entity/table description. Orchestrates ptah:forge (fields or --db from an existing table, optional --api), migrate, the mandatory post-scaffold fixes, then ptah:config for the BaseCrud listing. Use whenever the user wants to create/generate a new entity, model, or CRUD with Ptah ("gerar entidade", "novo CRUD", "scaffold X", "cria um CRUD de …"), or hands over a table/fields to turn into a full screen.
php artisan boost:add-skill jonytonet/ptah
Save this content to: .claude/skills/ptah-scaffold/SKILL.md
# Ptah — Scaffold a CRUD end-to-end
Turn "here is the entity/table" into a working web CRUD (Model + Service +
Repository + DTO + Livewire + migration + views) plus a configured BaseCrud
listing. This skill is the **runbook + guardrails**; for deep conventions
(SOLID layers, design tokens, performance rules, API, tests) defer to the
companion **`ptah-development`** skill (also shipped in the package at
`vendor/jonytonet/ptah/resources/boost/skills/ptah-development/SKILL.md`).
## Where the commands run
`ptah:*` are artisan commands of the **consuming Laravel app** (the app that
`composer require`d ptah), NOT the package itself. Run artisan from the app root
(the directory that contains the `artisan` file):
- working dir is the app root → `php artisan ptah:...`
- working dir is a parent workspace → `cd <app-dir> && php artisan ptah:...`
- **never** run `ptah:*` inside the ptah package / `vendor` directory (no `artisan` there).
If you edited the ptah package and the change isn't visible, run `composer dump-autoload` in the app.
## Step 0 — Collect the inputs (ask only what's missing)
- **Entity name** in PascalCase, optional subfolder → `Product` or `Catalog/Product`
(subfolder = namespace `App\Models\Catalog\Product` and menu group).
- **Source of fields** — one of:
- explicit fields: `name:string,price:decimal(10,2):nullable,category_id:unsignedBigInteger,is_active:boolean`
- an **existing DB table** → use `--db` (ptah reads the columns for you).
- **Flags**: `--api` (web+API), `--api-only` (API without web views),
`--no-soft-deletes`, `--no-menu`, `--table=` if it differs from the plural
snake_case default.
- **Field types** (`--fields`): only `integer`/`bigInteger`/…/`unsigned*`,
`foreignId`, `string`, `char`, `text`, `longText`, `decimal`, `float`,
`double`, `boolean`, `date`, `datetime`, `timestamp`, `json`, `enum` (plus the
aliases `int`, `bigint`, `uint`, `ubigint`, `bool`, `foreign`, `jsonb`). Any
other type is **refused** by `ptah:forge` — nothing is generated. State
`decimal(p,s)`; a bare `decimal` silently becomes `decimal(10,2)`.
If the user only gives a table that already exists, prefer `--db` — do not invent a `--fields` list.
## Step 1 — Generate
New entity from fields (SoftDeletes is ON by default — pass `--no-soft-deletes` to disable):
```bash
php artisan ptah:forge Catalog/Product \
--fields="name:string,price:decimal(10,2),category_id:unsignedBigInteger,is_active:boolean"
```
From an existing table (introspects the DB — matches "just give the table"):
```bash
php artisan ptah:forge Catalog/Product --db --table=products
```
Web + API together:
```bash
php artisan ptah:forge Catalog/Product --fields="..." --api
```
Factory + demo seeder from the field types: add `--factory`.
In web mode (no `--api-only`) forge also **writes a default `crud_configs` row
to the database** (needs the `crud_configs` table from `ptah:install`) — the
screen already lists its columns before Step 3; `--force` overwrites that row.
Step 3 only refines it.
**Several related entities?** Write a spec and let `ptah:blueprint` order them
by foreign key and run forge → migrate → config → menu → permissions → seed
(`--dry-run` first). **A column on an existing entity?** `ptah:field Entity add
name:type[:modifiers]` edits migration, model, rules, DTO and config at once.
Both: `docs/AgentTools.md`.
## Step 2 — Post-scaffold fixes (in order)
1. **FK imports — only if `ptah:forge` printed a ⚠.** The generator resolves a
foreign key's `use` itself when exactly one class of that name exists in
`app/Models`, and lists each FK as `✔` (done) or `⚠` (not generated yet, or
ambiguous). Scaffold related entities in dependency order and there is
nothing to fix. Never commit a `// TODO:` line.
2. **Format:** `./vendor/bin/pint`
3. **Migrate:** `php artisan migrate` ⚠️ plain `migrate` only — see Guardrails.
4. **Clear caches:** `php artisan view:clear && php artisan config:clear`
5. Unless you passed `--no-menu`, forge added a link to
`database/seeders/MenuRegistry.php` (grouped when the entity has a subfolder,
a flat root link when not) — sync the sidebar:
`php artisan ptah:menu-sync` (see Guardrails re: `--fresh`).
6. **Verify:** `php artisan ptah:check` renders the screen and flags config
that does not match the model/table (ghost columns, non-fillable fields).
## Step 3 — Configure the BaseCrud listing
`ptah:config` builds the listing config (columns/filters/actions/styles). It can
introspect the model/table to infer column types. Always pass `--non-interactive`
when driving it as an agent, and prefer `--dry-run` first to preview.
```bash
php artisan ptah:config "App\Models\Catalog\Product" --non-interactive \
--column="name:text:label=Nome:sortable" \
--column="price:number:label=Preço:renderer=money:sortable" \
--column="is_active:select:label=Status:renderer=badge:badges=1|success|Ativo,0|danger|Inativo" \
--filter="is_active:select:label=Ativos:options=1:Ativo,0:Inativo" \
--set="itemsPerPage=15"
# preview / inspect:
php artisan ptah:config "App\Models\Catalog\Product" --dry-run --column="..."
php artisan ptah:config "App\Models\Catalog\Product" --list
```
Column/filter/action/style formats are documented in the package skill — reuse
them verbatim rather than guessing.
## Relationships
For an FK column (e.g. `category_id`), configure it as a relation/searchdropdown
so the form shows a picker and the listing shows the related label:
`--column="category_id:text:label=Categoria:relation=category:relation_display=name"`
para exibir o rótulo na listagem, ou
`--column="category_id:searchdropdown:label=Categoria:sd_model=App\Models\Catalog\Category:sd_value=id:sd_label=name"`
para ter o picker no formulário — `searchdropdown` sem `sd_model` (ou
`sd_service`) é recusado pelo validador.
Confirm the relation name and display column against the actual model before
committing — the inference is heuristic.
## Guardrails (organization security rules apply)
- **Never run destructive DB commands** as part of scaffolding: no `migrate:fresh`,
`migrate:reset`, `db:wipe`, `migrate:rollback` on shared/prod. Use plain
`php artisan migrate`. If a migration must be undone, a human runs it.
- **`ptah:menu-sync --fresh` rewrites the `menus` table** — treat as destructive:
run plain `ptah:menu-sync`, and only use `--fresh` after explicit confirmation.
- **Confirm the environment** before migrating. Assume production unless told
otherwise; back up first for anything beyond local.
- **Pint before any commit.** Commit/push only when the user asks.
- Secrets always via `.env`, never hardcoded.
## Quick end-to-end example
> "Cria um CRUD de Fornecedor (Catalog/Supplier) com nome, cnpj, ativo."
```bash
cd petplace
php artisan ptah:forge Catalog/Supplier --fields="name:string,cnpj:string,is_active:boolean"
# pint → migrate → clears (FK imports resolved by the generator)
./vendor/bin/pint && php artisan migrate && php artisan view:clear && php artisan config:clear
php artisan ptah:config "App\Models\Catalog\Supplier" --non-interactive \
--column="name:text:label=Nome:sortable" \
--column="cnpj:text:label=CNPJ" \
--column="is_active:select:label=Status:renderer=badge:badges=1|success|Ativo,0|danger|Inativo" \
--filter="is_active:select:label=Ativos:options=1:Ativo,0:Inativo"
```
Turn "here is the entity/table" into a working web CRUD (Model + Service +
Repository + DTO + Livewire + migration + views) plus a configured BaseCrud
listing. This skill is the runbook + guardrails; for deep conventions
(SOLID layers, design tokens, performance rules, API, tests) defer to the
companion ptah-development skill (also shipped in the package at
vendor/jonytonet/ptah/resources/boost/skills/ptah-development/SKILL.md).
ptah:* are artisan commands of the consuming Laravel app (the app that
composer required ptah), NOT the package itself. Run artisan from the app root
(the directory that contains the artisan file):
php artisan ptah:...cd <app-dir> && php artisan ptah:...ptah:* inside the ptah package / vendor directory (no artisan there).If you edited the ptah package and the change isn't visible, run composer dump-autoload in the app.
Product or Catalog/Product
(subfolder = namespace App\Models\Catalog\Product and menu group).name:string,price:decimal(10,2):nullable,category_id:unsignedBigInteger,is_active:boolean--db (ptah reads the columns for you).--api (web+API), --api-only (API without web views),
--no-soft-deletes, --no-menu, --table= if it differs from the plural
snake_case default.--fields): only integer/bigInteger/…/unsigned*,
foreignId, string, char, text, longText, decimal, float,
double, boolean, date, datetime, timestamp, json, enum (plus the
aliases int, bigint, uint, ubigint, bool, foreign, jsonb). Any
other type is refused by ptah:forge — nothing is generated. State
decimal(p,s); a bare decimal silently becomes decimal(10,2).If the user only gives a table that already exists, prefer --db — do not invent a --fields list.
New entity from fields (SoftDeletes is ON by default — pass --no-soft-deletes to disable):
php artisan ptah:forge Catalog/Product \
--fields="name:string,price:decimal(10,2),category_id:unsignedBigInteger,is_active:boolean"
From an existing table (introspects the DB — matches "just give the table"):
php artisan ptah:forge Catalog/Product --db --table=products
Web + API together:
php artisan ptah:forge Catalog/Product --fields="..." --api
Factory + demo seeder from the field types: add --factory.
In web mode (no --api-only) forge also writes a default crud_configs row
to the database (needs the crud_configs table from ptah:install) — the
screen already lists its columns before Step 3; --force overwrites that row.
Step 3 only refines it.
Several related entities? Write a spec and let ptah:blueprint order them
by foreign key and run forge → migrate → config → menu → permissions → seed
(--dry-run first). A column on an existing entity? ptah:field Entity add name:type[:modifiers] edits migration, model, rules, DTO and config at once.
Both: docs/AgentTools.md.
ptah:forge printed a ⚠. The generator resolves a
foreign key's use itself when exactly one class of that name exists in
app/Models, and lists each FK as ✔ (done) or ⚠ (not generated yet, or
ambiguous). Scaffold related entities in dependency order and there is
nothing to fix. Never commit a // TODO: line../vendor/bin/pintphp artisan migrate ⚠️ plain migrate only — see Guardrails.php artisan view:clear && php artisan config:clear--no-menu, forge added a link to
database/seeders/MenuRegistry.php (grouped when the entity has a subfolder,
a flat root link when not) — sync the sidebar:
php artisan ptah:menu-sync (see Guardrails re: --fresh).php artisan ptah:check renders the screen and flags config
that does not match the model/table (ghost columns, non-fillable fields).ptah:config builds the listing config (columns/filters/actions/styles). It can
introspect the model/table to infer column types. Always pass --non-interactive
when driving it as an agent, and prefer --dry-run first to preview.
php artisan ptah:config "App\Models\Catalog\Product" --non-interactive \
--column="name:text:label=Nome:sortable" \
--column="price:number:label=Preço:renderer=money:sortable" \
--column="is_active:select:label=Status:renderer=badge:badges=1|success|Ativo,0|danger|Inativo" \
--filter="is_active:select:label=Ativos:options=1:Ativo,0:Inativo" \
--set="itemsPerPage=15"
# preview / inspect:
php artisan ptah:config "App\Models\Catalog\Product" --dry-run --column="..."
php artisan ptah:config "App\Models\Catalog\Product" --list
Column/filter/action/style formats are documented in the package skill — reuse them verbatim rather than guessing.
For an FK column (e.g. category_id), configure it as a relation/searchdropdown
so the form shows a picker and the listing shows the related label:
--column="category_id:text:label=Categoria:relation=category:relation_display=name"
para exibir o rótulo na listagem, ou
--column="category_id:searchdropdown:label=Categoria:sd_model=App\Models\Catalog\Category:sd_value=id:sd_label=name"
para ter o picker no formulário — searchdropdown sem sd_model (ou
sd_service) é recusado pelo validador.
Confirm the relation name and display column against the actual model before
committing — the inference is heuristic.
migrate:fresh,
migrate:reset, db:wipe, migrate:rollback on shared/prod. Use plain
php artisan migrate. If a migration must be undone, a human runs it.ptah:menu-sync --fresh rewrites the menus table — treat as destructive:
run plain ptah:menu-sync, and only use --fresh after explicit confirmation..env, never hardcoded."Cria um CRUD de Fornecedor (Catalog/Supplier) com nome, cnpj, ativo."
cd petplace
php artisan ptah:forge Catalog/Supplier --fields="name:string,cnpj:string,is_active:boolean"
# pint → migrate → clears (FK imports resolved by the generator)
./vendor/bin/pint && php artisan migrate && php artisan view:clear && php artisan config:clear
php artisan ptah:config "App\Models\Catalog\Supplier" --non-interactive \
--column="name:text:label=Nome:sortable" \
--column="cnpj:text:label=CNPJ" \
--column="is_active:select:label=Status:renderer=badge:badges=1|success|Ativo,0|danger|Inativo" \
--filter="is_active:select:label=Ativos:options=1:Ativo,0:Inativo"
How can I help you explore Laravel packages today?