Nothing breaks for calling code. What behaves differently:
NmcliBackend::connect()throwsNetworkNotFoundfor a network that is not in range. It used to throwCommandFailedwithnmcli's "No network with SSID … found". Code that catchesWiFiExceptionis unaffected; code that catchesCommandFailedaroundconnectTo()should catchNetworkNotFoundas well.WiFi::connect()already threw it from its own scan.- A wrong passphrase is
WrongPassphrase, a join without a DHCP lease isNoAddress. Both extendCommandFailed, so existingcatchblocks keep working. Their messages no longer contain the command line. NmcliBackend::startHotspot()runsnmcli connection addandnmcli --ask connection upinstead ofnmcli device wifi hotspot. The result is the same WPA2 hotspot under the same profile name,Hotspot. An existing Wi-Fi profile of that name is deleted first, so settings added to it by hand do not survive a restart of the hotspot. The polkit actions it needs are the three the README already lists.WpaCliBackend::connect()saves the derived key, not the passphrase. A network block it writes towpa_supplicant's config now holdspsk=<64 hex digits>where it used to holdpsk="<passphrase>". Blocks already there keep working. If something else reads the passphrase back out of that file, it will no longer find it.HotspotConfig's constructor gained$channel,$countryand$captivePortal, andNmcliBackend's a$dnsmasqSharedDirectory, all optional and at the end.
Nothing changes for calling code. Three things behave differently:
- Stop a running hotspot before upgrading on a
WpaCliBackendmachine. The pid files moved from the temp directory to/run/php-wifi, so the new code does not see a hotspot the old code raised:wifi hotspot statusanswersinactiveandwifi hotspot stopleaves itshostapdanddnsmasqrunning. The watchdog's state file moved too; a watchdog restarted across the upgrade counts its hotspot's age from the restart. wifi hotspot statusonWpaCliBackendneeds root./run/php-wifiis mode 0700, so an unprivileged user cannot read the pid files and getsinactivewhatever the hotspot's state. Starting and stopping it needed root already.NmcliBackendis unaffected.NmcliBackend::forget()throwsNetworkNotFoundfor a network that is not saved, asWpaCliBackend::forget()always has. It used to throwCommandFailedwithnmcli's own message. Both extendWiFiException. It also no longer deletes a profile of another type that shares the name.
WpaCliBackend::stopHotspot() now leaves addresses other than the hotspot's
own on the interface. If you relied on it to clear the interface, run
ip addr flush dev <iface> yourself.
Nothing breaking for calling code. Three things worth knowing:
WiFi::create()may now return a different backend on a NetworkManager-free Linux machine. It (andBackendFactory::forCurrentOs()) routes Linux through the newBackendFactory::forLinux(), which returnsWpaCliBackendinstead ofNmcliBackendwhennmcli -t -f RUNNING generaldoes not reportrunning— a missingnmcli, a stopped NetworkManager, or any other probe failure.WpaCliBackendimplements the sameBackend,SupportsKnownNetworksandSupportsHotspotinterfaces asNmcliBackend, so code that only goes through theWiFifacade needs no change. Code that builds a backend directly withBackendFactory::forOs(Os::Linux, …)is unaffected — that method still always returnsNmcliBackend.Commandgained two optional constructor parameters,$stdinand$stdinIsSecret, after the existing$secretIndexes. This only matters to code that constructsCommandby hand (a customCommandRunner, a test double) — both new parameters default tonull/false, so a call that already stops at$secretIndexes, whether positional or named, is unaffected.NmcliBackend::connect()with a passphrase now deletes an existing NetworkManager profile for that SSID before connecting, so a freshly supplied passphrase always wins over a stale saved one (measured on the Pi:nmcli --asksilently ignored a fresh passphrase and reused the old one otherwise). This also drops any other setting that profile held — a static IP address,autoconnect=no— which previously survived a reconnect. If a deployment relies on such settings persisting acrossconnect()calls, recreate them after connecting instead of relying on the old profile surviving.
Nothing to change in calling code — 3.1 is additive. Two things worth knowing:
KnownNetwork::$ssidnow means the SSID.knownNetworks()used to return the connection name in both$nameand$ssid, becausenmcli connection showin list mode cannot emit the SSID. 3.1 resolves the real SSID with one extranmcli -g 802-11-wireless.ssid connection show <name>call per profile, so$nameand$ssidnow differ for a hotspot profile saved under a name likeHotspot. If code compared$known->nameagainst$known->ssidto detect that case, it no longer matches — compare against the SSID you expect instead. A failed per-profile lookup falls back to the connection name, same as before.WiFi::connectTo(string $ssid, Credentials $credentials, ?Device $device = null)is new: it joins$ssidwithout scanning first. Existing calls toconnect()do not need to change — it still scans, to resolve an SSID and to raiseNetworkNotFoundon a typo. UseconnectTo()only where a scan is impossible, such as while the radio is running the provisioning hotspot.
3.0 replaces the whole 2.x surface — every WiFi:: static method, the
Collection filter/sort methods, AbstractNetwork and its public
properties, the seven global helper functions, and the
Sanchescom\WiFi\Exceptions\* namespace — with a single object API:
WiFi::create() (or new WiFi($backend)) returns a facade over one
Backend; scan() returns a NetworkCollection of immutable Network
value objects; connecting takes a Credentials value object instead of a
bare password string. There is no compatibility shim — every row below is
a breaking change.
| Was (2.x) | Now (3.0) |
|---|---|
WiFi::scan() |
WiFi::create()->scan() |
WiFi::getConnected() |
WiFi::create()->scan()->connected() |
WiFi::getStrongestNetwork() |
WiFi::create()->scan()->strongest() |
WiFi::get24GhzNetworks() / get5GhzNetworks() / get6GhzNetworks() |
->scan()->band(Band::GHz2_4) / Band::GHz5 / Band::GHz6 |
WiFi::getNetworksBySecurity('WPA2') |
->scan()->security(Security::WPA2) |
WiFi::setCommandClass() / WiFi::setPhpOperationSystem() |
Gone — construct new WiFi($backend) directly, e.g. new WiFi(BackendFactory::forOs(Os::Linux, new FakeCommandRunner([...]))) in tests |
Collection::getAll() |
->all() (inherited from Illuminate\Support\Collection) |
Collection::getBySsid(string $ssid) |
NetworkCollection::bySsid(string $ssid): Network |
Collection::getByBssid(string $bssid) |
NetworkCollection::byBssid(Bssid|string $bssid): Network |
Collection::getConnected() |
NetworkCollection::connected(): static |
Collection::getBySecurity(string $type) |
NetworkCollection::security(Security $security): static |
Collection::getByMinSignalStrength(float $dbm) |
NetworkCollection::strongerThan(Signal|float $threshold): static |
Collection::getByChannel(int $channel) |
Gone — filter yourself: ->filter(fn ($n) => $n->channel === 6) |
Collection::sortBySignalStrength() |
NetworkCollection::sortBySignal(): static |
Collection::getStrongest() |
NetworkCollection::strongest(): Network |
Collection::hasRedactedSsids() |
NetworkCollection::hasHiddenSsids(): bool |
AbstractNetwork public properties |
Network readonly properties: $ssidRedacted → $ssidHidden; $quality / $dbm → $signal?->quality / $signal?->dbm (a Signal value object, or null when the tool reported none); $bssid (string) → $bssid (?Bssid); $security (string) → $security (Security enum) plus $securityFlags (unchanged, still string) |
$network->getSecurityType() |
$network->security (a Security enum case, not a string) |
$network->connect(string $password, ?string $device = null) |
WiFi::connect(Network|string $network, Credentials $credentials, ?Device $device = null) — networks no longer connect themselves |
$network->disconnect(?string $device = null) |
WiFi::disconnect(?Device $device = null) |
to_dbm() / to_quality() (global functions) |
Gone — use Signal::fromDbm() / Signal::fromQuality() |
to_hex(), trim_first(), extract_bssid(), extract_after(), glue_commands() (global functions) |
Gone. These were parser internals; the 3.0 parsers (src/Parser/**) are pure functions over fixture text and do not need them from outside |
Sanchescom\WiFi\Exceptions\CommandException |
Sanchescom\WiFi\Exception\CommandFailed |
Sanchescom\WiFi\Exceptions\NetworkNotFoundException |
Sanchescom\WiFi\Exception\NetworkNotFound |
Sanchescom\WiFi\Exceptions\DeviceNotFoundException |
Sanchescom\WiFi\Exception\DeviceNotFound |
Sanchescom\WiFi\Exceptions\UnknownSystemException |
Sanchescom\WiFi\Exception\UnsupportedOperation, thrown by Sanchescom\WiFi\Shell\Os::current() when PHP_OS_FAMILY is not Linux, Darwin or Windows. (The lower-level Os::from() — a plain native enum::from() — still throws PHP's own ValueError; the library itself only ever calls Os::current().) |
Sanchescom\WiFi\System\Windows\Profile |
Sanchescom\WiFi\Backend\Windows\ProfileFile |
Two behavioural changes that are not renames:
- macOS
disconnect()no longer removes the preferred network. 2.x callednetworksetup -removepreferredwirelessnetworkas part of disconnecting, which also forgot the network. 3.0'sNetworksetupBackend::disconnect()only power-cycles the Wi-Fi radio (-setairportpower offthenon); if you relied on disconnect also forgetting the network on macOS, do that separately. - Sentinel values are gone. A network with no signal reading used to
be reported as
-100dBm /0%; a network with an unknown channel used to reportfrequency === 0. Both arenullnow ($network->signal,$network->frequency,$network->channel,$network->band) — check fornull, not for the old magic numbers.
connect() and disconnect() are now declared as
connect(string $password, ?string $device = null): void and
disconnect(?string $device = null): void. Any class that extends
AbstractNetwork must widen its own connect()/disconnect() signatures
to match, or PHP fatals with a "declaration must be compatible" error.
This is not theoretical: the project's own test suite has two anonymous
AbstractNetwork subclasses (tests/SecurityTypeTest.php and
tests/SignalTest.php) that fataled until their connect()/disconnect()
signatures were widened to ?string $device = null. This is the only
source-level change a subclass author needs to make.
Windows\Network::getProfileService() now declares its return type as
: Profile (2.0.1 had none), and Windows\Profile::$ssid /
$securityType are now typed string. A subclass that overrides
getProfileService() must declare the same : Profile return type, and a
subclass of Profile must not redeclare $ssid or $securityType
without the string type — PHP forbids dropping a parent's return type or
narrowing/removing a parent's property type, and fatals at class load
("declaration must be compatible") if either is done.
- Command strings are now quoted (
escapeshellarg()on Linux/macOS,",%and!replaced with a space on Windows). Anyone asserting ongetLastCommand()must update the expected strings. - The Windows profile path moved from
vendor/sanchescom/php-wifi/tmp/<ssid>.xmlto a random file undersys_get_temp_dir(), and the package'stmp/directory no longer exists. Anything that relied onProfile::getTmpFileName()'s old location breaks. - Band filters (
get24GhzNetworks(),get5GhzNetworks()) now select by frequency instead of channel number. On Windows,netshreports channels only, so 6 GHz networks are reported on their 5 GHz table frequency there.
Version 2.0 brings PHP 8.2+ support, modern syntax, extended functionality, and macOS improvements.
Before (v1.x):
"php": "^7.2.8"Now (v2.0):
"php": "^8.2"2.0.1 widened this to any 8.2+ release (8.4, 8.5) and allows illuminate/collections 11–13.
Before: tightenco/collect
Now: illuminate/collections
The API remains the same, but the underlying library changed.
All methods now have strict return types:
// Before
public function getAll()
// Now
public function getAll(): arrayv1.x Problem:
WARNING: The airport command line tool is deprecated and will be removed in a future release.
v2.0 Solution:
- Uses
system_profiler SPAirPortDataType(official Apple tool) - Dual-format parser for backward compatibility
- Zero deprecation warnings
system_profiler scanning has real limits that 2.0.0's docs did not
mention:
- Apple gates Wi-Fi details behind Location Services: unless the process
running PHP has been granted Location Services, every SSID comes back as
the literal string
<redacted>. system_profilernever reports BSSIDs for other networks, sobssidis always empty on macOS.- Because of that,
getByBssid()cannot find anything and the CLI'sconnect --bssiddoes not work on macOS — usegetBySsid().
use Sanchescom\WiFi\WiFi;
// Get only 5GHz networks
$networks5g = WiFi::scan()->get5GhzNetworks();
// Get networks with strong signal
$strongNetworks = WiFi::scan()->getByMinSignalStrength(-60);
// Get WPA2 networks
$secureNetworks = WiFi::scan()->getBySecurity('WPA2');
// Chain filters
$best = WiFi::scan()
->get5GhzNetworks()
->getBySecurity('WPA2')
->sortBySignalStrength()
->first();// Quick access methods
$connected = WiFi::getConnected();
$strongest = WiFi::getStrongestNetwork();
$networks5g = WiFi::get5GhzNetworks();composer require sanchescom/php-wificomposer update sanchescom/php-wifiNote: You may need to update your PHP version to 8.2+ first.
# Run tests
composer test
# Check code style
composer lint
# Test WiFi scanning
./vendor/bin/wifi listMost v1.x code will work without changes. The main requirement is PHP 8.2+.
<?php
use Sanchescom\WiFi\WiFi;
// This works in both v1.x and v2.0
$networks = WiFi::scan();
foreach ($networks->getAll() as $network) {
echo "{$network->ssid}: {$network->dbm} dBm\n";
}- Report issues: https://github.com/sanchescom/php-wifi/issues
- Read the docs: README.md
- Check examples in README.md