xpaw/php-minecraft-query
A lightweight PHP library for querying Minecraft servers via the GameSpy4 Query Protocol. Retrieve server status, player counts, MOTD, version, plugins, and map data with a simple, dependency-free API.
This package wraps the Minecraft server protocols: a TCP status ping (what the multiplayer server list shows) and the UDP GameSpy4 query (full player list, plugins, host info).
composer require <vendor>/<package> # requires PHP >= 8.0 as of 5.0.0
Where to look first:
MinecraftPing::Query($host, $port) — status ping. Works against any vanilla/Paper/Spigot/Proxy server with no server-side configuration. Returns MOTD, online/max players, version name, protocol, favicon.MinecraftQuery::Query($host, $queryPort) — full query. Requires enable-query=true (and usually an explicit query.port) in server.properties, and UDP open on that port. Returns the actual player name list, plugins, software version.First use case — a status card on an admin dashboard:
use MinecraftPing;
$status = Cache::remember("mc:status:{$server->id}", 30, fn () => MinecraftPing::Query($server->host, $server->port));
return view('server.status', [
'online' => $status->getPlayers()->getOnlinePlayers(),
'max' => $status->getPlayers()->getMaxPlayers(),
'motd' => $status->getDescription(),
'version' => $status->getVersion()->getRelease(),
]);
Start with the ping. Only reach for the query protocol when you genuinely need the per-player list or plugin inventory.
Status widget / public server list. Cache aggressively (20–60s). Cache::remember around the ping call is the single biggest win — a dashboard with 20 servers otherwise fires 20 TCP round-trips per page load.
Monitoring with notifications. Run the ping from a scheduled command, not from a request. Compare getPlayers()->getOnlinePlayers() against your threshold, dispatch a ServerWentDown event, and notify via mail/Slack. Keep the timeout short (1–2s) so a hung server doesn't stall the scheduler.
Multi-server config. Put hosts in config/minecraft.php and resolve a configured client per server, rather than hardcoding host/port at call sites:
$status = Cache::remember("mc:{$key}", 30, fn () => MinecraftPing::Query($host, $port));
Query vs ping in one flow. Use ping for the always-on status card (no server config needed), and query only on a detail page or a queued job to populate the player roster:
try {
$query = MinecraftQuery::Query($host, $queryPort);
$players = $query->getPlayers(); // array|Collection depending on wrapper
$plugins = $query->getPlugins();
} catch (\Throwable $e) {
report($e); // query disabled or UDP blocked — degrade gracefully
$players = [];
}
Favicon and MOTD rendering. Favicons come back as data:image/png;base64,.... Store or cache the base64 string; don't re-fetch on every render. MOTD contains legacy § colour codes — strip or convert them to HTML spans in a presenter, not in the Blade template.
Testing. Never hit a live server in the test suite. Bind a fake/stub client in the container (or use Http-style fakes if the wrapper offers them) and assert on your own presenter logic instead.
5.0.0 breaking changes to check before upgrading:
composer.json platform constraint, and any Laravel version pinned to a PHP 7.4 runner. This is the change most likely to fail your build rather than your runtime.MinecraftPing::Query now sends a handshake followed by the status ping packet, with protocol -1. This is the correct modern flow and fixes compatibility with 1.7+ servers, BungeeCord/Velocity proxies, and servers that reject a bare status request. But if you had a custom/old server or a mock that asserted on raw packet bytes, retest it. -1 means "protocol unknown"; most servers echo their own version back, but a few strict implementations may reject it — if you see handshake failures on an unusual fork, that's the first place to look.utf8_encode() / mb_convert_encoding() over the MOTD or player names to fix mojibake, remove those hacks now — you'll get double-encoded garbage. Conversely, if you were relying on the raw bytes for anything, that data is no longer raw. Test with a non-ASCII MOTD and a non-ASCII player name before shipping.Protocol-level pitfalls (version-independent):
MinecraftQuery needs enable-query=true. It is off by default, uses UDP, and its port is query.port — which is frequently not the same as server.port. A ping succeeding while query fails almost always means this.false. Always wrap in try/catch or rescue() — an uncaught exception on a dashboard with a dead server is a 500._minecraft._tcp. SRV for custom domains. If you support user-supplied addresses, resolve SRV yourself or you'll ping the wrong port.Cache::remember won't store a thrown exception, so a down server gets retried on every request — cache a sentinel "offline" result with a shorter TTL to avoid hammering it.How can I help you explore Laravel packages today?