From 09570c3b9d452be927ce7fda259dd6e947d48a47 Mon Sep 17 00:00:00 2001 From: Claudio Ferraro Date: Fri, 4 Sep 2026 14:35:18 +0200 Subject: [PATCH 1/5] Add generic postcode/address lookup with pluggable drivers --- README.md | 96 +++++++-- composer.json | 20 +- config/rapidez/postcode.php | 22 ++ config/rapidez/skeleton.php | 4 - configure.php | 217 -------------------- resources/js/components/Example.vue | 35 ---- resources/js/package.js | 62 ++++-- resources/views/.gitkeep | 0 resources/views/components/.gitkeep | 0 routes/api.php | 5 - routes/web.php | 6 +- src/Contracts/PostcodeDriver.php | 10 + src/DataTransferObjects/PostcodeResult.php | 32 +++ src/Drivers/PostcodeEuDriver.php | 37 ++++ src/Drivers/PostcodeserviceDriver.php | 40 ++++ src/Drivers/Pro6ppDriver.php | 68 ++++++ src/Http/Controllers/PostcodeController.php | 37 ++++ src/PostcodeManager.php | 40 ++++ src/PostcodeServiceProvider.php | 36 ++++ src/SkeletonServiceProvider.php | 64 ------ 20 files changed, 463 insertions(+), 368 deletions(-) create mode 100644 config/rapidez/postcode.php delete mode 100644 config/rapidez/skeleton.php delete mode 100644 configure.php delete mode 100644 resources/js/components/Example.vue delete mode 100644 resources/views/.gitkeep delete mode 100644 resources/views/components/.gitkeep delete mode 100644 routes/api.php create mode 100644 src/Contracts/PostcodeDriver.php create mode 100644 src/DataTransferObjects/PostcodeResult.php create mode 100644 src/Drivers/PostcodeEuDriver.php create mode 100644 src/Drivers/PostcodeserviceDriver.php create mode 100644 src/Drivers/Pro6ppDriver.php create mode 100644 src/Http/Controllers/PostcodeController.php create mode 100644 src/PostcodeManager.php create mode 100644 src/PostcodeServiceProvider.php delete mode 100644 src/SkeletonServiceProvider.php diff --git a/README.md b/README.md index ae4c38e..27f5f34 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,100 @@ -# Rapidez :package_name_without_prefix - -This repository can be used as template for a new Rapidez package. +# Rapidez Postcode -- Click on "Use this template" on the top of this Github repo page -- Run `php ./configure.php` +Generic postcode/address lookup for Rapidez, with pluggable drivers. Listens to the +`postcode-change` Vue event already wired into Rapidez's own address form, so it works +out of the box without needing to change any checkout/address form Blade templates. -Credits to [`spatie/package-skeleton-laravel`](https://github.com/spatie/package-skeleton-laravel) for the inpiration for this template. +Ships with three drivers out of the box, each calling its own API directly - no Magento +configuration required: -Keep in mind that if you contribute to this template; it should work for official and unofficial packages! -- `rapidez/something` -- `someone/rapidez-something` - -:package_description +- `postcodeeu` - [Postcode.eu](https://www.postcode.eu/) +- `pro6pp` - [Pro6pp](https://pro6pp.nl/) +- `postcodeservice` - [Postcodeservice](https://www.postcodeservice.com/) + +## Requirements + +- PHP ^8.2 +- `rapidez/core` ^5.0 ## Installation ``` -composer require :vendor_slug/:package_slug +composer require rapidez/postcode ``` ## Configuration -You can publish the config with: +Publish the config with: ``` -php artisan vendor:publish --tag=rapidez-:package_slug_without_prefix-config +php artisan vendor:publish --tag=rapidez-postcode-config ``` -## Views +This adds `config/rapidez/postcode.php`, which picks the active driver via `POSTCODE_DRIVER` and +holds each driver's credentials. Pick one driver and fill in its `.env` values: + +```env +POSTCODE_DRIVER=postcodeeu +POSTCODE_EU_API_KEY= +POSTCODE_EU_API_SECRET= +``` -You can publish the views with: +```env +POSTCODE_DRIVER=pro6pp +PRO6PP_API_KEY= ``` -php artisan vendor:publish --tag=rapidez-:package_slug_without_prefix-views + +```env +POSTCODE_DRIVER=postcodeservice +POSTCODESERVICE_CLIENT_ID= +POSTCODESERVICE_SECURE_CODE= ``` +`postcodeservice`'s defaults are the public test credentials, so that driver works out of the box +without any configuration for testing purposes. + +Switching drivers is purely a `.env`/config change - no code changes, and no changes to the +route, controller or JavaScript. + +## Response shape + +The `/api/postcode` endpoint (and each driver's `lookup()` method) returns: + +```json +{ + "found": true, + "street": "Dam", + "city": "Amsterdam", + "province": "Noord-Holland", + "postcode": "1012JS", + "houseNumber": "1", + "houseNumberAddition": "", + "houseNumberAdditions": [""] +} +``` + +`found` is `false` (with all other fields `null` or empty) for an invalid or non-existent +postcode/house number combination. + +## Adding a driver + +1. Create a class implementing `Rapidez\Postcode\Contracts\PostcodeDriver`: + ```php + class MyServiceDriver implements PostcodeDriver + { + public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult + { + // Call the external API and return a PostcodeResult. + } + } + ``` +2. Add a `createDriver()` method to `Rapidez\Postcode\PostcodeManager` that resolves it from + config (the name maps to the `drivers.` config key and the `POSTCODE_DRIVER` value, e.g. + `createMyserviceDriver()` for `myservice`). +3. Add a `drivers.` section to `config/rapidez/postcode.php` for its credentials/settings. + +No changes to the route, controller or JavaScript are needed - the manager resolves whichever +driver is configured, and the controller/JS are entirely driver-agnostic. + ## License GNU General Public License v3. Please see [License File](LICENSE) for more information. diff --git a/composer.json b/composer.json index 369a3c5..40cb58c 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,19 @@ { - "name": ":vendor_slug/:package_slug", - "description": ":package_description", + "name": "rapidez/postcode", + "description": "Generic postcode/address lookup for Rapidez, with pluggable drivers for Postcode.eu, Pro6pp and Postcodeservice", "keywords": [ "rapidez", - ":package_slug" + "postcode", + "address-lookup" ], - "homepage": "https://github.com/:vendor_slug/:package_slug", + "homepage": "https://github.com/rapidez/postcode", "license": "GPL-3.0-or-later", "authors": [ { - "name": ":author_name", - "email": "author@domain.com", - "role": "Developer" + "name": "Claudio Ferraro", + "email": "claudio@justbetter.nl", + "homepage": "https://justbetter.nl", + "role": "Developer at JustBetter" } ], "minimum-stability": "dev", @@ -22,7 +24,7 @@ }, "autoload": { "psr-4": { - "VendorName\\Skeleton\\": "src" + "Rapidez\\Postcode\\": "src" } }, "config": { @@ -34,7 +36,7 @@ "extra": { "laravel": { "providers": [ - "VendorName\\Skeleton\\SkeletonServiceProvider" + "Rapidez\\Postcode\\PostcodeServiceProvider" ] } }, diff --git a/config/rapidez/postcode.php b/config/rapidez/postcode.php new file mode 100644 index 0000000..4ca43ab --- /dev/null +++ b/config/rapidez/postcode.php @@ -0,0 +1,22 @@ + env('POSTCODE_DRIVER', 'postcodeeu'), + + 'drivers' => [ + 'postcodeeu' => [ + 'key' => env('POSTCODE_EU_API_KEY'), + 'secret' => env('POSTCODE_EU_API_SECRET'), + ], + + 'pro6pp' => [ + 'key' => env('PRO6PP_API_KEY'), + ], + + 'postcodeservice' => [ + // The defaults are the public test credentials. + 'client_id' => env('POSTCODESERVICE_CLIENT_ID', '1177'), + 'secure_code' => env('POSTCODESERVICE_SECURE_CODE', '9SRLYBCALURPE2B'), + ], + ], +]; diff --git a/config/rapidez/skeleton.php b/config/rapidez/skeleton.php deleted file mode 100644 index 12816ad..0000000 --- a/config/rapidez/skeleton.php +++ /dev/null @@ -1,4 +0,0 @@ - $version) { - if (in_array($name, $names, true)) { - unset($data['require-dev'][$name]); - } - } - - file_put_contents(__DIR__ . '/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); -} - -function remove_composer_script($scriptName) -{ - $data = json_decode(file_get_contents(__DIR__ . '/composer.json'), true); - - foreach ($data['scripts'] as $name => $script) { - if ($scriptName === $name) { - unset($data['scripts'][$name]); - break; - } - } - - file_put_contents(__DIR__ . '/composer.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); -} - -function remove_readme_paragraphs(string $file): void -{ - $contents = file_get_contents($file); - - file_put_contents( - $file, - preg_replace('/.*/s', '', $contents) ?: $contents - ); -} - -function safeUnlink(string $filename) -{ - if (file_exists($filename) && is_file($filename)) { - unlink($filename); - } -} - -function determineSeparator(string $path): string -{ - return str_replace('/', DIRECTORY_SEPARATOR, $path); -} - -function replaceForWindows(): array -{ - return preg_split('/\\r\\n|\\r|\\n/', run('dir /S /B * | findstr /v /i .git\ | findstr /v /i vendor | findstr /v /i ' . basename(__FILE__) . ' | findstr /r /i /M /F:/ ":author :vendor :package VendorName skeleton vendor_name vendor_slug author@domain.com"')); -} - -function replaceForAllOtherOSes(): array -{ - return explode(PHP_EOL, run('grep -E -r -l -i ":author|:vendor|:package|VendorName|skeleton|vendor_name|vendor_slug|author@domain.com" --exclude-dir=vendor ./* ./.github/* | grep -v ' . basename(__FILE__))); -} - -$gitName = run('git config user.name'); -$authorName = ask('Author name', $gitName); - -$gitEmail = run('git config user.email'); -$authorEmail = ask('Author email', $gitEmail); - -$usernameGuess = explode(':', run('git config remote.origin.url'))[1]; -$usernameGuess = dirname($usernameGuess); -$usernameGuess = basename($usernameGuess); -$authorUsername = ask('Author username', $usernameGuess); - -$vendorName = ask('Vendor name', $authorUsername); -$vendorSlug = slugify($vendorName); -$vendorNamespace = str_replace('-', '', ucwords($vendorName)); -$vendorNamespace = ask('Vendor namespace', $vendorNamespace); - -$currentDirectory = getcwd(); -$folderName = basename($currentDirectory); - -$packageName = ask('Package name', $folderName); -$packageNameWithoutPrefix = remove_prefix('Rapidez ', $packageName); -$packageSlug = slugify($packageName); -$packageSlugWithoutPrefix = remove_prefix('rapidez-', $packageSlug); - -$className = title_case($packageName); -$className = ask('Class name', $className); -$variableName = lcfirst($className); -$description = ask('Package description', "This is my package {$packageSlug}"); - -writeln('------'); -writeln("Author : {$authorName} ({$authorUsername}, {$authorEmail})"); -writeln("Vendor : {$vendorName} ({$vendorSlug})"); -writeln("Package : {$packageSlug} <{$description}>"); -writeln("Namespace : {$vendorNamespace}\\{$className}"); -writeln("Class name : {$className}"); -writeln('------'); - -writeln('This script will replace the above values in all relevant files in the project directory.'); - -if (! confirm('Modify files?', true)) { - exit(1); -} - -$files = (str_starts_with(strtoupper(PHP_OS), 'WIN') ? replaceForWindows() : replaceForAllOtherOSes()); - -foreach ($files as $file) { - replace_in_file($file, [ - ':author_name' => $authorName, - ':author_username' => $authorUsername, - 'author@domain.com' => $authorEmail, - ':vendor_name' => $vendorName, - ':vendor_slug' => $vendorSlug, - 'VendorName' => $vendorNamespace, - ':package_name_without_prefix' => $packageNameWithoutPrefix, - ':package_name' => $packageName, - ':package_slug_without_prefix' => $packageSlugWithoutPrefix, - ':package_slug' => $packageSlug, - 'Skeleton' => $className, - 'skeleton' => $packageSlug, - 'variable' => $variableName, - ':package_description' => $description, - ]); - - match (true) { - str_contains($file, determineSeparator('src/SkeletonServiceProvider.php')) => rename($file, determineSeparator('./src/' . $className . 'ServiceProvider.php')), - str_contains($file, determineSeparator('config/rapidez/skeleton.php')) => rename($file, determineSeparator('./config/rapidez/' . $packageSlugWithoutPrefix . '.php')), - str_contains($file, 'README.md') => remove_readme_paragraphs($file), - default => [], - }; -} - -confirm('Execute `composer install`?') && run('composer install'); - -confirm('Let this script delete itself?', true) && unlink(__FILE__); diff --git a/resources/js/components/Example.vue b/resources/js/components/Example.vue deleted file mode 100644 index 825e4a6..0000000 --- a/resources/js/components/Example.vue +++ /dev/null @@ -1,35 +0,0 @@ - diff --git a/resources/js/package.js b/resources/js/package.js index 9d558cf..82310d1 100644 --- a/resources/js/package.js +++ b/resources/js/package.js @@ -1,17 +1,47 @@ -// This file will be loaded automatically after installing the package. -// See: https://github.com/rapidez/rapidez/blob/master/resources/js/app.js - -document.addEventListener('vue:loaded', function (event) { - const vue = event.detail.vue - - // import { defineAsyncComponent } from 'vue' - // vue.component('example', defineAsyncComponent(() => import('./components/Example.vue'))) - // -- Or if you don't want it to be lazy loaded: - // import example from './components/Example.vue' - // vue.component('example', example) - // You can access the main Vue instance with "window.app" or "vue" - - // window.$on('event-name', () => { - // - // }); +import { useDebounceFn, useMemoize } from '@vueuse/core' +import { on } from 'Vendor/rapidez/core/resources/js/polyfills/emit.js' + +const getAddress = useMemoize(async function (postcode, housenumber, addition) { + return window.rapidezAPI('post', 'postcode', { postcode, housenumber, addition }) }) + +// Splits a housenumber value that may contain an embedded addition, e.g. +// "3T" or "3 T", into its numeric housenumber and addition parts. Used when +// there's no dedicated addition field (street_lines < 3) so users can still +// type the addition straight into the housenumber field. +function splitHouseNumber(value) { + const match = String(value ?? '').trim().match(/^(\d+)\s*(.*)$/) + + return match ? { housenumber: match[1], addition: match[2].trim() } : { housenumber: String(value ?? '').trim(), addition: '' } +} + +async function updateAddress(address, event) { + if ((address?.country_id || address?.country_code) != 'NL') { + return + } + + let rawHousenumber = address?.housenumber || address.street[1] + + if (!address.postcode || !rawHousenumber) { + return + } + + let { housenumber, addition } = splitHouseNumber(rawHousenumber) + // A dedicated addition field (street_lines >= 3) takes precedence over + // whatever was parsed out of the housenumber field itself. + addition = address.street[2] || addition + + let response = await getAddress(address.postcode, housenumber, addition) + + if (!response?.found || !response?.city || !response?.street) { + address.city = '' + address.street[0] = '' + return + } + + address.city = response.city + address.street[0] = response.street + event?.target?.parentElement?.dispatchEvent?.(new Event('change', { bubbles: true })) +} + +on('postcode-change', useDebounceFn(updateAddress, 100), { autoremove: false }) diff --git a/resources/views/.gitkeep b/resources/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/resources/views/components/.gitkeep b/resources/views/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/routes/api.php b/routes/api.php deleted file mode 100644 index 12a9fee..0000000 --- a/routes/api.php +++ /dev/null @@ -1,5 +0,0 @@ -prefix('api')->group(function () { - // -}); diff --git a/routes/web.php b/routes/web.php index a7cbabc..452fa17 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,9 @@ group(function () { - // + // We place this in this middleware for the CSRF protection. + Route::match(['get', 'post'], '/api/postcode', PostcodeController::class); }); diff --git a/src/Contracts/PostcodeDriver.php b/src/Contracts/PostcodeDriver.php new file mode 100644 index 0000000..e91b500 --- /dev/null +++ b/src/Contracts/PostcodeDriver.php @@ -0,0 +1,10 @@ + $this->found, + 'street' => $this->street, + 'city' => $this->city, + 'province' => $this->province, + 'postcode' => $this->postcode, + 'houseNumber' => $this->houseNumber, + 'houseNumberAddition' => $this->houseNumberAddition, + 'houseNumberAdditions' => $this->houseNumberAdditions, + ]; + } +} diff --git a/src/Drivers/PostcodeEuDriver.php b/src/Drivers/PostcodeEuDriver.php new file mode 100644 index 0000000..fe22471 --- /dev/null +++ b/src/Drivers/PostcodeEuDriver.php @@ -0,0 +1,37 @@ +key, $this->secret) + ->get("https://api.postcode.eu/nl/v1/addresses/postcode/{$postcode}/{$houseNumber}/" . ($addition ?? '')); + + if ($response->failed()) { + return new PostcodeResult(found: false); + } + + $data = $response->json(); + + return new PostcodeResult( + found: true, + street: $data['street'] ?? null, + city: $data['city'] ?? null, + province: $data['province'] ?? null, + postcode: $data['postcode'] ?? null, + houseNumber: $data['houseNumber'] ?? null, + houseNumberAddition: $data['houseNumberAddition'] ?? null, + houseNumberAdditions: $data['houseNumberAdditions'] ?? [], + ); + } +} diff --git a/src/Drivers/PostcodeserviceDriver.php b/src/Drivers/PostcodeserviceDriver.php new file mode 100644 index 0000000..b2e5ef1 --- /dev/null +++ b/src/Drivers/PostcodeserviceDriver.php @@ -0,0 +1,40 @@ + $this->clientId, + 'X-SecureCode' => $this->secureCode, + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]) + ->baseUrl('https://api.postcodeservice.com') + ->get('/nl/v5/getAddress', ['zipcode' => $postcode, 'houseno' => $houseNumber]); + + $data = $response->json(); + + if ($response->failed() || ! ($data['city'] ?? null) || ! ($data['street'] ?? null)) { + return new PostcodeResult(found: false); + } + + return new PostcodeResult( + found: true, + street: $data['street'] ?? null, + city: $data['city'] ?? null, + postcode: $postcode, + houseNumber: $houseNumber, + ); + } +} diff --git a/src/Drivers/Pro6ppDriver.php b/src/Drivers/Pro6ppDriver.php new file mode 100644 index 0000000..0221910 --- /dev/null +++ b/src/Drivers/Pro6ppDriver.php @@ -0,0 +1,68 @@ + 'application/json', + 'Accept' => 'application/json', + ]) + ->withQueryParameters(['auth_key' => $this->key]) + ->baseUrl('https://api.pro6pp.nl/v1') + ->get('/autocomplete', ['nl_sixpp' => $postcode]); + + if ($response->failed()) { + return new PostcodeResult(found: false); + } + + $result = collect($response->json('results')) + ->first(fn ($result) => $this->isHouseNumberValid($houseNumber, $addition, $result['streetnumbers'])); + + if (! $result) { + return new PostcodeResult(found: false); + } + + return new PostcodeResult( + found: true, + street: $result['street'] ?? null, + city: $result['city'] ?? null, + province: $result['province'] ?? null, + postcode: $postcode, + houseNumber: $houseNumber, + houseNumberAddition: $addition, + ); + } + + protected function isHouseNumberValid(string $houseNumber, ?string $addition, string $validRanges): bool + { + $houseNumberWithoutAddition = (int) preg_replace('/\D/', '', $houseNumber); + $fullHouseNumber = trim($houseNumber . ($addition ? ' ' . $addition : '')); + + return collect(explode(';', $validRanges))->contains(function ($range) use ($houseNumber, $fullHouseNumber, $houseNumberWithoutAddition) { + $range = trim($range); + + if ($range === $houseNumber || $range === $fullHouseNumber || (int) preg_replace('/\D/', '', $range) === $houseNumberWithoutAddition) { + return true; + } + + if (str_contains($range, '-')) { + [$start, $end] = array_map('trim', explode('-', $range)); + + return is_numeric($start) && is_numeric($end) && $houseNumberWithoutAddition >= $start && $houseNumberWithoutAddition <= $end; + } + + return false; + }); + } +} diff --git a/src/Http/Controllers/PostcodeController.php b/src/Http/Controllers/PostcodeController.php new file mode 100644 index 0000000..3a390bd --- /dev/null +++ b/src/Http/Controllers/PostcodeController.php @@ -0,0 +1,37 @@ +merge([ + 'postcode' => strtoupper(str_replace(' ', '', $request->postcode)), + ])->validate([ + 'postcode' => ['required', 'string', 'regex:/^[1-9][0-9]{3}[A-Z]{2}$/'], + 'housenumber' => ['required', 'string'], + 'addition' => ['nullable', 'string'], + ]); + + $driver = config('rapidez.postcode.driver'); + $addition = $request->addition ?: null; + $cacheKey = "postcode-{$driver}-{$request->postcode}-{$request->housenumber}-{$addition}"; + + $result = Cache::rememberForever($cacheKey, fn () => $this->manager->driver()->lookup( + $request->postcode, + $request->housenumber, + $addition, + )); + + return response()->json($result->toArray()); + } +} diff --git a/src/PostcodeManager.php b/src/PostcodeManager.php new file mode 100644 index 0000000..b630a3a --- /dev/null +++ b/src/PostcodeManager.php @@ -0,0 +1,40 @@ +config->get('rapidez.postcode.driver'); + } + + public function createPostcodeeuDriver(): PostcodeDriver + { + return new PostcodeEuDriver( + $this->config->get('rapidez.postcode.drivers.postcodeeu.key'), + $this->config->get('rapidez.postcode.drivers.postcodeeu.secret'), + ); + } + + public function createPro6ppDriver(): PostcodeDriver + { + return new Pro6ppDriver( + $this->config->get('rapidez.postcode.drivers.pro6pp.key'), + ); + } + + public function createPostcodeserviceDriver(): PostcodeDriver + { + return new PostcodeserviceDriver( + $this->config->get('rapidez.postcode.drivers.postcodeservice.client_id'), + $this->config->get('rapidez.postcode.drivers.postcodeservice.secure_code'), + ); + } +} diff --git a/src/PostcodeServiceProvider.php b/src/PostcodeServiceProvider.php new file mode 100644 index 0000000..dc83077 --- /dev/null +++ b/src/PostcodeServiceProvider.php @@ -0,0 +1,36 @@ +mergeConfigFrom(__DIR__ . '/../config/rapidez/postcode.php', 'rapidez.postcode'); + + $this->app->singleton(PostcodeManager::class); + } + + public function boot(): void + { + $this->bootRoutes()->bootPublishables(); + } + + public function bootRoutes(): self + { + $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); + + return $this; + } + + public function bootPublishables(): self + { + $this->publishes([ + __DIR__ . '/../config/rapidez/postcode.php' => config_path('rapidez/postcode.php'), + ], 'rapidez-postcode-config'); + + return $this; + } +} diff --git a/src/SkeletonServiceProvider.php b/src/SkeletonServiceProvider.php deleted file mode 100644 index 5d5ae14..0000000 --- a/src/SkeletonServiceProvider.php +++ /dev/null @@ -1,64 +0,0 @@ -mergeConfigFrom(__DIR__ . '/../config/rapidez/:package_slug_without_prefix.php', 'rapidez.:package_slug_without_prefix'); - } - - public function boot() - { - $this - ->bootRoutes() - ->bootViews() - ->bootPublishables() - ->bootFilters(); - } - - public function bootRoutes(): self - { - $this->loadRoutesFrom(__DIR__ . '/../routes/api.php'); - $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); - - return $this; - } - - public function bootViews(): self - { - $this->loadViewsFrom(__DIR__ . '/../resources/views', 'rapidez-:package_slug_without_prefix'); - - return $this; - } - - public function bootPublishables(): self - { - $this->publishes([ - __DIR__ . '/../resources/views' => resource_path('views/vendor/rapidez-:package_slug_without_prefix'), - ], 'rapidez-:package_slug_without_prefix-views'); - - $this->publishes([ - __DIR__ . '/../config/rapidez/:package_slug_without_prefix.php' => config_path('rapidez/:package_slug_without_prefix.php'), - ], 'rapidez-:package_slug_without_prefix-config'); - - return $this; - } - - public function bootFilters(): self - { - Eventy::addFilter('index.product.data', function ($data) { - // Manipulate the data - return $data; - }); - - Eventy::addFilter('index.product.mapping', fn ($mapping) => array_merge_recursive($mapping ?: [], [ - 'properties' => [ - // Additional mappings - ], - ])); - } -} From 659bf1c4ab809fe4e416143bc78ac05fb01979b0 Mon Sep 17 00:00:00 2001 From: claudio-ferraro Date: Fri, 4 Sep 2026 12:35:48 +0000 Subject: [PATCH 2/5] Apply fixes from Duster --- src/DataTransferObjects/PostcodeResult.php | 3 +-- src/Drivers/PostcodeEuDriver.php | 4 +--- src/Drivers/PostcodeserviceDriver.php | 4 +--- src/Drivers/Pro6ppDriver.php | 4 +--- src/Http/Controllers/PostcodeController.php | 4 +--- src/PostcodeManager.php | 2 +- 6 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/DataTransferObjects/PostcodeResult.php b/src/DataTransferObjects/PostcodeResult.php index a338152..0798fca 100644 --- a/src/DataTransferObjects/PostcodeResult.php +++ b/src/DataTransferObjects/PostcodeResult.php @@ -13,8 +13,7 @@ public function __construct( public readonly ?string $houseNumber = null, public readonly ?string $houseNumberAddition = null, public readonly array $houseNumberAdditions = [], - ) { - } + ) {} public function toArray(): array { diff --git a/src/Drivers/PostcodeEuDriver.php b/src/Drivers/PostcodeEuDriver.php index fe22471..b952523 100644 --- a/src/Drivers/PostcodeEuDriver.php +++ b/src/Drivers/PostcodeEuDriver.php @@ -8,9 +8,7 @@ class PostcodeEuDriver implements PostcodeDriver { - public function __construct(protected ?string $key, protected ?string $secret) - { - } + public function __construct(protected ?string $key, protected ?string $secret) {} public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult { diff --git a/src/Drivers/PostcodeserviceDriver.php b/src/Drivers/PostcodeserviceDriver.php index b2e5ef1..2e5b7e3 100644 --- a/src/Drivers/PostcodeserviceDriver.php +++ b/src/Drivers/PostcodeserviceDriver.php @@ -8,9 +8,7 @@ class PostcodeserviceDriver implements PostcodeDriver { - public function __construct(protected ?string $clientId, protected ?string $secureCode) - { - } + public function __construct(protected ?string $clientId, protected ?string $secureCode) {} public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult { diff --git a/src/Drivers/Pro6ppDriver.php b/src/Drivers/Pro6ppDriver.php index 0221910..3b395aa 100644 --- a/src/Drivers/Pro6ppDriver.php +++ b/src/Drivers/Pro6ppDriver.php @@ -8,9 +8,7 @@ class Pro6ppDriver implements PostcodeDriver { - public function __construct(protected ?string $key) - { - } + public function __construct(protected ?string $key) {} public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult { diff --git a/src/Http/Controllers/PostcodeController.php b/src/Http/Controllers/PostcodeController.php index 3a390bd..fe84e99 100644 --- a/src/Http/Controllers/PostcodeController.php +++ b/src/Http/Controllers/PostcodeController.php @@ -8,9 +8,7 @@ class PostcodeController { - public function __construct(protected PostcodeManager $manager) - { - } + public function __construct(protected PostcodeManager $manager) {} public function __invoke(Request $request) { diff --git a/src/PostcodeManager.php b/src/PostcodeManager.php index b630a3a..79aebaf 100644 --- a/src/PostcodeManager.php +++ b/src/PostcodeManager.php @@ -5,8 +5,8 @@ use Illuminate\Support\Manager; use Rapidez\Postcode\Contracts\PostcodeDriver; use Rapidez\Postcode\Drivers\PostcodeEuDriver; -use Rapidez\Postcode\Drivers\Pro6ppDriver; use Rapidez\Postcode\Drivers\PostcodeserviceDriver; +use Rapidez\Postcode\Drivers\Pro6ppDriver; class PostcodeManager extends Manager { From 038f5fc2fb077907c336f554c7e171c3d72ebb0e Mon Sep 17 00:00:00 2001 From: Claudio Ferraro Date: Mon, 7 Sep 2026 14:45:22 +0200 Subject: [PATCH 3/5] Require rapidez/coding-standards ^1.0.1 to avoid broken phpstan paths --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 40cb58c..e58e4a8 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,6 @@ }, "require-dev": { "larastan/larastan": "^3.9", - "rapidez/coding-standards": "^1.0" + "rapidez/coding-standards": "^1.0.1" } } From 695e9fa454779f48fb9c051761c5cca38c537559 Mon Sep 17 00:00:00 2001 From: Claudio Ferraro Date: Tue, 8 Sep 2026 09:46:11 +0200 Subject: [PATCH 4/5] Address PR review feedback --- README.md | 57 +++++++++++++++++---------- config/rapidez/postcode.php | 6 +-- src/Drivers/PostcodeEuDriver.php | 7 +++- src/Drivers/PostcodeserviceDriver.php | 7 +++- src/Drivers/Pro6ppDriver.php | 16 +++++++- 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 27f5f34..1d9ccf0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Generic postcode/address lookup for Rapidez, with pluggable drivers. Listens to `postcode-change` Vue event already wired into Rapidez's own address form, so it works out of the box without needing to change any checkout/address form Blade templates. +Dutch (NL) addresses only for now; postcode validation and all three drivers' APIs are +NL-specific. + Ships with three drivers out of the box, each calling its own API directly - no Magento configuration required: @@ -49,10 +52,12 @@ POSTCODESERVICE_CLIENT_ID= POSTCODESERVICE_SECURE_CODE= ``` -`postcodeservice`'s defaults are the public test credentials, so that driver works out of the box -without any configuration for testing purposes. +Test credentials for `postcodeservice` are documented at +[developers.postcodeservice.com](https://developers.postcodeservice.com/#authenticating-requests) +if you want to try that driver without your own account; they're not hardcoded as a default here +since the docs note they may change without prior notice. -Switching drivers is purely a `.env`/config change - no code changes, and no changes to the +Switching drivers is purely a `.env`/config change; no code changes, and no changes to the route, controller or JavaScript. ## Response shape @@ -77,23 +82,35 @@ postcode/house number combination. ## Adding a driver -1. Create a class implementing `Rapidez\Postcode\Contracts\PostcodeDriver`: - ```php - class MyServiceDriver implements PostcodeDriver - { - public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult - { - // Call the external API and return a PostcodeResult. - } - } - ``` -2. Add a `createDriver()` method to `Rapidez\Postcode\PostcodeManager` that resolves it from - config (the name maps to the `drivers.` config key and the `POSTCODE_DRIVER` value, e.g. - `createMyserviceDriver()` for `myservice`). -3. Add a `drivers.` section to `config/rapidez/postcode.php` for its credentials/settings. - -No changes to the route, controller or JavaScript are needed - the manager resolves whichever -driver is configured, and the controller/JS are entirely driver-agnostic. +A driver is a class implementing `Rapidez\Postcode\Contracts\PostcodeDriver`: + +```php +class MyServiceDriver implements PostcodeDriver +{ + public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult + { + // Call the external API and return a PostcodeResult. + } +} +``` + +Register it from your own project or package; no changes to this package needed; by extending +the manager, e.g. in a service provider's `boot()` method: + +```php +$this->app->make(\Rapidez\Postcode\PostcodeManager::class)->extend( + 'myservice', + fn () => new MyServiceDriver(config('rapidez.postcode.drivers.myservice.key')), +); +``` + +Then select it as usual with `POSTCODE_DRIVER=myservice`. No changes to the route, controller or +JavaScript are needed either way; they're entirely driver-agnostic. + +To contribute a new driver to this package itself instead, add a `createDriver()` method to +`PostcodeManager` (the name maps to the `drivers.` config key and the `POSTCODE_DRIVER` +value, e.g. `createMyserviceDriver()` for `myservice`) and a matching `drivers.` section to +`config/rapidez/postcode.php`. ## License diff --git a/config/rapidez/postcode.php b/config/rapidez/postcode.php index 4ca43ab..c85505b 100644 --- a/config/rapidez/postcode.php +++ b/config/rapidez/postcode.php @@ -14,9 +14,9 @@ ], 'postcodeservice' => [ - // The defaults are the public test credentials. - 'client_id' => env('POSTCODESERVICE_CLIENT_ID', '1177'), - 'secure_code' => env('POSTCODESERVICE_SECURE_CODE', '9SRLYBCALURPE2B'), + // Test credentials are documented at https://developers.postcodeservice.com/#authenticating-requests + 'client_id' => env('POSTCODESERVICE_CLIENT_ID'), + 'secure_code' => env('POSTCODESERVICE_SECURE_CODE'), ], ], ]; diff --git a/src/Drivers/PostcodeEuDriver.php b/src/Drivers/PostcodeEuDriver.php index b952523..1f8783a 100644 --- a/src/Drivers/PostcodeEuDriver.php +++ b/src/Drivers/PostcodeEuDriver.php @@ -8,7 +8,12 @@ class PostcodeEuDriver implements PostcodeDriver { - public function __construct(protected ?string $key, protected ?string $secret) {} + public function __construct(protected ?string $key, protected ?string $secret) + { + if (! $this->key || ! $this->secret) { + throw new \InvalidArgumentException('The postcodeeu driver requires POSTCODE_EU_API_KEY and POSTCODE_EU_API_SECRET to be set.'); + } + } public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult { diff --git a/src/Drivers/PostcodeserviceDriver.php b/src/Drivers/PostcodeserviceDriver.php index 2e5b7e3..4fe5d10 100644 --- a/src/Drivers/PostcodeserviceDriver.php +++ b/src/Drivers/PostcodeserviceDriver.php @@ -8,7 +8,12 @@ class PostcodeserviceDriver implements PostcodeDriver { - public function __construct(protected ?string $clientId, protected ?string $secureCode) {} + public function __construct(protected ?string $clientId, protected ?string $secureCode) + { + if (! $this->clientId || ! $this->secureCode) { + throw new \InvalidArgumentException('The postcodeservice driver requires POSTCODESERVICE_CLIENT_ID and POSTCODESERVICE_SECURE_CODE to be set.'); + } + } public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult { diff --git a/src/Drivers/Pro6ppDriver.php b/src/Drivers/Pro6ppDriver.php index 3b395aa..5ee9216 100644 --- a/src/Drivers/Pro6ppDriver.php +++ b/src/Drivers/Pro6ppDriver.php @@ -8,7 +8,12 @@ class Pro6ppDriver implements PostcodeDriver { - public function __construct(protected ?string $key) {} + public function __construct(protected ?string $key) + { + if (! $this->key) { + throw new \InvalidArgumentException('The pro6pp driver requires PRO6PP_API_KEY to be set.'); + } + } public function lookup(string $postcode, string $houseNumber, ?string $addition = null): PostcodeResult { @@ -42,6 +47,14 @@ public function lookup(string $postcode, string $houseNumber, ?string $addition ); } + /** + * Pro6pp returns one `streetnumbers` string per street, listing every house number (and + * addition) that exists on it, separated by semicolons, e.g. "1;11-13;21-27;1 A;1 B" means + * house numbers 1, 11 through 13 and 21 through 27, and 1A/1B, all exist on this street. + * + * This checks whether the given house number (with its addition) is covered by that + * list, either as an exact entry or within one of the numeric ranges. + */ protected function isHouseNumberValid(string $houseNumber, ?string $addition, string $validRanges): bool { $houseNumberWithoutAddition = (int) preg_replace('/\D/', '', $houseNumber); @@ -54,6 +67,7 @@ protected function isHouseNumberValid(string $houseNumber, ?string $addition, st return true; } + // A numeric range entry, e.g. "11-13": valid if the house number falls within it. if (str_contains($range, '-')) { [$start, $end] = array_map('trim', explode('-', $range)); From ce31e23125594128e5d788070dbdc550c9055d66 Mon Sep 17 00:00:00 2001 From: claudio-ferraro Date: Tue, 8 Sep 2026 07:46:36 +0000 Subject: [PATCH 5/5] Apply fixes from Duster --- src/Drivers/PostcodeEuDriver.php | 3 ++- src/Drivers/PostcodeserviceDriver.php | 3 ++- src/Drivers/Pro6ppDriver.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Drivers/PostcodeEuDriver.php b/src/Drivers/PostcodeEuDriver.php index 1f8783a..07a55f1 100644 --- a/src/Drivers/PostcodeEuDriver.php +++ b/src/Drivers/PostcodeEuDriver.php @@ -3,6 +3,7 @@ namespace Rapidez\Postcode\Drivers; use Illuminate\Support\Facades\Http; +use InvalidArgumentException; use Rapidez\Postcode\Contracts\PostcodeDriver; use Rapidez\Postcode\DataTransferObjects\PostcodeResult; @@ -11,7 +12,7 @@ class PostcodeEuDriver implements PostcodeDriver public function __construct(protected ?string $key, protected ?string $secret) { if (! $this->key || ! $this->secret) { - throw new \InvalidArgumentException('The postcodeeu driver requires POSTCODE_EU_API_KEY and POSTCODE_EU_API_SECRET to be set.'); + throw new InvalidArgumentException('The postcodeeu driver requires POSTCODE_EU_API_KEY and POSTCODE_EU_API_SECRET to be set.'); } } diff --git a/src/Drivers/PostcodeserviceDriver.php b/src/Drivers/PostcodeserviceDriver.php index 4fe5d10..77f3ecc 100644 --- a/src/Drivers/PostcodeserviceDriver.php +++ b/src/Drivers/PostcodeserviceDriver.php @@ -3,6 +3,7 @@ namespace Rapidez\Postcode\Drivers; use Illuminate\Support\Facades\Http; +use InvalidArgumentException; use Rapidez\Postcode\Contracts\PostcodeDriver; use Rapidez\Postcode\DataTransferObjects\PostcodeResult; @@ -11,7 +12,7 @@ class PostcodeserviceDriver implements PostcodeDriver public function __construct(protected ?string $clientId, protected ?string $secureCode) { if (! $this->clientId || ! $this->secureCode) { - throw new \InvalidArgumentException('The postcodeservice driver requires POSTCODESERVICE_CLIENT_ID and POSTCODESERVICE_SECURE_CODE to be set.'); + throw new InvalidArgumentException('The postcodeservice driver requires POSTCODESERVICE_CLIENT_ID and POSTCODESERVICE_SECURE_CODE to be set.'); } } diff --git a/src/Drivers/Pro6ppDriver.php b/src/Drivers/Pro6ppDriver.php index 5ee9216..1b9e16c 100644 --- a/src/Drivers/Pro6ppDriver.php +++ b/src/Drivers/Pro6ppDriver.php @@ -3,6 +3,7 @@ namespace Rapidez\Postcode\Drivers; use Illuminate\Support\Facades\Http; +use InvalidArgumentException; use Rapidez\Postcode\Contracts\PostcodeDriver; use Rapidez\Postcode\DataTransferObjects\PostcodeResult; @@ -11,7 +12,7 @@ class Pro6ppDriver implements PostcodeDriver public function __construct(protected ?string $key) { if (! $this->key) { - throw new \InvalidArgumentException('The pro6pp driver requires PRO6PP_API_KEY to be set.'); + throw new InvalidArgumentException('The pro6pp driver requires PRO6PP_API_KEY to be set.'); } }