PHPackages                             gcgov/arcgisphpsdk - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. gcgov/arcgisphpsdk

ActiveLibrary[Framework](/categories/framework)

gcgov/arcgisphpsdk
==================

Open source framework for PHP interactions with Garrett County, Maryland's ArcGIS implementation

v2.2.0(1mo ago)226MITPHPPHP &gt;=8.1

Since Mar 7Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/gcgov/arcgisphpsdk)[ Packagist](https://packagist.org/packages/gcgov/arcgisphpsdk)[ RSS](/packages/gcgov-arcgisphpsdk/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (6)Dependencies (4)Versions (17)Used By (0)

arcgisphpsdk
============

[](#arcgisphpsdk)

An open source PHP SDK for interacting with Garrett County, Maryland's ArcGIS Enterprise implementation. It wraps the [ArcGIS REST API](https://developers.arcgis.com/rest/) with strongly typed PHP classes so server-side applications can authenticate, query, create, update, and delete features on hosted feature services, manage layer schemas (fields and coded-value domains), and geocode addresses.

The SDK is the server-side half of Garrett County's GIS integrations: PHP applications use it to keep their own databases in sync with hosted ArcGIS feature layers, while browser applications render those same layers with the [ArcGIS Maps SDK for JavaScript](https://developers.arcgis.com/javascript/latest/) (`@arcgis/core`). The county's bridges application (`gcgov/bridge-api` + `gcgov/bridge-app`) is the reference implementation and the source of the examples in this document.

Requirements
------------

[](#requirements)

- PHP &gt;= 8.1
- [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) ^7.8 (HTTP client)
- [andrewsauder/json-deserialize](https://github.com/andrewsauder/json-deserialize) ^3.0 (typed JSON hydration/serialization — every model in this SDK extends it)

Installation
------------

[](#installation)

```
composer require gcgov/arcgisphpsdk
```

Production applications pin a tagged release (for example `"gcgov/arcgisphpsdk": "^2.0"`). During local development, consuming applications typically require `dev-main` and register the SDK as a symlinked path repository so edits take effect immediately:

```
{
	"require": {
		"gcgov/arcgisphpsdk": "dev-main"
	},
	"repositories": [
		{
			"type": "path",
			"url": "C:/inetpub/www/packages/arcgis/",
			"options": { "symlink": true }
		}
	]
}
```

Architecture at a glance
------------------------

[](#architecture-at-a-glance)

Classes autoload from `src/` with PSR-4 prefix `gcgov\arcgis\`. The code is split into two layers:

NamespacePurpose`gcgov\arcgis\sdk\*`Generic ArcGIS REST machinery: configuration, token generation, the `FeatureService` base class (query/add/update/delete/schema operations), the `LocatorService` base class (geocoding), typed models for service and layer metadata, and typed response models.`gcgov\arcgis\*` (root)Garrett County–specific service wrappers bound to specific published services. Each wrapper pairs with a sub-namespace of typed `Feature` / `FeatureProperties` / `FeatureQuery` classes that mirror that layer's schema.Prebuilt service wrappers:

ClassArcGIS serviceLayerNotes`\gcgov\arcgis\MajorMinorFeatureService``Hosted/Major_and_Minor_Structures/FeatureServer`0The bridges application's system-of-record layer. When `config` has `development=true`, targets `Hosted/DEVELOPMENT_Major_and_Minor_Structures` instead.`\gcgov\arcgis\BridgesCulvertsFeatureService``Hosted/Bridges_and_Culverts/FeatureServer`0Legacy bridges/culverts inventory layer; used for one-time imports into the bridges application.`\gcgov\arcgis\CenterLinesCulvertsFeatureService``Addresses/Garrett_Centerlines/FeatureServer`1County road centerlines (Maryland SHA schema, uppercase field names). Includes `getAllRoadNames()`.`\gcgov\arcgis\DPUWaterSystemEditorFeatureService``Hosted/DPU_Water_System_Editor/FeatureServer`21 layers + 5 tablesDepartment of Public Utilities water system assets (hydrants, valves, meters, mains, boundaries, inspection tables). One typed `getAll…Features()` method and a `LAYER_*`/`TABLE_*` id constant per sublayer and table. When `config` has `development=true`, targets `Hosted/DEVELOPMENT_DPU_Water_System_Editor` instead and transparently translates the `LAYER_*`/`TABLE_*` constants to that service's re-sequenced layer ids (0–25).`\gcgov\arcgis\AddressLocator``Addresses/Garrett_Address_Locator/GeocodeServer`—Address geocoding via `findAddressCandidates`.Every model extends `andrewsauder\jsonDeserialize\jsonDeserialize`, which gives it:

- `ClassName::jsonDeserialize( $json )` — build a typed instance graph from a JSON string or Guzzle response body. Array properties are hydrated to the class named in the property's `/** @var Type[] */` docblock.
- Serialization back to JSON via `json_encode()`, with `#[excludeJsonSerialize]` / `#[excludeJsonDeserialize]` attributes and `_afterJsonSerialize()` / `_afterJsonDeserialize()` hooks to shape the payload.

Quick start
-----------

[](#quick-start)

```
use gcgov\arcgis\config;
use gcgov\arcgis\MajorMinorFeatureService;

// baseUrl, username, password, development flag
$config  = new config( 'https://gis.garrettcounty.org/', 'username', 'password', false );
$service = new MajorMinorFeatureService( $config );

// fetch every feature on the layer (automatically pages through transfer limits)
$features = $service->getAllFeatures();

foreach( $features as $feature ) {
	// $feature->properties is typed to the layer schema
	echo $feature->properties->countystructurenumber . ' — ' . $feature->properties->roadname . PHP_EOL;

	// geometry is GeoJSON: coordinates are [ longitude, latitude ] in WGS84
	$coordinates = $feature->geometry?->coordinates;
}
```

All SDK methods throw `\GuzzleHttp\Exception\GuzzleException` (transport failures) and `\andrewsauder\jsonDeserialize\exceptions\jsonDeserializeException` (unexpected response shape); wrap calls accordingly:

```
try {
	$features = $service->getAllFeatures();
}
catch( \GuzzleHttp\Exception\GuzzleException | \andrewsauder\jsonDeserialize\exceptions\jsonDeserializeException $e ) {
	// log and handle
}
```

Configuration and authentication
--------------------------------

[](#configuration-and-authentication)

`\gcgov\arcgis\config` carries the portal base URL, credentials, and a development flag:

```
$config = new \gcgov\arcgis\config(
	baseUrl:     'https://gis.garrettcounty.org/', // default
	username:    'serviceAccount',
	password:    'secret',
	development: false
);
```

- `config::getToken()` lazily requests a token from `{baseUrl}portal/sharing/rest/generateToken` the first time it is needed, caches it on the config instance, and automatically requests a fresh one when the cached token expires. Every request the SDK makes appends this token, so you never handle tokens directly — just share one `config` instance across the services you construct.
- The token request is made with `client=referer` and a referer of `https://apps.garrettcountymd.gov` (see `sdk\Token::get()`).
- `development: true` switches wrappers that support it (currently `MajorMinorFeatureService`) to their `DEVELOPMENT_`-prefixed service so you can exercise edits without touching production data. Consuming applications typically pass their own "is local environment" flag here.

Real-world construction from `bridge-api`, where connection settings live in the app's environment config:

```
$arcgisServiceConfig = new \gcgov\arcgis\config(
	config::getEnvironmentConfig()->appDictionary['arcgisBaseUrl'],
	config::getEnvironmentConfig()->appDictionary['arcgisUsername'],
	config::getEnvironmentConfig()->appDictionary['arcgisPassword'],
	config::getEnvironmentConfig()->isLocal()
);
$majorMinorFeatureService = new \gcgov\arcgis\MajorMinorFeatureService( $arcgisServiceConfig );
```

Reading features
----------------

[](#reading-features)

Each service wrapper provides `getAllFeatures()` — or, for multi-layer services, one `getAll…Features()` method per sublayer — which queries the layer with `f=geojson`, `where=1=1`, and `outFields=*`, requesting 1,000 records per call and recursing with `resultOffset` while the response reports `exceededTransferLimit`. The merged result is an array of typed `Feature` objects:

```
$roadsService = new \gcgov\arcgis\CenterLinesCulvertsFeatureService( $config );

/** @var \gcgov\arcgis\CenterLinesFeatureService\Feature[] $roads */
$roads = $roadsService->getAllFeatures();

// convenience helper: distinct, sorted local road names
$roadNames = $roadsService->getAllRoadNames();
```

Multi-layer services expose one typed method and one layer id constant per sublayer and table. The constants are also how you target a sublayer in edit and schema operations. The constants always hold the production layer ids — in development mode the service translates them to the development copy's ids on every request, so consuming code never branches on environment:

```
use gcgov\arcgis\DPUWaterSystemEditorFeatureService;

$waterSystemService = new DPUWaterSystemEditorFeatureService( $config );

/** @var \gcgov\arcgis\DPUWaterSystemEditorFeatureService\FireHydrant\Feature[] $hydrants */
$hydrants = $waterSystemService->getAllFireHydrantFeatures();

$updateResponse = $waterSystemService->updateFeatures( $hydrants, DPUWaterSystemEditorFeatureService::LAYER_FIRE_HYDRANT );

// tables hydrate the same way — rows are Features with null geometry, and edits post attribute-only payloads
$inspections = $waterSystemService->getAllHydrantMaintenanceInspectionFeatures();
```

Layers that are searched by attribute also expose a where-filtered variant that pages exactly like `getAll…Features()` but sends your ArcGIS SQL where clause instead of `1=1` (escape embedded single quotes by doubling them):

```
/** @var \gcgov\arcgis\DPUWaterSystemEditorFeatureService\ServiceMeter\Feature[] $meters */
$meters = $waterSystemService->getServiceMeterFeaturesWhere( "meterid='81234567'" );
```

A `Feature` follows the GeoJSON shape:

- `$feature->properties` — a `FeatureProperties` subclass with one typed public property per layer field. Property names match the layer's field names exactly (lowercase snake\_case on the county's hosted layers, e.g. `county_number`; uppercase on the SHA centerlines schema, e.g. `RDNAMELOCAL`). Date fields arrive as epoch milliseconds.
- `$feature->geometry` — `sdk\Feature\Geometry` with `type` and a `coordinates` array. **Coordinate order is GeoJSON `[ longitude, latitude ]`**, spatial reference WGS84 (wkid 4326). Line features carry nested coordinate arrays.
- `$feature->getObjectId()` / `$feature->getGlobalId()` — the ESRI identifiers, read from the layer's objectid/globalid fields.

Editing features
----------------

[](#editing-features)

`sdk\FeatureService` exposes `addFeatures()`, `updateFeatures()`, and `deleteFeatures()`, each accepting an array of `Feature` objects and an optional layer id (default `0`). Internally the SDK converts each GeoJSON-shaped `Feature` into the ESRI edit format (`{ attributes: {...}, geometry: { x, y, spatialReference: { wkid: 4326 } } }`) via `sdk\AddFeature::fromFeature()` / `sdk\UpdateFeature::fromFeature()`. Features without coordinates serialize with no geometry, making an attribute-only edit.

### Add

[](#add)

```
$feature = new \gcgov\arcgis\MajorMinorFeatureService\Feature();
$feature->properties->roadname              = 'Bumble Bee Road';
$feature->properties->countystructurenumber = 'G-0042';
$feature->geometry->coordinates             = [ -79.344935, 39.522503 ]; // [ lon, lat ]

$addResponse = $service->addFeatures( [ $feature ] );
```

Check both the response-level error and each per-feature result — this is the pattern `bridge-api` uses, including capturing the new ESRI ids back onto its own record so future syncs can match:

```
if( isset( $addResponse->error ) ) {
	throw new \Exception( $addResponse->error->message ?? 'Top level error' );
}
elseif( !isset( $addResponse->addResults[0] ) ) {
	throw new \Exception( 'No result returned' );
}
elseif( isset( $addResponse->addResults[0]->error ) ) {
	throw new \Exception( $addResponse->addResults[0]->error->description ?? 'Feature error' );
}

$structure->esriObjectId = $addResponse->addResults[0]->objectId;
$structure->esriGlobalId = $addResponse->addResults[0]->globalId;
```

### Update

[](#update)

Fetch existing features, mutate their typed properties, and send them back. `updateFeatures()` matches on the feature's objectid:

```
$features = $service->getAllFeatures();

foreach( $features as $feature ) {
	if( $feature->properties->globalid === $myRecord->esriGlobalId ) {
		$feature->properties->rating       = '7';
		$feature->properties->roadname     = $myRecord->roadName;
		$feature->geometry->coordinates[0] = round( $myRecord->longitude, 5 );
		$feature->geometry->coordinates[1] = round( $myRecord->latitude, 5 );

		$updateResponse = $service->updateFeatures( [ $feature ] );
	}
}
```

`$updateResponse->updateResults` contains a per-feature `success` flag and optional `error`, exactly like add results.

### Delete

[](#delete)

```
$deleteResponse = $service->deleteFeatures( $featuresToDelete );
```

Deletion is by object id — each passed feature must have its objectid populated (`getObjectId()`); features without one are skipped.

### Attachments

[](#attachments)

For layers published with `hasAttachments`, files can be attached to a feature by object id (POST `{layer}/{objectId}/addAttachment`) and listed back (GET `{layer}/{objectId}/attachments`):

```
$attachmentInfos = $service->getAttachmentInfos( $objectId, $layerId );
foreach( $attachmentInfos->attachmentInfos ?? [] as $attachmentInfo ) {
	echo $attachmentInfo->id . ': ' . $attachmentInfo->name . ' (' . $attachmentInfo->contentType . ', ' . $attachmentInfo->size . ' bytes)' . PHP_EOL;
}

$addResponse = $service->addAttachment( $objectId, '/path/to/photo.jpg', 'photo.jpg', 'image/jpeg', $layerId );
if( $addResponse->error!==null || !( $addResponse->addAttachmentResult?->success ?? false ) ) {
	// handle failure: $addResponse->error?->message or $addResponse->addAttachmentResult?->error?->description
}
```

`addAttachment()` streams the file as a multipart upload; the attachment name defaults to the file's base name and the mime type part is omitted when `$contentType` is null. ArcGIS does not deduplicate attachments — upload with deterministic names and check `getAttachmentInfos()` first when an operation may run more than once (see the utility-account-lookup-api meter replacement sync).

Managing layer schema (fields and domains)
------------------------------------------

[](#managing-layer-schema-fields-and-domains)

The `FeatureService` base class can administer the layer definition through the ArcGIS admin REST endpoint. `bridge-api` uses this to project its application model onto the GIS layer: it reflects over its own classes and creates/updates one ESRI field per property, including coded-value domains for dropdown-style fields.

Inspect the current schema:

```
$layer = $service->getFeatureServerLayer( 0 );

foreach( $layer->fields as $field ) {
	echo $field->name . ' (' . $field->type . ')' . PHP_EOL;
	if( $field->domain !== null ) {
		foreach( $field->domain->codedValues as $codedValue ) {
			echo '  ' . $codedValue->code . ' => ' . $codedValue->name . PHP_EOL;
		}
	}
}
```

Add or update fields:

```
use gcgov\arcgis\sdk\FeatureServerLayer\CodedValue;
use gcgov\arcgis\sdk\FeatureServerLayer\Domain;
use gcgov\arcgis\sdk\FeatureServerLayer\Field;

$domain              = new Domain();
$domain->name        = 'structure_rating';
$domain->type        = 'codedValue';
$domain->splitPolicy = 'esriSPTDefaultValue';
$domain->mergePolicy = 'esriMPTDefaultValue';
$domain->codedValues = [];

$codedValue            = new CodedValue();
$codedValue->code      = '7';
$codedValue->name      = 'Good';
$domain->codedValues[] = $codedValue;

$field           = new Field();
$field->name     = 'rating';
$field->alias    = 'Current Overall Rating';
$field->type     = 'esriFieldTypeString'; // esriFieldTypeDouble, esriFieldTypeInteger, esriFieldTypeDate, ...
$field->length   = 256;
$field->nullable = true;
$field->editable = true;
$field->domain   = $domain;

$addDefinition    = $service->addFeatureLayerFields( [ $field ], 0 );    // POST {layer}/addToDefinition
$updateDefinition = $service->updateFeatureLayerFields( [ $field ], 0 ); // POST {layer}/updateDefinition
```

Both return a response with `success` and `error`. `addFeatureLayerFields()` additionally returns `domainMap` entries — when ArcGIS has to rename a requested domain (name collision), `createdDomainName` differs from `originalDomainName`, and the caller should persist the created name for future lookups (see `bridge-api`'s `gisIntegration::updateFeatureLayerDefinition()`).

Service metadata
----------------

[](#service-metadata)

```
$featureServer = $service->getFeatureServer();       // service-level metadata: layers, extents, capabilities
foreach( $featureServer->layers as $layer ) {
	echo $layer->id . ': ' . $layer->name . ' (' . $layer->geometryType . ')' . PHP_EOL;
}

$layer = $service->getFeatureServerLayer( 0 );        // layer-level metadata: fields, indexes, renderer, templates
```

Both calls cache their result on the service instance; pass `forceFetch: true` to re-request (for example after changing the schema — `updateFeatureLayerFields()` does this automatically).

`FeatureServerLayer` also provides lookup helpers for translating human readable values into stored codes (all name matching is case-insensitive):

```
$field  = $layer->getField( 'lifecyclestatus' );                   // ?Field — includes type, length, alias, domain
$domain = $layer->getFieldCodedValueDomain( 'lifecyclestatus' );   // ?Domain — field-level domain, or the subtype domain when the field defines none

$code = $domain?->getCodedValueByName( 'In Service' )?->code;      // '8' — cast to int for integer fields
$validNames = $domain?->getCodedValueNames();                      // [ 'Unknown', 'Proposed', 'In Service', ... ]
```

Geocoding
---------

[](#geocoding)

`\gcgov\arcgis\AddressLocator` wraps the county's geocode service. Set any of the standard [findAddressCandidates](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm) parameters as public properties, then call `findAddressCandidates()` — every non-null property becomes a query string parameter:

```
$locator               = new \gcgov\arcgis\AddressLocator( $config );
$locator->singleLine   = '203 S 4th St, Oakland, MD 21550';
$locator->maxLocations = 5;

$response = $locator->findAddressCandidates();

foreach( $response->candidates as $candidate ) {
	echo $candidate->address . ' (score ' . $candidate->score . '): '
		. $candidate->location->x . ', ' . $candidate->location->y . PHP_EOL;
	// $candidate->attributes carries the full locator output (Match_addr, City, Postal, X/Y, ...)
}
```

How the bridges application uses this SDK (reference integration)
-----------------------------------------------------------------

[](#how-the-bridges-application-uses-this-sdk-reference-integration)

The bridges asset-management system demonstrates the intended full-stack pattern.

**Server side (`bridge-api`, PHP):** MongoDB is the system of record for `structure` documents; the `Major_and_Minor_Structures` hosted layer is the GIS projection of that data. Scheduled CLI endpoints keep the two in sync:

1. **Schema sync** (`/cli/updateFeatureLayerDefinition`) — reflects over the application's `structure` model. Properties tagged with an `#[esriField]` attribute map to ESRI fields (deriving field name, type, and alias from the PHP property); properties tagged with a category attribute get coded-value domains built from the application's category/type tables. Fields are then created or updated on layer 0 via `addFeatureLayerFields()` / `updateFeatureLayerFields()`.
2. **Feature sync** (`/cli/updateGisFeatures`) — pulls all features with `getAllFeatures()`, matches them to Mongo documents by stored `esriGlobalId`, then: updates matched features (copying application values onto `$feature->properties` and rounding coordinates to 5 decimals), adds features for unmatched documents (persisting the returned `objectId`/`globalId` back to Mongo), and deletes features that no longer have a matching document.
3. **Reference-data pulls** — road names and extents are imported from the centerlines layer (`CenterLinesCulvertsFeatureService::getAllFeatures()`), and the legacy `Bridges_and_Culverts` layer was imported one-time into Mongo the same way.

**Client side (`bridge-app`, Vue 3 + `@arcgis/core` ^4.29):** the browser never talks to the PHP SDK — it renders the same ArcGIS services directly with the ArcGIS Maps SDK for JavaScript, and plots application data (structure coordinates served by `bridge-api` from Mongo) as graphics:

```
import esriConfig from "@arcgis/core/config.js";
import Map from "@arcgis/core/Map";
import FeatureLayer from "@arcgis/core/layers/FeatureLayer";
import Graphic from "@arcgis/core/Graphic";
import GraphicsLayer from "@arcgis/core/layers/GraphicsLayer";

esriConfig.portalUrl = "https://gis.garrettcounty.org/portal";

// the same centerlines service the PHP SDK reads server-side
const centerlines = new FeatureLayer({
  url: "https://gis.garrettcounty.org/server/rest/services/Addresses/Garrett_Centerlines/FeatureServer/1",
});
map.add(centerlines);

// application records (synced to GIS by the PHP SDK) drawn from their stored coordinates
const graphicsLayer = new GraphicsLayer();
graphicsLayer.add(new Graphic({
  geometry: { type: "point", longitude: structure.coordinates[0], latitude: structure.coordinates[1] },
  symbol: { type: "simple-marker", color: [226, 119, 40] },
}));
```

The contract that makes the two halves line up:

- **Same services** — the JS `FeatureLayer` URLs are the same `FeatureServer` endpoints the PHP wrappers are bound to.
- **Same coordinates** — both sides use WGS84 (wkid 4326) with GeoJSON `[ longitude, latitude ]` ordering; the PHP SDK writes point geometry as `x = longitude`, `y = latitude`.
- **Stable identifiers** — `esriObjectId` / `esriGlobalId` captured at add time let either side correlate a GIS feature with an application record.
- **Domains as dropdowns** — coded-value domains written by the schema sync drive both ArcGIS pop-ups/editors and the application's own select lists (the app stores each type's `esriCode`).

Adding support for a new feature service
----------------------------------------

[](#adding-support-for-a-new-feature-service)

Wrapping another published service is a four-file pattern (see `src/MajorMinorFeatureService*` for the canonical example):

1. **Service wrapper** — `src/MyServiceFeatureService.php`, extending `sdk\FeatureService` and hard-coding the service and admin URLs (branch on `$config->isDevelopment()` if a development copy of the service exists):

```
namespace gcgov\arcgis;

use gcgov\arcgis\sdk\FeatureService;

class MyServiceFeatureService extends FeatureService {

	public function __construct( config $config ) {
		parent::__construct(
			$config,
			$config->getBaseUrl( 'server/rest/services/Hosted/My_Service/FeatureServer/' ),
			$config->getBaseUrl( 'server/rest/admin/services/Hosted/My_Service/FeatureServer/' )
		);
	}

	/** @return \gcgov\arcgis\MyServiceFeatureService\Feature[] */
	public function getAllFeatures( int $offset = 0, int $featuresPerCall = 1000, array $collections = [] ): array {
		$token  = $this->getConfig()->getToken();
		$url    = $this->getServiceUrl( '0/query?f=geojson&where=1%3D1&outFields=*&resultOffset=' . $offset . '&resultRecordCount=' . $featuresPerCall . '&token=' . $token );
		$client = new \GuzzleHttp\Client();
		$res    = $client->request( 'GET', $url );

		$collection    = \gcgov\arcgis\MyServiceFeatureService\FeatureQuery::jsonDeserialize( $res->getBody() );
		$collections[] = $collection;

		if( $collection->exceededTransferLimit ) {
			return $this->getAllFeatures( $offset + $featuresPerCall, $featuresPerCall, $collections );
		}

		$features = [];
		foreach( $collections as $collection ) {
			$features = array_merge( $features, $collection->features );
		}
		return $features;
	}

}
```

2. **Properties** — `src/MyServiceFeatureService/FeatureProperties.php` extending `sdk\FeatureProperties`, with one nullable typed public property per layer field, **named exactly as the field is named on the layer** (check `getFeatureServerLayer()->fields`).
3. **Feature** — `src/MyServiceFeatureService/Feature.php` extending `sdk\Feature`; type the `$properties` property to your `FeatureProperties`, instantiate it in the constructor, and implement `getProperties()`, `getObjectId()`, and `getGlobalId()` against the layer's id fields.
4. **Query** — `src/MyServiceFeatureService/FeatureQuery.php` extending `sdk\FeatureQuery`, re-declaring `$features` so the docblock hydrates elements to your `Feature` class:

```
namespace gcgov\arcgis\MyServiceFeatureService;

class FeatureQuery extends \gcgov\arcgis\sdk\FeatureQuery {

	/** @var \gcgov\arcgis\MyServiceFeatureService\Feature[] $features */
	public ?array $features = [];

}
```

The `/** @var ... */` docblock on `$features` is what tells the deserializer which class to hydrate — without it (or with the wrong class name) features will not be typed correctly.

License
-------

[](#license)

MIT — see [LICENSE](LICENSE).

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance92

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity59

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 84.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~94 days

Recently: every ~199 days

Total

10

Last Release

40d ago

Major Versions

v1.1.4 → v2.0.02025-08-28

### Community

Maintainers

![](https://www.gravatar.com/avatar/e97ad9c5583ee03e1f670eba13bd11b81e3374da1941dce10f6da80e5dc6d8eb?d=identicon)[andrewsauder](/maintainers/andrewsauder)

---

Top Contributors

[![andrewsauder](https://avatars.githubusercontent.com/u/1380472?v=4)](https://github.com/andrewsauder "andrewsauder (28 commits)")[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (5 commits)")

### Embed Badge

![Health badge](/badges/gcgov-arcgisphpsdk/health.svg)

```
[![Health](https://phpackages.com/badges/gcgov-arcgisphpsdk/health.svg)](https://phpackages.com/packages/gcgov-arcgisphpsdk)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.9k556.2M21.5k](/packages/laravel-framework)[neuron-core/neuron-ai

The PHP Agentic Framework.

2.0k832.6k55](/packages/neuron-core-neuron-ai)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k21](/packages/tempest-framework)[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

19467.3M1.9k](/packages/drupal-core)[unopim/unopim

UnoPim Laravel PIM

10.8k2.5k](/packages/unopim-unopim)[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3661.3M49](/packages/tencentcloud-tencentcloud-sdk-php)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
