m-tech-stack / laravel-api-model-client
A powerful Laravel package that enables Eloquent-like models to interact seamlessly with external APIs instead of a local database
Requires
- php: ^8.0|^8.1|^8.2|^8.3
- guzzlehttp/guzzle: ^7.0
- illuminate/database: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- mockery/mockery: ^1.5
- orchestra/testbench: ^6.0|^7.0|^8.0|^9.0
- phpunit/phpunit: ^9.0|^10.0
- symfony/yaml: ^5.0|^6.0
README
A powerful Laravel package that enables Eloquent-like models to interact seamlessly with external APIs instead of a local database, including relationships, caching, error handling, and more.
Table of Contents
- Features
- Installation
- Configuration
- Basic Usage
- API Relationships
- Query Builder
- Caching
- Authentication
- Events
- Middleware Pipeline
- Error Handling
- Advanced Usage
- Troubleshooting
- Contributing
- License
Features
- Eloquent-like API Models: Use familiar Laravel Eloquent syntax with external APIs
- API Relationships: Define and use relationships between API resources (
hasMany
,belongsTo
, etc.) - Smart Caching: Cache API responses with configurable TTL and strategies
- Query Builder: Use Eloquent-like query builder methods for API queries
- Pagination Support: Handle paginated API responses like Eloquent collections
- Authentication Strategies: Support for multiple API authentication methods (Bearer, Basic, API Key)
- Error Handling: Comprehensive error handling and logging for API failures
- Local DB Integration: Optionally merge API data with local database records
- Middleware Pipeline: Process API requests through a configurable middleware pipeline
- Event System: Hook into API request lifecycle with Laravel events
- Response Transformers: Transform API responses into your desired format
- API Mocking: Mock API responses for testing
- Developer Tools: Generate models from OpenAPI/Swagger specs, debug API calls, and generate documentation
- Lazy Loading: Lazy load API relationships to improve performance
- Modularized Traits: Use individual traits to add API model capabilities to your own models
- Complex Relationships: Support for hasManyThrough, morphMany, and other complex relationships
Installation
You can install the package via composer:
composer require m-tech-stack/laravel-api-model-client
After installing, publish the configuration file:
php artisan vendor:publish --provider="MTechStack\LaravelApiModelClient\MTechStack\LaravelApiModelClientServiceProvider" --tag="config"
Configuration
The package can be configured via the config/api-model-client.php
file:
return [ 'client' => [ 'base_url' => env('API_MODEL_RELATIONS_BASE_URL'), 'timeout' => env('API_MODEL_RELATIONS_TIMEOUT', 30), 'connect_timeout' => env('API_MODEL_RELATIONS_CONNECT_TIMEOUT', 10), ], 'auth' => [ 'strategy' => env('API_MODEL_RELATIONS_AUTH_STRATEGY', 'bearer'), // 'bearer', 'basic', 'api_key' 'credentials' => [ 'token' => env('API_MODEL_RELATIONS_AUTH_TOKEN'), 'username' => env('API_MODEL_RELATIONS_AUTH_USERNAME'), 'password' => env('API_MODEL_RELATIONS_AUTH_PASSWORD'), 'api_key' => env('API_MODEL_RELATIONS_AUTH_API_KEY'), 'header_name' => env('API_MODEL_RELATIONS_AUTH_HEADER_NAME', 'X-API-KEY'), 'use_query_param' => env('API_MODEL_RELATIONS_AUTH_USE_QUERY', false), 'query_param_name' => env('API_MODEL_RELATIONS_AUTH_QUERY_NAME', 'api_key'), ], ], 'cache' => [ 'enabled' => env('API_MODEL_RELATIONS_CACHE_ENABLED', true), 'ttl' => env('API_MODEL_RELATIONS_CACHE_TTL', 3600), // seconds 'store' => env('API_MODEL_RELATIONS_CACHE_STORE', 'file'), 'prefix' => env('API_MODEL_RELATIONS_CACHE_PREFIX', 'api_model_'), ], 'error_handling' => [ 'log_requests' => env('API_MODEL_RELATIONS_LOG_REQUESTS', true), 'log_responses' => env('API_MODEL_RELATIONS_LOG_RESPONSES', true), 'log_channel' => env('API_MODEL_RELATIONS_LOG_CHANNEL', 'stack'), ], 'rate_limiting' => [ 'enabled' => env('API_MODEL_RELATIONS_RATE_LIMIT_ENABLED', true), 'max_attempts' => env('API_MODEL_RELATIONS_RATE_LIMIT_MAX', 60), 'decay_minutes' => env('API_MODEL_RELATIONS_RATE_LIMIT_DECAY', 1), ], 'debug' => env('API_MODEL_RELATIONS_DEBUG', false), 'events' => [ 'enabled' => env('API_MODEL_RELATIONS_EVENTS_ENABLED', true), ], 'models' => [ 'merge_local' => env('API_MODEL_RELATIONS_MERGE_LOCAL', false), 'cache_ttl' => env('API_MODEL_RELATIONS_MODEL_CACHE_TTL', 3600), ], ];
Basic Usage
Creating an API Model
Create a model that extends ApiModel
and use the SyncWithApi
trait:
<?php namespace App\Models\Api; use MTechStack\LaravelApiModelClient\Models\ApiModel; use MTechStack\LaravelApiModelClient\Traits\SyncWithApi; class Product extends ApiModel { use SyncWithApi; /** * The API endpoint for this model. * * @var string */ protected $apiEndpoint = 'products'; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'id', 'name', 'description', 'price', 'category_id', ]; /** * The attributes that should be cast. * * @var array */ protected $casts = [ 'price' => 'float', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; /** * Get the category that owns the product. */ public function category() { return $this->belongsToFromApi(Category::class, 'category_id'); } /** * Get the reviews for the product. */ public function reviews() { return $this->hasManyFromApi(Review::class, 'product_id'); } }
Using API Models
Use API models just like regular Eloquent models:
// Find a product by ID $product = Product::find(1); // Get all products $products = Product::all(); // Query products $expensiveProducts = Product::where('price', '>', 100)->get(); // Use relationships $category = $product->category; $reviews = $product->reviews; // Create a new product $newProduct = Product::create([ 'name' => 'New Product', 'description' => 'This is a new product', 'price' => 99.99, 'category_id' => 2, ]); // Update a product $product->update(['price' => 149.99]); // Delete a product $product->delete();
API Model Lifecycle
Understanding the API model lifecycle helps you work with the package more effectively:
// Creating a new model instance $product = new Product(); $product->name = 'New Product'; $product->price = 99.99; // Save to API - triggers API POST request $product->save(); // At this point, the model has been saved to the API and has an ID // Refresh from API - triggers API GET request $product->refreshFromApi(); // Update model - triggers API PUT/PATCH request $product->price = 149.99; $product->save(); // Delete from API - triggers API DELETE request $product->delete();
API Relationships
Available Relationships
The package supports the following relationship types:
hasManyFromApi
: One-to-many relationshipbelongsToFromApi
: Many-to-one relationshiphasOneFromApi
: One-to-one relationshipbelongsToManyFromApi
: Many-to-many relationshiphasManyThroughFromApi
: One-to-many relationship through an intermediate modelmorphManyFromApi
: Polymorphic one-to-many relationship
Defining Relationships
Define relationships in your models:
// One-to-many relationship public function reviews() { return $this->hasManyFromApi(Review::class, 'product_id'); } // Many-to-one relationship public function category() { return $this->belongsToFromApi(Category::class, 'category_id'); } // One-to-one relationship public function featuredImage() { return $this->hasOneFromApi(Image::class, 'product_id'); } // Many-to-many relationship public function tags() { return $this->belongsToManyFromApi( Tag::class, 'product_tags', // pivot endpoint 'product_id', 'tag_id' ); } // One-to-many relationship through an intermediate model public function comments() { return $this->hasManyThroughFromApi( Comment::class, // Final model we want to access Post::class, // Intermediate model 'user_id', // Foreign key on intermediate model 'post_id' // Foreign key on final model ); } // Polymorphic one-to-many relationship public function comments() { return $this->morphManyFromApi( Comment::class, 'commentable' // The name of the relationship ); } // In your Comment model public function commentable() { return $this->morphToFromApi(); }
Working with Relationships
Here are examples of working with different relationship types:
// One-to-many relationship $product = Product::find(1); $reviews = $product->reviews; // Get all reviews for this product // Create a new related model $newReview = $product->reviews()->create([ 'rating' => 5, 'comment' => 'Great product!' ]); // Many-to-one relationship $review = Review::find(1); $product = $review->product; // Get the product for this review // One-to-one relationship $product = Product::find(1); $image = $product->featuredImage; // Get the featured image // Update a related model $product->featuredImage->update([ 'url' => 'https://example.com/new-image.jpg' ]); // Many-to-many relationship $product = Product::find(1); $tags = $product->tags; // Get all tags for this product // Attach a tag to a product $product->tags()->attach(5); // Detach a tag from a product $product->tags()->detach(3); // Sync tags (remove existing and add new ones) $product->tags()->sync([1, 2, 5]); // One-to-many through relationship $user = User::find(1); $comments = $user->comments; // Get all comments on the user's posts // Polymorphic relationships $product = Product::find(1); $comments = $product->comments; // Get comments for this product $post = Post::find(1); $comments = $post->comments; // Get comments for this post
Query Builder
Use the query builder to filter API results:
// Basic where clauses $products = Product::where('category_id', 1) ->where('price', '>', 50) ->get(); // Where with array of conditions $products = Product::where([ ['status', '=', 'active'], ['price', '>', 100] ])->get(); // Where with OR condition $products = Product::where('category_id', 1) ->orWhere('featured', true) ->get(); // Where with nested conditions $products = Product::where('category_id', 1) ->where(function($query) { $query->where('price', '>', 100) ->orWhere('featured', true); }) ->get(); // Order by $products = Product::orderBy('price', 'desc')->get(); // Multiple order by $products = Product::orderBy('category_id') ->orderBy('price', 'desc') ->get(); // Limit and offset $products = Product::limit(10)->offset(20)->get(); // Pagination $products = Product::paginate(15); $products = Product::where('category_id', 1)->paginate(15); // Custom query parameters $products = Product::withQueryParam('include', 'category,tags') ->withQueryParam('fields', 'id,name,price') ->get(); // Custom macros $products = Product::whereContains('name', 'phone')->get();
Caching
API responses are automatically cached based on your configuration. You can customize caching behavior:
// Set a custom cache TTL for a specific model class Product extends ApiModel { use SyncWithApi; protected $apiEndpoint = 'products'; protected $cacheTtl = 1800; // 30 minutes } // Disable caching for a query $products = Product::withoutCache()->get(); // Refresh cache for a model $product = Product::find(1); $product->refreshFromApi(); // Clear cache for a model Product::clearCache(); // Clear cache for a specific model instance $product = Product::find(1); $product->clearCache(); // Clear cache for a specific query Product::where('category_id', 1)->clearCache(); // Set custom cache key $products = Product::withCacheKey('featured_products') ->where('featured', true) ->get();
Authentication
The package supports multiple authentication strategies:
Bearer Token
// In your .env file API_MODEL_RELATIONS_AUTH_STRATEGY=bearer API_MODEL_RELATIONS_AUTH_TOKEN=your-token-here // Or set dynamically in your code ApiClient::setAuthStrategy('bearer'); ApiClient::setAuthToken('your-dynamic-token');
Basic Auth
// In your .env file API_MODEL_RELATIONS_AUTH_STRATEGY=basic API_MODEL_RELATIONS_AUTH_USERNAME=your-username API_MODEL_RELATIONS_AUTH_PASSWORD=your-password // Or set dynamically in your code ApiClient::setAuthStrategy('basic'); ApiClient::setBasicAuth('username', 'password');
API Key
// In your .env file API_MODEL_RELATIONS_AUTH_STRATEGY=api_key API_MODEL_RELATIONS_AUTH_API_KEY=your-api-key API_MODEL_RELATIONS_AUTH_HEADER_NAME=X-API-KEY // Or set dynamically in your code ApiClient::setAuthStrategy('api_key'); ApiClient::setApiKey('your-api-key', 'X-API-KEY');
Custom Authentication
You can implement custom authentication strategies:
use MTechStack\LaravelApiModelClient\Auth\AuthStrategyInterface; class CustomAuthStrategy implements AuthStrategyInterface { public function apply($request) { // Apply your custom authentication to the request return $request->withHeader('X-Custom-Auth', 'custom-value'); } } // Register your custom strategy app()->bind('api-model-relations.auth.custom', function() { return new CustomAuthStrategy(); }); // Use your custom strategy ApiClient::setAuthStrategy('custom');
Events
The package dispatches events during the API request lifecycle:
use MTechStack\LaravelApiModelClient\Events\ApiRequestEvent; use MTechStack\LaravelApiModelClient\Events\ApiResponseEvent; use MTechStack\LaravelApiModelClient\Events\ApiExceptionEvent; use MTechStack\LaravelApiModelClient\Events\ModelCreatedEvent; use MTechStack\LaravelApiModelClient\Events\ModelUpdatedEvent; use MTechStack\LaravelApiModelClient\Events\ModelDeletedEvent; use Illuminate\Support\Facades\Event; // Listen for API request events Event::listen(ApiRequestEvent::class, function (ApiRequestEvent $event) { $method = $event->method; $endpoint = $event->endpoint; $options = $event->options; // Do something before the API request logger()->info("API Request: {$method} {$endpoint}"); }); // Listen for API response events Event::listen(ApiResponseEvent::class, function (ApiResponseEvent $event) { $response = $event->response; $statusCode = $event->statusCode; // Do something with the API response logger()->info("API Response: {$statusCode}"); }); // Listen for API exception events Event::listen(ApiExceptionEvent::class, function (ApiExceptionEvent $event) { $exception = $event->exception; // Handle API exceptions logger()->error("API Exception: {$exception->getMessage()}"); }); // Listen for model lifecycle events Event::listen(ModelCreatedEvent::class, function (ModelCreatedEvent $event) { $model = $event->model; logger()->info("Model created: " . get_class($model) . " #{$model->id}"); });
Middleware Pipeline
The package uses a middleware pipeline to process API requests. You can add custom middleware:
use MTechStack\LaravelApiModelClient\Middleware\AbstractApiMiddleware; class CustomMiddleware extends AbstractApiMiddleware { public function __construct() { $this->priority = 50; // Set middleware priority } public function handle($request, \Closure $next) { // Modify the request $request = $request->withHeader('X-Custom-Header', 'custom-value'); // Call the next middleware $response = $next($request); // Modify the response return $response->withHeader('X-Response-Time', microtime(true) - LARAVEL_START); } } // Register your middleware app()->bind('api-model-relations.middleware.custom', function() { return new CustomMiddleware(); }); // Add your middleware to the pipeline config(['api-model-relations.middleware' => array_merge( config('api-model-relations.middleware', []), ['custom'] )]);
Error Handling
The package provides comprehensive error handling for API requests:
// Try to find a model that doesn't exist try { $product = Product::findOrFail(999); } catch (\MTechStack\LaravelApiModelClient\Exceptions\ModelNotFoundException $e) { // Handle not found exception logger()->error("Product not found: {$e->getMessage()}"); } // Try to create a model with validation errors try { $product = Product::create([ 'name' => '', // Required field 'price' => 'invalid' // Should be a number ]); } catch (\MTechStack\LaravelApiModelClient\Exceptions\ValidationException $e) { // Get validation errors $errors = $e->getErrors(); logger()->error("Validation errors: " . json_encode($errors)); } // Handle API connection errors try { $products = Product::all(); } catch (\MTechStack\LaravelApiModelClient\Exceptions\ApiConnectionException $e) { // Handle connection error logger()->error("API connection error: {$e->getMessage()}"); } // Get the last API response $lastResponse = ApiClient::getLastResponse(); $statusCode = $lastResponse->getStatusCode(); $body = $lastResponse->getBody()->getContents(); // Get the last API request $lastRequest = ApiClient::getLastRequest(); $method = $lastRequest->getMethod(); $uri = $lastRequest->getUri();
Advanced Usage
Local Database Integration
You can integrate API models with local database records:
use MTechStack\LaravelApiModelClient\Traits\SyncWithApi; use MTechStack\LaravelApiModelClient\Traits\MergesWithDatabase; use Illuminate\Database\Eloquent\Model; class Product extends Model { use SyncWithApi, MergesWithDatabase; protected $apiEndpoint = 'products'; protected $table = 'products'; // Specify which attributes should be stored only in the database protected $dbOnly = ['local_stock', 'last_checked_at']; // Specify which attributes should be sent to the API protected $apiOnly = ['name', 'description', 'price']; // Override the shouldMergeWithDatabase method to control when to merge public function shouldMergeWithDatabase() { return true; // Always merge with database } } // Usage $product = Product::find(1); // Gets from API and merges with local DB $product->local_stock = 10; // This will only be saved to the database $product->price = 149.99; // This will be saved to both API and database $product->save(); // Saves to both API and database in a transaction
Custom Response Transformers
You can transform API responses before they're converted to models:
use MTechStack\LaravelApiModelClient\Transformers\AbstractResponseTransformer; class CustomProductTransformer extends AbstractResponseTransformer { public function transform($response) { $data = json_decode($response->getBody()->getContents(), true); // Transform the data if (isset($data['products'])) { return $data['products']; } return $data; } } // Register your transformer app()->bind('api-model-relations.transformers.product', function() { return new CustomProductTransformer(); }); // Use your transformer in your model class Product extends ApiModel { use SyncWithApi; protected $apiEndpoint = 'products'; protected $responseTransformer = 'product'; }
API Mocking
For testing, you can mock API responses:
use MTechStack\LaravelApiModelClient\Testing\MocksApiResponses; use Tests\TestCase; class ProductTest extends TestCase { use MocksApiResponses; public function testGetProducts() { // Mock a response for GET /products $this->mockApiResponse('GET', 'products', [ 'data' => [ ['id' => 1, 'name' => 'Product 1', 'price' => 99.99], ['id' => 2, 'name' => 'Product 2', 'price' => 149.99], ] ]); // Now when Product::all() is called, it will use the mocked response $products = Product::all(); $this->assertCount(2, $products); $this->assertEquals('Product 1', $products[0]->name); } public function testCreateProduct() { // Mock a response for POST /products $this->mockApiResponse('POST', 'products', [ 'id' => 3, 'name' => 'New Product', 'price' => 199.99 ]); $product = Product::create([ 'name' => 'New Product', 'price' => 199.99 ]); $this->assertEquals(3, $product->id); $this->assertEquals('New Product', $product->name); } }
Performance Optimization
Tips for optimizing performance:
// Eager load relationships to reduce API calls $products = Product::with('category', 'reviews')->get(); // Select only the fields you need $products = Product::select(['id', 'name', 'price'])->get(); // Use pagination for large datasets $products = Product::paginate(20); // Use caching effectively $products = Product::withCacheTtl(3600)->get(); // Cache for 1 hour // Batch operations when possible $products = Product::whereIn('id', [1, 2, 3, 4, 5])->get(); // Use custom endpoints for specific operations $featuredProducts = Product::withEndpoint('products/featured')->get();
Troubleshooting
Common issues and solutions:
API Connection Issues
// Enable debug mode to see detailed request/response information config(['api-model-relations.debug' => true]); // Check the last request and response $lastRequest = ApiClient::getLastRequest(); $lastResponse = ApiClient::getLastResponse(); // Log all API requests and responses Event::listen(ApiRequestEvent::class, function ($event) { logger()->debug('API Request', [ 'method' => $event->method, 'endpoint' => $event->endpoint, 'options' => $event->options ]); }); Event::listen(ApiResponseEvent::class, function ($event) { logger()->debug('API Response', [ 'status' => $event->statusCode, 'body' => (string) $event->response->getBody() ]); });
Caching Issues
// Clear all cache \Illuminate\Support\Facades\Cache::store(config('api-model-relations.cache.store'))->flush(); // Disable caching temporarily config(['api-model-relations.cache.enabled' => false]); // Debug cache keys logger()->debug('Cache key: ' . Product::where('id', 1)->getCacheKey());
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
License
The MIT License (MIT). Please see License File for more information.