rahmatsyaparudin / yii3-api-boilerplate
Yii3 API project template
No longer found in upstream repository
Requires
- php: 8.2 - 8.5
- ext-filter: *
- firebase/php-jwt: ^7.0.2
- httpsoft/http-message: ^1.1.6
- psr/clock: ^1.0
- psr/container: ^2.0.2
- psr/http-factory: ^1.1
- psr/http-message: ^2.0
- psr/http-server-handler: ^1.0.2
- psr/http-server-middleware: ^1.0.2
- psr/log: ^3.0.2
- sentry/sentry: ^4.19
- symfony/console: ^7.4.3
- vlucas/phpdotenv: ^5.6.3
- yiisoft/access: 2.0
- yiisoft/aliases: ^3.1.1
- yiisoft/cache: ^3.2
- yiisoft/cache-file: ^3.2
- yiisoft/config: ^1.6.2
- yiisoft/data: ^1.0.1
- yiisoft/data-response: ^2.1.2
- yiisoft/db: ^2.0
- yiisoft/db-migration: ^2.0.1
- yiisoft/db-pgsql: ^2.0
- yiisoft/definitions: ^3.4.1
- yiisoft/di: ^1.4.1
- yiisoft/error-handler: ^4.3.2
- yiisoft/http: ^1.3
- yiisoft/hydrator: ^1.6.3
- yiisoft/injector: ^1.2.1
- yiisoft/input-http: ^1.0.1
- yiisoft/log: ^2.2.0
- yiisoft/log-target-file: ^3.1
- yiisoft/middleware-dispatcher: ^5.4
- yiisoft/request-body-parser: ^1.2.1
- yiisoft/request-provider: ^1.2
- yiisoft/router: ^4.0.2
- yiisoft/router-fastroute: ^4.0.3
- yiisoft/security: ^1.2
- yiisoft/translator: ^3.2.1
- yiisoft/translator-message-php: ^1.1.2
- yiisoft/validator: ^2.5
- yiisoft/yii-console: ^2.4.2
- yiisoft/yii-http: ^1.1.1
- yiisoft/yii-runner-console: ^2.2.1
- yiisoft/yii-runner-http: ^3.2.1
Requires (Dev)
- codeception/c3: ^2.9
- codeception/codeception: ^5.3.3
- codeception/lib-innerbrowser: ^4.0.8
- codeception/module-asserts: ^3.3.0
- codeception/module-cli: ^2.0.1
- codeception/module-db: ^3.2.2
- codeception/module-phpbrowser: ^3.0.2
- codeception/module-rest: ^3.4.3
- friendsofphp/php-cs-fixer: ^3.92
- phpunit/phpunit: ^11.5
- rector/rector: ^2.3.0
- roave/infection-static-analysis-plugin: ^1.43
- shipmonk/composer-dependency-analyser: ^1.8.4
- vimeo/psalm: ^6.14
- yiisoft/log-target-syslog: ^2.1
- yiisoft/profiler: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-24 08:23:50 UTC
README
Yii3 API Skeleton is a starter project for building RESTful APIs using Yii3 with Domain-Driven Design (DDD) architecture. It provides a ready-to-use structure, helper scripts, and example configurations to accelerate your API development with clean architecture principles.
ποΈ Architecture Overview
This skeleton follows Domain-Driven Design (DDD) principles with clean architecture layers:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β API Layer (Controllers & Middleware) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Application Layer (Services & Use Cases) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Domain Layer (Entities & Business Logic) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Infrastructure Layer (Repositories & External APIs) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Features
- π― Domain-Driven Design: Clean separation of business logic
- π§ Type Safety: Full Psalm static analysis integration
- π§ͺ Testing Ready: Complete test suite setup
- π Security: Authentication, authorization, and audit trail
- π Quality Assurance: Automated code quality checks
- π³ Docker Ready: Complete containerization setup
- π Documentation: Comprehensive documentation included
π Quick Start
Prerequisites
- PHP 8.3+ with required extensions
- Composer for dependency management
- PostgreSQL database
- MongoDB (optional, for audit trails)
- Docker (optional, for containerized development)
1. Create New Project
composer create-project --prefer-dist yiisoft/app-api ./
2. Add the repository and package to composer.json
Open your project's composer.json and add the following sections:
Add this to composer.json repositories
"repositories": [ { "type": "composer", "url": "https://asset-packagist.org" }, { "type": "vcs", "url": "https://github.com/rahmatsyaparudin/yii3-api-boilerplate.git" } ],
Add this to composer.json require-dev
"rahmatsyaparudin/yii3-api-boilerplate": "dev-main"
Add this to composer.json scripts
"skeleton:scripts": [ "@php scripts/skeleton-scripts.php" ], "skeleton:version": [ "@php scripts/skeleton-version.php" ], "skeleton:update": [ "composer update rahmatsyaparudin/yii3-api-boilerplate --ignore-platform-reqs", "@php scripts/skeleton-scripts.php", "@php scripts/skeleton-update.php" ], "skeleton:copy-config": [ "@php scripts/skeleton-copy-config.php" ], "skeleton:copy-examples": [ "@php scripts/skeleton-copy-examples.php" ], "skeleton:generate-module": [ "@php scripts/skeleton-generate-module.php" ]
3. Update Composer
Update composer dependencies
composer install --ignore-platform-reqs
4. Copy skeleton scripts
Make directory scripts and Copy the scripts folder from the package to your project root:
mkdir scripts; cp -r -Force vendor/rahmatsyaparudin/yii3-api-boilerplate/scripts/* ./scripts
5. Install Skeleton
Install skeleton structure
composer skeleton:update
Copy config files (first time only)
This copies .env.example β .env, .gitignore, message files (resources/messages/{en,id}/), and other skeleton configuration files.
composer skeleton:copy-config
Copy example files (first time only)
composer skeleton:copy-examples
Check for Skeleton Updates
Compare your installed skeleton version (scripts/skeleton.version) with the version shipped by the package:
composer skeleton:version
It reports whether the project is up to date or an update is available (e.g. 1.2.6 β 1.2.7), in which case run composer skeleton:update.
6. Generate New Module
Use the built-in module generator to create new API modules with complete structure: Generate a new module (e.g., Product)
composer skeleton:generate-module -- --module=Product --table=product_management
Or use direct PHP script (alternative):
php scripts/skeleton-generate-module.php --module=Product --table=product_management
By default the module's migrations run on the default connection. Pass --db=<name> to map the module to another connection (a db.<name>.* env block):
composer skeleton:generate-module -- --module=AuditLog --db=audit
Note: The skeleton comes with an Example module that demonstrates the complete structure. Use the generator above to create additional modules for your specific needs.
What the Generator Creates
The module generator creates a complete module structure following DDD architecture. Here's what you get when generating a new module (based on the existing Example module):
π API Layer (src/Api/V1/{Module}/)
src/Api/V1/Product/
βββ Action/
β βββ ProductCreateAction.php # POST /product/create
β βββ ProductDataAction.php # GET/POST /product & /product/data
β βββ ProductDeleteAction.php # DELETE /product/{id}
β βββ ProductRestoreAction.php # POST /product/{id}/restore
β βββ ProductUpdateAction.php # PUT /product/{id}
β βββ ProductViewAction.php # GET /product/{id}
βββ Validation/
βββ ProductInputValidator.php # Request validation rules
π Application Layer (src/Application/{Module}/)
src/Application/Product/
βββ Command/
β βββ CreateProductCommand.php # Create command DTO
β βββ UpdateProductCommand.php # Update command DTO
βββ Dto/
β βββ ProductResponse.php # Response DTO
βββ ProductApplicationService.php # Application service
π Domain Layer (src/Domain/{Module}/)
src/Domain/Product/
βββ Entity/
β βββ Product.php # Domain entity
βββ Repository/
β βββ ProductRepositoryInterface.php # Repository interface
βββ Service/
βββ ProductDomainService.php # Domain service
π Infrastructure Layer (src/Infrastructure/Common/Persistence/{Module}/)
src/Infrastructure/Common/Persistence/Product/
βββ ProductRepository.php # Repository implementation
βββ MdbProductSchema.php # MongoDB schema
π Database & Seeding
src/Migration/
βββ Product/
βββ M20240130123457CreateProductTable.php # Database migration (namespace App\Migration\Product)
src/Seeder/
βββ SeedProductData.php # Seeder class
βββ Fixtures/
βββ product.yaml # Alice fixtures for test data
βοΈ Configuration Updates
The generator automatically updates configuration files:
config/common/repository.php- Adds repository DI bindingconfig/common/access.php- Adds access control rulesconfig/common/routes.php- Adds API routes with proper permissionsconfig/common/migration.php- Registers the module's migration connection (moduleConnections)
π§ Features Included
- β Complete CRUD Operations - Create, Read, Update, Delete, Restore
- β RESTful API Endpoints - Following REST conventions
- β Request Validation - Input validation rules
- β Permission System - Role-based access control
- β Database Migration - Schema management
- β Data Seeding - Test data generation with Alice fixtures
- β Type Safety - Full Psalm compatibility
- β Error Handling - Standardized error responses
Generated API Endpoints
For each module, the following endpoints are automatically created:
| Method | Endpoint | Action | Permission |
|---|---|---|---|
| GET | /v1/{module} |
List items | {module}.index |
| POST | /v1/{module}/data |
Create item | {module}.data |
| GET | /v1/{module}/{id} |
View item | {module}.view |
| POST | /v1/{module}/create |
Create item | {module}.create |
| PUT | /v1/{module}/{id} |
Update item | {module}.update |
| DELETE | /v1/{module}/{id} |
Delete item | {module}.delete |
| POST | /v1/{module}/{id}/restore |
Restore item | {module}.restore |
π Current Available Modules
The skeleton includes the following modules out of the box:
β Example Module (Included)
- Purpose: Demonstrates complete module structure
- Endpoints:
/v1/example/* - Usage: Reference implementation for learning and testing
- Files: Complete DDD structure with all layers
π§ Custom Modules (Generate as needed)
- Product, Category, Brand, Order, User, etc.
- Purpose: Your business-specific modules
- Generation: Use
composer skeleton:generate-module -- --module=ModuleName --table=table_nameorphp scripts/skeleton-generate-module.php --module=ModuleName --table=table_name - Custom Table: Use
--table=table_namefor table names (e.g.,--module=Product --table=product_management) - Custom Connection: Use
--db=connection_nameto migrate on a non-default database (e.g.,--module=AuditLog --db=audit) - Customization: Modify generated files according to your business logic
π Project Structure
After installation, your project will have this structure:
yii3-api/
βββ config/ # Application configuration
β βββ common/ # Shared configuration
β βββ console/ # Console configuration
β βββ environments/ # Environment configs
β βββ web/ # Web configuration
βββ docs/ # Documentation
β βββ architecture-guide.md # Architecture documentation
β βββ quality-guide.md # Quality assurance guide
β βββ setup-guide.md # This setup guide
βββ public/ # Web root
β βββ index.php # Application entry point
βββ resources/ # Application resources
β βββ messages/ # Translation files
βββ scripts/ # Utility scripts
β βββ skeleton-generate-module.php # Module generator
β βββ skeleton-scripts.php # Skeleton script installer
β βββ skeleton-update.php # Skeleton installer
β βββ skeleton-copy-examples.php # Example files copier
β βββ skeleton-copy-config.php # Config files copier
β βββ skeleton.version # Installed skeleton version marker
βββ src/ # Source code
β βββ Api/ # API layer
β β βββ V1/ # API version 1
β β β βββ Example/ # Example API endpoints
β β β βββ Shared/ # Shared API components
β β βββ Shared/ # Shared API components
β βββ Application/ # Application layer
β β βββ Example/ # Application services
β β βββ Shared/ # Shared application services
β βββ Domain/ # Domain layer
β β βββ Example/ # Domain entities
β β βββ Shared/ # Shared domain components
β βββ Infrastructure/ # Infrastructure layer
β β βββ Core/ # Core infrastructure
β β β βββ Audit/ # Audit services
β β β βββ Database/ # Database implementations
β β β βββ Security/ # Security services
β β βββ Common/ # Common infrastructure
β β βββ Persistence/ # Repository implementations
β β βββ Example/ # Example repository
β βββ Migration/ # Database migrations (module subfolders are isolated)
β β βββ Auditable/ # audit_logs + rate_limits (opt-in, via migrate:module)
β β βββ Example/ # Example module migrations (via migrate:module)
β βββ Seeder/ # Data seeders
β β βββ Fixtures/ # Alice fixtures
β β β βββ example.yaml
β β βββ Faker/ # Faker providers
β β βββ SeedExampleData.php
β βββ Shared/ # Shared utilities
β βββ ApplicationParams.php
β βββ Common/ # Common shared helpers
β βββ Core/ # Core shared components
β βββ Context/ # Validation context
β βββ Dto/ # Data Transfer Objects
β βββ Enums/ # Shared enumerations
β βββ ErrorHandler/ # Error handling utilities
β βββ Exception/ # Custom exceptions
β βββ Middleware/ # HTTP middleware
β βββ Query/ # Query utilities
β βββ Request/ # Request handling
β βββ Security/ # Security utilities
β βββ Utility/ # General utilities
β βββ Validation/ # Validation classes
β βββ ValueObject/ # Value objects
βββ tests/ # Test suite
β βββ Api/ # API tests
β βββ Functional/ # Functional tests
β βββ Support/ # Test support classes
β βββ Unit/ # Unit tests
βββ vendor/ # Dependencies
π§ Configuration
Environment Setup
1. Copy Environment Files
# Copy environment configuration
cp .env.example .env
2. Configure Environment
Edit .env file:
# Application Environment APP_ENV=dev APP_DEBUG=1 app.config.code=appAPI app.config.name=appAPI app.config.language=en app.time.timezone=Asia/Jakarta app.pagination.defaultPageSize=10 app.pagination.maxPageSize=100 app.rateLimit.maxRequests=100 app.rateLimit.windowSize=60 app.hsts.maxAge=31536000 app.hsts.includeSubDomains=true app.hsts.preload=false app.cors.allowedOrigins=["http://example.com:3000"] app.cors.maxAge=86400 app.cors.allowCredentials=true app.cors.allowedMethods=["GET","POST","PUT","PATCH","DELETE","OPTIONS"] app.cors.allowedHeaders=["Content-Type","Authorization","X-Requested-With","Accept","Origin"] app.cors.exposedHeaders=["X-Pagination-Total-Count","X-Pagination-Page-Count"] app.trusted_hosts.allowedHosts=["127.0.0.1","::1","localhost"] # Optimistic Lock Configuration app.optimistic_lock.enabled=true app.optimistic_lock.disabled.values=["example","example_1"] # SSO Configuration (External Keycloak) app.jwt.secret=secret-key-harus-panjang-256-bit app.jwt.algorithm=HS256 app.jwt.issuer=https://sso.example.com app.jwt.audience=https://sso.example.com db.default.driver=pgsql db.default.host=localhost db.default.port=5432 db.default.name=dev_yii3 db.default.user=postgres db.default.password=postgres db.mongodb.dsn=localhost:27017 db.mongodb.name=db_example db.mongodb.enabled=true redis.default.host=127.0.0.1 redis.default.port=6379 redis.default.db=0 redis.default.password=null
Note:
app.config.languagesets the application language (used for translations). WhenAPP_ENV=dev(ordevelopment),ApplicationParams::$environmentis set todevelopmentand the root index endpoint (GET /) includes it in the response:{ "name": "appAPI", "version": "1.0", "language": "en", "environment": "development" }In production (
APP_ENVother thandev/development) theenvironmentfield is omitted.
3. Database Migration
# Run database migrations ./yii migrate:up # Seed initial data (development only) ./yii seed --module=example # Or seed with custom options (development only) ./yii seed --module=example --count=10 # Note: Seed commands only work in development environment (APP_ENV=dev)
Isolated Module Migrations
Migrations live in per-module subfolders of src/Migration/ (namespace App\Migration\<Module>) and are applied with migrate:module. Every module must be mapped to a connection in config/common/migration.php β moduleConnections; without a mapping the command fails. Migration history ({{%migration}} table) is tracked in the module's own database.
# Apply migrations from src/Migration/Example on its mapped connection (default) ./yii migrate:module example # Apply migrations from src/Migration/Auditable (audit_logs + rate_limits tables) ./yii migrate:module auditable # Override the mapped connection for this run (uses db.<name>.* env keys) ./yii migrate:module auditable --db=audit # Options ./yii migrate:module example -y # skip confirmation ./yii migrate:module example -l 1 # limit number of migrations
The module β connection map lives in config/common/migration.php:
'moduleConnections' => [ 'Example' => 'default', 'Auditable' => 'audit', // uses db.audit.* env keys ],
The skeleton ships with two isolated groups:
| Folder | Namespace | Tables | Required? |
|---|---|---|---|
src/Migration/Example/ |
App\Migration\Example |
example, another_example |
Only for the demo module |
src/Migration/Auditable/ |
App\Migration\Auditable |
audit_logs, rate_limits |
Opt-in, see Audit Trail |
New module migrations generated by skeleton-generate-module.php are placed in src/Migration/<Module>/ and registered in moduleConnections automatically (connection default, or the value of --db).
4. Optimistic Lock Configuration
The skeleton includes configurable optimistic locking to prevent concurrent update conflicts:
# Enable/disable optimistic locking (global) app.optimistic_lock.enabled=true # Default: true # Disable optimistic locking for specific validators (JSON array) app.optimistic_lock.disabled.values=["example","example_1"]
π§ Optimistic Lock Features:
- β
Automatic Version Management - Each entity has a
lock_versionfield - β Concurrent Update Prevention - Throws exception on version mismatch
- β Configurable - Can be enabled/disabled globally or per validator
- β Performance Optimized - Skips verification when disabled
- β Per-Validator Control - Fine-grained control per validator type
- β Smart Normalization - Automatic validator name normalization
π Configuration Options:
| Setting | Type | Default | Description |
|---|---|---|---|
app.optimistic_lock.enabled |
boolean | true |
Enable/disable optimistic locking globally |
app.optimistic_lock.disabled.values |
JSON array | [] |
List of disabled validators (normalized names) |
π Usage Examples:
# Disable optimistic locking globally app.optimistic_lock.enabled=false # Disable for specific validators app.optimistic_lock.disabled.values=["example","user","product"] # Enable all validators (empty disabled list) app.optimistic_lock.disabled.values=[] # Enable in production for data integrity app.optimistic_lock.enabled=true app.optimistic_lock.disabled.values=[]
π§ Validator Name Normalization:
The system automatically normalizes validator names for configuration:
// Validator Class β Normalized Name β Environment Key ExampleInputValidator β "example" β app.optimistic_lock.disabled.values=["example"] UserInputValidator β "user" β app.optimistic_lock.disabled.values=["user"] ProductInputValidator β "product" β app.optimistic_lock.disabled.values=["product"]
π§ Implementation in Validators:
Optimistic lock validation is automatically integrated into validators:
// In your InputValidator class final class ExampleInputValidator extends AbstractValidator { protected function rules(string $context): array { return match ($context) { ValidationContext::UPDATE => [ 'id' => [new Required(), new Integer(min: 1)], 'name' => [new StringValue(skipOnEmpty: true)], // Unique validation with optimistic lock awareness 'name' => [ new Required(), new StringValue(), new UniqueValue( targetClass: ExampleRepository::class, targetAttribute: 'name', filter: fn() => $this->getFilterForUnique(), // Automatically respects optimistic lock configuration skipOnEmpty: fn() => !$this->isOptimisticLockEnabled() ), ], // lock_version automatically added/removed based on configuration 'lock_version' => [ new Required( when: fn() => $this->isOptimisticLockEnabled() ), new Integer( min: 1, skipOnEmpty: fn() => !$this->isOptimisticLockEnabled() ), ], ], // ... other contexts }; } }
π§ Advanced Validation Features:
The system includes advanced validation rules that integrate with optimistic locking:
// UniqueValue Rule - Prevents duplicate names with optimistic lock support new UniqueValue( targetClass: ExampleRepository::class, targetAttribute: 'name', filter: fn() => $this->getFilterForUnique(), message: 'Name must be unique', skipOnEmpty: true ) // HasNoDependencies Rule - Validates entity has no dependencies before deletion new HasNoDependencies( dependencyChecker: $this->dependencyChecker, errorMessage: 'Cannot delete entity with existing dependencies', skipOnEmpty: false )
π§ Implementation in Entities:
Entities use the OptimisticLock trait for automatic version management:
// In your Entity class use App\Domain\Shared\Core\Concerns\Entity\OptimisticLock; final class Example extends Entity { use OptimisticLock; // Automatic lock_version management // - verifyLockVersion() for validation // - upgradeLockVersion() for increment // - getLockVersion() for current version }
π Configuration Examples:
# Development: Disable for testing entities app.optimistic_lock.enabled=true app.optimistic_lock.disabled.values=["example","test"] # Production: Enable for all entities app.optimistic_lock.enabled=true app.optimistic_lock.disabled.values=[] # Maintenance: Disable all optimistic locking app.optimistic_lock.enabled=false
π§ API Usage:
When optimistic locking is enabled, include lock_version in UPDATE/DELETE requests:
# Update with optimistic lock curl -X PUT http://localhost:8080/v1/example/1 \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Name", "lock_version": 5 }' # Delete with optimistic lock curl -X DELETE http://localhost:8080/v1/example/1 \ -H "Content-Type: application/json" \ -d '{"lock_version": 5}'
When disabled for a validator, lock_version is optional:
# Update without lock_version (when disabled) curl -X PUT http://localhost:8080/v1/example/1 \ -H "Content-Type: application/json" \ -d '{"name": "Updated Name"}'
5. Translation Message Files
Message files in resources/messages/{en,id}/ are split into skeleton-managed and project-owned files:
| File | Owner | Notes |
|---|---|---|
app.php |
Project | Add your custom messages here. Never overwritten by composer skeleton:update. |
error.php |
Skeleton | Do not edit or add keys β overwritten by composer skeleton:update. |
success.php |
Skeleton | Do not edit or add keys β overwritten by composer skeleton:update. |
validation.php |
Skeleton | Do not edit or add keys β overwritten by composer skeleton:update. |
Put project-specific error, success, or validation messages in app.php for each locale:
// resources/messages/en/app.php return [ 'success' => 'Success', 'validation.custom_rule' => 'The {field} is invalid.', ];
Messages are referenced in code via Message::create():
use App\Shared\Core\ValueObject\Message; throw new BadRequestException( translate: Message::create( domain: 'validation', // message file: validation.php key: 'resource.not_deleted', params: ['resource' => 'example', 'id' => $id] ) );
π― Development Workflow
Quality Assurance
The skeleton includes comprehensive quality assurance tools:
# Run complete quality check suite php quality quality:check # Auto-fix code style issues php quality quality:check --fix # Generate test coverage reports php quality quality:check --coverage # Generate detailed analysis reports php quality quality:check --report
Testing
# Run all tests vendor/bin/phpunit # Run specific test suite vendor/bin/phpunit tests/Unit/ vendor/bin/phpunit tests/Api/ vendor/bin/phpunit tests/Functional/ # Run tests with coverage vendor/bin/phpunit --coverage-html tests/coverage/html
Static Analysis
# Run Psalm static analysis vendor/bin/psalm # Clear cache and re-run vendor/bin/psalm --clear-cache # Check specific file vendor/bin/psalm src/Domain/Example/Entity/Example.php
ποΈ Architecture Components
Domain Layer
The domain layer contains business logic and entities:
// src/Domain/Example/Entity/Example.php final class Example { use Identifiable, Stateful, OptimisticLock; public static function create(string $name, Status $status, DetailInfo $detailInfo): self { self::guardInitialStatus($status, null, self::RESOURCE); return new self(null, $name, $status, $detailInfo, null, LockVersion::create()); } }
Application Layer
Application services coordinate use cases:
// src/Application/Example/ExampleApplicationService.php final class ExampleApplicationService { public function create(CreateExampleCommand $command): ExampleResponse { // Business logic validation $this->domainService->ensureUnique(...); // Entity creation $example = Example::create(...); // Persistence return ExampleResponse::fromEntity($this->repository->insert($example)); } }
Infrastructure Layer
Repository implementations handle data persistence:
// src/Infrastructure/Common/Persistence/Example/ExampleRepository.php final class ExampleRepository implements ExampleRepositoryInterface { public function insert(Example $example): Example { return $this->db->transaction(function() use ($example) { // Database operations with MongoDB sync }); } }
API Layer
Controllers handle HTTP requests:
// src/Api/V1/Action/Example/ExampleCreateAction.php final class ExampleCreateAction { public function run(ServerRequestInterface $request): ResponseInterface { // Request validation $command = new CreateExampleCommand(...); // Business logic $response = $this->applicationService->create($command); // Response formatting return $this->responseFactory->success($response->toArray()); } }
π Security Features
Authentication & Authorization
// JWT Authentication $app->addMiddleware(new AuthenticationMiddleware($jwtAuthenticator)); // RBAC Authorization $app->addMiddleware(new AuthorizationMiddleware($rbacAuthorizer));
Audit Trail
Audit logging is opt-in β the audit_logs table is only created when you run the isolated migration group:
./yii migrate:module auditable # creates audit_logs + rate_limits
AuditServiceInterface is already bound to DatabaseAuditService in config/common/di/audit.php. Inject it where you need logging:
use App\Domain\Shared\Core\Audit\AuditServiceInterface; final class ExampleApplicationService { public function __construct( private AuditServiceInterface $audit, ) {} public function update(int $id, array $oldValues, array $newValues): void { // ... update logic ... $this->audit->log('example', $id, 'UPDATE', $oldValues, $newValues); } }
// Read audit history $this->audit->getHistory('example', $recordId); // history per record $this->audit->getUserActivity($userId, $from, $to); // activity per user
Note: The
App\Infrastructure\Core\Concerns\Auditabletrait is designed for Active Record-style classes (beforeSave/afterSave/getIsNewRecord). This project uses the Query Builder in repositories, so useAuditServiceInterfacedirectly instead.
Database Rate Limiter (opt-in)
The active RateLimitMiddleware uses in-memory storage and does not need a table. A persistent DB-backed limiter (App\Infrastructure\Core\RateLimit\DatabaseRateLimiter) is available but not wired anywhere β it uses the rate_limits table created by migrate:module auditable:
use App\Infrastructure\Core\RateLimit\DatabaseRateLimiter; public function __construct(private DatabaseRateLimiter $limiter) {} $key = "login:{$clientIp}"; if (!$this->limiter->isAllowed($key, limit: 5, window: 60)) { throw new TooManyRequestsException(/* ... */); } $this->limiter->hit($key); $remaining = $this->limiter->getRemaining($key, 5, 60); $resetAt = $this->limiter->getResetTime($key, 60);
To enforce it through middleware, modify RateLimitMiddleware to use DatabaseRateLimiter, or call the limiter manually in specific actions (e.g., login).
Current Actor
CurrentUser::getActor() always returns an ActorInterface β it is never null. Unauthenticated/system contexts get the default actor (id: 0, username: 'system'). Audit logging and DetailInfoFactory rely on this, so you can call $actor->getUsername() directly without null-safe operators.
π Data Synchronization
The skeleton ships with value objects and a factory for tracking record synchronization β both to MongoDB and between master/origin instances.
MongoDB Sync Flag β SyncMdb
App\Domain\Shared\Core\ValueObject\SyncMdb wraps the sync_mdb column (null = synced, 1 = pending):
use App\Domain\Shared\Core\ValueObject\SyncMdb; $sync = SyncMdb::pending(); // mark record as needing MongoDB sync $sync = SyncMdb::synced(); // mark record as synced $sync = SyncMdb::fromInt($row['sync_mdb']); $sync = SyncMdb::fromString($input); // accepts string input, throws on non-numeric $sync->isPending(); // true when sync_mdb = 1 $sync->isSynced(); // true when sync_mdb = null $sync->toInt(); // null | 1 β for DB writes
MasterβOrigin Sync β SyncFlag
App\Domain\Shared\Core\ValueObject\SyncFlag manages the origin_id / sync_flag columns plus a sync direction. origin_id is an integer (default null); sync_flag is a smallint (null = synced, 1 = not synced, default 1). Status and direction are typed enums in App\Domain\Shared\Core\Enum:
| Enum | Case | DB Value | Meaning |
|---|---|---|---|
SyncStatus |
SYNCED |
null |
Record already synced |
SyncStatus |
NOT_SYNCED |
1 |
Record needs syncing |
SyncDirection |
NONE |
0 |
No direction |
SyncDirection |
MASTER_TO_ORIGIN |
1 |
Push from master to origin |
SyncDirection |
ORIGIN_TO_MASTER |
2 |
Push from origin to master |
SyncDirection |
BIDIRECTIONAL |
3 |
Sync both ways |
use App\Domain\Shared\Core\Enum\SyncStatus; use App\Domain\Shared\Core\ValueObject\SyncFlag; // Direction is auto-resolved when not given: // sync_flag=null -> SyncDirection::NONE // sync_flag=1, no origin -> SyncDirection::MASTER_TO_ORIGIN // sync_flag=1, origin set -> SyncDirection::ORIGIN_TO_MASTER $sync = SyncFlag::create(originId: null, status: SyncStatus::NOT_SYNCED); $sync = SyncFlag::masterToOrigin(); // to all origins $sync = SyncFlag::masterToOrigin(5); // to origin #5 $sync = SyncFlag::originToMaster(5); // origin #5 -> master $sync = SyncFlag::bidirectional(5); // both ways $sync = SyncFlag::synced(); $sync = SyncFlag::fromArray($row); // from request/DB array $sync = SyncFlag::fromEntity($entity); // reads getOriginId()/getSyncFlag()/getSyncDirection() $sync->needsSyncToOrigin(); // pending && master->origin direction $sync->needsSyncToMaster(); // pending && origin->master direction $sync->markForSync(); // returns new instance with sync_flag=1 $sync->markSynced(); // returns new instance with sync_flag=null $sync->toArray(); // origin_id + sync_flag + direction $sync->toDbArray(); // origin_id + sync_flag only
Invalid sync_flag values (not null/1) or directions (not 0β3) throw BadRequestException with translated messages (sync_flag.invalid_value, sync_flag.invalid_direction).
SyncFlagFactory
App\Application\Shared\Core\Factory\SyncFlagFactory is the application-layer helper that wraps SyncFlag and adds actor/timestamp-aware payload building:
use App\Application\Shared\Core\Factory\SyncFlagFactory; final class ExampleApplicationService { public function __construct( private SyncFlagFactory $syncFlagFactory, ) {} public function create(CreateExampleCommand $command): void { // Build from request data / entity / record $sync = $this->syncFlagFactory->fromRequest($command->data); if ($this->syncFlagFactory->shouldPushToOrigin($sync)) { // Queue payload includes table, record_id, origin_id, direction, // operation, payload, created_at and created_by (current actor) $payload = $this->syncFlagFactory->buildMasterToOriginPayload( originId: $sync->getOriginId(), table: 'example', recordId: $id, operation: 'INSERT', data: $command->data, ); } // Merge sync state into detail_info $detailInfo = $this->syncFlagFactory->mergeIntoDetailInfo($sync, $detailInfo); // Audit-style sync log entry (timestamp + current actor) $log = $this->syncFlagFactory->buildSyncLog($sync, 'example', $id, 'INSERT'); } }
π API Usage Examples
Create Resource
curl -X POST http://localhost:8080/api/v1/examples \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -d '{ "name": "Example Resource", "status": "active", "detail_info": { "description": "Example description" } }'
List Resources
curl -X GET "http://localhost:8080/api/v1/examples?page=1&pageSize=10&sort=name&dir=asc" \ -H "Authorization: Bearer YOUR_JWT_TOKEN"
Update Resource
curl -X PUT http://localhost:8080/api/v1/examples/1 \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -d '{ "name": "Updated Resource", "lock_version": 1 }'
Delete Resource
curl -X DELETE http://localhost:8080/api/v1/examples/1 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
π³ Docker Development
Development Environment
# Start development containers docker-compose -f docker/dev/compose.yml up -d # Run commands in container docker-compose -f docker/dev/compose.yml exec app php yii migrate docker-compose -f docker/dev/compose.yml exec app php quality
Production Environment
# Build and run production containers docker-compose -f docker/prod/compose.yml up -d --build # View logs docker-compose -f docker/prod/compose.yml logs -f
π Documentation
Available Documentation
- Architecture Guide: Complete architecture overview
- Quality Guide: Quality assurance procedures
- API Documentation: API endpoint documentation
- Sync Flag Guide: Masterβorigin record synchronization (
origin_id/sync_flag) - Development Guide: Development setup and guidelines
Generating Documentation
# Run quality checks with coverage php quality quality:check --coverage # Run quality checks with detailed reports php quality quality:check --report # Run quality checks with both coverage and reports php quality quality:check --coverage --report # Fix code style issues automatically php quality quality:check --fix
π§ͺ Testing Strategy
Test Types
- Unit Tests: Test individual classes and methods
- Functional Tests: Test application workflows
- API Tests: Test API endpoints
- Integration Tests: Test database and external service integration
Running Tests
# Run all tests using quality script php quality test:run # Run only unit tests php quality test:run --unit # Run only integration tests php quality test:run --integration # Run tests with coverage php quality test:run --coverage # Run specific test with filter php quality test:run --filter=ExampleTest # Alternative: Direct PHPUnit commands vendor/bin/phpunit vendor/bin/phpunit --coverage-html tests/coverage/html vendor/bin/phpunit tests/Unit/Domain/Example/ExampleTest.php
π§ Maintenance
Regular Tasks
Weekly
- Update dependencies:
composer update - Check skeleton updates:
composer skeleton:version - Run quality checks:
php quality - Review test coverage trends
- Check security advisories
Monthly
- Review and update quality configuration
- Update coding standards
- Add new quality checks as needed
- Performance optimization review
Quarterly
- Major dependency updates
- Quality gate threshold reviews
- Tool version upgrades
- Architecture review meetings
Troubleshooting
Common Issues
# Clear all caches vendor/bin/psalm --clear-cache # Reinstall dependencies composer install --no-dev --optimize-autoloader
π Support & Resources
Documentation
- Yii3 Documentation: Official Yii3 guide
- Yii3 Validator Guide: Official Yii3 Validator guide
- Psalm Documentation: Static analysis tool
- PHPUnit Documentation: Testing framework
Community
- Yii3 API GitHub: Official repository
- Yii3 Discord: Community chat
Quality Tools
- PHP CS Fixer: Code style fixer
- Composer Audit: Security audit
- Codeception: Testing framework
π― Best Practices
Code Quality
- Type Safety: Always use strict types and type annotations
- Error Handling: Implement proper exception handling
- Testing: Maintain high test coverage (>80%)
- Documentation: Keep documentation up-to-date
Security
- Input Validation: Validate all user inputs
- Authentication: Use JWT tokens for API authentication
- Authorization: Implement RBAC for access control
- Audit Trail: Log all important operations
Performance
- Database Optimization: Use proper indexes and query optimization
- Caching: Implement multi-level caching strategy
- Async Processing: Use queues for long-running operations
- Monitoring: Monitor application performance
π Conclusion
The Yii3 API Skeleton provides a solid foundation for building modern, scalable, and maintainable RESTful APIs with Domain-Driven Design principles. The included quality assurance tools, comprehensive documentation, and clean architecture patterns ensure that your API development follows best practices from day one.
Key benefits:
- ποΈ Clean Architecture: DDD principles for maintainable code
- π Type Safety: Full static analysis with Psalm
- π§ͺ Testing Ready: Complete test suite setup
- π Quality Assurance: Automated quality checks
- π³ Docker Ready: Containerization support
- π Comprehensive Docs: Complete documentation included
Start building your next API project with confidence using the Yii3 API Skeleton! π