myopensoft / laravel-jakim-esolat-api
Laravel package for JAKIM e-Solat prayer times API - fetch, cache, and manage Malaysian prayer times
Package info
gitlab.com/myopensoft/laravel-jakim-esolat-api
pkg:composer/myopensoft/laravel-jakim-esolat-api
Requires
- php: ^8.3
- illuminate/cache: ^11.0||^12.0
- illuminate/contracts: ^11.0||^12.0
- illuminate/database: ^11.0||^12.0
- illuminate/http: ^11.0||^12.0
- illuminate/log: ^11.0||^12.0
- illuminate/support: ^11.0||^12.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
README
A Laravel package that wraps the JAKIM (Jabatan Kemajuan Islam Malaysia) e-Solat API to fetch, cache, and manage Malaysian prayer times. It provides an intelligent fallback chain (Cache, Database, Live API), an expressive enum-based zone system covering all 58 Malaysian zones, and Artisan commands for automated data management.
Table of Contents
Requirements
- PHP 8.3 or higher
- Laravel 11 or 12
Installation
Install the package via Composer:
composer require myopensoft/laravel-jakim-esolat-api
Publish the configuration file:
php artisan vendor:publish --tag="jakim-esolat-api-config"
Publish and run the migration:
php artisan vendor:publish --tag="jakim-esolat-api-migrations"
php artisan migrate
Usage
Using the Facade
The EsolatApi facade provides quick access to prayer time data. It accepts either a SolatZone enum or a plain zone code string.
use Myopensoft\LaravelJakimEsolatApi\Facades\EsolatApi;
use Myopensoft\LaravelJakimEsolatApi\Enums\SolatZone;
// Get today's prayer times for Kuala Lumpur
$prayerTime = EsolatApi::getPrayerTime(SolatZone::WLY01);
// Get prayer times for a specific date
$prayerTime = EsolatApi::getPrayerTime('WLY01', '2026-03-17');
if ($prayerTime) {
echo $prayerTime->fajr; // "5:57"
echo $prayerTime->dhuhr; // "1:14"
echo $prayerTime->maghrib; // "7:18"
echo $prayerTime->hijri; // "18 Ramadan 1447"
}
Using Dependency Injection
You can inject the EsolatApi service directly via the contract interface or the concrete class:
use Myopensoft\LaravelJakimEsolatApi\Contracts\EsolatApi;
use Myopensoft\LaravelJakimEsolatApi\Exceptions\EsolatApiException;
class PrayerTimeController extends Controller
{
public function __construct(
private readonly EsolatApi $esolatApi,
) {}
public function show(string $zone)
{
try {
$prayerTime = $this->esolatApi->getPrayerTime($zone);
} catch (EsolatApiException $e) {
abort(422, $e->getMessage());
}
if (! $prayerTime) {
abort(404, 'Prayer times not available for this zone.');
}
return response()->json($prayerTime->toArray());
}
}
Note: Passing an invalid zone code string throws
EsolatApiException. UseSolatZoneenum for type-safe zone references.
Using SolatZone Enum
The SolatZone enum provides all 58 Malaysian prayer time zones as type-safe values with rich metadata.
use Myopensoft\LaravelJakimEsolatApi\Enums\SolatZone;
// Use a specific zone
$zone = SolatZone::WLY01;
echo $zone->value; // "WLY01"
echo $zone->state(); // "Wilayah Persekutuan"
echo $zone->district();// "Kuala Lumpur, Putrajaya"
echo $zone->label(); // "Wilayah Persekutuan | Kuala Lumpur, Putrajaya"
// Get all zones as a key-value array (useful for dropdowns)
$allZones = SolatZone::toArray();
// ["JHR01" => "Johor | Pulau Aur dan Pulau Pemanggil", ...]
// Group zones by state
$grouped = SolatZone::groupByState();
// ["Johor" => [SolatZone::JHR01, SolatZone::JHR02, ...], ...]
// Create from string value
$zone = SolatZone::from('SGR01'); // throws ValueError if invalid
$zone = SolatZone::tryFrom('SGR01'); // returns null if invalid
Accessing Zone Info via Attributes
Each zone case is annotated with a #[ZoneInfo] PHP attribute that stores the state and district metadata.
use Myopensoft\LaravelJakimEsolatApi\Enums\SolatZone;
use Myopensoft\LaravelJakimEsolatApi\Attributes\ZoneInfo;
$zone = SolatZone::SGR01;
$info = $zone->info(); // Returns a ZoneInfo instance
echo $info->state; // "Selangor"
echo $info->district; // "Gombak, Petaling, Sepang, Hulu Langat, Hulu Selangor, Rawang, S.Alam"
Working with PrayerTimeData DTO
The PrayerTimeData is a readonly data transfer object returned by getPrayerTime(). It contains formatted, display-ready values.
use Myopensoft\LaravelJakimEsolatApi\Facades\EsolatApi;
$data = EsolatApi::getPrayerTime('WLY01');
// Access individual properties
$data->zone; // "WLY01"
$data->zoneLabel; // "Wilayah Persekutuan | Kuala Lumpur, Putrajaya"
$data->date; // "2026-03-17"
$data->hijri; // "18 Ramadan 1447"
$data->fajr; // "5:57"
$data->syuruk; // "7:08"
$data->dhuhr; // "1:14"
$data->asr; // "4:22"
$data->maghrib; // "7:18"
$data->isha; // "8:30"
// Convert to array (useful for JSON responses)
$array = $data->toArray();
// [
// 'zone' => 'WLY01',
// 'zone_label' => 'Wilayah Persekutuan | Kuala Lumpur, Putrajaya',
// 'date' => '2026-03-17',
// 'hijri' => '18 Ramadan 1447',
// 'fajr' => '5:57',
// 'syuruk' => '7:08',
// 'dhuhr' => '1:14',
// 'asr' => '4:22',
// 'maghrib' => '7:18',
// 'isha' => '8:30',
// ]
Using PrayerType Enum for Labels
The PrayerType enum maps prayer type identifiers to their Malay names. This is useful for building multilingual UI labels.
use Myopensoft\LaravelJakimEsolatApi\Enums\PrayerType;
echo PrayerType::Fajr->value; // "fajr"
echo PrayerType::Fajr->label(); // "Subuh"
echo PrayerType::Dhuhr->value; // "dhuhr"
echo PrayerType::Dhuhr->label(); // "Zohor"
echo PrayerType::Asr->value; // "asr"
echo PrayerType::Asr->label(); // "Asar"
echo PrayerType::Maghrib->label(); // "Maghrib"
echo PrayerType::Isha->label(); // "Isyak"
echo PrayerType::Syuruk->label(); // "Syuruk"
// Iterate over all prayer types
foreach (PrayerType::cases() as $type) {
echo "{$type->label()}: {$type->value}";
}
HijriDateConverter Usage
The HijriDateConverter converts Hijri date strings in YYYY-MM-DD format to a human-readable format.
use Myopensoft\LaravelJakimEsolatApi\Services\HijriDateConverter;
// Default format: "{d} {m} {y}"
echo HijriDateConverter::format('1447-09-18');
// "18 Ramadan 1447"
// Custom format
echo HijriDateConverter::format('1447-09-18', '{m} {d}, {y}');
// "Ramadan 18, 1447"
echo HijriDateConverter::format('1447-01-01');
// "01 Muharram 1447"
The converter supports all 12 Hijri months via the HijriMonth enum:
| Code | Month Name |
|---|---|
| 01 | Muharram |
| 02 | Safar |
| 03 | Rabiulawwal |
| 04 | Rabiulakhir |
| 05 | Jamadilawwal |
| 06 | Jamadilakhir |
| 07 | Rejab |
| 08 | Syaaban |
| 09 | Ramadan |
| 10 | Syawal |
| 11 | Zulkaedah |
| 12 | Zulhijjah |
Artisan Commands
esolat:fetch
Fetches prayer times from the JAKIM e-Solat API and stores them in the database. By default, fetches data for all 58 zones. Zones that already have a full 7 days of data are automatically skipped.
# Fetch all zones
php artisan esolat:fetch
# Fetch a specific zone only
php artisan esolat:fetch --zone=WLY01
# Preview what would be fetched without making any changes
php artisan esolat:fetch --dry-run
esolat:prune
Removes prayer time records with dates in the past to keep the database lean.
# Delete all past records
php artisan esolat:prune
# Keep the last 7 days of history
php artisan esolat:prune --days=7
# Preview how many records would be deleted
php artisan esolat:prune --dry-run
Scheduling
For automated data management, add both commands to your Laravel scheduler:
// Laravel 11+ (routes/console.php)
use Illuminate\Support\Facades\Schedule;
Schedule::command('esolat:fetch')->daily();
Schedule::command('esolat:prune')->daily();
Configuration
After publishing the config file, you can modify config/jakim-esolat-api.php:
return [
/*
|--------------------------------------------------------------------------
| JAKIM e-Solat API Base URL
|--------------------------------------------------------------------------
|
| The base URL for the JAKIM e-Solat API endpoint. You should not need
| to change this unless JAKIM changes their API location.
|
*/
'base_url' => env('ESOLAT_API_URL', 'https://www.e-solat.gov.my/index.php'),
/*
|--------------------------------------------------------------------------
| Cache Configuration
|--------------------------------------------------------------------------
|
| Configure how prayer time data is cached. By default, cached data
| expires at end of day. Set ttl to a specific number of seconds
| to override this behavior.
|
*/
'cache' => [
'prefix' => env('ESOLAT_CACHE_PREFIX', 'esolat'),
'ttl' => null, // null = end of day, or seconds (e.g. 3600)
],
/*
|--------------------------------------------------------------------------
| HTTP Retry Configuration
|--------------------------------------------------------------------------
|
| Configure retry behavior for API requests to handle transient failures.
|
*/
'timeout' => 15, // seconds
'retry' => [
'times' => 3,
'delay' => 200, // milliseconds
],
'fetch' => [
'throttle' => 500, // milliseconds delay between API calls
],
];
Environment Variables
| Variable | Default | Description |
|---|---|---|
ESOLAT_API_URL | https://www.e-solat.gov.my/index.php | Base URL of the e-Solat API |
ESOLAT_CACHE_PREFIX | esolat | Prefix for cache keys |
Configuration Options
| Key | Type | Default | Description |
|---|---|---|---|
base_url | string | See above | JAKIM e-Solat API base URL |
cache.prefix | string | esolat | Cache key prefix |
cache.ttl | int\|null | null | Cache TTL in seconds. null means cache until end of day. |
timeout | int | 15 | HTTP request timeout in seconds |
retry.times | int | 3 | Number of retry attempts for failed HTTP requests |
retry.delay | int | 200 | Delay between retries in milliseconds |
fetch.throttle | int | 500 | Delay between zones in esolat:fetch command (milliseconds) |
Testing
The package uses Pest for testing.
composer test
To run tests with coverage:
composer test-coverage
To run static analysis:
composer analyse
To format code with Laravel Pint:
composer format
Credits
- Muhammad Syafiq Bin Zainuddin (myopensoft)
- All Contributors
License
The MIT License (MIT). Please see License File for more information.