🧪 الدرس الأول: استخدام OpenWeatherMap API لعرض الطقس باستخدام Laravel HTTP Client

🧪 الدرس الأول: استخدام OpenWeatherMap API لعرض الطقس باستخدام Laravel HTTP Client

2025-08-19 وقت القراءه : 10 دقائق

نبدأ رحلتنا في تعلم استخدام Laravel HTTP Client بمثال واقعي بسيط: إنشاء أداة "طقس مباشر" تعرض حالة الطقس الحالية في مدينة غزة، بالاعتماد على OpenWeatherMap API.

حيث اذا كان هناك بيانات بدون أخطاء سنحصل على هذا الشكل

وفي حال وجود خطأ ان يظهر الشكل التالي مع رسالة الخطأ، حيث كما نلاحظ بالصوره أدناه أن الخطأ هو invalid API key




🔍 فهم بنية OpenWeatherMap API

في معظم الأحيان، يجب عليك مراجعة التوثيق الرسمي لأي API خارجي تستخدمه.

في حالة OpenWeatherMap، عند قراءة التوثيق الرسمي، فإن طريقة استدعاء حالة الطقس الحالية تكون كالتالي:

GET https://api.openweathermap.org/data/2.5/weather?lat=51.5074&lon=-0.1279&appid=YOUR_API_KEY&units=metric

حيث نلاحظ أنه يجب أن نحصل على api key ويتم ذلك من بعد التسجيل بالموقع ومن ثم my api keys

حسب التوثيق الرسمي لموقع openweather الإستجابه تكون بالشكل التالي

{
   "coord": {
      "lon": 7.367,
      "lat": 45.133
   },
   "weather": [
      {
         "id": 501,
         "main": "Rain",
         "description": "moderate rain",
         "icon": "10d"
      }
   ],
   "base": "stations",
   "main": {
      "temp": 284.2,
      "feels_like": 282.93,
      "temp_min": 283.06,
      "temp_max": 286.82,
      "pressure": 1021,
      "humidity": 60,
      "sea_level": 1021,
      "grnd_level": 910
   },
   "visibility": 10000,
   "wind": {
      "speed": 4.09,
      "deg": 121,
      "gust": 3.47
   },
   "rain": {
      "1h": 2.73
   },
   "clouds": {
      "all": 83
   },
   "dt": 1726660758,
   "sys": {
      "type": 1,
      "id": 6736,
      "country": "IT",
      "sunrise": 1726636384,
      "sunset": 1726680975
   },
   "timezone": 7200,
   "id": 3165523,
   "name": "Province of Turin",
   "cod": 200
}                    


🛠️ تنفيذ الكود خطوة بخطوة

1. إعداد المسار (Route)

// routes/web.php
use App\Http\Controllers\HomeController;
Route::get('/', [HomeController::class, 'index']);

2. إنشاء الكنترولر

namespace App\Http\Controllers;

use App\Services\WeatherService;

class HomeController extends Controller
{
    public function index(WeatherService $weatherService)
    {
        $weatherData = $weatherService->getCurrentWeather();
        return view('welcome', compact('weatherData'));
    }
}

📌 الشرح:

في هذا الكنترولر قمنا باستخدام تقنية الحقن (Dependency Injection) لتمرير كائن من WeatherService إلى الدالة index.

الدالة index() تستدعي getCurrentWeather() من كلاس الخدمة (Service)، ثم تُرسل النتيجة إلى واجهة العرض welcome.blade.php.

الهدف من هذا التصميم هو فصل منطق التعامل مع API عن الكنترولر، مما يجعل الكود أكثر تنظيمًا، قابلية للاختبار (testable)، وسهولة في الصيانة.

✅ ملاحظة مهمة:

يفضّل دائمًا عند التعامل مع واجهات خارجية (External APIs) أن تتم هذه العمليات داخل Service Classes مخصصة، وليس مباشرةً في الكنترولر.

ذلك لأن الكنترولر يجب أن يكون مسؤولًا فقط عن استقبال الطلبات وتوجيهها، وليس تنفيذ منطق الأعمال أو التواصل مع الخدمات الخارجية.


3. إنشاء كلاس الخدمة (WeatherService)

الفكرة هنا هي فصل منطق استدعاء الـ API داخل كلاس مستقل لجعل الكود نظيفًا وقابلًا لإعادة الاستخدام:

php artisan make:class Services/WeatherService
<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Exception;

class WeatherService
{
    private string $apiKey;
    private string $baseUrl;

    public function __construct()
    {
        $this->apiKey = config('services.openweather.key');
        $this->baseUrl = config('services.openweather.url');
    }

