projectsaturnstudios / venusian-stargazer
National Aeronautics and Space Administration (NASA) integration for Venusian Framework applications.
Package info
github.com/projectsaturnstudios/venusian-stargazer
pkg:composer/projectsaturnstudios/venusian-stargazer
Requires
- php: ^8.4|^8.5
- venusian-voyager/http: ^0.10.0
- venusian-voyager/io-pools: ^0.10.0
- venusian-voyager/nuts-and-bolts: ^0.10.0
Requires (Dev)
- mockery/mockery: ^1.6
- pestphp/pest: ^4
- venusian-voyager/config: ^0.10.0
- venusian-voyager/vessel: ^0.10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-04 17:47:02 UTC
README
NASA's open APIs for Venusian apps.
$picture = nasa()->apod()->date('2015-06-03')->get(); echo $picture->title; // "Flyby Image of Saturn's Sponge Moon Hyperion"
Requirements
- PHP 8.4 or 8.5
- A Venusian 0.10 app, or standalone
venusian-voyager/http,io-poolsandnuts-and-bolts0.10+ - For GIBS: the
xmlreader,simplexmlandzlibextensions (bundled with PHP builds)
Installation
composer require projectsaturnstudios/venusian-stargazer
After install, call nasa().
Stargazer uses NASA's shared DEMO_KEY by default, which is rate-limited to 30 requests an hour. Get a free key at api.nasa.gov and set it in .env:
NASA_API_KEY=your-key-here
To publish the config file:
php computer vendor:publish --tag=nasa-config
Usage
Pick an API family, then an endpoint. Nothing is sent until get() or async(). get() blocks and returns a DTO for a single object, or a Collection of DTOs for a list. A non-2xx response throws StargazerException, whose status() is the HTTP status, so a caller can tell a 429 from a 404 from a 503. async() returns a promise on the event loop that fulfils with what get() returns and rejects with what it throws. With no loop bound, async() throws StargazerException.
$flares = nasa()->donki()->flr('2017-09-06', '2017-09-06')->get(); echo $flares->first()->flrID; // "2017-09-06T08:57:00-FLR-001"
On the event loop
nasa()->neows()->feed()->async() ->then(fn ($feed) => printf("%d objects near Earth\n", $feed->element_count)) ->error(fn (\Throwable $e) => printf("NeoWs failed: %s\n", $e->getMessage()));
Two async() calls run concurrently. wait() resolves each one to its DTOs:
$feed = nasa()->neows()->feed()->async(); $flares = nasa()->donki()->flr()->async(); printf("%d objects, %d flares\n", $feed->wait()->element_count, $flares->wait()->count());
Query parameters
with() adds any parameter NASA accepts and returns a new request:
$events = nasa()->eonet()->events()->with('status', 'open')->with('limit', 5)->get();
Fetching media
Some DTOs can fetch their own media on the loop. Each method returns a promise of the HTTP response. The bytes are in body().
| DTO | Method | Notes |
|---|---|---|
AstronomyPicture (APOD) |
render(bool $hd = false) |
Fetches the screen-sized picture, the full-size hdurl with $hd, or a video day's mp4. Returns null on an embed day, such as a YouTube video. |
EpicImage (EPIC) |
render(EpicCollection $collection, EpicImageType $type = EpicImageType::PNG) |
Builds the archive URL from the image's date. |
ImageLocation (Image Library) |
fetch() |
Follows the location pointer returned by asset(), metadata() and captions(). |
use ProjectSaturnStudios\Stargazer\EPIC\Enums\EpicCollection; use ProjectSaturnStudios\Stargazer\EPIC\Enums\EpicImageType; $image = nasa()->epic()->natural()->get()->first(); $image->render(EpicCollection::NATURAL, EpicImageType::JPG) ->then(fn ($response) => file_put_contents("{$image->image}.jpg", $response->body()));
Supported APIs
| Accessor | API | Endpoints |
|---|---|---|
apod() |
Astronomy Picture of the Day | date, range, count |
neows() |
Near Earth Object Web Service | feed, lookup, browse |
donki() |
Space Weather Database (DONKI) | cme, cmeAnalysis, gst, ips, flr, sep, mpc, rbe, hss, wsaEnlilSimulations, notifications |
eonet() |
Earth Observatory Natural Event Tracker v3 | events, categories, sources, layers, magnitudes |
epic() |
Earth Polychromatic Imaging Camera | natural, enhanced, naturalAvailable, enhancedAvailable |
insight() |
InSight Mars Weather | weather |
tle() |
Two-Line Element sets | collection, search, satellite |
techtransfer() |
NASA Technology Transfer | patent, software, spinoff |
imageLibrary() |
NASA Image and Video Library | search, asset, metadata, captions |
gibs() |
Global Imagery Browse Services | wmts(), wms(), twms(), colormap, legend, layerMetadata, vectorMetadata, vectorStyle |
Your key goes to NeoWs, EPIC and InSight. APOD comes from science.nasa.gov's APOD API, which replaced the api.nasa.gov one (archived December 1, 2026). DONKI is served by NASA's CCMC (ccmc.gsfc.nasa.gov/DONKI-API) since its September 2026 move. Neither takes a key.
Not yet supported
trek(), exoplanet(), openScience(), ssc(), ssd() and techport() exist and throw NotYetSupportedException.
API reference
Dates are Y-m-d strings. A null argument is left off the request, so NASA's default applies. Enums live under each family's Enums namespace, for example ProjectSaturnStudios\Stargazer\DONKI\Enums\DonkiCatalog.
APOD
nasa()->apod()->range('2015-06-01', '2015-06-03')->get()->pluck('title'); // "Pulsating Aurora over Iceland", "Polaris and Comet Lovejoy", "Flyby Image of Saturn's Sponge Moon Hyperion"
| Method | Returns |
|---|---|
date(?string $date = null) |
AstronomyPicture. With no date, it asks for today in your app's timezone. |
range(string $start, ?string $end = null) |
Collection<AstronomyPicture>, oldest first. $end defaults to today. At most 100 days; more throws InvalidArgumentException. |
count(int $count) |
Collection<AstronomyPicture>: $count (1 to 100) consecutive days from a random point in the archive, oldest first. The API has no random pick, so it asks for a random page. |
AstronomyPicture has date, title, explanation, url, hdurl, media_type, copyright, thumbnail_url, permalink and alt.
urlis the media: the picture at screen size (1600 px on its longest side), a video day's mp4, or an embed day's player address.hdurlis the full-size picture. On a video day it is the still, andthumbnail_urlcarries it too.permalinkis the day's article on science.nasa.gov.title,explanation,copyrightandaltare plain text. The explanation drops its "Explanation:" lead-in and the site notices after it.
NeoWs
$rock = nasa()->neows()->lookup('3542519')->get(); $rock->name; // "(2010 PK9)" $rock->is_potentially_hazardous_asteroid; // true
| Method | Returns |
|---|---|
feed(?string $start_date = null, ?string $end_date = null) |
NeoFeed: element_count, and near_earth_objects keyed by date |
lookup(string $asteroid_id) |
NearEarthObject |
browse(?int $page = null, ?int $size = null) |
NeoBrowse: near_earth_objects, and page with total_elements and total_pages |
NearEarthObject has id, name, absolute_magnitude_h, estimated_diameter, is_potentially_hazardous_asteroid, close_approach_data, orbital_data and is_sentry_object.
DONKI
Every endpoint takes $from and $to, sent as startDate and endDate, and returns a Collection of the DTO below.
nasa()->donki()->flr('2017-09-06', '2017-09-10')->get()->pluck('classType'); // "X2.2", "X9.3", "M2.5", ... "X8.2"
| Method | Extra arguments | DTO | ID field |
|---|---|---|---|
cme() |
Cme |
activityID |
|
cmeAnalysis() |
CmeAnalysis |
associatedCMEID |
|
gst() |
GeomagneticStorm |
gstID |
|
ips() |
?string $location, DonkiCatalog|string|null $catalog |
InterplanetaryShock |
activityID |
flr() |
?string $class, DonkiCatalog|string|null $catalog |
Flare |
flrID |
sep() |
SolarEnergeticParticle |
sepID |
|
mpc() |
MagnetopauseCrossing |
mpcID |
|
rbe() |
RadiationBeltEnhancement |
rbeID |
|
hss() |
HighSpeedStream |
hssID |
|
wsaEnlilSimulations() |
WsaEnlilSimulation |
simulationID |
|
notifications() |
DonkiNotificationType|string|null $type |
Notification |
messageID |
DonkiIpsLocation lists the values ips() accepts for $location: Earth, Mars, MESSENGER, STEREO A, STEREO B and ALL. Pass its ->value. CmeAnalysis reports its featureCode as one of the codes in DonkiAnalysisFeature.
EONET
use ProjectSaturnStudios\Stargazer\EONET\Enums\EonetEventStatus; nasa()->eonet()->events()->with('status', EonetEventStatus::OPEN)->with('limit', 3)->get()->events->pluck('title'); // "Tropical Cyclone 01B", "Hurricane Polo", "Wildfire Round Prarie, Morehouse, Louisiana"
| Method | Returns |
|---|---|
events() |
EonetEventsPage, with events |
categories(?string $id = null) |
EonetCategoriesPage, with categories |
sources() |
EonetSourcesPage, with sources |
layers(?string $id = null) |
EonetLayersPage, with categories |
magnitudes() |
EonetMagnitudesPage, with magnitudes |
EONET filters go through with(), for example status, limit, days, category and source. EonetEvent has id, title, closed, categories, sources and geometry.
EPIC
$images = nasa()->epic()->natural('2015-10-31')->get(); $images->count(); // 12 $images->first()->image; // "epic_1b_20151031003633"
| Method | Returns |
|---|---|
natural(?string $date = null) |
Collection<EpicImage>. With no date, the most recent day. |
enhanced(?string $date = null) |
Collection<EpicImage> |
naturalAvailable() |
Collection<EpicAvailableDate>, each with a date |
enhancedAvailable() |
Collection<EpicAvailableDate> |
EpicImage has identifier, caption, image, date, centroid, the dscovrPosition, lunarPosition and sunPosition vectors, and attitude. archiveUrl() builds the image URL, and render() fetches it.
InSight
$weather = nasa()->insight()->weather()->get(); $weather->solKeys; // ["675", "676", "677", "678", "679", "680", "681"]
| Method | Returns |
|---|---|
weather() |
InsightWeather: solKeys, sols and validity |
Each InsightSol has sol, season as an InsightSeason, firstUtc, lastUtc, and temperature, windSpeed, pressure and windDirection summaries.
TLE
$iss = nasa()->tle()->satellite(25544)->get(); $iss->name; // "ISS (ZARYA)" $iss->line1; // "1 25544U 98067A 26265.85181744 .00007689 00000+0 14639-3 0 9994"
| Method | Returns |
|---|---|
collection() |
TleCollection: totalItems and members |
search(string $query) |
TleCollection |
satellite(int|string $id) |
TleRecord: satelliteId, name, date, line1 and line2 |
TechTransfer
nasa()->techtransfer()->spinoff('battery')->get()->total; // 142
| Method | Returns |
|---|---|
patent(string $query) |
TechTransferPage |
software(string $query) |
TechTransferPage |
spinoff(string $query) |
TechTransferPage |
TechTransferPage has results, count, total, perPage and page. Each TechTransferRecord has title, description, category, center, imageUrl and detailUrl.
Image and Video Library
use ProjectSaturnStudios\Stargazer\ImageLibrary\Enums\ImageMediaType; $page = nasa()->imageLibrary()->search('apollo 11')->with('media_type', ImageMediaType::IMAGE)->get(); $page->totalHits; // 1522 $page->items->first()->data->first()->nasaId; // "jsc2007e034221"
| Method | Returns |
|---|---|
search(?string $q = null) |
ImageSearchPage: totalHits, and items, each with data and links |
asset(string $nasa_id) |
ImageAssetManifest: items, one per file |
metadata(string $nasa_id) |
ImageLocation, which points at a JSON file |
captions(string $nasa_id) |
ImageLocation, which points at a caption file |
metadata() and captions() return where the file lives, not the file itself. fetch() gets it:
$location = nasa()->imageLibrary()->metadata('as11-40-5874')->get(); $location->fetch()->wait()->json('AVAIL:Title'); // "Apollo 11 Mission image - Astronaut Edwin Aldrin poses beside th"
Each ImageItemData has nasaId, title, description, center, dateCreated, mediaType as an ImageMediaType, keywords and photographer.
GIBS
NASA's Global Imagery Browse Services: daily satellite imagery and data layers as tiles and maps. No key. Images come back as the Http Response; decode body() with whatever draws them.
use ProjectSaturnStudios\Stargazer\GIBS\DataObjects\GibsBox; use ProjectSaturnStudios\Stargazer\GIBS\Enums\GibsMapFormat; use ProjectSaturnStudios\Stargazer\GIBS\Enums\GibsTileFormat; $wmts = nasa()->gibs()->wmts(); // EPSG:4326, best imagery $capabilities = $wmts->capabilities()->get(); // 1,300+ layers, read as a stream $modis = $capabilities->layer('MODIS_Terra_CorrectedReflectance_TrueColor'); $zoom2 = $capabilities->tileMatrixSetOf($modis->identifier)->matrix(2); foreach ($zoom2->covering(new GibsBox(-30, 0, 60, 45))?->tiles() ?? [] as [$row, $col]) { $jpeg = $wmts->tile($modis->identifier, '250m', 2, $row, $col, GibsTileFormat::JPEG, '2021-09-21')->get()->body(); } $map = nasa()->gibs()->wms()->map('MODIS_Terra_CorrectedReflectance_TrueColor', new GibsBox(-130, 20, -60, 55), 700, 350, GibsMapFormat::PNG, '2021-09-21')->get(); $fires = $wmts->vectorTile('VIIRS_NOAA20_Thermal_Anomalies_375m_All', '500m', 4, 3, 4, '2020-10-01')->get();
| Method | Returns |
|---|---|
wmts($projection, $set)->capabilities() / tile() / vectorTile() / domains() |
WmtsCapabilities / Response / GibsVectorTile / WmtsDomains; RESTful or KVP |
wms(…)->capabilities($version) / map() / legendGraphic() |
WmsCapabilities / Response / Response |
twms(…)->capabilities() / tileService() / tile($pattern) / map() |
WmsCapabilities / TwmsTileService / Response / Response |
colormap($id, $version) |
GibsColorMaps (v1.0 or v1.3) |
legend($id, $orientation, $format) |
Response (SVG or PNG) |
layerMetadata($id), vectorMetadata($id) |
GibsLayerMetadata |
vectorStyle($id) |
GibsVectorStyle, whose layers evaluate their paint and layout for a feature at a zoom |
Projections: GibsProjection::EPSG4326, EPSG3857, EPSG3413 (Arctic), EPSG3031 (Antarctic). Imagery sets: BEST, STANDARD, NEAR_REAL_TIME, ALL. Capabilities run to megabytes: hold on to the WmtsCapabilities you get, or cache the document at capabilities()->url() and rebuild it with WmtsCapabilities::fromXml().
Testing
composer test
The suite never calls NASA. It runs against captured JSON fixtures and Http fakes on a real event loop.
License
MIT. See LICENSE.