spatie/ping
Run ICMP pings from PHP and get structured results instead of raw output. Spatie's package wraps the system ping command and exposes packet loss, response times, connectivity status, and individual ping lines as clean, readable objects.
Install it like any other Spatie utility package — there is no service provider, no config file, no facade, no publish step:
composer require spatie/ping
The entire public surface is two classes and two enums. Read these first, in this order:
Spatie\Ping\Ping — the builder. Constructor takes hostname, timeoutInSeconds, count, intervalInSeconds, packetSizeInBytes, ttl, showLostPackets, ipVersion. Every one of those also has a fluent setter, so you can either construct-then-run or chain.Spatie\Ping\PingResult — the value object returned by run(). This is where you'll spend 90% of your time.Spatie\Ping\Enums\PingError and Spatie\Ping\Enums\IpVersion — small, but they're the difference between brittle string matching and real branching logic.Your first use case is almost certainly an uptime/latency check:
use Spatie\Ping\Ping;
$result = (new Ping('8.8.8.8', count: 3))->run();
if ($result->isSuccess()) {
logger()->info('ping ok', [
'host' => $result->host,
'avg_ms' => $result->averageTimeInMs(),
'loss' => $result->packetLossPercentage(),
]);
} else {
logger()->warning('ping failed', ['error' => $result->error()?->value]);
}
Before you build anything on top of it, run one ping from the same user context your app runs as. The package shells out to the system ping binary, so if www-data can't execute ping or lacks cap_net_raw, every call will fail regardless of network health.
Fluent builder as a reusable health-check helper. Rather than scattering new Ping(...) around, wrap it:
final class HostProbe
{
public function probe(string $host, int $count = 3): PingResult
{
return (new Ping($host))
->count($count)
->timeoutInSeconds(2)
->intervalInSeconds(0.5)
->run();
}
}
The builder is immutable-enough to be safe here: each fluent call returns a new instance, so a shared base configuration won't leak between calls.
Tune the options to the job, not to the defaults. The defaults are "interactive debugging" defaults, not "background monitoring" defaults. For a web request path you want count: 1, timeoutInSeconds: 1 (worst case ~1s blocking). For a nightly network-quality report, count: 10, intervalInSeconds: 1.0 gives you a meaningful standardDeviationTimeInMs() — which is the number that actually tells you whether a link is unhealthy, far more than isSuccess().
Persist and rehydrate results. toArray() / PingResult::fromArray() is the killer feature for anything scheduled. Ping in a queued job or Artisan command, store the array as JSON in an Eloquent model cast, and you keep full fidelity — timings, per-packet lines(), and raw output — without re-running the command to inspect history:
class PingCheck extends Model
{
protected $casts = ['result' => 'array'];
public function result(): PingResult
{
return PingResult::fromArray($this->result);
}
}
This also makes testing trivial: capture a handful of real toArray() payloads (success, 100% loss, DNS failure, unreachable host) into fixtures and build your result-handling logic against them without touching the network.
Branch on the enum, not on the message. error() returns a PingError case; HostnameNotFound is the one you'll hit constantly in user-supplied-host scenarios, so validate hostnames before pinging and treat a HostnameNotFound at runtime as a genuine surprise. Other cases are network-layer and generally map to the same remediation ("host unreachable, alert").
IPv6 / dual-stack. Default is IPv4. Use IpVersion::Auto when you genuinely don't know what a hostname resolves to, and IpVersion::IPv6 when you're explicitly verifying AAAA records — e.g. an IPv6 readiness audit that pings the same host twice and compares. $result->ipVersion()->value is on the result, so you can assert which path was actually taken.
Reporting. standardDeviationTimeInMs(), packetLossPercentage(), and minimumTimeInMs()/maximumTimeInMs() in combination give you a decent jitter indicator for free. Emit them as gauges/metrics; a rising standard deviation with 0% loss is the early warning signal that matters operationally.
ping requires privileges. Containers and locked-down PHP-FPM pools routinely lack CAP_NET_RAW or setuid on ping. This is the #1 cause of "the package doesn't work" — it's the environment, not the code. Verify with a shell ping as the app user first.count × intervalInSeconds (plus timeouts) is your worst-case wall time. Never call it inside a request cycle with defaults, and never call it in a loop over dozens of hosts from a web request. Push it to a queue or a scheduled command.showLostPackets is Linux-only. It maps to -O on iputils and is silently ignored on macOS. Don't build parsing logic that depends on no answer yet for icmp_seq=... lines being present — on macOS they never will be, and with the option disabled they won't be on Linux either. Treat lines() as "successful replies", and derive loss from packetLossPercentage().ping doesn't support -4/-6; the package works around this (fixed in 1.2.1), but it means your option values aren't all honoured identically across platforms. If you support both Linux and macOS hosts, test on both — a dev laptop passing tells you nothing about prod.ping's stderr/stdout strings to PingError cases, and 1.2.2 exists specifically because more iputils error strings needed recognising. Consequences: (a) pin a recent version — 1.2.2 or later — and (b) error() may legitimately return null on an unsuccessful result if the failure mode is new to the parser. Always handle "not success AND no error case" by falling back to raw in your logs.averageResponseTimeInMs(), while the result section documents averageTimeInMs(). Trust averageTimeInMs() (it matches toArray()'s average_time_in_ms key) and confirm against the class before shipping; a typo'd method call here is a fatal error at runtime, not a soft failure.raw is your debugging lifeline. When a result looks wrong, log $result->raw. It's the only way to tell "ping said 100% loss" from "ping printed something the parser didn't understand."Ping. The package is not responsible for your input validation.skipOnGitHubActions for real pings. Use PingResult::fromArray() fixtures for logic tests, and gate any genuinely live ping behind an env flag.How can I help you explore Laravel packages today?