urban-brussels / wfs-client
Fluent PHP client to query WFS layers as objects, optimized for GeoServer.
Requires
- php: ^8.1
- ext-json: *
- ext-libxml: *
- ext-simplexml: *
- psr/cache: ^3.0
- psr/log: ^1.0|^2.0|^3.0
- symfony/http-client-contracts: ^2.5|^3.0
Requires (Dev)
- roave/security-advisories: dev-latest
- symfony/http-client: ^6.0|^7.0
Suggests
- symfony/http-client: Allows using the native Symfony HTTP Client (recommended implementation).
README
A PHP wrapper for interacting with Web Feature Service (WFS) interfaces. While compatible with standard WFS 1.0.0/2.0.0, it is specifically optimized for GeoServer quirks and requirements.
Features
- Fluent Interface: Chainable methods (
from,where,take,sort) for readable query building. - GeoServer Optimized: Automatically handles hybrid POST/GET requests and version negotiation.
- Advanced Filtering: Supports standard operators, Spatial Filters (Distance, Intersect), and Date ranges.
- Pagination & Sorting: Easy management of limit, offset, and sorting (essential for GeoServer CSV exports).
- Multiple Formats: Retrieve data as JSON arrays, raw CSV, or hit counts.
- Resilience: Built-in
SafeModeto suppress errors and robust Exception handling for common WFS issues. - Metadata Discovery: Methods to fetch capabilities, layer lists, and schema descriptions.
Requirements
- PHP 8.1 or higher
symfony/http-clientpsr/log(Optional, for error logging)ext-simplexmlandext-json
Installation
This client relies on the Symfony HTTP Client.
composer require symfony/http-client
Quick Start
1. Initialization
Inject the HttpClientInterface and your WFS base URL into the client.
use App\Service\WfsClient;
use Symfony\Component\HttpClient\HttpClient;
$httpClient = HttpClient::create();
$wfsUrl = '[https://demo.geoserver.org/geoserver/wfs](https://demo.geoserver.org/geoserver/wfs)';
$client = new WfsClient($httpClient, $wfsUrl);
2. Basic Query
Fetch the first 10 items from a specific layer.
$features = $client->from('topp:states')
->take(10)
->get();
foreach ($features as $feature) {
// Access properties (structure depends on GeoServer response)
echo $feature['properties']['STATE_NAME'];
}
Filtering Data
Standard Conditions (where)
Use the where method for standard attribute filtering. Supported operators: =, <, >, <=, >=, <>, LIKE, ILIKE, IN, IS.
$client->from('ne:countries')
->where('POP_EST', '>', 10000000)
->where('CONTINENT', '=', 'Europe')
->get();
Complex Filtering (Nested AND / OR)
To create grouped conditions (like (A OR B) AND C), use addConditions.
Example: Find permits that are either Refused OR Withdrawn, AND were submitted after 2023.
$client->from('urban:permits')
// 1. First condition (AND is default)
->where('date_submission', '>=', '2023-01-01')
// 2. Grouped conditions with 'OR'
->addConditions([
['status', '=', 'Refused'],
['status', '=', 'Withdrawn']
], 'OR')
->get();
// Resulting CQL Filter: `(date_submission >= '2023-01-01') AND ((status = 'Refused') OR (status = 'Withdrawn'))`
Custom CQL Filter
$client->from('urban:permits')
// Use a custom CQL filter, without any constraint
->setCustomCqlFilter("area(geometry) > 1000 AND status IN ('Delivered', 'Approved')")
->get();
Spatial Filtering
The client includes helpers to generate complex CQL spatial filters.
Filter by Distance (DWITHIN):
Find features within a specific radius (in meters) of a lat/lon point.
$client->from('osm:buildings')
->filterByDistance(
latitude: 48.8566,
longitude: 2.3522,
distance: 500, // 500 meters
geometryName: 'the_geom' // Default is 'geometry'
)
->get();
Filter by Geometry (Intersects, Contains, etc.):
Filter features interacting with a WKT (Well-Known Text) geometry.
$wktPolygon = 'POLYGON((...))';
$client->from('osm:roads')
->filterByGeometry($wktPolygon, 'Intersects')
->get();
Date Filtering
Filter by a specific time range.
$start = new \DateTimeImmutable('2023-01-01');
$end = new \DateTimeImmutable('2023-12-31');
$client->from('events:earthquakes')
->filterByDateRange('timestamp', $start, $end)
->get();
Pagination & Sorting
Important: GeoServer often returns an error ("Cannot do natural order") if you try to use pagination (take/skip) or export to CSV without defining a sort order.
$results = $client->from('topp:states')
->sortBy('STATE_NAME', 'ASC') // 'ASC' or 'DESC'
->skip(0) // Offset
->take(25) // Limit
->get();
Export & Formats
Counting Results
Count the total number of features matching your query (ignoring take/limit).
$totalCount = $client->from('topp:states')->count();
CSV Export
Get the raw CSV string directly from WFS. Note: Requires sortBy.
$csvData = $client->from('topp:states')
->sortBy('id')
->getCsv();
file_put_contents('export.csv', $csvData);
Result Mapping
Transform the results on the fly using a callback.
$dtos = $client->from('users')
->map(function ($feature) {
return new UserDTO(
$feature['properties']['name'],
$feature['properties']['email']
);
})
->get();
Metadata & Discovery
Use these methods to explore the WFS server capabilities.
// 1. Get full capabilities (Service info + Layer list)
$capabilities = $client->getCapabilities();
// 2. Get a simple array of [LayerName => LayerTitle] (Useful for UI Selects)
$layers = $client->getAvailableLayers();
// 3. Get schema definition for a specific layer
$schema = $client->from('topp:states')->describeLayer();
// 4. Get available output formats
$formats = $client->getAvailableFormats();
Configuration & Error Handling
HTTP Method
By default, the client uses POST to handle large CQL filters. You can force GET.
$client->setHttpMethod('GET');
Safe Mode
By default, the client throws exceptions on HTTP errors or WFS exceptions. Enable Safe Mode to catch errors, log them (if a Logger is provided), and return empty results.
$client->safeMode(true);
$result = $client->from('non_existent_layer')->get();
// $result is [] instead of throwing Exception
WFS Version
Defaults to 2.0.0. Can be downgraded for older servers.
$client->setWfsVersion('1.0.0');
License
This code is provided as-is. Feel free to modify and use it in your projects.