---
url: https://record-api-docs.vercel.app/guide/installation.md
---

# Installation

## Installation & Setup

### Step 1: Install the Package

#### Option A: Via Composer (Recommended for Production)

```bash
composer require mylekha/record-api
```

#### Option B: Local Development (VCS Repository)

Add this to your project's `composer.json`:

```json
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/MYLEKHATECH/laravel-record-api.git"
        }
    ],
    "require": {
        "mylekha/record-api": "*"
    }
}
```

Then run:

```bash
composer update mylekha/record-api -W
```

### Step 2: Publish/Generate Configuration Files

```bash
php artisan record-api:setup
```

This command publishes package configs (tag: `record-api-config`) and creates missing app config files:

* `config/record.php` - Database table configurations and relationships
* `config/audit.php` - Audit logging settings
* `config/record-api.php` - Package settings (auth guard, OpenAPI output)

To overwrite existing generated configs, run:

```bash
php artisan record-api:setup --force
```

### Step 3: Environment Configuration

Add these environment variables to your `.env` file:

```env
# Audit logging
AUDIT_LOG_ENABLED=true
AUDIT_LOG_RETENTION_DAYS=365

# Which Laravel auth guard the package uses
RECORD_API_AUTH_GUARD=api
```

### Step 4: Publish Migrations and Run Migrations

```bash
php artisan vendor:publish --tag=record-api-migrations
php artisan migrate
```

### Step 5: Configure Rate Limiters

The package routes use `throttle:api-reads`, `throttle:api-writes`, and `throttle:api-functions`. Define them in your app (example in `app/Providers/AppServiceProvider.php`):

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api-reads', function (Request $request): Limit {
        $key = $request->user()?->getAuthIdentifier() ?? $request->ip();
        return Limit::perMinute(200)->by((string) $key);
    });

    RateLimiter::for('api-writes', function (Request $request): Limit {
        $key = $request->user()?->getAuthIdentifier() ?? $request->ip();
        return Limit::perMinute(100)->by((string) $key);
    });

    RateLimiter::for('api-functions', function (Request $request): Limit {
        $key = $request->user()?->getAuthIdentifier() ?? $request->ip();
        return Limit::perMinute(100)->by((string) $key);
    });
}
```

### Step 6: Validate Installation

Verify your installation is working correctly:

```bash
# Validate the complete setup
php artisan record-api:validate

# Run with verbose output for detailed information
php artisan record-api:validate --verbose

