laravel-api-relations maintained by carlin
Laravel API Relations
Eloquent-like API relationships for Laravel with composite key support and N+1 prevention through intelligent batch loading.
English | 简体中文
Features
- 🚀 Eloquent-like syntax - Define API relationships just like database relationships
- 🔑 Composite key support - Handle complex relationships with multiple keys
- ⚡ N+1 prevention - Automatic batch loading for optimal performance
- 🎯 Lazy & Eager loading - Full support for both loading strategies
- 🔤 Case-insensitive matching - Optional case-insensitive key matching for flexible API integration
Requirements
- PHP 8.1 or higher
- Laravel 8.x, 9.x, or 10.x
Installation
Install the package via Composer:
composer require carlin/laravel-api-relations
Why This Package?
Before: The Traditional Approach ❌
Without this package, you typically need Service classes to fetch and attach API data:
// UserService.php
class UserService
{
public function getUserWithProfile($userId)
{
$user = User::find($userId);
// Fetch profile from external API
$response = Http::post('https://api.example.com/profiles', [
'user_ids' => [$userId]
]);
$profiles = $response->json();
$user->profile = $profiles[0] ?? null;
return $user;
}
public function getUsersWithProfiles($userIds)
{
$users = User::whereIn('id', $userIds)->get();
// Batch fetch to avoid N+1
$response = Http::post('https://api.example.com/profiles', [
'user_ids' => $userIds
]);
$profiles = collect($response->json())->keyBy('user_id');
// Manually attach profiles to users
foreach ($users as $user) {
$user->profile = $profiles->get($user->id);
}
return $users;
}
}
// Controller usage
class UserController extends Controller
{
public function index(UserService $userService)
{
// Must remember to use the service method
$users = $userService->getUsersWithProfiles([1, 2, 3]);
return view('users.index', compact('users'));
}
public function show($id, UserService $userService)
{
// Different method for single user
$user = $userService->getUserWithProfile($id);
return view('users.show', compact('user'));
}
}
Problems:
- 🔴 Need separate Service classes for API data fetching
- 🔴 Controllers must remember to use specific service methods
- 🔴 Different methods for single vs. multiple records
- 🔴 Manual data attachment in every service method
- 🔴 Easy to forget batch loading, causing N+1 problems
- 🔴 Can't use Eloquent's
with()for eager loading - 🔴 Breaking Eloquent conventions and patterns
After: With Laravel API Relations ✅
API relationships work exactly like Eloquent relationships:
// User.php
class User extends Model
{
use HasApiRelations;
public function profile()
{
return $this->hasOneApi(
callback: fn($userIds) => Http::post('https://api.example.com/profiles', [
'user_ids' => $userIds
])->json(),
foreignKey: 'user_id',
localKey: 'id'
);
}
public function posts()
{
return $this->hasManyApi(
callback: fn($userIds) => Http::post('https://api.example.com/posts', [
'user_ids' => $userIds
])->json(),
foreignKey: 'user_id',
localKey: 'id'
);
}
}
// Controller usage - just like regular Eloquent!
class UserController extends Controller
{
public function index()
{
// Automatic batch loading - single API call for all users
$users = User::with('profile', 'posts')->get();
return view('users.index', compact('users'));
}
public function show($id)
{
// Lazy loading - works seamlessly
$user = User::find($id);
$profile = $user->profile()->getResults();
return view('users.show', compact('user', 'profile'));
}
}
Benefits:
- ✅ No service layer needed for API relationships
- ✅ Use standard Eloquent
with()for eager loading - ✅ Automatic N+1 prevention through intelligent batching
- ✅ Consistent API with database relationships
- ✅ Single model definition works for all scenarios
- ✅ Controllers stay clean and follow Laravel conventions
- ✅ Built-in composite key support
Quick Start
1. Add the Trait to Your Model
use Carlin\LaravelApiRelations\Traits\HasApiRelations;
class User extends Model
{
use HasApiRelations;
// Define a has-one API relationship
public function profile()
{
return $this->hasOneApi(
callback: fn($userIds) => $this->fetchProfilesFromApi($userIds),
foreignKey: 'user_id',
localKey: 'id'
);
}
// Define a has-many API relationship
public function posts()
{
return $this->hasManyApi(
callback: fn($userIds) => $this->fetchPostsFromApi($userIds),
foreignKey: 'user_id',
localKey: 'id'
);
}
private function fetchProfilesFromApi(array $userIds): array
{
// Call your external API
$response = Http::post('https://api.example.com/profiles', [
'user_ids' => $userIds
]);
return $response->json();
}
private function fetchPostsFromApi(array $userIds): array
{
// Call your external API
$response = Http::post('https://api.example.com/posts', [
'user_ids' => $userIds
]);
return $response->json();
}
}
2. Use Like Regular Eloquent Relationships
// Lazy loading (single API call per model)
$user = User::find(1);
$profile = $user->profile()->getResults();
$posts = $user->posts()->getResults();
// Eager loading (single batched API call for all models)
$users = User::with('profile', 'posts')->get();
foreach ($users as $user) {
echo $user->profile['name'];
foreach ($user->posts as $post) {
echo $post['title'];
}
}
Advanced Usage
Case-Insensitive Key Matching
By default, key matching is case-sensitive. You can enable case-insensitive matching for scenarios where API keys might have inconsistent casing:
class User extends Model
{
use HasApiRelations;
public function profile()
{
return $this->hasOneApi(
callback: fn($userIds) => Http::post('https://api.example.com/profiles', [
'user_ids' => $userIds
])->json(),
foreignKey: 'user_id',
localKey: 'id',
caseInsensitive: true // Enable case-insensitive matching
);
}
}
// Example: Model has user_code = 'ABC'
// API returns data with user_code = 'abc' or 'Abc' or 'ABC'
// All variations will match successfully
When to use case-insensitive matching:
- External APIs return inconsistent key casing
- Legacy systems with mixed-case identifiers
- Case-insensitive database collations
- Multi-source data integration
Composite Keys
Handle relationships with multiple key fields:
class Order extends Model
{
use HasApiRelations;
public function orderDetails()
{
return $this->hasOneApi(
callback: fn($keys) => $this->fetchOrderDetails($keys),
foreignKey: ['customer_id', 'order_number'],
localKey: ['customer_id', 'order_number']
);
}
private function fetchOrderDetails(array $compositeKeys): array
{
// $compositeKeys = [
// ['customer_id' => 1, 'order_number' => 'ORD-001'],
// ['customer_id' => 2, 'order_number' => 'ORD-002'],
// ]
$response = Http::post('https://api.example.com/order-details', [
'keys' => $compositeKeys
]);
return $response->json();
}
}
// Usage
$order = Order::find(1);
$details = $order->orderDetails()->getResults();
API Callback Format
Your API callback receives an array of keys and should return an array of results:
For hasOneApi:
// Input: [1, 2, 3]
// Output: [
// ['user_id' => 1, 'name' => 'John', 'email' => 'john@example.com'],
// ['user_id' => 2, 'name' => 'Jane', 'email' => 'jane@example.com'],
// ['user_id' => 3, 'name' => 'Bob', 'email' => 'bob@example.com'],
// ]
For hasManyApi:
// Input: [1, 2, 3]
// Output: [
// ['user_id' => 1, 'title' => 'Post 1'],
// ['user_id' => 1, 'title' => 'Post 2'],
// ['user_id' => 2, 'title' => 'Post 3'],
// ['user_id' => 3, 'title' => 'Post 4'],
// ['user_id' => 3, 'title' => 'Post 5'],
// ]
N+1 Prevention Example
// ❌ BAD: N+1 problem (100 users = 100 API calls)
$users = User::all();
foreach ($users as $user) {
$profile = $user->profile()->getResults(); // API call per user
}
// ✅ GOOD: Batch loading (100 users = 1 API call)
$users = User::with('profile')->get();
foreach ($users as $user) {
$profile = $user->profile; // No additional API calls
}
How It Works
- Lazy Loading: When you access a relationship on a single model, it calls the API with that model's key
- Eager Loading: When you use
with(), it collects all keys from all models and makes a single batched API call - Composite Keys: Multiple fields are combined into an associative array and properly matched
- Result Matching: API results are automatically matched back to the correct models using the foreign key
API Reference
hasOneApi
Define a has-one API relationship.
public function hasOneApi(
callable $apiCallback,
string|array $foreignKey,
string|array $localKey = 'id',
bool $caseInsensitive = false
): HasOneApi
Parameters:
$apiCallback- Function that receives array of keys and returns API results$foreignKey- Field name(s) in the API response to match against$localKey- Field name(s) in the local model (defaults to 'id')$caseInsensitive- Enable case-insensitive key matching (defaults to false)
Returns: null or array when no match found
hasManyApi
Define a has-many API relationship.
public function hasManyApi(
callable $apiCallback,
string|array $foreignKey,
string|array $localKey = 'id',
bool $caseInsensitive = false
): HasManyApi
Parameters:
$apiCallback- Function that receives array of keys and returns API results$foreignKey- Field name(s) in the API response to match against$localKey- Field name(s) in the local model (defaults to 'id')$caseInsensitive- Enable case-insensitive key matching (defaults to false)
Returns: Empty array [] when no matches found
Use Cases
Perfect for scenarios where you need to:
- Fetch user profiles from a separate authentication service
- Load product details from an external catalog API
- Retrieve order information from a third-party system
- Access microservice data while maintaining Eloquent-like syntax
- Handle multi-tenant relationships with composite keys
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This package is open-sourced software licensed under the MIT license.
Credits
Support
If you discover any issues, please email rjwangnixingfu@gmail.com or create an issue on GitHub.