Add form value persistence across cache revalidation re-renders 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
6.1 KiB
Executable File
6.1 KiB
Executable File
name, description
| name | description |
|---|---|
| model-fetch | Loading model data from JavaScript using Model.fetch() with secure opt-in, authorization, and lazy relationships. Use when implementing fetch() methods on models, loading records from JavaScript, accessing relationships via await, or understanding the Ajax_Endpoint_Model_Fetch attribute. |
RSX Model Fetch System
Overview
RSX allows JavaScript to securely access ORM models through explicit opt-in. Unlike Laravel's API routes which can expose all fields, RSX requires each model to implement its own fetch() method with authorization and data filtering.
Security Model
- Explicit Opt-In: Models must implement
fetch()with#[Ajax_Endpoint_Model_Fetch] - No Default Access: No models are fetchable by default
- Individual Authorization: Each model controls who can fetch its records
- Data Filtering: Complete control over what data JavaScript receives
Implementing fetch()
Basic Implementation
use Ajax_Endpoint_Model_Fetch;
class Product_Model extends Rsx_Model_Abstract
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
// Authorization check
if (!Session::is_logged_in()) {
return false;
}
// Fetch single record
$model = static::find($id);
return $model ?: false;
}
}
With Data Filtering
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!Session::is_logged_in()) {
return false;
}
$user = static::find($id);
if (!$user) {
return false;
}
// Remove sensitive fields
unset($user->password_hash);
unset($user->remember_token);
return $user;
}
With Authorization Check
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
$current_user = Session::get_user();
if (!$current_user) {
return false;
}
$order = static::find($id);
if (!$order) {
return false;
}
// Only allow access to own orders or admin users
if ($order->user_id !== $current_user->id && !$current_user->is_admin) {
return false;
}
return $order;
}
Augmented Array Return (Computed Fields)
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!Session::is_logged_in()) {
return false;
}
$contact = static::find($id);
if (!$contact) {
return false;
}
// Start with toArray() to preserve __MODEL for hydration
$data = $contact->toArray();
// Add computed fields
$data['full_name'] = $contact->full_name();
$data['avatar_url'] = $contact->get_avatar_url();
return $data;
}
Important: Always use toArray() as the base - it preserves __MODEL for JavaScript hydration.
JavaScript Usage
Fetch Single Record
// Throws if not found
const project = await Project_Model.fetch(123);
console.log(project.name);
// Returns null if not found
const maybe = await Project_Model.fetch_or_null(999);
if (maybe) {
console.log(maybe.name);
}
Enum Properties (BEM-Style)
const project = await Project_Model.fetch(1);
// Instance properties (from fetched data)
console.log(project.status_id__label); // "Active"
console.log(project.status_id__badge); // "bg-success"
// Static constants
if (project.status_id === Project_Model.STATUS_ACTIVE) {
// ...
}
Lazy Relationships
Relationships can be loaded on-demand from JavaScript. The related model must also implement fetch() with #[Ajax_Endpoint_Model_Fetch].
belongsTo Relationship
// In Project_Model
#[Ajax_Endpoint_Model_Fetch]
public function client()
{
return $this->belongsTo(Client_Model::class);
}
const project = await Project_Model.fetch(123);
const client = await project.client(); // Returns Client_Model or null
console.log(client.name);
hasMany Relationship
// In Project_Model
#[Ajax_Endpoint_Model_Fetch]
public function tasks()
{
return $this->hasMany(Task_Model::class);
}
const project = await Project_Model.fetch(123);
const tasks = await project.tasks(); // Returns Task_Model[]
for (const task of tasks) {
console.log(task.title);
}
morphTo Relationship
// In Activity_Model
#[Ajax_Endpoint_Model_Fetch]
public function subject()
{
return $this->morphTo();
}
const activity = await Activity_Model.fetch(1);
const subject = await activity.subject(); // Returns polymorphic model
Return Value Rules
| Return | Meaning |
|---|---|
| Model object | Serialized via toArray(), includes __MODEL for hydration |
Array (from toArray()) |
Preserves __MODEL, can add computed fields |
false |
Record not found or unauthorized |
MUST return false (not null) when record is not found or unauthorized.
Anti-Aliasing Policy
NEVER alias enum properties in fetch() - BEM-style naming exists for grepability:
// WRONG - Aliasing obscures data source
$data['type_label'] = $record->type_id__label;
// RIGHT - Use full BEM-style names in JavaScript
contact.type_id__label // Grepable, self-documenting
The fetch() method's purpose is security (removing private data), not aliasing.
Common Patterns
In SPA Action on_load()
async on_load() {
const project = await Project_Model.fetch(this.args.id);
this.data.project = project;
}
Loading with Relationships
async on_load() {
const project = await Project_Model.fetch(this.args.id);
const [client, tasks] = await Promise.all([
project.client(),
project.tasks()
]);
this.data.project = project;
this.data.client = client;
this.data.tasks = tasks;
}
Conditional Relationship Loading
async on_load() {
const order = await Order_Model.fetch(this.args.id);
this.data.order = order;
// Only load customer if needed
if (this.args.show_customer) {
this.data.customer = await order.customer();
}
}
More Information
Details: php artisan rsx:man model_fetch