dcardenasl/ci4-api-scaffolding

CRUD scaffolding engine for CodeIgniter 4 APIs built on dcardenasl/ci4-api-core. Generates DTOs, services, controllers, migrations, routes, tests and OpenAPI docs via spark commands.

Maintainers

Package info

github.com/dcardenasl/ci4-api-scaffolding

pkg:composer/dcardenasl/ci4-api-scaffolding

Statistics

Installs: 502

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.7.3 2026-05-30 00:35 UTC

This package is auto-updated.

Last update: 2026-05-30 01:00:34 UTC


README

CI License: MIT PHP CI4 Latest Stable Version Total Downloads

CRUD scaffolding engine for CodeIgniter 4 APIs built on dcardenasl/ci4-api-core. One command generates DTOs, service, controller, migration, routes, language files, and tests — all wired to the ci4-api-core base classes.

Pre-1.0 policy: MINOR bumps may contain breaking changes. Pin to ~0.x.0 or exact version until v1.0.0.

Table of Contents

Requirements

Installation

composer require --dev dcardenasl/ci4-api-scaffolding:^0.7

Quick Start

bash vendor/bin/make-crud.sh Article Blog \
  'title:string:required|searchable,body:text:required,published:bool:nullable' yes

php spark module:check Article --domain Blog
php spark migrate

Configuration

Create app/Config/Scaffolding.php extending BaseScaffoldingConfig. If your project follows ci4-api-starter conventions, the bundled defaults work without any overrides:

<?php

declare(strict_types=1);

namespace Config;

use dcardenasl\Ci4ApiScaffolding\Config\BaseScaffoldingConfig;
use dcardenasl\Ci4ApiScaffolding\Config\ScaffoldingConfig;

class Scaffolding extends BaseScaffoldingConfig
{
    public function build(): ScaffoldingConfig
    {
        return ScaffoldingConfig::defaults();
    }
}

To customize base classes, paths, or route filters, pass named arguments to ScaffoldingConfig:

use dcardenasl\Ci4ApiScaffolding\Config\BaseScaffoldingConfig;
use dcardenasl\Ci4ApiScaffolding\Config\ScaffoldingConfig;
use dcardenasl\Ci4ApiScaffolding\Config\ScaffoldingPaths;

class Scaffolding extends BaseScaffoldingConfig
{
    public function build(): ScaffoldingConfig
    {
        $defaults = ScaffoldingConfig::defaults();

        return new ScaffoldingConfig(
            ...(array) $defaults,
            // Lock new routes behind a custom permission instead of the default
            // superadmin-only gate:
            protectedRouteFilters: ['jwtauth', 'permission:catalog.admin', 'throttle'],
            // Override where generated controllers are written:
            paths: new ScaffoldingPaths(controllers: 'Controllers/Api/V2'),
        );
    }
}

Default route filters (when no Scaffolding config is found): ['jwtauth', 'permission:iam.superadmin-access', 'throttle']. New resources are unreachable by non-superadmins until you intentionally loosen this filter.

All configurable options (ScaffoldingConfig constructor parameters):

Option Default Purpose
controllerBaseClass dcardenasl\Ci4ApiCore\Http\ApiController Base class generated controllers extend
serviceBaseClass dcardenasl\Ci4ApiCore\Services\BaseCrudService Base class generated services extend
requestDtoBaseClass dcardenasl\Ci4ApiCore\Dto\BaseRequestDTO Base class generated request DTOs extend
responseDtoInterface dcardenasl\Ci4ApiCore\Dto\DataTransferObjectInterface Interface response DTOs implement
modelBaseClass dcardenasl\Ci4ApiCore\Models\BaseAuditableModel Base class generated models extend
protectedRouteFilters ['jwtauth', 'permission:iam.superadmin-access', 'throttle'] Filters on the protected route group
appNamespace App Top-level namespace of the consumer app
paths ScaffoldingPaths::defaults() All output directories (see ScaffoldingPaths)
openApiTagPrefix null (uses domain name) Custom OpenAPI tag prefix
conditionalControllerTraits [] fieldName => TraitFQCN map for auto-injected controller traits

Path overrides (ScaffoldingPaths constructor parameters, all relative to APPPATH):

Option Default
controllers Controllers/Api/V1
services Services
interfaces Interfaces
requestDtos DTO/Request
responseDtos DTO/Response
models Models
entities Entities
migrations Database/Migrations
routes Config/Routes/v1
documentation Documentation
languageEn Language/en
languageEs Language/es
unitTests tests/Unit/Services (relative to ROOTPATH)
integrationTests tests/Integration/Models (relative to ROOTPATH)
featureTests tests/Feature/Controllers (relative to ROOTPATH)

Available Commands

Command Shell wrapper Description
php spark make:crud bash vendor/bin/make-crud.sh Generate a full CRUD module
php spark make:crud:remove Remove a previously scaffolded module
php spark module:check bash vendor/bin/validate-crud.sh Validate 14 post-scaffold wiring checkpoints
php spark scaffold:check Verify Config\Scaffolding exists and all FQCNs resolve
php spark swagger:generate Generate public/swagger.json from OpenAPI annotations

