Plugin storage and HTTP
PluginStorage confinement rules, PluginHttpClient host allowlist and redirect behaviour, testing with Http::fake / Storage::fake, and the Xero client example.
HTTP — PluginHttpClient
Obtain a client:
Signals::http(string $package): PluginHttpClient
// PluginBase::http()
// PluginContext::http()
Allowlist rules
- Exact host match against manifest
network[](case-insensitive). - No subdomain wildcards — declare each concrete host.
- Client uses
withoutRedirecting()so a3xx Locationcannot escape the allowlist. - Timeouts: request
30s, connect10s. - Blocked hosts throw
PluginHttpExceptionand never hit the transport.
Failure message shape:
Plugin [{$package}] is not allowed to request host [{$host}].
Manifest validation for network entries: Plugin manifest (network.wildcard rules).
Xero worked example
Manifest:
network:
- api.xero.com
- identity.xero.com
Usage pattern (from the Xero client / pushers):
$response = Signals::http('signals/xero-sync')
->post('https://api.xero.com/api.xro/2.0/Contacts', $body);
XeroClient::forPlugin($plugin) builds on $plugin->http() (or PluginContext::http() inside hooks).
Storage — PluginStorage
Signals::storage(string $package): PluginStorage
// PluginBase::storage()
// PluginContext::storage()
Confinement
Root path:
plugins/{vendor_package_snake}/
/ and - in the package name become _:
| Package | Root |
|---|---|
acme/example |
plugins/acme_example/ |
signals/xero-sync |
plugins/signals_xero_sync/ |
Rejected paths: empty, null byte (\0), absolute (/ or C:\…), any .. segment, or any path that escapes the root after normalisation.
API
| Method | Role |
|---|---|
put |
Write |
get |
Read |
exists |
Presence |
delete |
Remove |
files |
List under a relative prefix |
path |
Resolve confined absolute path |
confine |
Validate / normalise a relative path |
Violations throw PluginStorageException.
Testing patterns
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
Http::preventStrayRequests();
Http::fake([
'https://identity.xero.com/connect/token' => Http::response([...], 200),
'https://api.xero.com/api.xro/2.0/Contacts' => Http::response([...], 200),
]);
Storage::fake(); // when asserting PluginStorage writes on the default disk
Combine with InstallsFixturePlugins / local PluginDiscovery when you need a real package context. See Examples and tests/Support/Plugins/.