# Auto-fix common issues
php artisan record-api:validate --fix
```

### Step 7: Configure Database Tables

Edit `config/record.php` to configure your database tables for the dynamic API. See the [examples directory](https://github.com/MYLEKHATECH/laravel-record-api/blob/main/examples/config/record.php) for a complete configuration example:

```php
use Mylekha\RecordApi\Types\RecordTableType;
use Mylekha\RecordApi\Types\RecordHasManyType;
use Mylekha\RecordApi\Types\RecordBelongsToType;
use Mylekha\RecordApi\Types\RecordTableTriggerType;
return [
    'api_prefix' => 'api/v1',

    'tables' => [
        'users' => new RecordTableType(
            pmsName: 'user',
            table: 'users',
            isAuthRead: true,
            isAuthWrite: true,
            relationships: [
                'posts' => new RecordHasManyType(
                    table: 'posts',
                    foreignKey: 'user_id',
                    localKey: 'id',
                ),
            ],
            softDeletes: false,
            hasTenantId: false,
            createValidator: function (\Illuminate\Http\Request $request, ?int $id = null): \Illuminate\Contracts\Validation\Validator {
                return \Illuminate\Support\Facades\Validator::make($request->all(), [
                    'name' => 'required|string|max:255',
                    'email' => 'required|email',
                ]);
            },
            updateValidator: function (\Illuminate\Http\Request $request, ?int $id = null): \Illuminate\Contracts\Validation\Validator {
                return \Illuminate\Support\Facades\Validator::make($request->all(), [
                    'name' => 'sometimes|required|string|max:255',
                ]);
            },
            deleteValidator: function (\Illuminate\Http\Request $request, ?int $id = null): \Illuminate\Contracts\Validation\Validator {
                return \Illuminate\Support\Facades\Validator::make(['id' => $id], [
                    'id' => 'required|integer',
                ]);
            },
            beforeCreate: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'beforeCreate',
            ),
            afterCreate: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'afterCreate',
            ),
            beforeUpdate: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'beforeUpdate',
            ),
            afterUpdate: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'afterUpdate',
            ),
            beforeDelete: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'beforeDelete',
            ),
            afterDelete: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'afterDelete',
            ),
            beforeRead: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'beforeRead',
            ),
            afterRead: new RecordTableTriggerType(
                class: \App\Record\Triggers\UserTriggers::class,
                functionName: 'afterRead',
            ),
        ),
    ],
];
```

The package routes are loaded automatically by `Mylekha\RecordApi\RecordApiServiceProvider` using this prefix. Record endpoints authorize per-table using `isAuthRead` / `isAuthWrite` and permissions; `public` remains as legacy compatibility and is derived from auth flags.

You can override permission evaluation by setting `record.authorization` in `config/record.php`:

```php
'authorization' => \App\Security\RecordAuthorization::class,
```

Handler contract:

* class-string: container-resolved and must expose `handle($user, $permission, $table, $action): bool`
* callable/closure: invoked as `fn($user, string $permission, string $table, string $action): bool`
* `null`: fallback to default `Gate::forUser($user)->allows($permission)`

### Config-Driven Middleware Map (Client Use Case)

You can apply different middleware stacks per route action and per table without editing package routes.

```php
// config/record.php
'middleware_map' => [
    'default' => [
        '*' => [],
        'read' => [],
        'write' => ['auth:sanctum'],
        'function' => ['auth:sanctum'],
    ],
    'tables' => [
        // Public query routes
        'customers' => [
            'read' => [],
        ],
        // Auth + subscription routes
        'bills' => [
            'write' => ['auth:sanctum', 'subscribed'],
            'table_function' => ['auth:sanctum', 'subscribed'],
        ],
    ],
],
```

Action names available in the middleware map:

* `list`, `show`
* `create`, `update`, `delete`, `restore`, `force_delete`, `upsert`
* `bulk`, `bulk_create`, `bulk_update`, `bulk_delete`, `bulk_upsert`
* `table_function`, `global_function`
* grouped keys: `read`, `write`, `function`, and wildcard `*`

### Request Context for Hooks and Custom Audit

Request context is built-in and always available for hooks and custom audit callbacks.
Tenant resolution keeps backward compatibility:

* first from request attribute `resolved_tenant_id` (or `record_context.tenant_id`)
* then fallback to tenant header (`X-Tenant-ID` by default)

Client middleware can set tenant before CRUD/controller logic:

```php
public function handle($request, \Closure $next)
{
    $request->attributes->set('resolved_tenant_id', $request->user()?->tenant_id);

    return $next($request);
}
```

Or set directly into request context:

```php
$request->attributes->set('record_context', [
    'tenant_id' => $request->user()?->tenant_id,
]);
```

Use context inside trigger:

```php
public static function beforeCreate(\Illuminate\Http\Request $request, string $table, array $context): array
{
    $requestContext = $context['request_context'] ?? $request->attributes->get('record_context', []);
    $tenantId = $requestContext['tenant_id'] ?? null;
    $userId = $requestContext['user']['id'] ?? null;

    $payload = $request->all();
    $payload['tenant_id'] = $tenantId;
    $payload['created_by'] = $userId;
    $request->replace($payload);

    return [$request, $table, $context];
}
```

Alternatively, you can keep `config/record.php` focused on global options and define per-table configurations under `config/records/tables` using the Artisan helper:

### Table-Level Custom Audit Logger

You can override the default audit logging behavior for a specific table by providing a `customAuditLog` callback on the `RecordTableType` configuration. When set, this callback is invoked instead of the built-in `AuditLogService::insertAuditLog` calls for that table.

**Example table config (config/records/tables/invoices.php):**

```php
use Mylekha\RecordApi\Types\RecordTableType;

return new RecordTableType(
    table: 'invoices',
    pmsName: 'invoice',
    isAuthRead: false,
    isAuthWrite: false,
    // String callback formats are supported...
    // customAuditLog: \App\Http\Controllers\InvoiceAuditLogger::class . '@handle',

    // ...and so is native PHP callable array syntax
    customAuditLog: [\App\Http\Controllers\InvoiceAuditLogger::class, 'handle'],
);
```

**Example custom audit handler:**

```php
namespace App\Http\Controllers;

use Mylekha\RecordApi\Enums\AuditLogEventEnum;
use Mylekha\RecordApi\Services\AuditLogService;

class InvoiceAuditLogger
{
    public function handle(
        AuditLogEventEnum $event,
        string $entityClass,
        array $auditData,
        mixed $tenantId,
        array $context
    ): void {
        // Optionally transform or enrich $auditData here

        // Delegate to the core audit logic with your customized payload
        AuditLogService::handleAuditDataEntry(
            event: $event,
            entityName: AuditLogService::getTableNameFromEntityType($entityClass),
            entityType: AuditLogService::getTableNameFromEntityType($entityClass),
            queryData: $auditData,
            tenantId: $tenantId,
        );
    }
}
```

The `$context` parameter contains useful runtime information you can use for more advanced scenarios:

* `request` – The current `Illuminate\Http\Request` instance.
* `table` – The logical table name used in the API (e.g. `invoices`).
* `operation` – One of `create`, `update`, `delete`, or `upsert`.
* `record_context` – The internal record context used by `RecordService` (includes `id`, `payload`, `response`, etc. depending on the operation).

```bash
php artisan record-api:record customers
```

This generates `config/records/tables/customers.php` returning a `RecordTableType` for the `customers` table. After creating the file and the underlying database table, you can sync its `columns` definition from the DB schema:

```bash
php artisan record-api:sync-record-columns --force
```

### Server-Generated Unique IDs

List columns in `uniqueIds` to have the engine generate them, e.g. an immutable UUID secondary key next to the integer primary key:

```php
return new RecordTableType(
    table: 'stores',
    uniqueIds: ['uuid'],
);
```

Every insert the engine performs into the table (create, bulk create, upserts that insert, nested relationship creates) sets each listed column to a fresh lowercase UUIDv7, ignoring any client-supplied value. Updates never change it: it is stripped from update payloads and left out of an upsert's update columns, so an upsert that matches an existing row keeps that row's value. Default validation adds no rules for these columns, and an upsert cannot use one in `match_on` (`422`). The column is still returned in responses like any other column. See `docs/api-documentation.md` for the full list of write paths.

### Computed Attributes (Lazy Response Fields)

You can attach computed fields to any table's read responses using the `attributes` property on `RecordTableType`. Resolvers are **lazy** — they only execute when the field key appears explicitly in `?select=`.

```php
use Mylekha\RecordApi\Types\RecordTableType;