Always use vendor/bin/make-crud.sh in non-TTY environments (CI, Claude Code, scripts). php spark make:crud falls back to interactive mode when --fields is not provided, which hangs in non-TTY contexts.

make:crud — full options

# Shell wrapper (recommended for scripts and CI)
bash vendor/bin/make-crud.sh <Resource> <Domain> '<fields>' [softDelete=yes] [route]
    [--dry-run] [--no-wire] [--migrate]

# Direct spark command (TTY only)
php spark make:crud <Resource> \
    [--domain <Domain>] \
    [--fields '<fields>'] \
    [--route <route-slug>] \
    [--soft-delete yes|no] \
    [--dry-run] \
    [--no-wire] \
    [--skip-fk-validation]
Option Default Purpose
--domain / arg 2 Catalog Domain folder (groups related resources)
--fields / arg 3 interactive Field definition string (see Field Types)
--route / arg 5 kebab-case plural of resource Route slug used in the URL
--soft-delete / arg 4 yes Emit deleted_at column and soft-delete logic
--version v1 Target a versioned route directory (e.g. --version v2 writes routes to Config/Routes/v2/)
--dry-run off Preview planned files and wiring without writing anything
--no-wire off Generate files but skip Services.php injection; prints snippets to paste manually
--skip-fk-validation off Skip the FK target existence check when the database is unreachable
--migrate (wrapper only) off Auto-run php spark migrate after scaffolding

make:crud:remove — full options

php spark make:crud:remove <Resource> [--domain <Domain>] [--force]

--force skips the confirmation prompt (useful in CI). Without --force, the command lists the files it would delete and asks for confirmation.

module:check / validate-crud.sh

# Via spark (inside consumer project)
php spark module:check <Resource> --domain <Domain>

# Via shell wrapper
bash vendor/bin/validate-crud.sh <Resource> <Domain>

Validates 14 post-scaffold wiring checkpoints: migration exists, table naming, soft-delete consistency, controller/model/service/route presence, Services.php wiring, language files, test files. Exits non-zero if any checkpoint fails.

scaffold:check

php spark scaffold:check

Read-only diagnostic — never writes files. Verifies that app/Config/Scaffolding.php exists, extends BaseScaffoldingConfig, and that all 14 FQCNs it declares (base classes, interfaces, traits) are loadable. Run after first install or after bumping dcardenasl/ci4-api-core to confirm the config still points at real classes.

If the file is missing, the command prints the cp command to bootstrap a default config from the bundled example.

swagger:generate

php spark swagger:generate

Generates public/swagger.json from OpenAPI annotations. Scans app/Config/OpenApi.php, app/Controllers/, app/Documentation/, app/DTO/, and vendor/dcardenasl/ci4-api-core/src/Dto/ by default. Requires zircote/swagger-php in the consumer's require-dev:

composer require --dev zircote/swagger-php

To scan additional directories, subclass the command and override scanPaths():

class MySwaggerGenerate extends \dcardenasl\Ci4ApiScaffolding\Commands\SwaggerGenerate
{
    protected function scanPaths(): array
    {
        return [...parent::scanPaths(), APPPATH . 'Modules/'];
    }
}

Field Types

Field type codes used in the --fields string. All types are recognized case-sensitively.

Type Alias PHP type DB column OpenAPI Validation (auto-added)
string string VARCHAR(255) string string|max_length[255]
text string TEXT string string
int integer int INT integer integer
decimal float DECIMAL(10,2) number (float) decimal
bool bool TINYINT(1) boolean boolean_like
email string VARCHAR(255) string (email) string|valid_email|max_length[255]
date string DATE string (date) valid_date[Y-m-d]
datetime string DATETIME string (date-time) valid_date
json array JSON object permit_empty
fk int INT + FK constraint integer is_natural_no_zero|is_not_unique[table.id]

FK field syntax — uses a 4-segment form because the target table name is a required third segment:

author_id:fk:users:required
category_id:fk:categories:required|filterable

Field Modifiers

Modifiers follow the type (or the FK table) and are separated by |:

name:type:modifier1|modifier2
name:fk:target_table:modifier1|modifier2
Modifier Effect
required required validation rule; non-nullable column
nullable Nullable column; permit_empty validation rule
searchable Adds FULLTEXT index; controller gets HasSearchableIndex trait
filterable Adds field to the model's $filterableFields whitelist
unique Adds UNIQUE index + is_unique[table.column] validation
index Adds a plain (non-unique) index
cascade FK only — ON DELETE CASCADE (default for fk fields)
restrict FK only — ON DELETE RESTRICT
setnull FK only — ON DELETE SET NULL

Full example:

'title:string:required|searchable,price:decimal:required|filterable,author_id:fk:users:required|restrict'

Boolean validation contractbool currently maps to boolean_like, which is part of the supported starter contract (ci4-api-starter and ci4-domain-starter both ship and register that custom rule). If a non-starter consumer uses this package, it must expose an equivalent boolean_like validation rule or adapt the generated rule set after scaffolding.

