Internal API Methods (Business Logic)
The package provides internal methods that allow developers to execute CRUD operations directly from their business logic (e.g., inside custom controllers, jobs, or commands) using the exact same dynamic query syntax as the REST API. This ensures consistent behavior, relationship loading, and filtering across both HTTP requests and internal code.
Available Methods
All methods are available statically on Sopheak\Core\Services\RecordService.
executeGetByFilter
Fetch multiple records using API filter syntax.
public static function executeGetByFilter(
string $table,
array|string $queryParams = [],
mixed $tenantId = null,
bool $isArray = true,
string $orderBy = 'id'
): arrayexecuteGetById
Fetch a single record by ID, optionally loading relationships.
public static function executeGetById(
string $table,
mixed $id,
array|string $queryParams = [],
mixed $tenantId = null
): arrayexecuteCreate
Create a record and return it fully loaded with requested relationships.
public static function executeCreate(
string $table,
array $payload,
array|string $queryParams = [],
mixed $tenantId = null
): arrayexecuteUpdate
Update a record and return it fully loaded with requested relationships.
public static function executeUpdate(
string $table,
mixed $id,
array $payload,
array|string $queryParams = [],
mixed $tenantId = null
): arrayexecuteDelete
Delete a record and return the fully loaded record before it was deleted.
public static function executeDelete(
string $table,
mixed $id,
array|string $queryParams = [],
mixed $tenantId = null
): arrayWrite Side-Effects (Record Lifecycle Events)
Internal write methods (executeCreate, executeUpdate, executeDelete) do not run the HTTP pipeline: no before*/after* hooks (so no webhooks delivered through them), no table or default validators and no RecordMutated broadcast. They reject unknown payload fields, process nested relationships, invalidate caches and dispatch the lifecycle events below. The MCP data tools and the AI SDK record tools call these methods inside the hook pipeline (record.mcp.run_record_hooks), so hooks, validators and broadcasts do run for them.
After a successful write, the package dispatches these events:
Sopheak\Core\Events\RecordCreatedSopheak\Core\Events\RecordUpdatedSopheak\Core\Events\RecordDeleted
These events are Laravel 13 safe (they do not carry the full Illuminate\Http\Request; instead they include an auditContext array with primitive fields like ip, user_agent, user_id, and request_id).
What happens via listeners:
- Cache invalidation is handled by
InvalidateRecordCacheListener - Audit insertion is handled by
LogRecordAuditListener(queued when audit queue is enabled)
These methods run as the authenticated user: viewOwn scoping applies to them when a restricted user is logged in (not in console or queue code with no user). An update or delete that matches nothing in scope dispatches no RecordUpdated / RecordDeleted event, so no audit row is written. Nested child writes made through these methods are trusted and not permission-checked; the MCP and AI SDK tools wrap them in that check themselves.
Detailed Examples
1. Fetching Records with Relationships
You can pass query parameters as an array or a URL-encoded string.
use Sopheak\Core\Services\RecordService;
// Using array syntax
$invoice = RecordService::executeGetById(
table: 'invoices',
id: 1,
queryParams: [
'select' => '*,customer(*),items(*,product(*))'
]
);
// Using string syntax
$activeUsers = RecordService::executeGetByFilter('users', 'select=*,profile(*)&status=eq.active');2. Creating a Record and Getting it Back
When creating a record, you often need the newly generated ID or default database values immediately. executeCreate handles this and can even load relationships in the same step.
$newOrder = RecordService::executeCreate('orders',
// Payload
[
'customer_id' => 5,
'total' => 150.00,
'status' => 'pending'
],
// Query params for the returned record
['select' => '*,customer(name,email)']
);
echo $newOrder['data']['id']; // The new ID
echo $newOrder['data']['customer']['name']; // Loaded relationship3. Updating a Record
Similar to creation, you can update a record and immediately get the fresh data back.
$updatedOrder = RecordService::executeUpdate('orders', 12,
// Payload
['status' => 'completed'],
// Query params
'select=*,customer(*)'
);4. Deleting a Record
Sometimes you need the data of the record you just deleted (e.g., to send a cancellation email). executeDelete fetches the record before deleting it and returns it.
$deletedUser = RecordService::executeDelete('users', 42, [
'select' => '*,profile(*)'
]);
// Send email using the data we just deleted
Mail::to($deletedUser['data']['email'])->send(new AccountDeleted($deletedUser['data']));5. Handling Tenancy
If your application uses multi-tenancy, you can pass the $tenantId as the last parameter to ensure the operation is scoped correctly.
$tenantId = 99;
$tenantUsers = RecordService::executeGetByFilter('users', [], $tenantId);Function Caching
Caching is only applied for GET requests and when record.cache.enabled is true. For a focused cache guide (config, TTL, invalidation), see Record Cache.
Table functions
- Caching is disabled by default (
disableCache: true) — opt in per function withRecordFunctionType(disableCache: false). - Table cache can be disabled globally for a table using
record.cache.per_table[table] = false. - Default TTL uses
record.cache.per_table_ttl[table]when set; otherwiserecord.cache.ttl. - Set
cacheTTLin the function config to override the computed TTL for this function. - For write methods (
POST,PUT,PATCH,DELETE), table function cache for the executed function is invalidated automatically. - For write methods (
POST,PUT,PATCH,DELETE), table functions automatically clear cache for the current table after a successful response. UseclearCacheTablesto clear additional tables.
Global functions
- Caching is disabled by default (
disableCache: true) — opt in per function withRecordFunctionType(disableCache: false). - Default TTL uses
record.cache.ttl. - Set
cacheTTLin the function config to override the default TTL for this function. - For write methods (
POST,PUT,PATCH,DELETE), global function cache for the executed function is invalidated automatically. - Global functions can additionally clear table caches by setting
clearCacheTables.
Manual cache clear
You can clear a table cache manually from any controller, job, or command:
use Sopheak\Core\Services\RecordCacheService;
$service = app(RecordCacheService::class);
$service->clearTableCache('settings', $tenantId);
$service->clearCacheForTables(['settings', 'users'], $tenantId);Controller cache example
use Illuminate\Http\Request;
use Sopheak\Core\Services\QueryCacheService;
use Sopheak\Core\Services\RecordCacheService;
use Sopheak\Core\Services\RecordService;
public function list(Request $request)
{
$filters = $request->query();
$cacheKey = QueryCacheService::tableKey('settings', $filters, [], 1, 50);
return QueryCacheService::remember($cacheKey, function () use ($request) {
return app(RecordService::class)->listRecords($request, 'settings');
}, 600);
}
public function update(Request $request, RecordCacheService $cacheService)
{
app(RecordService::class)->updateRecord($request, 'settings', 1);
$tenantId = $request->header('X-Tenant-Id');
$cacheService->clearTableCache('settings', $tenantId);
return response()->json(['ok' => true]);
}