    public function getCurrentWeather(): array
    {
        $lat = 31.5017;
        $lon = 34.4668;

        if (empty($this->apiKey)) {
            return $this->getErrorResponse('OpenWeather API key not configured.');
        }

        try {
            $response = Http::get("{$this->baseUrl}/weather", [
                'lat'   => $lat,
                'lon'   => $lon,
                'appid' => $this->apiKey,
                'units' => 'metric',
            ]);

            if ($response->successful()) {
                $data = $response->json();
                return $this->formatWeatherData($data);
            }

            $this->logApiError($response);
            return $this->getErrorResponse("API error ({$response->status()}): " . ($response->json()['message'] ?? 'Unknown error'));

        } catch (Exception $e) {
            Log::error('Weather API exception: ' . $e->getMessage());
            return $this->getErrorResponse('Unexpected error occurred.');
        }
    }

    private function formatWeatherData(array $data): array
    {
        return [
            'success' => true,
            'city' => $data['name'] ?? 'Unknown',
            'country' => $data['sys']['country'] ?? '',
            'temperature' => round($data['main']['temp'] ?? 0),
            'feels_like' => round($data['main']['feels_like'] ?? 0),
            'description' => ucfirst($data['weather'][0]['description'] ?? 'No description'),
            'icon' => $data['weather'][0]['icon'] ?? '01d',
            'humidity' => $data['main']['humidity'] ?? 0,
            'pressure' => $data['main']['pressure'] ?? 0,
            'wind_speed' => $data['wind']['speed'] ?? 0,
            'visibility' => isset($data['visibility']) ? $data['visibility'] / 1000 : null,
            'clouds' => $data['clouds']['all'] ?? 0,
            'sunrise' => isset($data['sys']['sunrise']) ? date('H:i', $data['sys']['sunrise']) : null,
            'sunset' => isset($data['sys']['sunset']) ? date('H:i', $data['sys']['sunset']) : null,
        ];
    }

    private function logApiError($response): void
    {
        Log::warning("Weather API failed with status {$response->status()}.", [
            'body' => $response->body()
        ]);
    }

    private function getErrorResponse(string $message): array
    {
        return [
            'error'   => true,
            'message' => $message,
        ];
    }
}



شرح الكلاس: WeatherService

كلاس WeatherService مسؤول عن الاتصال بواجهة OpenWeather API للحصول على بيانات الطقس الحالية لموقع جغرافي محدد (هنا سوف أستخدم latitude, longitude ثابتين وهم لمدينة غزة، فلسطين).

🔹 1. __construct()

public function __construct()
{
    $this->apiKey = config('services.openweather.key');
    $this->baseUrl = config('services.openweather.url');
}

📌 الوصف:

كما نعلم الدالة __construct() هي دالة خاصة تُنفّذ تلقائيًا عند إنشاء كائن (Object) من هذا الكلاس، وتُستخدم هنا لتهيئة المتغيرات الأساسية المطلوبة للتعامل مع API.

في هذا السياق، نقوم بتخزين:

  • apiKey: مفتاح الوصول الخاص بـ OpenWeather API
  • baseUrl: الرابط الأساسي (Base URL) لاستدعاء نقاط النهاية (Endpoints)


وفي ملف config/service.php نقوم بتعريف openweather

    'openweather' => [
        'key' => env('OPENWEATHER_API_KEY'),
        'url' => env('OPENWEATHER_BASE_URL'),
    ],

وفي ملف .env نقوم بتعريف ووضع قيم المتغيرات

OPENWEATHER_API_KEY=YOUR_API_KEY //from openWeatherMap
OPENWEATHER_BASE_URL=https://api.openweathermap.org/data/2.5

✅ لماذا نستخدم config/services.php بدلًا من استخدام env() مباشرة في الكود؟

نحن نستخدم ملف config/services.php كوسيط بين الكود وملف .env، وذلك لأسباب مهمة:

📌 1. تنظيم الكود

بدل ما نكتب env() في كل مكان في الكود، نضعه مرة واحدة في ملف الإعدادات (services.php)، ثم نستدعي القيم عن طريق config()، مثل:

$this->apiKey = config('services.openweather.key');

هذا يجعل الكود أنظف وأسهل للفهم والصيانة.


📌 2. سهولة التبديل بين البيئات

عند تشغيل المشروع في بيئات مختلفة (مثل: محلي - سيرفر - بيئة اختبار)، نغير القيم فقط في ملف .env دون لمس الكود.


📌 3. التوافق مع التخزين المؤقت للإعدادات