'brands' => new RecordTableType(
    table: 'brands',
    attributes: [
        // [Class, method] — class resolved via Laravel container
        'full_label' => [\App\Attributes\BrandAttribute::class, 'getFullLabel'],
        // 'Class@method' string
        'logo_url'   => \App\Attributes\BrandAttribute::class . '@getLogoUrl',
        // inline Closure
        'is_premium' => fn($row, $table) => ($row->tier ?? null) === 'premium',
    ],
),
```

**Resolver receives** `($row, $table)` where `$row` is the raw DB row (`stdClass`) and `$table` is the table name string.

| Request | Behaviour |
|---|---|
| `GET /api/v1/brands` | No resolvers called |
| `GET /api/v1/brands?select=id,name` | No resolvers called |
| `GET /api/v1/brands?select=id,full_label` | Only `full_label` resolver fires |
| `GET /api/v1/brands?select=*,logo_url` | `logo_url` fires; `*` fetches all DB columns |
| `GET /api/v1/brands/1?select=id,logo_url` | Works identically on single-record endpoint |

Attribute keys are automatically excluded from the SQL `SELECT` to prevent "Unknown column" database errors.

### Column Casts

Use the top-level `casting` property on `RecordTableType` — a `[column => cast]` map, mirroring Laravel's `$casts` on Eloquent models. Only columns listed in `casting` are transformed; all others are untouched. `null` values are always preserved.

**Flat keys** target main-table columns. **Dot-notation keys** target columns inside eagerly-loaded relationships (`'relation.column'`), and work for both single-object relations (`belongsTo`/`hasOne`) and collection relations (`hasMany`/`hasManyThrough`).

```php
use App\Record\Casts\GlobalCasting;

// config/record.php
'casting' => [
    'is_active' => 'bool',
    'amount' => 'decimal:2',
],

'products' => new RecordTableType(
    table: 'products',
    columns: [
        'is_active' => ['type' => 'boolean'],
        'quantity' => ['type' => 'bigint'],
        'price' => ['type' => 'decimal(12,2)'],
    ],
    casting: [
        // flat main-table casts
        'price'      => 'float',
        'quantity'   => 'int',
        'is_active'  => 'bool',
        'metadata'   => 'array',          // JSON string → array
        'score'      => 'decimal:4',
        'created_at' => 'datetime',       // ISO 8601
        // static method cast
        'is_cloud'   => [GlobalCasting::class, 'bool'],
        // inline Closure
        'status'     => fn($v) => strtoupper($v),

        // dot-notation: cast columns inside a hasMany relation
        'variants.price'    => 'float',
        'variants.qty'      => 'int',

        // dot-notation: cast a column inside a belongsTo relation
        'brand.is_active'   => 'bool',
        'brand.founded_at'  => 'date',
    ],
),
```

Built-in cast strings mirror Laravel model cast names: `int`, `integer`, `float`, `double`, `real`, `decimal`, `decimal:N`, `string`, `bool`, `boolean`, `array`, `json`, `object`, `date`, `datetime`, `timestamp`.

Custom callable forms (all receive `($value, $column, $row)`): `Closure`, `[Class, 'method']` (static or instance), `'Class@method'`, `'ClassName'` (calls `->get($value, $column, $row)`).

### Step 8: Test Your Installation

Test the dynamic API endpoints:

```bash
# Test basic API functionality
curl -X GET http://your-app.test/api/v1/users

# Test with authentication (if configured)
curl -X GET http://your-app.test/api/v1/users \
  -H "Authorization: Bearer your-access-token"

# Test creating a record
curl -X POST http://your-app.test/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Test User","email":"test@example.com"}'
```

## Database-Specific Instructions

### MySQL Configuration

```env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_password
```

**Recommended MySQL Settings:**

```sql
-- For better performance with large datasets
SET GLOBAL innodb_buffer_pool_size = 1G;
SET GLOBAL query_cache_size = 256M;
SET GLOBAL max_connections = 200;
```

### PostgreSQL Configuration

```env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_password
```

### SQLite Configuration

```env
DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database.sqlite
```

**Note:** SQLite is suitable for development but not recommended for production use with this package.
