PHPackages                             nieknijland/rdw-opendata-php - 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. [API Development](/categories/api)
4. /
5. nieknijland/rdw-opendata-php

ActiveLibrary[API Development](/categories/api)

nieknijland/rdw-opendata-php
============================

A typed PHP client for the RDW Open Data Voertuigen datasets.

v0.4.0(1w ago)0113↑138.9%MITPHPPHP ^8.4CI passing

Since May 16Pushed 1w agoCompare

[ Source](https://github.com/NiekNijland/rdw-opendata-php)[ Packagist](https://packagist.org/packages/nieknijland/rdw-opendata-php)[ Docs](https://github.com/NiekNijland/rdw-opendata-php)[ GitHub Sponsors](https://github.com/NiekNijland)[ RSS](/packages/nieknijland-rdw-opendata-php/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (5)Dependencies (8)Versions (7)Used By (0)

rdw-opendata-php
================

[](#rdw-opendata-php)

[![Latest Version on Packagist](https://camo.githubusercontent.com/4d730c4debead250127627ca3d9492502f1850aaf7d60c1a9fea83a7dbcd8ecc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e69656b6e696a6c616e642f7264772d6f70656e646174612d7068702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/nieknijland/rdw-opendata-php)[![Tests](https://camo.githubusercontent.com/d455fccdd7aac8d6ec2453be6ab8cfedb6c21f0e5fc2cfd5eacbc04fc26a4aa5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f4e69656b4e696a6c616e642f7264772d6f70656e646174612d7068702f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/NiekNijland/rdw-opendata-php/actions/workflows/run-tests.yml)[![Total Downloads](https://camo.githubusercontent.com/9fbbfe1d38808cf9390af169f70a8a7d532d51533bd9cc16f0303abbb6a75c90/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6e69656b6e696a6c616e642f7264772d6f70656e646174612d7068702e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/nieknijland/rdw-opendata-php)

A typed PHP client for the [RDW Open Data](https://opendata.rdw.nl) `Voertuigen`datasets. Avoids magic Dutch field strings: every dataset has a generated field enum with English case names, every record is a typed value object, and dates are `CarbonImmutable` in UTC.

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

[](#installation)

```
composer require nieknijland/rdw-opendata-php
```

Requires PHP 8.4+.

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

[](#quick-start)

```
use NiekNijland\RDW\Rdw;
use NiekNijland\RDW\Fields\RegisteredVehicleField;
use NiekNijland\RDW\Query\SortDirection;

$rdw = new Rdw();

$vehicles = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::CommercialName, 'POLO')
    ->where(RegisteredVehicleField::CanBeTransferred, true)
    ->orderBy(RegisteredVehicleField::RegistrationDate, SortDirection::Desc)
    ->limit(10)
    ->get();

foreach ($vehicles as $vehicle) {
    echo $vehicle->licensePlate.' '.$vehicle->brand.' '.$vehicle->commercialName.PHP_EOL;
    echo '  registered: '.$vehicle->registrationDate?->toDateString().PHP_EOL;
    echo '  apk: '.$vehicle->apkExpiryDate?->toDateString().PHP_EOL;
}
```

Configuration
-------------

[](#configuration)

```
use NiekNijland\RDW\Rdw;
use NiekNijland\RDW\Http\Configuration;

$rdw = new Rdw(new Configuration(
    appToken: 'YOUR_SOCRATA_APP_TOKEN', // optional, raises your rate limit
    userAgent: 'your-app/1.0',
    timeoutSeconds: 10.0,
));
```

The HTTP layer raises `NiekNijland\RDW\Exceptions\RateLimitException` on HTTP `429` (with `retryAfterSeconds` extracted from the `Retry-After` header) and `HttpException` on any other non-2xx response.

Supported datasets
------------------

[](#supported-datasets)

Method on `Rdw`RDW dataset idRecord class`registeredVehicles()``m9d7-ebf2``Records\RegisteredVehicle``registeredVehicleFuels()``8ys7-d773``Records\RegisteredVehicleFuel``registeredVehicleAxles()``3huj-srit``Records\RegisteredVehicleAxle``registeredVehicleBodyworks()``vezc-m2t6``Records\RegisteredVehicleBodywork``registeredVehicleBodyworkSpecifications()``jhie-znh9``Records\RegisteredVehicleBodyworkSpecification``registeredVehicleClasses()``kmfi-hrps``Records\RegisteredVehicleClass``registeredVehicleSubcategories()``2ba7-embk``Records\RegisteredVehicleSubcategory``registeredVehicleSpecialFeatures()``7ug8-2dtt``Records\RegisteredVehicleSpecialFeature``registeredVehicleTrackSets()``3xwf-ince``Records\RegisteredVehicleTrackSet``odometerJudgementExplanations()``jqs4-4kvw``Records\OdometerJudgementExplanation`Query builder
-------------

[](#query-builder)

```
$builder = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->whereIn(RegisteredVehicleField::VehicleType, ['Personenauto', 'Bedrijfsauto'])
    ->whereBetween(RegisteredVehicleField::FirstAdmissionDate, $from, $to)
    ->select(
        RegisteredVehicleField::LicensePlate,
        RegisteredVehicleField::Brand,
        RegisteredVehicleField::CommercialName,
    )
    ->orderBy(RegisteredVehicleField::RegistrationDate, SortDirection::Desc)
    ->limit(25);

$vehicles = $builder->get();        // list
$first    = $builder->first();      // ?RegisteredVehicle
$any      = $builder->exists();     // bool — single-row probe, no hydration
$plates   = $builder->pluck(RegisteredVehicleField::LicensePlate); // list
```

The builder is immutable: every chained method returns a clone. You can share a partially built query across functions safely.

### Where predicates

[](#where-predicates)

MethodEmitsNotes`where($field, $value, $op = '=')``field op value`Operator must be one of `=`, `!=`, ``, `=`, `LIKE`, `NOT LIKE`. Rejects `null` — use `whereNull` instead.`whereIn($field, $values)``field IN (…)`Rejects an empty list.`whereNotIn($field, $values)``field NOT IN (…)``whereNull($field)` / `whereNotNull($field)``field IS [NOT] NULL``whereBetween($field, $min, $max)``field BETWEEN x AND y`Encodes dates as Socrata datetime literals.`whereNotBetween($field, $min, $max)``field NOT BETWEEN x AND y``whereLike($field, $pattern)``field LIKE '…'`SQL `%` wildcards, case-sensitive.`whereStartsWith($field, $prefix)``starts_with(field, '…')`Case-sensitive.`whereContains($field, $sub)``contains(field, '…')`**Case-insensitive** per Socrata.`whereAny(fn ($q) => …)``((a) OR (b) OR …)`OR-group. The callback **must return** the chained builder — the builder is immutable, a void closure throws.`whereNot(fn ($q) => …)``NOT ((a) AND (b))`Same callback contract as `whereAny`. Combine with `whereAny` inside for `NOT (a OR b)`.`whereRaw($expression)`passes throughUse RDW field keys (not English aliases).`search($query)`sets `$q`Socrata full-text search across every string column. Whitespace-tokenized; rows must contain every token.Every field-typed argument takes a case of the dataset's generated field enum — `RegisteredVehicleField`, `RegisteredVehicleFuelField`, etc. The generated enums live under `src/Fields/`; PascalCase case names map to the RDW field key, e.g. `RegisteredVehicleField::CommercialName` → `handelsbenaming`.

### Selection, ordering, pagination

[](#selection-ordering-pagination)

MethodEmitsNotes`select($field, …)``$select=fields`Variadic; chained calls accumulate. Pass field-enum cases.`selectRaw($expr, $alias = null)`appends to `$select`Use for arbitrary SoQL projections. Aliases must match `[A-Za-z_][A-Za-z0-9_]*`.`groupBy($field, …)``$group=fields`Combine with the aggregate helpers below.`orderBy($field, $direction = Asc)``$order=field DIR``SortDirection::Asc` / `SortDirection::Desc`.`orderByRaw($expr)`appends to `$order`Use for arbitrary SoQL like `count DESC`.`limit($n)``$limit=$n`Must be ≥ 1.`offset($n)``$offset=$n`Must be ≥ 0.### Aggregates and projections

[](#aggregates-and-projections)

Combine these with `groupBy()` + `getProjection()` for analytic queries:

```
$top = $rdw->registeredVehicles()
    ->select(RegisteredVehicleField::Brand)
    ->count(null, 'n')
    ->min(RegisteredVehicleField::SeatCount, 'min_seats')
    ->max(RegisteredVehicleField::SeatCount, 'max_seats')
    ->groupBy(RegisteredVehicleField::Brand)
    ->havingRaw('count(*) > 100000')
    ->orderByRaw('n DESC')
    ->limit(5)
    ->getProjection();
```

MethodEmits`count($field = null, $alias = 'count')``count(field)` or `count(*)``countDistinct($field, $alias = 'count')``count(distinct field)``sum($field, $alias = 'sum')``sum(field)``avg($field, $alias = 'avg')``avg(field)``min($field, $alias = 'min')``min(field)``max($field, $alias = 'max')``max(field)``distinct()`prepends `distinct ` to `$select` (requires at least one `select()` call).`havingRaw($expr)`sets `$having`; reference the aliases you used above.`selectRaw($expr, $alias = null)`escape hatch for arbitrary SoQL projections.### Executing the query

[](#executing-the-query)

MethodReturnsNotes`get()``list`Single page, hydrated.`first()``?TRecord`Adds `$limit=1`.`exists()``bool`Single-row probe; skips hydration.`pluck($field)``list`One column's values, cast through the same `ValueCaster` records use.`iterate($pageSize = 1000)``Generator`Pages lazily. Outer `limit()` is a hard ceiling. See [Pagination](#pagination).`getProjection()``list`Raw associative rows. Use for aggregate / groupBy / selectRaw queries that don't fit the record schema.`toSoqlParams()``array`The `$select`/`$where`/`$order`/… map as it will be sent. Useful for debugging — see the [Cookbook](#cookbook).### Boolean fields

[](#boolean-fields)

Fields backed by RDW's `Ja`/`Nee` text values are typed as `bool` in the records and in the where-clauses:

```
$rdw->registeredVehicles()
    ->where(RegisteredVehicleField::CanBeTransferred, true)  // → tenaamstellen_mogelijk='Ja'
    ->where(RegisteredVehicleField::HasOpenRecall, false)    // → openstaande_terugroepactie_indicator='Nee'
    ->get();
```

### Dates

[](#dates)

Calendar-date fields (`*_dt`) hydrate to `CarbonImmutable` at midnight UTC. When you pass any `DateTimeInterface` to `where()` for a date field, it is serialized as a Socrata datetime literal:

```
use Carbon\CarbonImmutable;

$rdw->registeredVehicles()
    ->where(RegisteredVehicleField::FirstAdmissionDate, CarbonImmutable::parse('1991-01-01'), '>=')
    ->where(RegisteredVehicleField::FirstAdmissionDate, CarbonImmutable::parse('1992-01-01'), 'rawRows(DatasetId::RegisteredVehicles, [
    '$where'  => "kenteken = 'AB-12-CD'",
    '$select' => 'kenteken, merk, handelsbenaming',
]);

// Raw metadata document (column types, descriptions, last update, …).
$meta = $rdw->rawMetadata(DatasetId::RegisteredVehicles);
```

### Pagination

[](#pagination)

`get()` issues a single request and returns one page. For large result sets use `iterate()`, which yields hydrated records lazily and pages internally:

```
foreach ($rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->iterate(pageSize: 1000) as $vehicle) {
    // process one vehicle at a time without buffering the whole set
}
```

An outer `->limit(N)` is respected as a hard ceiling.

Relations
---------

[](#relations)

A registered vehicle is the entry point for every other dataset. Relations return pre-filtered query builders, so you can chain more filters before hitting the API:

```
$vehicle = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::LicensePlate, '6ZNS30')
    ->first();

$fuels         = $rdw->relations()->fuelsFor($vehicle)->get();
$axles         = $rdw->relations()->axlesFor($vehicle)->get();
$bodyworks     = $rdw->relations()->bodyworksFor($vehicle)->get();
$subcategories = $rdw->relations()->subcategoriesFor($vehicle)->get();
$specials      = $rdw->relations()->specialFeaturesFor($vehicle)->get();
$trackSets     = $rdw->relations()->trackSetsFor($vehicle)->get();
$judgement     = $rdw->relations()->odometerJudgementFor($vehicle)->first();

// Composite key: bodywork → bodywork specifications / vehicle classes
$bodywork        = $bodyworks[0];
$specifications  = $rdw->relations()->specificationsFor($bodywork)->get();
$vehicleClasses  = $rdw->relations()->vehicleClassesFor($bodywork)->get();
```

Relations throw `RdwException` when a required join key is `null` on the source record — that prevents accidentally querying "everything where kenteken IS NULL".

Cookbook
--------

[](#cookbook)

Worked examples against the live RDW API. Numbers come from a run on `2026-05-16`.

**Count: how many white VW Ups are insured?**

```
$count = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->where(RegisteredVehicleField::CommercialName, 'UP')
    ->where(RegisteredVehicleField::PrimaryColor, 'WIT')
    ->where(RegisteredVehicleField::IsWamInsured, true)
    ->count()
    ->getProjection();
// → [['count' => '21084']]
```

**Group + having: the top-5 brands with more than 100k registrations**

```
$top = $rdw->registeredVehicles()
    ->select(RegisteredVehicleField::Brand)
    ->count(null, 'n')
    ->min(RegisteredVehicleField::SeatCount, 'min_seats')
    ->max(RegisteredVehicleField::SeatCount, 'max_seats')
    ->groupBy(RegisteredVehicleField::Brand)
    ->havingRaw('count(*) > 100000')
    ->orderByRaw('n DESC')
    ->limit(5)
    ->getProjection();
// → [
//     ['merk' => 'VOLKSWAGEN', 'n' => '1521987', 'min_seats' => '1', 'max_seats' => '23'],
//     ['merk' => 'PEUGEOT',    'n' => '852665',  'min_seats' => '1', 'max_seats' => '17'],
//     ...
// ]
```

**Fuzzy model search with OR-group**

```
$count = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->whereAny(fn ($q) => $q
        ->whereStartsWith(RegisteredVehicleField::CommercialName, 'GTI')
        ->whereContains(RegisteredVehicleField::CommercialName, 'R32'))
    ->where(RegisteredVehicleField::IsWamInsured, true)
    ->count()
    ->getProjection();
```

`whereContains` is case-insensitive (Socrata `contains()`), `whereStartsWith`is case-sensitive (Socrata `starts_with()`). Use `whereLike` with `%`wildcards if you need SQL-style patterns.

**Date range: VWs first admitted in 2020-2024**

```
use Carbon\CarbonImmutable;

$count = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->whereBetween(
        RegisteredVehicleField::FirstAdmissionDate,
        CarbonImmutable::parse('2020-01-01', 'UTC'),
        CarbonImmutable::parse('2024-12-31', 'UTC'),
    )
    ->count()
    ->getProjection();
// → [['count' => '348046']]
```

**Pluck: every license plate of a model variant**

```
$plates = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->where(RegisteredVehicleField::CommercialName, 'UP')
    ->where(RegisteredVehicleField::PrimaryColor, 'WIT')
    ->orderBy(RegisteredVehicleField::LicensePlate)
    ->limit(5)
    ->pluck(RegisteredVehicleField::LicensePlate);
// → ['00TKZ5', '00TKZ6', '00TPB4', '00TPL4', '00TTL4']
```

`pluck` of a date field returns `CarbonImmutable` instances; the cast goes through the same `ValueCaster` the typed records use.

**Existence probe (no hydration)**

```
$found = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::LicensePlate, 'ZZ-ZZ-ZZ')
    ->exists();
// → false
```

**Full-text search across all string columns**

```
$hit = $rdw->registeredVehicles()
    ->search('polo gti')
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->select(
        RegisteredVehicleField::LicensePlate,
        RegisteredVehicleField::CommercialName,
    )
    ->first();
```

`$q` is tokenized on whitespace; rows must contain every token. It's slow on big datasets without an app token — narrow it with a `where()` first.

**Lazy iteration over a large fleet**

```
foreach ($rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->iterate(pageSize: 1000) as $vehicle) {
    // one row at a time, no buffering
}
```

An outer `->limit(N)` caps the iteration.

**Debugging: inspect the SoQL the builder will emit**

```
$params = $rdw->registeredVehicles()
    ->where(RegisteredVehicleField::Brand, 'VOLKSWAGEN')
    ->whereBetween(
        RegisteredVehicleField::FirstAdmissionDate,
        CarbonImmutable::parse('2020-01-01', 'UTC'),
        CarbonImmutable::parse('2024-12-31', 'UTC'),
    )
    ->toSoqlParams();
// → [
//   '$where' => "(merk = 'VOLKSWAGEN') AND "
//             . "(datum_eerste_toelating_dt BETWEEN '2020-01-01T00:00:00.000' "
//             . "AND '2024-12-31T00:00:00.000')",
// ]
```

`toSoqlParams()` returns the exact `$select`/`$where`/`$order`/etc. that will be sent to Socrata, without performing the request.

Exceptions
----------

[](#exceptions)

All exceptions extend `NiekNijland\RDW\Exceptions\RdwException`. Catch the base class for blanket handling, or the specific type for control flow.

ExceptionThrown when`HttpException`Non-2xx response (excluding 429) or transport failure. Carries `statusCode` and `responseBody`.`RateLimitException`HTTP 429. Carries `retryAfterSeconds` extracted from the `Retry-After` header.`DatasetNotFoundException`Asking the registry for an unknown dataset id.`RdwException`Catch-all base: invalid relation join keys, malformed JSON, scalar payloads from Socrata, etc.`MissingFieldOverrideException`Generator-time only: RDW exposes a field with no matching override.Schema introspection (for AI / dynamic consumers)
-------------------------------------------------

[](#schema-introspection-for-ai--dynamic-consumers)

`SchemaRegistry` is the entry point for anything that needs to reason about the dataset shape at runtime — e.g. an LLM-driven natural-language → SoQL pipeline that has to know which fields exist, how they're typed, and which values they accept.

```
use NiekNijland\RDW\Datasets\DatasetId;
use NiekNijland\RDW\Schema\SchemaRegistry;

$schema = (new SchemaRegistry())->get(DatasetId::RegisteredVehicles);

foreach ($schema->exposedFields() as $field) {
    // $field->enumCase     — 'CommercialName'
    // $field->rdwKey       — 'handelsbenaming'
    // $field->propertyName — 'commercialName'
    // $field->cast         — CastType case
    // $field->vocabulary   — ?ValueVocabulary
}

foreach ($schema->fieldsWithVocabulary() as $field) {
    $vocab = $field->vocabulary;
    // $vocab->values     — list
    // $vocab->exhaustive — true ⇒ full known set, false ⇒ representative examples
}
```

`ValueVocabulary::closed(...)` marks small Dutch code lists (vehicle type, colors) whose full set is known; `ValueVocabulary::examples(...)` marks fields like brand and commercial name where the list is too large to enumerate and consumers get a curated sample to anchor prompts or autocompletion. The library never enforces the vocabulary during hydration: RDW occasionally introduces new values, and a strict cast would turn that into a crash. Treat `exhaustive` as a hint, not a contract.

Schema regeneration
-------------------

[](#schema-regeneration)

Field enums and record classes live under `src/Fields/` and `src/Records/`. They are generated from the curated override classes under `src/Schema/Overrides/` and validated against checked-in metadata snapshots under `metadata/`.

To refresh after a change to overrides (or after pulling a new RDW schema snapshot):

```
composer rdw:generate
```

Generation fails loudly when an RDW metadata field has no override or when an override points at a field RDW no longer exposes — that is the public API stability contract from the implementation plan.

Testing
-------

[](#testing)

```
composer test
```

The default suite is fully offline and uses mocked HTTP. To run live tests against `opendata.rdw.nl`, add your own tests behind an environment guard; the package does not ship live tests in the default suite.

Architecture
------------

[](#architecture)

```
src/
  Rdw.php                      # entry point
  Http/
    Configuration.php          # app token, user agent, timeout
    SocrataClient.php          # thin Guzzle wrapper
  Datasets/
    DatasetId.php              # backed enum of in-scope RDW dataset ids
    DatasetRegistry.php
  Schema/
    CastType.php               # how a single raw value is transformed
    FieldDescriptor.php
    DatasetSchema.php
    SchemaRegistry.php
    Overrides/                 # the canonical English name + cast map (one per dataset)
  Fields/                      # GENERATED — enum cases for typed queries
  Records/                     # GENERATED — typed value objects per dataset
    Hydrator.php
    ValueCaster.php
  Query/
    QueryBuilder.php
    SortDirection.php
  Relations/
    Relations.php              # typed loaders (fuelsFor, axlesFor, …)
  Generator/
    EnumGenerator.php          # produces src/Fields and src/Records from overrides
    SchemaSnapshot.php         # reads metadata/{id}.json
  Exceptions/
    RdwException.php           # base
    HttpException.php
    RateLimitException.php
    DatasetNotFoundException.php
    MissingFieldOverrideException.php

```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for what has changed recently.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance98

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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 ~2 days

Total

6

Last Release

10d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/4d9cb12c53e7e4ec9fdc27f74e8bbd725c61a8280780cffe7136c74f1df7570e?d=identicon)[NiekNijland](/maintainers/NiekNijland)

---

Top Contributors

[![NiekNijland](https://avatars.githubusercontent.com/u/43474728?v=4)](https://github.com/NiekNijland "NiekNijland (20 commits)")

---

Tags

NiekNijlandrdw-opendata-php

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/nieknijland-rdw-opendata-php/health.svg)

```
[![Health](https://phpackages.com/badges/nieknijland-rdw-opendata-php/health.svg)](https://phpackages.com/packages/nieknijland-rdw-opendata-php)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.7k532.1M19.2k](/packages/laravel-framework)[statamic/cms

The Statamic CMS Core Package

4.8k3.5M901](/packages/statamic-cms)[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3751.2M45](/packages/tencentcloud-tencentcloud-sdk-php)[smodav/mpesa

M-Pesa API implementation

16167.1k1](/packages/smodav-mpesa)[imdhemy/google-play-billing

Google Play Billing

491.4M5](/packages/imdhemy-google-play-billing)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

232.5k](/packages/eslazarev-wildberries-sdk)

PHPackages © 2026

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