zirvu / api
Zirvu API
Requires
- php: ^8.2
- illuminate/support: ^11.2.0
README
A Laravel package that provides a modular service-repository architecture with built-in CRUD traits, dynamic service loading, and a scaffolding CLI command β so you can spin up a full backend module in one command.
composer require zirvu/api
π Table of Contents
- Installation
- Scaffolding β
zirvu:add_module - Route Auto-Discovery
- Architecture Overview
- Traits Reference
- Service Methods
- Controller Methods
- Customization
- Dynamic Service Loading
- Configuration
- Suggested Project Structure
Installation
1. Require the package
composer require zirvu/api
2. Publish config
php artisan vendor:publish --provider="Zirvu\Api\ZirvuApiPackageServiceProvider" --tag=config
Publishes config/zirvu/api/classes.php β the registry of all your module class names.
3. Publish RepositoryServiceProvider
php artisan vendor:publish --provider="Zirvu\Api\ZirvuApiPackageServiceProvider" --tag=providers
Publishes app/Providers/RepositoryServiceProvider.php β a dynamic provider that automatically reads classes.php and binds every *Contract β *Repository and *Contract β *Service pair. No manual use/bind per class needed.
4. Register the provider
How you register depends on your Laravel version:
Laravel 11+ β bootstrap/providers.php
// bootstrap/providers.php return [ App\Providers\AppServiceProvider::class, App\Providers\RepositoryServiceProvider::class, // β add this ];
Auto-registered:
zirvu:add_moduledetects Laravel 11+ and adds this entry automatically on first run.
Laravel 10 and below β config/app.php
// config/app.php 'providers' => [ // ... App\Providers\RepositoryServiceProvider::class, // β add this ],
For Laravel 10 and below, the command will print a reminder with the line to copy β it cannot safely auto-edit
config/app.php.
5. (Optional) AppServiceProvider β auto-observer
If you use Auditing observers, add this to your AppServiceProvider::boot() to auto-register an observer for every model in classes.php:
public function boot(): void { $this->observerModels(); } public function observerModels(): void { $configs = config('zirvu.api.classes'); foreach ($configs as $key => $value) { foreach ($value as $class) { $model = ($key !== 'root') ? "App\\Models\\{$key}\\{$class}" : "App\\Models\\{$class}"; if (class_exists($model)) { $model::observe(\App\Observers\Observer::class); } } } }
Scaffolding
The package ships with a scaffolding command that generates a full backend module in one shot.
Basic Usage
php artisan zirvu:add_module --name=ProductDetail
With a Sub-folder Group
php artisan zirvu:add_module --name=ProductDetail --group=MasterData
Multi-word Name (comma separator)
Use this if the auto-detection of word boundaries doesn't produce the result you want:
php artisan zirvu:add_module --name=Product,Detail
All three examples above produce the same result. The command will show you a confirmation table before creating anything:
Module: ProductDetail
Group: <none>
prefix: productdetail
prefix_script: productDetail
table_name: product_details
migration: 2026_05_24_091902_create_product_details_table.php
Create module with the above settings? (yes/no) [yes]
Naming Convention
| Input | class_name |
prefix |
prefix_script |
table_name |
|---|---|---|---|---|
--name=ProductDetail |
ProductDetail |
productdetail |
productDetail |
product_details |
--name=Product |
Product |
product |
product |
products |
--name=Product,Detail |
ProductDetail |
productdetail |
productDetail |
product_details |
Files Generated
Running --name=ProductDetail (no group) creates 9 files:
| File | Description |
|---|---|
routes/productdetail/api.php |
JWT-protected route group |
app/Http/Controllers/Api/ProductDetailController.php |
API controller |
app/Models/ProductDetail.php |
Eloquent model |
app/Services/Contract/ProductDetailContract.php |
Service interface |
app/Services/ProductDetailService.php |
Business logic |
app/Repositories/Contract/ProductDetailContract.php |
Repository interface |
app/Repositories/ProductDetailRepository.php |
Data access layer |
database/migrations/{timestamp}_create_product_details_table.php |
Migration |
postman/ProductDetail.json |
Postman collection (v2.1) |
With --group=MasterData, all files go into a MasterData/ sub-folder and namespace. The route folder and prefix are also grouped:
- Route folder:
routes/MasterData/productdetail/api.php - Route prefix:
masterdata/productdetail - Postman file:
postman/MasterData/ProductDetail.json
Note: Migration files are always created flat in
database/migrations/regardless of group.
After Scaffolding
config/zirvu/api/classes.php is updated automatically β the class is appended to the correct key (root or your group name). No manual step needed.
Then just run the migration:
php artisan migrate
Route Auto-Discovery
The scaffolded route files are automatically picked up β no manual require needed β as long as your routes/api.php uses the following auto-discovery pattern:
// routes/api.php function getAllPath($path = "routes") { $paths = []; $directories = File::directories(base_path($path)); foreach ($directories as $dir) { $subPath = $path . '/' . basename($dir); $paths[] = $subPath; $paths = array_merge($paths, getAllPath($subPath)); } return $paths; } $paths = getAllPath(); foreach ($paths as $key => $path) { $apiFilePath = base_path() . "/" . $path . "/api.php"; if (File::exists($apiFilePath)) { require $apiFilePath; } }
This recursively walks every sub-folder under routes/ and includes any api.php it finds. So when zirvu:add_module creates:
routes/productdetail/api.php
routes/MasterData/productdetail/api.php
β¦both are loaded automatically at boot β no changes to routes/api.php required.
Each generated route file registers these endpoints:
| Method | Endpoint | Action |
|---|---|---|
GET |
/{prefix}/data |
Paginated list |
GET |
/{prefix}/one/{id} |
Single record |
GET |
/{prefix}/first |
First matching record |
POST |
/{prefix}/save |
Create or update |
POST |
/{prefix}/savemany |
Bulk create/update |
DELETE |
/{prefix}/delete |
Delete records |
Architecture Overview
Request β Controller (API trait)
β
Service (Service trait) β validation, business logic
β
Repository (Repository trait) β DB queries
β
Model (Relation trait) β Eloquent + safe-delete check
Each layer communicates through its Contract (interface), bound via Laravel's service container.
Traits Reference
API Trait
Used in API controllers. Bundles CommonController which provides all standard CRUD endpoint methods automatically.
use Zirvu\Api\Traits\API; class ProductDetailController { use API; protected $baseService; public function __construct(ProductDetailContract $baseService) { $this->baseService = $baseService; } }
The
APItrait gives youdata,one,first,save,update,delete, andsaveManyendpoints for free β no extra code needed in the controller.
Service Trait
Used in service classes. Bundles CommonService (CRUD logic) and LoaderService (dynamic service loading).
use Zirvu\Api\Traits\Service; class ProductDetailService implements ProductDetailContract { use Service; public $table_name = "product_details"; public $type = "api"; public $extra = []; public function __construct(ProductDetailRepositoryContract $baseRepository) { $this->baseRepository = $baseRepository; } public function buildValidation($fields) { $this->rule_edit["id"] = [ "required", Rule::exists($this->table_name, "id")->whereNull("deleted_at") ]; $this->rules = [ "rule_new" => $this->rule_new, "rule_edit" => $this->rule_edit, "rule_public" => $this->rule_public, ]; } }
Repository Trait
Used in repository classes. Bundles CommonRepository (CRUD operations) and Eloquent (query building, filtering, pagination).
use Zirvu\Api\Traits\Repository; use App\Models\ProductDetail; class ProductDetailRepository implements ProductDetailContract { use Repository; protected $model; public $table_name = "product_details"; public $class_name = "App\Models\ProductDetail"; public $table_detail; public $ignore_save = []; public $with = []; public function __construct(ProductDetail $model) { $this->model = $model; $this->table_detail = $this->getColumnsFromTable(); } }
Key properties:
| Property | Description |
|---|---|
$table_name |
Used for column introspection and pagination |
$class_name |
Fully-qualified model class name |
$table_detail |
Auto-populated map of DB columns β type/nullable/default |
$ignore_save |
Array of field names to skip when auto-mapping on save |
$with |
Default eager-loaded relations on find() and first() |
Relation Trait
Used in Eloquent models. Provides a checkDelete() method that inspects loaded relations before allowing deletion.
use Zirvu\Api\Traits\Relation; class ProductDetail extends Model implements Auditable { use HasFactory, SoftDeletes, \OwenIt\Auditing\Auditable, Relation; }
The repository's delete() calls $model->checkDelete() automatically β if any loaded relation has children, deletion is blocked and returns false.
Service Methods
All methods are available through the Service trait (CommonService):
| Method | Description |
|---|---|
get(object $options) |
Paginated list with filtering and ordering |
find($id) |
Find a single record by primary key |
first(object $options) |
First record matching filters |
save($id, array $fields) |
Create (id=null) or update (id=N) with validation |
update($id, array $fields) |
Partial update β merges with existing data then calls save() |
delete(array $ids) |
Soft-delete one or more records in a transaction |
saveMany(array $datas) |
Bulk create/update in a single transaction |
get() β Filter & Pagination Options
$data = $this->baseService->get((object)[ "page" => 1, "take" => 15, "filter" => "name:contains:product", "order" => "created_at", "order_method" => "DESC", "with" => ["category"], // eager load relations "select" => ["id", "name"], // column selection ]);
Supported filter operators: =, !=, >, <, >=, <=, in, notin, contains
Filter string format: {column}:{operator}:{value}
save() β Create or Update
// Create (pass null or 0 as id) $record = $this->baseService->save(null, [ "name" => "New Product", "price" => 9900, ]); // Update $record = $this->baseService->save(42, [ "id" => 42, "name" => "Updated Name", ]);
saveMany() β Bulk Save
$results = $this->baseService->saveMany([ ["name" => "Item A", "price" => 100], ["id" => 5, "name" => "Item B updated"], ]);
Controller Methods
All methods are provided automatically by the API trait (CommonController). Your controller needs zero additional methods for standard CRUD:
| HTTP | Route | Method | Description |
|---|---|---|---|
GET |
/data |
data() |
Paginated list |
GET |
/one/{id} |
one() |
Single record |
GET |
/first |
first() |
First matching record |
POST |
/save |
save() |
Create or update |
POST |
/update |
update() |
Partial update |
DELETE |
/delete |
delete() |
Delete records (pass data: [ids]) |
POST |
/save-many |
saveMany() |
Bulk create/update |
Standard JSON Response Format
{
"success": true,
"message": "Success!",
"data": { ... }
}
With pagination (from data() endpoint):
{
"success": true,
"message": "Success!",
"pagination": {
"take": 10,
"page": 1,
"totalPage": 5,
"total": 48
},
"data": [ ... ]
}
You can also call $this->response($statusCode, $withPaginate) manually if you override a method:
public function data(Request $request) { $this->data = $this->baseService->get((object)$request->all()); $this->pagination = $this->baseService->extra["pagination"] ?? []; return $this->response(200, true); }
Customization
All CRUD logic lives in the traits, but every method can be freely overridden by simply declaring the same method in your own class. PHP will always use your version instead of the trait's.
The source of truth for all default implementations is the package itself β browse or copy any method from:
Zirvu\Api\Traits\ControllerExtension\CommonControllerβ controller methodsZirvu\Api\Traits\ServiceExtension\CommonServiceβ service methodsZirvu\Api\Traits\RepositoryExtension\CommonRepositoryβ repository methodsZirvu\Api\Traits\RepositoryExtension\Eloquentβ query building methods
Example β Custom Service save()
Say you want to add business logic before saving (e.g. hash a password, fire an event, call an external API). Copy the default save() from the trait and put it in your Service class:
// app/Services/ProductDetailService.php use Zirvu\Api\Traits\Service; use Illuminate\Validation\Rule; use \Exception; use \DB; class ProductDetailService implements ProductDetailContract { use Service; // ... constructor, buildValidation, etc. /** * Override the trait's save() to add custom business logic. */ public function save($id, array $fields) { $this->loadUtilsService(); $this->buildValidation($fields); $validator = $this->utilsService->validation($this->rules, $fields); if ($validator->fails()) { $this->message = $validator->errors()->first(); $this->data = $validator->errors(); throw new Exception($this->message, 1); } DB::beginTransaction(); try { // βοΈ Your custom logic here: $fields['slug'] = str()->slug($fields['name'] ?? ''); $data = $this->baseRepository->save($id, $fields); // βοΈ Or after save: // event(new ProductDetailSaved($data)); DB::commit(); } catch (Exception $e) { throw new Exception($e->getMessage(), 1); } return $data; } }
Example β Custom Controller endpoint
Need extra logic on the data() endpoint? Override it in your controller:
// app/Http/Controllers/Api/ProductDetailController.php use Zirvu\Api\Traits\API; class ProductDetailController { use API; protected $baseService; public function __construct(ProductDetailContract $baseService) { $this->baseService = $baseService; } /** * Override data() to inject extra fields into the response. */ public function data(Request $request) { try { $this->data = $this->baseService->get((object)$request->all()); $this->pagination = $this->baseService->extra["pagination"] ?? []; // βοΈ Append anything extra: $this->data = $this->data->map(fn($item) => [ ...$item->toArray(), 'display_name' => strtoupper($item->name), ]); } catch (Exception $e) { $this->success = false; $this->message = $e->getMessage(); } return $this->response(200, true); } }
Example β Custom Repository query
Need to filter by a scope or join a table? Override getData() or any query method:
// app/Repositories/ProductDetailRepository.php use Zirvu\Api\Traits\Repository; class ProductDetailRepository implements ProductDetailContract { use Repository; // ... constructor, properties /** * Override get() to always filter by active status. */ public function get(object $object, string $type) { $this->model = $this->model->where('is_active', 1); return parent::get($object, $type); // or call $this->getData($type) directly after your custom scoping } }
Rule of thumb: If a method exists in the trait and you declare the same method in your class, your version wins. No extra configuration needed.
Dynamic Service Loading
The LoaderService trait (included via Service) lets any service or controller load another registered service on the fly using magic methods:
// Loads App\Services\Contract\UserContract via the container $this->loadUserService(); $users = $this->userService->get((object)[]); // Loads App\Services\Contract\MasterData\ProductContract $this->loadProductService();
The method name pattern is load{ClassName}Service() β it resolves the class from config/zirvu/api/classes.php and instantiates it via the service container.
The class must be registered in
config/zirvu/api/classes.phpfor this to work.
Configuration
config/zirvu/api/classes.php β registers all module class names for dynamic loading and container binding:
return [ "root" => [ "Example", "Role", "User", "ProductDetail", // β your new module ], "MasterData" => [ "Jabatan", "Product", // β your group module ], ];
"root"β classes live directly inApp\Services\,App\Repositories\, etc.- Any other key (e.g.
"MasterData") β classes live in the matching sub-folder namespace.
Suggested Project Structure
app/
βββ Http/
β βββ Controllers/
β βββ Api/
β βββ ProductDetailController.php β use API trait
β βββ MasterData/
β βββ ProductController.php
βββ Models/
β βββ ProductDetail.php β use Relation trait
β βββ MasterData/
β βββ Product.php
βββ Services/
β βββ Contract/
β β βββ ProductDetailContract.php
β β βββ MasterData/
β β βββ ProductContract.php
β βββ ProductDetailService.php β use Service trait
β βββ MasterData/
β βββ ProductService.php
βββ Repositories/
βββ Contract/
β βββ ProductDetailContract.php
β βββ MasterData/
β βββ ProductContract.php
βββ Repositories/
β βββ Contract/
β β βββ ProductDetailContract.php
β β βββ MasterData/
β β βββ ProductContract.php
β βββ ProductDetailRepository.php β use Repository trait
β βββ MasterData/
β βββ ProductRepository.php
routes/
βββ productdetail/
β βββ api.php β auto-discovered (save, savemany, etc.)
βββ MasterData/
βββ productdetail/
βββ api.php β auto-discovered (group)
postman/
βββ ProductDetail.json β Postman v2.1 collection
βββ MasterData/
βββ ProductDetail.json β group collection
database/
βββ migrations/
βββ 2026_05_24_000000_create_product_details_table.php
Made by Zirvu