geerlingguy/ping
A lightweight PHP wrapper around the system ping command. Quickly check whether a host or IP address is reachable and measure response times through a simple, dependency-free API that works on Linux, macOS, and Windows.
Install
composer require geerlingguy/ping
There is no service provider, facade, or config file — it is a single, dependency-free class (JJG\Ping). That is the whole selling point: 476 stars for ~200 lines of code that wraps the system ping binary.
Prerequisites to verify before you write a line of app code
exec() (or the function the class uses) is not in disable_functions in php.ini.ping binary exists on the host and in every container image you deploy. php:8.x-cli has it; php:8.x-fpm-alpine often does not (busybox ping and GNU ping also take different flags).First use case — a 30-second smoke check
use JJG\Ping;
$ping = new Ping('example.com'); // host, ttl, timeout — check the constructor
$latency = $ping->ping(); // float ms on success, false on failure
if ($latency === false) {
echo "unreachable\n" . $ping->getOutput();
} else {
echo "{$latency} ms\n";
}
Where to look first
src/JJG/Ping.php — read ping() and the private command-builder. It branches on PHP_OS, so the exact flags used on your platform are the single most important thing to understand. Everything surprising downstream comes from that branch.setTimeout(), setTtl(), setHost()).php -i | grep disable_functions and which ping on the target host.Adopt it as a diagnostic/secondary signal (is this host up?), not as the primary health check for your own application — use an HTTP or TCP check for that.
The class is instantiable but shells out, which makes it hostile to unit tests. Bind an interface so you can fake it in CI:
namespace App\Services;
interface Reachability
{
/** @return float|false Latency in ms, or false when unreachable. */
public function check(string $host, int $timeout = 2): float|false;
}
namespace App\Services;
use JJG\Ping;
class IcmpReachability implements Reachability
{
public function check(string $host, int $timeout = 2): float|false
{
$ping = new Ping($host);
$ping->setTimeout($timeout);
return $ping->ping();
}
}
// AppServiceProvider::register()
$this->app->bind(Reachability::class, IcmpReachability::class);
// config/monitoring.php
return [
'targets' => [
'api' => ['host' => env('MONITOR_API_HOST', 'api.example.com'), 'timeout' => 2],
'database' => ['host' => env('MONITOR_DB_HOST', 'db.internal'), 'timeout' => 1],
],
'cache_ttl' => 60,
];
class PingHost extends Command
{
protected $signature = 'net:ping {host} {--timeout=2}';
public function handle(Reachability $reachability): int
{
$latency = $reachability->check($this->argument('host'), (int) $this->option('timeout'));
if ($latency === false) {
$this->error('Unreachable (timeout, DNS failure, or ICMP blocked).');
return self::FAILURE;
}
$this->info(sprintf('%s responded in %.2f ms', $this->argument('host'), $latency));
return self::SUCCESS;
}
}
Never ping inside a web request. A dead host costs you the full timeout on every page load.
class RecordPingResult implements ShouldQueue
{
public int $tries = 1;
public int $timeout = 15;
public function __construct(public string $host) {}
public function handle(Reachability $reachability): void
{
$latency = $reachability->check($this->host);
PingResult::create([
'host' => $this->host,
'latency_ms' => $latency === false ? null : $latency,
'reachable' => $latency !== false,
'checked_at' => now(),
]);
}
}
Schedule it and cache the latest value for the dashboard:
// routes/console.php
Schedule::command('monitor:ping-all')->everyFiveMinutes()->withoutOverlapping();
$latest = Cache::remember("ping:{$host}", 60, fn () => PingResult::where('host', $host)->latest('checked_at')->first());
Use it as one component of a /health route, with a hard cap on how many hosts you touch, and short-circuit if exec is unavailable:
if (! function_exists('exec')) {
return response()->json(['icmp' => 'unavailable'], 200);
}
getOutput() for triage, not for user-facing messages$ping = new Ping($host, 255, 2);
$latency = $ping->ping();
if ($latency === false) {
Log::warning('ICMP probe failed', ['host' => $host, 'raw' => $ping->getOutput()]);
}
The raw string is what distinguishes "unknown host" (DNS), "Destination Host Unreachable" (routing), and a plain timeout — the boolean return cannot.
false vs 0.0. The return type is mixed: a float latency on success, false on any failure. Always compare with === false. Treating a falsy check if (!$latency) will misreport a sub-millisecond response on loopback.
Silent failure when exec is disabled. If exec is in disable_functions, you get false and an empty getOutput() — indistinguishable from a dead host. Assert function_exists('exec') at boot or in a health check, and log config drift.
ICMP is blocked far more often than hosts are dead. Cloud VMs, containers, and PaaS environments routinely drop echo requests. A false here is not proof of a down service. Pair it with a TCP/HTTP check, and document that in the UI so nobody pages at 3am because AWS dropped ICMP.
Locale-dependent parsing. The class extracts latency with a regex that expects time= in the output. On a Windows or Unix box with a non-English locale, ping prints temps=, Zeit=, etc., and the regex misses — you get false for a host that clearly replied. Pin LC_ALL=C for the PHP process, or verify on the actual target image.
Timeout units are not portable. GNU ping takes -W in seconds (integer, so setTimeout(0.5) gets rounded), macOS takes milliseconds, Windows takes milliseconds via -w. Sub-second timeouts silently become 1s on Linux. Budget accordingly when fanning out over N hosts.
Budget your worst case. N hosts × timeout is the real latency of your command/job if they are sequential. Keep N small, or dispatch one job per host.
Host input is a shell-adjacent surface. Newer versions escape the host, but do not bet your security posture on that: validate against a hostname/IP pattern (filter_var($h, FILTER_VALIDATE_IP) or a strict hostname regex) before it ever reaches the class, especially if the host comes from a user form or tenant config. Otherwise you have SSRF and potential command injection in one package.
IPv6 needs different flags. ping6/-6 behavior varies by platform and is not uniformly handled. If you have AAAA-only targets, test explicitly rather than assuming.
DNS resolution is inside the measurement. The reported latency includes name resolution on a cold cache, so the first call is often much slower. Resolve once, cache the IP, and ping the literal address if you need clean numbers.
One packet, no statistics. You get a single sample and no packet-loss percentage. For flaky-link detection, run several probes (or your own loop) and compute loss yourself — do not infer stability from one float.
Testing. Never let this class hit the network in the test suite: it is slow and non-deterministic. Bind a fake Reachability in TestCase::setUp() (or use $this->instance(...)) and assert against your job/command logic instead.
Extension point is the wrapper, not the class. There is no plugin surface and no configuration; the class is deliberately final-minded. Wrap it behind your own interface so you can swap in a curl/socket-based TCP check on hosts where ICMP is blocked — same interface, different implementation, no call-site changes.
Version pinning. With no active feature development and no repo link on the listing, pin the exact version in composer.json and skim the class after upgrading — the command-building logic is the part that changes and the part that breaks.
How can I help you explore Laravel packages today?