Install
$ agentstack add skill-marcoax-skills-eracms-admin-module ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
eraCms Admin Module Creator
Creates a new CRUD module in the eraCms admin panel following the config-driven pattern documented in .ai/guidelines/ADMIN_ARCHITECTURE.md.
Before anything else, read .ai/guidelines/ADMIN_ARCHITECTURE.md to get the full context on getFieldSpec(), DataTypeFactory, ValueObject, admin config, and form saving.
> Language: always respond in the language of the user's message.
Step 1: Gather Information — PLAN MODE
Collect the required information. Some may already be in the user's message — do not repeat already-answered questions.
Required (ask if missing)
| Info | Example | |---|---| | Entity name (singular) | "Event" → class Event, table events | | Fields and DB types | title string, date_start date, is_active boolean default 1 | | Relations | belongsTo Category, M2M with Tag, etc. | | Roles | ['su', 'admin'] — always ask, never assume | | Translatable? | yes/no — if yes: which fields are translated? |
With defaults (show in the plan; only ask if the user seems to want customisation)
| Info | Default | |---|---| | Actions | edit=1, create=1, delete=1, copy=0 | | Edit panels | all disabled (showMedia, showSeo, showBlock = 0) | | List order | orderBy: sort, orderType: ASC | | Sidebar menu | home=true, top-bar show=true action=[add] | | Template | no override (uses admin.edit / admin.view) |
If required information is missing, ask before presenting the plan.
Step 2: Implementation Plan
Present the plan to the user using this structure:
## Plan — Admin Module: [EntityName]
### Files to create
- database/migrations/YYYY_MM_DD_create_[entities]_table.php
- database/factories/[Entity]Factory.php
- app/eraCms/Builders/[Entity]Builder.php (only if custom query logic is needed; otherwise use EraCmsBuilder directly)
- app/Models/[Entity].php
- tests/Feature/Admin/[Entity]AdminTest.php
### Files to modify
- config/eraCms/admin/list.php ← adds '[entities]' section
- resources/lang/it/admin.php ← adds models.[entities]
### Table schema
[name] | [type] | [nullable] | [translated]
...
### Roles: [roles]
### Translatable: yes/no [fields: ...]
### Actions: edit, create, delete, ...
Wait for explicit confirmation before proceeding. Do not create any files in plan mode.
⚠️ Plan Mode Gate
After confirmation, call ExitPlanMode and proceed step by step.
Step 3: Migration
php artisan make:migration create_[entities]_table --no-interaction
Edit the generated file. Key conventions:
- Use
foreignId()->constrained()for foreign keys - Add
->index()on columns used frequently in WHERE/ORDER - Always include the
down()method withSchema::dropIfExists - Common types:
string(255),text()->nullable(),boolean()->default(1),integer()->default(0),date()->nullable()
For translatable models, also add the translations table:
Schema::create('[entities]_translations', function (Blueprint $table) {
$table->id();
$table->foreignId('[entity]_id')->constrained('[entities]')->cascadeOnDelete();
$table->string('locale', 10)->index();
// translated fields...
$table->unique(['[entity]_id', 'locale']);
});
Run:
php artisan migrate --no-interaction
Step 4: Factory
Create the factory with:
php artisan make:factory [Entity]Factory --model=[Entity] --no-interaction
Edit the generated factory to define meaningful default values for all fillable fields.
Step 5: Builder
Create app/eraCms/Builders/[Entity]Builder.php only if the model has domain-specific query logic (semantic scopes, active() overrides, etc.). If not needed, skip the file and use EraCmsBuilder directly in the model attribute.
namespace App\eraCms\Builders;
class [Entity]Builder extends EraCmsBuilder
{
// Only override active() if the logic differs from EraCmsBuilder
// Add domain-relevant semantic scopes
public function findPublished(): static
{
return $this->active()->orderBy('sort')->orderBy('id');
}
}
Step 6: Model
Create app/Models/[Entity].php. Section order: attributes → use → class → traits → properties → casts → relations → getFieldSpec.
Mandatory rules:
- PHP 8 Attributes: always use
#[UseFactory]and#[UseEloquentBuilder]— never overridenewEloquentBuilder(). - HasFactory: always include the
HasFactorytrait. - Accessor/mutator: always use the Laravel 9+
Attributepattern (fnarrow). NevergetXxxAttribute()/setXxxAttribute(). - Dates: use the
DatePresentertrait if the model hasdate_start,date_end,valid_from,valid_until. For other date fields, create accessors in the model or in a dedicated presenter trait underapp/eraCms/Domain/[Entity]/. - casts(): define as a method, not as a
$castsproperty (project convention). - M2M: for every
RelationObjectwithmultiple: 1, addsave{FieldPluralCamelCase}():
``php public function saveTags(array $tags): void { $this->tags()->sync($tags); } ``
Base structure (non-translatable)
namespace App\Models;
use App\eraCms\Admin\Decorators\DataTypeFactory;
use App\eraCms\Builders\[Entity]Builder;
use App\eraCms\Tools\ValueObject\Form\CheckBoxObject;
use App\eraCms\Tools\ValueObject\Form\InputObject;
// ... other ValueObjects
use Database\Factories\[Entity]Factory;
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[UseFactory([Entity]Factory::class)]
#[UseEloquentBuilder([Entity]Builder::class)]
class [Entity] extends Model
{
use HasFactory;
protected $fillable = ['title', 'is_active', 'sort'];
protected array $fieldspec = [];
protected function casts(): array
{
return ['is_active' => 'boolean'];
}
public function getFieldSpec(): array
{
return (new DataTypeFactory(fieldspec: $this->fieldspec))
->addId()
->add('title', new InputObject(required: true))
->add('is_active', (new CheckBoxObject)->setOptions(['default_value' => 1]))
->add('sort', new NumberObject)
->get();
}
}
> If the model does not need a custom builder, use #[UseEloquentBuilder(EraCmsBuilder::class)] instead.
Addition for translatable models
use App\eraCms\Translatable\MaTranslatableHelperTrait;
use App\eraCms\Translatable\Translatable;
class [Entity] extends Model
{
use MaTranslatableHelperTrait;
use Translatable;
public array $translatedAttributes = ['title', 'description'];
// Translated fields must NOT be in $fillable
}
Refer to ADMIN_ARCHITECTURE.md § "Layer 5: getFieldSpec()" for available ValueObjects (Input, Wysiwyg, DatePicker, CheckBox, Number, Hidden, Vue, FileManager, MediaDocUpload, RelationObject) and their parameters.
Step 7: Translation
Open resources/lang/it/admin.php and add under the models key, in alphabetical order:
'[entities]' => 'Italian Label',
Step 8: Admin Config
Add the section in config/eraCms/admin/list.php → 'section':
'[entities]' => [
'model' => '[Entity]',
'title' => '[Section Title]',
'icon' => '[fa-icon]', // Font Awesome 5, without the fa- prefix
'section' => '[group]', // Optional: sidebar grouping key (e.g. 'cms', 'store')
'sectionTitle' => '[Group Title]', // Optional: sidebar group label (e.g. 'Cms', 'Store')
'roles' => [/* from data collected in Step 1 */],
'field' => [
'id',
'title' => ['type' => 'text', 'field' => 'title', 'orderable' => true],
'pub' => ['type' => 'boolean', 'field' => 'is_active', 'editable' => true, 'orderable' => true],
'sort' => ['type' => 'editable','field' => 'sort', 'orderable' => true, 'class' => 'col-1'],
],
'field_searchable' => [
'title' => ['type' => 'text', 'label' => 'title', 'field' => 'title'],
],
'orderBy' => 'sort',
'orderType' => 'ASC',
'actions' => [
'edit' => 1, 'create' => 1, 'delete' => 1, 'copy' => 0,
'export_csv' => 0, 'selectable' => 0, 'preview' => 0,
],
// Add only if export_csv action is enabled:
// 'field_exportable' => [
// 'id' => ['type' => 'integer', 'field' => 'id', 'label' => 'id'],
// 'title' => ['type' => 'text', 'field' => 'title', 'label' => 'Title'],
// ],
// Add only if active:
// 'showMedia' => 1, 'showSeo' => 1, 'showBlock' => 1,
'menu' => [
'home' => true,
'top-bar' => ['show' => true, 'action' => ['add']],
],
],
For translatable models that require a join on the translations table:
'withRelation' => ['translations'],
'joinTable' => '[entities]_translations',
'foreignJoinKey' => '[entity]_id',
'localJoinKey' => 'id',
'whereFilter' => 'locale="it"',
Step 9: PHPUnit Tests
php artisan make:test Feature/Admin/[Entity]AdminTest --phpunit --no-interaction
Use #[Test] attribute (from PHPUnit\Framework\Attributes\Test) on every test method — do not rely on test prefix.
Test setUp pattern:
use App\Models\AdminUser;
use App\Models\Role;
use Illuminate\Foundation\Testing\RefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class [Entity]AdminTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->adminOption = $this->getAdminOptionSetUp();
$this->adminUser = AdminUser::factory()->create();
$role_admin = Role::factory()->create(['name' => 'admin', 'level' => 10]);
$this->adminUser->roles()->attach($role_admin);
}
}
Tests must cover:
- List —
GET /admin/[entities]→ 200 - Create form —
GET /admin/[entities]/create→ 200 - Valid store —
POST /admin/[entities]with valid data → redirect + record in DB - Invalid store —
POSTwithout required fields → validation errors - Edit form —
GET /admin/[entities]/{id}/edit→ 200 - Update —
PUT /admin/[entities]/{id}→ redirect + DB updated - Delete —
DELETE /admin/[entities]/{id}→ record removed from DB
Use actingAs($this->adminUser, 'admin') for all requests.
Run:
php artisan test --compact tests/Feature/Admin/[Entity]AdminTest.php
If they pass, ask the user whether they want to run the full test suite with php artisan test --compact.
Step 10: Formatting
vendor/bin/pint --dirty --format agent
Step 11: Summary
Show a final summary with created/modified files and test results. If any tests are failing, do not mark the task as complete — help the user resolve the errors before closing.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: marcoax
- Source: marcoax/skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.