carlin / laravel-api-relations
Eloquent-like API relationships for Laravel with composite key support and N+1 prevention
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/carlin/laravel-api-relations
Requires
- php: ^8.1
- illuminate/database: ^8.0|^9.0|^10.0
- illuminate/support: ^8.0|^9.0|^10.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-12-15 06:38:17 UTC
README
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.