ronanguilloux/php-gpio
PHP-GPIO provides an object-oriented interface to Raspberry Pi GPIO pins, letting PHP applications read inputs, set outputs, and configure pin modes. Useful for prototyping and controlling LEDs, relays, sensors, and other hardware from PHP.
Install and verify prerequisites
composer require ronanguilloux/php-gpio
Before writing code, confirm the host is actually a Pi with the legacy sysfs GPIO interface available:
ls /sys/class/gpio # should list export, unexport, gpiochip*
groups # your user should be in the `gpio` group
sudo usermod -aG gpio $USER # then log out/in
If /sys/class/gpio is missing (some newer kernels/distros, Pi 5), the library has nothing to talk to — stop and evaluate libgpiod instead.
Where to look first
vendor/ronanguilloux/php-gpio/src/ — this is the authoritative source for the exact class names and method signatures. The library is from 2013, so README examples may lag the code. Look for the Gpio/Pin-style classes.tests/ in the repo (or on GitHub) — the tests are the most reliable usage reference./sys/class/gpio/gpio<N>/ on a live device — every method in the library is a thin file write; watching these files is the fastest way to debug behavior.First real use case: blink an LED / pulse a relay
<?php
require __DIR__ . '/vendor/autoload.php';
use PhpGpio\Gpio; // confirm namespace in vendor/.../src
$gpio = new Gpio();
$pin = $gpio->getPin(17); // BCM numbering, not physical header pin
$pin->setDirection('out');
try {
$pin->setValue(1);
sleep(1);
$pin->setValue(0);
} finally {
$pin->unexport(); // leave the sysfs state clean
}
Run it from the CLI as the same user that will run it in production (gpio group). If it works as your shell user but not as www-data, the problem is permissions, not PHP.
Wrap the library behind a small service
Do not sprinkle new Gpio() across controllers. Bind a single wrapper into the container and expose domain verbs:
class RelayBoard
{
/** @var array<string,int> logical name => BCM pin */
public function __construct(
private Gpio $gpio,
private array $pinMap
) {}
public function switch(string $name, bool $on): void
{
$pin = $this->gpio->getPin($this->pinMap[$name]);
$pin->setDirection('out');
$pin->setValue($on ? 1 : 0);
}
}
In Laravel this is a singleton in a service provider; in Symfony, a service definition with the pin map injected from config/services.yaml. This keeps the hardware detail out of the request path and makes the board mockable.
Actuate from CLI commands, not web requests
Pins hold state across requests, so a web call only needs to flip a value — but sysfs writes can fail silently or race. The robust pattern is:
gpio-group user) performs setValue() and logs the result.// Artisan command outline
public function handle(RelayBoard $board)
{
$board->switch($this->argument('relay'), $this->argument('state') === 'on');
$this->info('OK');
}
Polling for input pins
The library has no interrupt/edge support, so buttons and sensors are read by polling. Keep loops cheap and bounded:
$pin = $gpio->getPin(27);
$pin->setDirection('in');
while (true) {
if ((string) $pin->getValue() === '0') {
// handle press; debounce in software
}
usleep(50_000); // 20 Hz, don't spin the CPU
}
Prefer a long-running worker (systemd service, Supervisor) over a request-triggered loop, and always release the pin in a finally or registered shutdown function.
Testing
Extract an interface (PinWriter with write(int $pin, bool $value)) and mock it in unit tests. Reserve the real Gpio/Pin classes for a single integration test that is skipped when /sys/class/gpio is absent, so CI on non-Pi runners stays green.
www-data (or your php-fpm user) must be in gpio, and the pool must be restarted after the group change. Failing that, writes to /sys/class/gpio/.../value succeed only for root.export is not idempotent. If the pin is already exported (by a previous crash or another process), export can error or be a no-op depending on kernel. Wrap export in a try/catch and treat “already exported” as success; check is_dir('/sys/class/gpio/gpio'.$pin).in to out mid-run may not take effect until you unexport() and export again. Design your flow so direction is set once at startup.libgpiod character device interface. Treat this library as legacy — fine for a hobby controller, risky for a long-lived product.getValue() mirrors the sysfs file contents ("0"/"1"), so use strict comparisons carefully; 0 == "0" is true but '0' === 0 is not.dev-master or vendor a small compatibility patch. Pin the version and add the Pi integration test to CI so an upgrade doesn't silently break.flock or funnel all writes through one worker.unexport() on shutdown; an orphaned export survives your process and confuses the next run.cat /sys/class/gpio/gpio17/direction, cat .../value, ls /sys/class/gpio, and dmesg | tail for kernel-level complaints. If the file changes but the LED doesn't, the problem is wiring, not code.Pin to add logging, retry-on-write-failure, or metrics — that is the easiest seam the package offers without forking it.How can I help you explore Laravel packages today?