urban-brussels/wfs-client

Fluent PHP client to query WFS layers as objects, optimized for GeoServer.

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

pkg:composer/urban-brussels/wfs-client

v0.1.1 2025-12-08 10:47 UTC

This package is auto-updated.

Last update: 2025-12-08 10:49:47 UTC


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 SafeMode to 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-client
  • psr/log (Optional, for error logging)
  • ext-simplexml and ext-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.