Generated Artifacts

make:crud Article Blog 'title:string:required' yes creates 17 files:

app/DTO/Request/Blog/ArticleIndexRequestDTO.php
app/DTO/Request/Blog/ArticleCreateRequestDTO.php
app/DTO/Request/Blog/ArticleUpdateRequestDTO.php
app/DTO/Response/Blog/ArticleResponseDTO.php
app/Database/Migrations/<timestamp>_CreateArticlesTable.php
app/Entities/ArticleEntity.php
app/Models/ArticleModel.php
app/Interfaces/Blog/ArticleServiceInterface.php
app/Services/Blog/ArticleService.php
app/Controllers/Api/V1/Blog/ArticleController.php
app/Documentation/Blog/ArticleEndpoints.php
app/Config/Routes/v1/blog.php
app/Language/en/Articles.php
app/Language/es/Articles.php
tests/Unit/Services/Blog/ArticleServiceTest.php
tests/Integration/Models/ArticleModelTest.php
tests/Feature/Controllers/Blog/ArticleControllerTest.php

Services.php is also updated (or a snippet is printed with --no-wire) to register the new service and response mapper.

Scaffolding Boundaries

make:crud is designed for flat CRUD resources. It gives you a correct starting module for entities that behave like:

  • one primary table
  • standard list/show/create/update/delete
  • scalar fields and conventional foreign keys
  • standard DTO/model/controller/service wiring

It is not a full aggregate generator. If your resource needs any of the following, expect manual extension after scaffolding:

  • custom workflow actions such as publish, archive, approve
  • nested sub-resources such as /items/{id}/media
  • relation arrays in payloads such as tag_ids[] or embedded media[]
  • pivot tables with composite primary keys and no synthetic id
  • cross-field invariants such as "if is_for_sale=true, then price and currency are required"
  • response enrichment that joins child collections or computed data

Practical rule:

  • use make:crud to establish the module skeleton and conventions
  • then evolve the generated service/controller/routes into an aggregate by hand

The scaffolder is successful when it removes boilerplate and preserves architectural consistency. It is not trying to replace domain modeling for complex aggregates.

Compatibility Matrix

PHP 8.2 PHP 8.3 PHP 8.4
CI4 4.7.* ✅*

* PHP 8.4 is tested against the locked CI4 version in the test job. The explicit CI4-compatibility matrix covers PHP 8.2 and 8.3. CI4 4.5.x and 4.6.x were dropped: 4.5.x for security advisories, 4.6.x because v0.3.2 widened the floor to ^4.7.

CI runs on every push: PHPStan level 8, PHP CS Fixer, full unit suite, E2E smoke test (creates a real CI4 project and scaffolds into it). PHP 8.2 additionally collects coverage.

Development

# Run all quality checks
composer quality   # PHPStan level 8 + PHPUnit + CS-Fixer + security audit

# Run tests only
composer test -- --testsuite Unit
composer test -- --testsuite E2E

# Run E2E smoke test against a real vanilla CI4 project
CI4_CORE_PATH=../ci4-api-core bash bin/e2e-smoke.sh

# Auto-fix code style
composer cs-fix

For architecture constraints that generated code must satisfy, see docs/ARCHITECTURE_CONTRACT.md.

Troubleshooting

--fields is empty / scaffold produces a partial module Always single-quote the fields string. Unquoted pipes (|) are consumed by the shell before the command sees them:

# Wrong — shell eats the pipe
bash vendor/bin/make-crud.sh Article Blog title:string:required|searchable yes

# Correct
bash vendor/bin/make-crud.sh Article Blog 'title:string:required|searchable' yes

php spark make:crud hangs in a script / CI It entered interactive mode because --fields was empty and stdin is not a TTY. Use vendor/bin/make-crud.sh instead, which guards against this and requires --fields in non-TTY contexts.

Wiring failed / Services.php was not modified The ConfigWireman uses AST-based injection and expects Services.php to follow the CI4 convention (a class with static factory methods in a trait). Run with --no-wire to get the snippet to paste manually:

php spark make:crud Article --domain Blog --fields '...' --no-wire

FK validation aborts because the DB is unreachable Pass --skip-fk-validation when you know the target tables exist but the DB isn't available (e.g. in a fresh setup before migrate):

php spark make:crud Article --domain Blog --fields 'author_id:fk:users:required' --skip-fk-validation

Scaffolded routes don't appear in php spark routes:list New route files are not hot-reloaded. Restart the server after scaffolding:

pkill -f 'spark serve'; php spark serve --port 8080 &

module:check fails on a valid module Run php spark module:check <Resource> --domain <Domain> to see which of the 14 checkpoints failed and why.

Example Project

ci4-api-core-example is a complete, runnable Catalog API (Categories + Products) built entirely with this scaffolding engine — minimal hand-written code. Each step is a separate git commit so you can trace exactly what make-crud.sh generates, from a blank CI4 project to a production-ready API with filtering, searching, pagination, and OpenAPI docs.

License

MIT — see LICENSE.