zirvu/api

There is no license information available for the latest version (2.0.0) of this package.

Zirvu API

Maintainers

Package info

github.com/zirvu/api

pkg:composer/zirvu/api

Statistics

Installs: 46

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.0.0 2026-05-24 03:36 UTC

This package is auto-updated.

Last update: 2026-05-24 03:36:45 UTC


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

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_module detects 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 API trait gives you data, one, first, save, update, delete, and saveMany endpoints 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 methods
  • Zirvu\Api\Traits\ServiceExtension\CommonService β€” service methods
  • Zirvu\Api\Traits\RepositoryExtension\CommonRepository β€” repository methods
  • Zirvu\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.php for 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 in App\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