Laravel يسمح بتسريع التطبيق بأمر:

php artisan config:cache

✅ هذا الأمر يعمل فقط إذا كانت القيم موجودة في ملفات config وليس في الكود مباشرة.

❌ أما إذا استخدمنا env() داخل الكود، فقد لا تعمل القيم بشكل صحيح بعد تنفيذ هذا الأمر.


🔹 2. getCurrentWeather()

public function getCurrentWeather(): array
{
    // إحداثيات الموقع الجغرافي - هنا نستخدم غزة (خط العرض والطول)
    $lat = 31.5017;
    $lon = 34.4668;

    // تحقق من وجود مفتاح API، إذا لم يكن موجودًا نُعيد رسالة خطأ منظمة
    if (empty($this->apiKey)) {
        return $this->getErrorResponse('OpenWeather API key not configured.');
    }

    try {
        // تنفيذ طلب HTTP GET إلى واجهة OpenWeather API مع إرسال المعاملات المطلوبة
        $response = Http::get("{$this->baseUrl}/weather", [
            'lat'   => $lat,                // خط العرض
            'lon'   => $lon,                // خط الطول
            'appid' => $this->apiKey,       // مفتاح API للمصادقة
            'units' => 'metric',            // استخدام النظام المتري (درجة الحرارة بالـ°C)
        ]);
//حيث سيقوم بتنفيذ الطلب 
//GET https://api.openweathermap.org/data/2.5/weather?lat=51.5074&lon=-0.1279&appid=YOUR_KEY&units=metric

        // إذا كانت الاستجابة ناجحة (كود الحالة 2xx)
        if ($response->successful()) {
            // تحويل البيانات من JSON إلى مصفوفة PHP
            $data = $response->json();
            // تمرير البيانات إلى دالة formatWeatherData لتنسيقها قبل عرضها
            return $this->formatWeatherData($data);
        }

        // إذا لم تكن الاستجابة ناجحة، نقوم بتسجيل الخطأ في سجل النظام
        $this->logApiError($response);

        // نُعيد للمستخدم رسالة خطأ واضحة بناءً على كود الحالة والرسالة القادمة من API
        return $this->getErrorResponse("API error ({$response->status()}): " . ($response->json()['message'] ?? 'Unknown error'));

    } catch (Exception $e) {
        // في حال حدث استثناء غير متوقع أثناء الطلب أو أثناء تحليل الاستجابة
        Log::error('Weather API exception: ' . $e->getMessage());

        // نُعيد رسالة خطأ عامة للمستخدم بدلًا من تعطل التطبيق
        return $this->getErrorResponse('Unexpected error occurred.');
    }

    // نستخدم try-catch لضمان استقرار التطبيق والتعامل مع أي أخطاء غير متوقعة
}

📌 الوصف:

هذه هي الدالة الأساسية التي تنفذ الاتصال الفعلي مع OpenWeather API.

تقوم بما يلي:

  • 🔹 الدالة getCurrentWeather() مسؤولة عن تنفيذ الاتصال مع OpenWeather API، وتشمل المهام التالية:
  • ✅ التحقق من وجود مفتاح API قبل بدء الاتصال.
  • 🌐 إرسال طلب GET إلى المسار /weather مع تمرير إحداثيات الموقع (خط الطول والعرض) كوحدات استعلام (Query Parameters).
  • 📥 معالجة استجابة الخادم (API Response):
  • إذا كانت الاستجابة ناجحة ✅، يتم تحويل البيانات وتنسيقها عبر الدالة formatWeatherData() ثم إرجاعها.
  • إذا فشلت ❌، يتم تسجيل الخطأ عبر logApiError() وإرجاع استجابة خطأ مناسبة باستخدام getErrorResponse().
  • 🛡️ استخدام try-catch لضبط أي استثناء غير متوقع (مثل فشل الاتصال أو خطأ داخلي).
  • 🧾 تسجيل الأحداث (سواء نجاح أو فشل) باستخدام Laravel Log لتسهيل التتبع والتحليل.
  • 📦 الفصل بين منطق الاتصال (HTTP Call) ومنطق تنسيق البيانات (Data Formatting).
  • 📤 إرجاع استجابة منظمة وموحدة في حالة الخطأ لتسهيل عرضها في الواجهة.


🔹 3. formatWeatherData(array $data)

