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-27 22:42:25 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 |
exoplanet() |
NASA Exoplanet Archive (TAP) | adql, planets, planet |
trek() |
Solar System Treks WMTS (Moon, Mars, Vesta) | layers, capabilities, tile |
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, and nor do the Exoplanet Archive and Trek.
Not yet supported
openScience(), ssc(), ssd() and techport() exist and throw NotYetSupportedException.
Exoplanets and Trek
use ProjectSaturnStudios\Stargazer\Trek\Enums\TrekBody; use ProjectSaturnStudios\Stargazer\Trek\Enums\TrekProjection; $kepler = nasa()->exoplanet()->planet('Kepler-22 b')->get(); printf("%s: %.1f days, %.2f Earth radii\n", $kepler->name, $kepler->orbitalPeriodDays, $kepler->radiusEarth); $newest = nasa()->exoplanet()->planets(where: 'disc_year >= 2025', limit: 10)->get(); $moon = nasa()->trek()->layers(TrekBody::MOON)->get(); $tile = nasa()->trek()->tile(TrekBody::MOON, TrekProjection::EQUIRECTANGULAR, 'LRO_WAC_Mosaic_Global_303ppd_v02', 0, 0, 0)->get(); file_put_contents('moon-0-0-0.jpg', $tile->body());
planets() and planet() read pscomppars, one row per planet; adql() runs any ADQL. A limit goes in maxrec, because the archive applies TOP before ORDER BY. Trek's capabilities() answers the same WmtsCapabilities as GIBS, so its layer formats and tile matrix sets read the same way.
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.