terminaldz/laravel-yalidine
Composer 安装命令:
composer require terminaldz/laravel-yalidine
包简介
Laravel package for integrating with the Yalidine delivery API
README 文档
README
A Laravel package for integrating with the Yalidine delivery API. This package provides a clean, fluent interface for managing parcels, tracking shipments, retrieving location data, and calculating delivery fees through the Yalidine REST API.
Features
- 🚀 Simple and intuitive API
- 📦 Complete parcel management (create, retrieve, update, delete)
- 📍 Location data retrieval (wilayas, communes, centers)
- 📊 Delivery history tracking
- 💰 Fee calculation with weight and dimension support
- ⚡ Rate limit handling and monitoring
- 🔄 Pagination support for large datasets
- ✅ Comprehensive validation
- 🧪 Testing utilities included
- 📝 Full Laravel integration (Service Provider, Facade, DI)
- 🔒 Secure credential management
- 📋 Detailed error messages and exceptions
Requirements
- PHP >= 8.1
- Laravel >= 10.0
- Guzzle >= 7.0
Installation
Install the package via Composer:
composer require terminaldz/laravel-yalidine
Publish Configuration
Publish the configuration file:
php artisan vendor:publish --provider="Terminaldz\LaravelYalidine\YalidineServiceProvider"
This will create a config/yalidine.php configuration file.
Environment Variables
Add your Yalidine API credentials to your .env file:
YALIDINE_API_ID=your_api_id_here YALIDINE_API_TOKEN=your_api_token_here YALIDINE_BASE_URL=https://api.yalidine.app/v1
You can obtain your API credentials from your Yalidine dashboard.
Configuration
The config/yalidine.php file contains all configuration options:
return [ // API Credentials 'api_id' => env('YALIDINE_API_ID'), 'api_token' => env('YALIDINE_API_TOKEN'), 'base_url' => env('YALIDINE_BASE_URL', 'https://api.yalidine.app/v1'), // HTTP Client Options 'timeout' => env('YALIDINE_TIMEOUT', 30), 'retry_times' => env('YALIDINE_RETRY_TIMES', 3), 'retry_delay' => env('YALIDINE_RETRY_DELAY', 1000), // Rate Limit Settings 'rate_limit' => [ 'log_warnings' => true, 'warning_threshold' => 10, ], // Caching 'cache' => [ 'enabled' => true, 'ttl' => 3600, 'prefix' => 'yalidine', ], // Logging 'logging' => [ 'enabled' => env('YALIDINE_LOGGING', true), 'channel' => env('YALIDINE_LOG_CHANNEL', 'stack'), ], ];
Basic Usage
Using the Facade
use Terminaldz\LaravelYalidine\Facades\Yalidine; // Create a parcel $result = Yalidine::parcels()->create([ 'order_id' => 'ORDER-12345', 'from_wilaya_name' => 'Alger', 'firstname' => 'Ahmed', 'familyname' => 'Benali', 'contact_phone' => '0550123456', 'address' => 'Cité des Martyrs, Bâtiment A', 'to_commune_name' => 'Bordj El Kiffan', 'to_wilaya_name' => 'Alger', 'product_list' => 'Electronics - Smartphone', 'price' => 5000, 'do_insurance' => true, 'declared_value' => 5000, 'length' => 30, 'width' => 20, 'height' => 10, 'weight' => 2, 'freeshipping' => false, 'is_stopdesk' => false, 'has_exchange' => false, ]); echo "Tracking Number: " . $result->tracking; echo "Label URL: " . $result->label;
Using Dependency Injection
use Terminaldz\LaravelYalidine\Client\YalidineClient; class ShippingController extends Controller { public function __construct( private YalidineClient $yalidine ) {} public function createShipment(Request $request) { $result = $this->yalidine->parcels()->create([ 'order_id' => $request->order_id, 'firstname' => $request->firstname, 'familyname' => $request->familyname, // ... other fields ]); return response()->json([ 'tracking' => $result->tracking, 'label' => $result->label, ]); } }
Usage Examples
Parcel Management
Create a Single Parcel
$result = Yalidine::parcels()->create([ 'order_id' => 'ORDER-12345', 'from_wilaya_name' => 'Alger', 'firstname' => 'Ahmed', 'familyname' => 'Benali', 'contact_phone' => '0550123456', 'address' => 'Cité des Martyrs', 'to_commune_name' => 'Bordj El Kiffan', 'to_wilaya_name' => 'Alger', 'product_list' => 'Electronics', 'price' => 5000, 'do_insurance' => true, 'declared_value' => 5000, 'length' => 30, 'width' => 20, 'height' => 10, 'weight' => 2, 'freeshipping' => false, 'is_stopdesk' => false, 'has_exchange' => false, ]);
Create Multiple Parcels (Batch)
$parcels = [ [ 'order_id' => 'ORDER-001', 'firstname' => 'Ahmed', // ... other fields ], [ 'order_id' => 'ORDER-002', 'firstname' => 'Fatima', // ... other fields ], ]; $results = Yalidine::parcels()->createBatch($parcels);
Retrieve a Parcel
$parcel = Yalidine::parcels()->get('yal-123456'); echo $parcel->tracking; echo $parcel->lastStatus; echo $parcel->dateCreation->format('Y-m-d H:i');
List Parcels with Filters
$parcels = Yalidine::parcels()->list([ 'status' => 'Livré', 'wilaya' => 16, 'from_date' => '2025-01-01', 'to_date' => '2025-01-31', 'page' => 1, 'page_size' => 50, ]); foreach ($parcels->items() as $parcel) { echo $parcel->tracking . ': ' . $parcel->lastStatus . "\n"; } if ($parcels->hasNextPage()) { $nextUrl = $parcels->getNextPageUrl(); }
Update a Parcel
// Only parcels with status "en préparation" can be updated $parcel = Yalidine::parcels()->update('yal-123456', [ 'contact_phone' => '0551234567', 'address' => 'New Address', ]);
Delete a Parcel
// Only parcels with status "en préparation" can be deleted $deleted = Yalidine::parcels()->delete('yal-123456');
Location Data
Get All Wilayas
$wilayas = Yalidine::locations()->wilayas(); foreach ($wilayas->items() as $wilaya) { echo $wilaya->id . ': ' . $wilaya->name . "\n"; }
Get Communes for a Wilaya
$communes = Yalidine::locations()->communes([ 'wilaya_id' => 16, ]); foreach ($communes->items() as $commune) { echo $commune->name . ' - Delivery time: ' . $commune->deliveryTimeParcel . ' days' . "\n"; }
Get Communes with Stop Desks
$stopDeskCommunes = Yalidine::locations()->communes([ 'wilaya_id' => 16, 'has_stop_desk' => true, ]);
Get Centers
$centers = Yalidine::locations()->centers([ 'wilaya_id' => 16, 'commune_id' => 1630, ]); foreach ($centers->items() as $center) { echo $center->name . ' - ' . $center->address . "\n"; echo 'GPS: ' . $center->getLatitude() . ', ' . $center->getLongitude() . "\n"; }
Delivery History
Get History for a Parcel
$history = Yalidine::histories()->get('yal-123456'); foreach ($history as $status) { echo $status->dateStatus->format('Y-m-d H:i') . ': '; echo $status->status; if ($status->reason) { echo ' (' . $status->reason . ')'; } echo "\n"; }
List Histories with Filters
$histories = Yalidine::histories()->list([ 'tracking' => 'yal-123456', 'status' => 'Livré', 'from_date' => '2025-01-01', 'to_date' => '2025-01-31', ]);
Fee Calculation
Calculate Fees Between Wilayas
$fees = Yalidine::fees()->calculate( fromWilayaId: 5, // Batna toWilayaId: 16 // Alger ); echo "Zone: " . $fees->zone . "\n"; echo "COD Percentage: " . $fees->codPercentage . "%\n"; echo "Insurance Percentage: " . $fees->insurancePercentage . "%\n"; // Get fee for a specific commune $communeFee = $fees->getFeeForCommune(1630); echo "Express Home: " . $communeFee->expressHome . " DA\n"; echo "Express Stop Desk: " . $communeFee->expressStopDesk . " DA\n";
Calculate Total Fee Including Weight
$total = Yalidine::fees()->calculateTotal([ 'from_wilaya_id' => 5, 'to_wilaya_id' => 16, 'commune_id' => 1630, 'price' => 5000, 'declared_value' => 5000, 'weight' => 7, 'length' => 30, 'width' => 20, 'height' => 10, 'delivery_type' => 'express_home', ]); echo "Total Fee: " . $total . " DA";
Rate Limit Monitoring
$rateLimits = Yalidine::getRateLimitInfo(); echo "Second quota left: " . $rateLimits['second'] . "\n"; echo "Minute quota left: " . $rateLimits['minute'] . "\n"; echo "Hour quota left: " . $rateLimits['hour'] . "\n"; echo "Day quota left: " . $rateLimits['day'] . "\n";
Error Handling
The package provides specific exceptions for different error scenarios:
use Terminaldz\LaravelYalidine\Exceptions\ValidationException; use Terminaldz\LaravelYalidine\Exceptions\RateLimitExceededException; use Terminaldz\LaravelYalidine\Exceptions\AuthenticationException; use Terminaldz\LaravelYalidine\Exceptions\YalidineException; try { $result = Yalidine::parcels()->create($parcelData); } catch (ValidationException $e) { // Handle validation errors foreach ($e->getErrors() as $field => $errors) { echo "$field: " . implode(', ', $errors) . "\n"; } } catch (RateLimitExceededException $e) { // Handle rate limit $retryAfter = $e->getRetryAfter(); Log::warning("Rate limit exceeded. Retry after {$retryAfter} seconds"); } catch (AuthenticationException $e) { // Handle authentication errors Log::error('Invalid API credentials: ' . $e->getMessage()); } catch (YalidineException $e) { // Handle other Yalidine errors Log::error('Yalidine API error: ' . $e->getMessage()); }
Testing
The package includes testing utilities to help you test your integration without making real API calls:
use Terminaldz\LaravelYalidine\Facades\Yalidine; use Terminaldz\LaravelYalidine\DataTransferObjects\Parcel; // In your test Yalidine::fake([ 'parcels.create' => [ 'tracking' => 'yal-TEST123', 'order_id' => 'ORDER-12345', 'label' => 'https://example.com/label.pdf', 'success' => true, ], ]); // Your code that uses Yalidine $result = Yalidine::parcels()->create([...]); // Assert expectations Yalidine::assertParcelCreated(); Yalidine::assertParcelCreated(function ($data) { return $data['order_id'] === 'ORDER-12345'; });
Advanced Usage
Fluent Query Builder
$parcels = Yalidine::parcels() ->query() ->whereStatus('Livré') ->whereWilaya(16) ->whereDateBetween('2025-01-01', '2025-01-31') ->whereOrderId('ORDER-12345') ->paginate(50);
Caching Location Data
Location data is automatically cached based on your configuration. You can manually clear the cache:
// Clear all Yalidine cache Cache::tags(['yalidine'])->flush(); // Or use the cache prefix from config $prefix = config('yalidine.cache.prefix'); Cache::forget("{$prefix}:wilayas");
Common Use Cases
E-commerce Integration
// In your order processing logic public function processOrder(Order $order) { try { // Create parcel when order is confirmed $result = Yalidine::parcels()->create([ 'order_id' => $order->id, 'from_wilaya_name' => config('shop.wilaya'), 'firstname' => $order->customer->first_name, 'familyname' => $order->customer->last_name, 'contact_phone' => $order->customer->phone, 'address' => $order->shipping_address, 'to_commune_name' => $order->commune, 'to_wilaya_name' => $order->wilaya, 'product_list' => $order->items->pluck('name')->implode(', '), 'price' => $order->total_with_shipping, 'do_insurance' => $order->total > 10000, 'declared_value' => $order->total, 'freeshipping' => $order->has_free_shipping, ]); // Save tracking number $order->update([ 'tracking_number' => $result->tracking, 'shipping_label_url' => $result->label, ]); // Notify customer $order->customer->notify(new ShipmentCreated($result->tracking)); } catch (ValidationException $e) { Log::error('Invalid order data for Yalidine', [ 'order_id' => $order->id, 'errors' => $e->getErrors(), ]); throw $e; } }
Tracking Status Updates
// Create a scheduled job to update parcel statuses public function handle() { $pendingOrders = Order::whereIn('status', ['shipped', 'in_transit'])->get(); foreach ($pendingOrders as $order) { try { $parcel = Yalidine::parcels()->get($order->tracking_number); // Update order status based on parcel status if ($parcel->lastStatus === 'Livré') { $order->markAsDelivered(); $order->customer->notify(new OrderDelivered($order)); } elseif (in_array($parcel->lastStatus, ['Échec', 'Retour'])) { $order->markAsFailed(); $order->customer->notify(new DeliveryFailed($order, $parcel->lastStatus)); } } catch (YalidineException $e) { Log::warning('Failed to update tracking for order', [ 'order_id' => $order->id, 'error' => $e->getMessage(), ]); } } }
Dynamic Fee Calculator
// Calculate shipping fees in real-time during checkout public function calculateShipping(Request $request) { $validated = $request->validate([ 'wilaya_id' => 'required|integer', 'commune_id' => 'required|integer', 'cart_total' => 'required|numeric', ]); try { $fees = Yalidine::fees()->calculate( fromWilayaId: config('shop.wilaya_id'), toWilayaId: $validated['wilaya_id'] ); $communeFee = $fees->getFeeForCommune($validated['commune_id']); return response()->json([ 'express_home' => $communeFee->expressHome, 'express_stopdesk' => $communeFee->expressStopDesk, 'classic_home' => $communeFee->classicHome, 'classic_stopdesk' => $communeFee->classicStopDesk, ]); } catch (YalidineException $e) { return response()->json([ 'error' => 'Unable to calculate shipping fees', ], 500); } }
Location Dropdown Population
// Populate location dropdowns for checkout form public function getLocations(Request $request) { // Cache wilayas for 24 hours $wilayas = Cache::remember('yalidine:wilayas', 86400, function () { return Yalidine::locations()->wilayas()->items(); }); // Get communes for selected wilaya if ($request->has('wilaya_id')) { $communes = Cache::remember( "yalidine:communes:{$request->wilaya_id}", 86400, function () use ($request) { return Yalidine::locations()->communes([ 'wilaya_id' => $request->wilaya_id, ])->items(); } ); return response()->json([ 'wilayas' => $wilayas, 'communes' => $communes, ]); } return response()->json(['wilayas' => $wilayas]); }
Error Handling Patterns
Graceful Degradation
try { $result = Yalidine::parcels()->create($parcelData); return redirect()->route('orders.show', $order)->with('success', 'Shipment created'); } catch (ValidationException $e) { // Show validation errors to user return back()->withErrors($e->getErrors())->withInput(); } catch (RateLimitExceededException $e) { // Queue for retry CreateYalidineParcel::dispatch($order)->delay(now()->addSeconds($e->getRetryAfter())); return back()->with('warning', 'Shipment will be created shortly'); } catch (YalidineException $e) { // Log and show generic error Log::error('Yalidine API error', ['error' => $e->getMessage()]); return back()->with('error', 'Unable to create shipment. Please try again.'); }
Retry Logic with Queues
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; class CreateYalidineParcel implements ShouldQueue { use InteractsWithQueue, SerializesModels; public $tries = 3; public $backoff = [60, 300, 900]; // 1 min, 5 min, 15 min public function __construct( public Order $order ) {} public function handle() { try { $result = Yalidine::parcels()->create([ 'order_id' => $this->order->id, // ... other fields ]); $this->order->update([ 'tracking_number' => $result->tracking, 'shipping_label_url' => $result->label, ]); } catch (RateLimitExceededException $e) { // Release back to queue with delay $this->release($e->getRetryAfter()); } catch (ValidationException $e) { // Don't retry validation errors $this->fail($e); } } }
Monitoring Rate Limits
// Create a middleware to monitor rate limits class MonitorYalidineRateLimits { public function handle($request, Closure $next) { $response = $next($request); // Check rate limits after each request $rateLimits = Yalidine::getRateLimitInfo(); // Alert if any quota is running low foreach ($rateLimits as $period => $remaining) { if ($remaining < 10) { Log::warning("Yalidine {$period} quota running low", [ 'remaining' => $remaining, ]); // Send alert to admin if ($remaining < 5) { Notification::route('slack', config('services.slack.webhook')) ->notify(new YalidineQuotaLow($period, $remaining)); } } } return $response; } }
Validation Rules
The package validates data before sending to the API. Here are the validation rules:
Phone Number Validation
- Mobile: Must start with 0 followed by 9 digits (e.g., 0550123456)
- Landline: Must start with 0 followed by 8 digits (e.g., 021123456)
Price and Value Validation
price: Must be between 0 and 150,000 DAdeclared_value: Must be between 0 and 150,000 DA
Dimension Validation
length,width,height: Must be >= 0weight: Must be >= 0
Conditional Validation
- If
is_stopdeskis true,stopdesk_idis required - If
has_exchangeis true,product_to_collectis required
Performance Tips
1. Cache Location Data
Location data (wilayas, communes, centers) rarely changes. Cache it aggressively:
// In your AppServiceProvider public function boot() { // Warm up cache on application boot Cache::remember('yalidine:wilayas', 86400, function () { return Yalidine::locations()->wilayas()->items(); }); }
2. Use Batch Operations
When creating multiple parcels, use batch creation:
// Instead of this: foreach ($orders as $order) { Yalidine::parcels()->create($order->toParcelData()); } // Do this: $parcelsData = $orders->map->toParcelData()->toArray(); Yalidine::parcels()->createBatch($parcelsData);
3. Queue Heavy Operations
Queue operations that aren't time-sensitive:
// Queue parcel creation CreateYalidineParcel::dispatch($order); // Queue status updates UpdateParcelStatuses::dispatch()->everyFiveMinutes();
4. Optimize Pagination
Use appropriate page sizes based on your needs:
// For UI display (smaller pages) $parcels = Yalidine::parcels()->list(['page_size' => 20]); // For batch processing (larger pages) $parcels = Yalidine::parcels()->list(['page_size' => 500]);
Troubleshooting
Authentication Errors
Problem: Getting 401 Unauthorized errors
Solution:
- Verify your credentials in
.envfile - Ensure credentials are published to config:
php artisan config:clear - Check that credentials are correct in Yalidine dashboard
Rate Limit Errors
Problem: Getting 429 Too Many Requests errors
Solution:
- Implement retry logic with exponential backoff
- Monitor rate limits using
getRateLimitInfo() - Use queues to spread requests over time
- Contact Yalidine to increase your quota
Validation Errors
Problem: Getting validation errors when creating parcels
Solution:
- Check phone number format (0 + 9 digits for mobile)
- Verify price is within 0-150,000 range
- Ensure required fields are present
- Check conditional requirements (stopdesk_id, product_to_collect)
Connection Timeouts
Problem: Requests timing out
Solution:
- Increase timeout in config:
'timeout' => 60 - Check your network connection
- Verify Yalidine API is accessible
- Use queues for non-critical operations
API Reference
Parcel Resource
create(array $parcelData): ParcelCreationResult
Create a single parcel.
createBatch(array $parcels): array
Create multiple parcels in one request.
get(string $tracking): Parcel
Retrieve a parcel by tracking number.
list(array $filters = []): PaginatedResponse
List parcels with optional filters.
update(string $tracking, array $data): Parcel
Update a parcel (only if status is "en préparation").
delete(string $tracking): bool
Delete a parcel (only if status is "en préparation").
query(): ParcelQueryBuilder
Get a fluent query builder for parcels.
Location Resource
wilayas(array $filters = []): PaginatedResponse
Get all wilayas.
getWilaya(int $id): Wilaya
Get a specific wilaya by ID.
communes(array $filters = []): PaginatedResponse
Get communes with optional filters (wilaya_id, has_stop_desk).
getCommune(int $id): Commune
Get a specific commune by ID.
centers(array $filters = []): PaginatedResponse
Get centers with optional filters (wilaya_id, commune_id, center_id).
getCenter(int $centerId): Center
Get a specific center by ID.
History Resource
get(string $tracking): array
Get delivery history for a specific tracking number.
list(array $filters = []): PaginatedResponse
List histories with optional filters.
query(): HistoryQueryBuilder
Get a fluent query builder for histories.
Fee Resource
calculate(int $fromWilayaId, int $toWilayaId): FeeCalculation
Calculate fees between two wilayas.
calculateTotal(array $parcelSpecs): float
Calculate total fee including weight, insurance, and COD.
Documentation
- Quick Start Guide - Get started in 5 minutes
- API Reference - Complete API reference for all classes and methods
- Usage Examples - Practical examples for common use cases
- Testing Guide - Comprehensive testing documentation
External Resources
Contributing
Please see CONTRIBUTING.md for details on how to contribute to this package.
Changelog
Please see CHANGELOG.md for recent changes.
Security
If you discover any security-related issues, please email boukemoucheidriss@gmail.com instead of using the issue tracker.
Credits
Support
For issues, questions, or contributions, please visit our GitHub repository.
License
This package is open-sourced software licensed under the MIT license.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 1
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-10-09