private function formatWeatherData(array $data): array
{
    return [
        'success'     => true, // توضيح أن العملية تمت بنجاح
        'city'        => $data['name'] ?? 'Unknown', // اسم المدينة أو "Unknown" إذا لم يكن موجودًا
        'country'     => $data['sys']['country'] ?? '', // رمز الدولة
        'temperature' => round($data['main']['temp'] ?? 0), // درجة الحرارة الحالية
        'feels_like'  => round($data['main']['feels_like'] ?? 0), // درجة الحرارة المحسوسة
        'description' => ucfirst($data['weather'][0]['description'] ?? 'No description'), // وصف الطقس
        'icon'        => $data['weather'][0]['icon'] ?? '01d', // رمز الأيقونة لعرض صورة الطقس
        'humidity'    => $data['main']['humidity'] ?? 0, // نسبة الرطوبة
        'pressure'    => $data['main']['pressure'] ?? 0, // الضغط الجوي
        'wind_speed'  => $data['wind']['speed'] ?? 0, // سرعة الرياح
        'visibility'  => isset($data['visibility']) ? $data['visibility'] / 1000 : null, // مدى الرؤية (تحويله من متر إلى كم)
        'clouds'      => $data['clouds']['all'] ?? 0, // نسبة الغيوم في السماء
        'sunrise'     => isset($data['sys']['sunrise']) ? date('H:i', $data['sys']['sunrise']) : null, // وقت الشروق
        'sunset'      => isset($data['sys']['sunset']) ? date('H:i', $data['sys']['sunset']) : null, // وقت الغروب
    ];
}

📌 الوصف:

  • تقوم هذه الدالة باستخلاص وتنسيق المعلومات الأساسية فقط من الاستجابة الكاملة لـ OpenWeather API.
  • الهدف منها هو تجهيز البيانات بشكل منظم وسهل الاستخدام في الواجهة (Blade أو JSON).
  • يتم استخدام عامل الدمج Null (??) لتوفير قيم افتراضية في حال غياب بعض الحقول — وهي أفضل ممارسة عند التعامل مع واجهات خارجية، لأن بعض الحقول قد تكون مفقودة في بعض الحالات.


🔹 4. logApiError($response)

private function logApiError($response): void
{
    Log::warning("Weather API failed with status {$response->status()}.", [
        'body' => $response->body()
    ]);
}

📌 الوصف:

هذه الدالة مسؤولة عن توثيق أي خطأ يحدث أثناء استدعاء OpenWeather API في ملفات السجلات (storage/logs/laravel.log).

يتم تسجيل:

  • رمز الحالة (Status Code) مثل: 401, 500, 404... لتحديد نوع الخطأ.
  • نص الجسم الكامل (Response Body) لمعرفة تفاصيل الخطأ التي أرجعها الـ API.


✅ استخدمنا Log::warning() بدلًا من Log::error() لأن هذا النوع من الخطأ قادم من خدمة خارجية (وليس من داخل النظام)، وغالبًا لا يتطلب توقف التطبيق، لكنه يستحق المتابعة والتحقيق.

🧠 فائدة هذا التوثيق:

يساعد في تحليل الأسباب إذا توقفت بيانات الطقس عن العمل، دون التأثير على المستخدم.


🔹 5. getErrorResponse(string $message)

private function getErrorResponse(string $message): array
{
    return [
        'error'   => true,           // توضيح أن هذه الاستجابة تمثل حالة فشل
        'message' => $message,       // رسالة الخطأ التي سيتم عرضها للمستخدم أو تسجيلها
    ];
}

📌 الوصف:

تُستخدم هذه الدالة لإنشاء هيكل موحد لاستجابات الخطأ التي تُعاد من الدالة getCurrentWeather() في حال فشل الاتصال بالـ API أو حدوث استثناء.

تُرجع مصفوفة تحتوي على:

  • error = true: تشير إلى أن العملية لم تتم بنجاح.
  • message: رسالة توضح سبب الخطأ، يمكن عرضها مباشرة في واجهة المستخدم أو استخدامها في سجل الأخطاء (logs).


✅ الفائدة:

  • تبسيط التعامل مع الأخطاء داخل التطبيق.
  • توحيد شكل استجابات الخطأ، مما يسهل عرضها وتنسيقها في الواجهة.
  • يمكن توسيع هذه الدالة لاحقًا لتشمل عناصر إضافية مثل code أو details بدون التأثير على باقي أجزاء النظام.




4. إعداد مفاتيح API في Laravel

ملف .env

OPENWEATHER_API_KEY=your_api_key_here
OPENWEATHER_BASE_URL=https://api.openweathermap.org/data/2.5

ملف config/services.php

'openweather' => [
    'key' => env('OPENWEATHER_API_KEY'),
    'url' => env('OPENWEATHER_BASE_URL'),
],


✅ التحقق من حالة الاستجابة

Laravel يوفر دوال جاهزة للتعامل مع استجابات HTTP:

$response->successful(); // 2xx
$response->ok(); // 200
$response->clientError(); // 4xx
$response->serverError(); // 5xx
$response->status(); // كود الحالة مثل 200، 404، إلخ


🌤️ عرض البيانات في الواجهة (Blade)

<div class="container">
    <div class="row justify-content-center">
        <div class="col-lg-8 col-md-10">
            <div class="weather-card">
                @if(isset($weatherData['error']) && $weatherData['error'] === true)
                    {{-- حالة الخطأ --}}
                    <div class="bg-white rounded shadow p-4 text-center">
                        <div class="display-3 text-danger mb-3">⚠️</div>
                        <h2 class="h3 text-danger fw-bold mb-3">Weather Service Error</h2>
                        <p class="lead text-muted mb-4">Unable to fetch weather data</p>


                        <div class="alert alert-danger text-start" role="alert">
                            <strong>{{ $weatherData['message'] ?? 'Unknown error occurred' }}</strong>
                        </div>
                    </div>
                @else
                    {{-- حالة النجاح --}}
                    <div class="weather-header">
                        <div class="weather-icon">
                            @if(isset($weatherData['icon']))
                                <img src="https://openweathermap.org/img/wn/{{ $weatherData['icon'] }}@4x.png"
                                     alt="{{ $weatherData['description'] ?? 'Weather' }}"
                                     class="weather-icon">
                            @endif
                        </div>


                        <h1 class="city-name" id="cityName">{{ $weatherData['city'] }} - {{ $weatherData['country'] }}</h1>
                        <p class="temperature" id="temperature">{{ round($weatherData['temperature'] ?? 0) }}°</p>
                        <p class="weather-description" id="weatherDescription">{{ $weatherData['description'] ?? 'غير متوفر' }}</p>
                    </div>


                    <div class="weather-stats">
                        <div class="row g-4">
                            <div class="col-md-4">
                                <div class="stat-card">
                                    <i class="fas fa-tint stat-icon humidity-icon"></i>
                                    <div class="stat-value">{{ $weatherData['humidity'] ?? '--' }}%</div>
                                    <div class="stat-label">الرطوبة</div>
                                </div>
                            </div>


                            <div class="col-md-4">
                                <div class="stat-card">
                                    <i class="fas fa-thermometer-half stat-icon pressure-icon"></i>
                                    <div class="stat-value">{{ $weatherData['pressure'] ?? '--' }}</div>
                                    <div class="stat-label">الضغط الجوي (hPa)</div>
                                </div>
                            </div>


                            <div class="col-md-4">
                                <div class="stat-card">
                                    <i class="fas fa-wind stat-icon wind-icon"></i>
                                    <div class="stat-value">{{ $weatherData['wind_speed'] ?? '--' }}</div>
                                    <div class="stat-label">سرعة الرياح (km/h)</div>
                                </div>
                            </div>
                        </div>
                    </div>
                @endif


            </div>
        </div>
    </div>
</div>

 

📌 كما نلاحظ في ملف Blade، قمنا بالتحقق أولًا مما إذا كانت هناك بيانات ($weatherData) ثم تأكدنا من وجود خطأ داخل هذه البيانات من خلال:

@if(isset($weatherData['error']) && $weatherData['error'] === true)

✅ هذا الشرط يُستخدم لتحديد ما إذا كانت الاستجابة تحتوي على خطأ (أي أن جلب الطقس فشل).

إذا تحقق الشرط، يتم عرض رسالة تنبيه للمستخدم تفيد بوجود مشكلة في الاتصال بواجهة الطقس.



🧠 الخلاصة

هذا الدرس يُعدّ حجر الأساس للتعامل مع أي API خارجي في Laravel. باستخدام Http::get() وتنظيم الكود في Service Classes:

✅ الفوائد التعليمية من هذا الدرس:

  • - فهم كيفية استخدام Laravel HTTP Client مع APIs خارجية
  • - تنظيم الكود باستخدام Service Classes
  • - التعامل مع أخطاء الشبكة واستجابات API الغير متوقعة
  • - استخدام ملفات البيئة `.env` بطريقة آمنة
  • - عرض البيانات بطريقة ديناميكية باستخدام Blade


📚 تابع الدورة لتوسيع مهاراتك في العمل مع Laravel وواجهات الـ API الاحترافية.


يمكنك استعراض وتنزيل المشروع عبر GitHub من خلال الرابط التالي:

إضافة تعليق
Loading...