hostmaster / ua
Modern PHP 8.2+ EPP client library for the .UA registry (epp.hostmaster.ua).
Requires
- php: ^8.2
- ext-dom: *
- ext-libxml: *
- ext-mbstring: *
- ext-openssl: *
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.50
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^10.5 || ^11.0
README
Modern PHP 8.2+ EPP client library for the Ukrainian (.UA) domain registry, epp.hostmaster.ua.
- Strict types, readonly DTOs, typed enums, full PSR-4 / Composer
- Native RFC 5730/5734 EPP-over-TLS transport (4-byte length-prefixed frames)
- Hostmaster.ua dialect:
contact-1.1,domain-1.1,host-1.1,uaepp-1.1(license, force-host-delete),rgp-1.1,balance-1.0 - Per-result-code exception hierarchy — handle
ObjectExistsException,BillingException,AuthorizationException, etc. directly - PSR-3 logging hook for wire-level debugging
Install
composer require hostmaster/ua
Requires PHP 8.2+ with ext-dom, ext-libxml, ext-openssl, ext-mbstring.
Quick start
use Hostmaster\Ua\Client; use Hostmaster\Ua\Config; use Hostmaster\Ua\Enum\ContactType; use Hostmaster\Ua\Enum\PostalType; use Hostmaster\Ua\Model\Address; use Hostmaster\Ua\Model\AuthInfo; use Hostmaster\Ua\Model\ContactCreate; use Hostmaster\Ua\Model\DomainContact; use Hostmaster\Ua\Model\DomainCreate; use Hostmaster\Ua\Model\Nameserver; use Hostmaster\Ua\Model\Period; use Hostmaster\Ua\Model\PostalInfo; $client = new Client(new Config( username: 'MYREG', password: 'super-secret', // host: 'epp-test.hostmaster.ua', // OT&E // localCert: '/etc/ssl/registrar.pem', // localPk: '/etc/ssl/registrar.key', )); $client->login(); $results = $client->domain->check(['example.ua', 'example2.ua']); foreach ($results as $r) { printf("%s -> %s%s\n", $r->name, $r->available ? 'free' : 'taken', $r->reason ? " ({$r->reason})" : ''); } $client->contact->create(new ContactCreate( id: 'CT-EXAMPLE', postalInfo: [ new PostalInfo( type: PostalType::International, name: 'John Doe', address: new Address( street: ['10 Khreshchatyk St'], city: 'Kyiv', countryCode: 'UA', postalCode: '01001', ), ), ], email: 'john@example.com', authInfo: new AuthInfo('contact-secret'), )); $client->domain->create(new DomainCreate( name: 'example.ua', registrant: 'CT-EXAMPLE', period: new Period(1), nameservers: [ Nameserver::reference('ns1.example.com'), Nameserver::reference('ns2.example.com'), ], contacts: [ new DomainContact('CT-EXAMPLE', ContactType::Admin), new DomainContact('CT-EXAMPLE', ContactType::Tech), ], authInfo: new AuthInfo('domain-secret'), license: 'A0000000', // optional uaepp:license extension )); $client->logout();
Architecture
Hostmaster\Ua
├── Client Facade — connects services with a Session
├── Config Readonly connection / login config
├── Session Owns a Connection; runs login/logout/dispatch
├── Connection
│ ├── Connection Transport interface
│ └── SocketConnection TLS stream socket, length-prefixed frames
├── Protocol
│ ├── Namespaces XML namespace constants
│ ├── XmlBuilder Build EPP request frames (DOM-based)
│ ├── XmlParser Parse server <response> envelopes
│ ├── EppResponse Successful response with raw DOM + xpath
│ ├── ResultCode enum: every EPP 1xxx/2xxx code
│ ├── Hydrator DOM → DTO conversion
│ └── TridGenerator clTRID strategy (default = random)
├── Service
│ ├── DomainService check/info/create/update/delete/renew/transfer + RGP restore
│ ├── ContactService check/info/create/update/delete/transfer*
│ ├── HostService check/info/create/update/delete (+ uaepp force delete)
│ ├── PollService poll request/ack
│ └── BalanceService balance:info
├── Notification
│ └── EmailNotificationParser Parses notify@epp.hostmaster.ua emails
├── Logging
│ ├── Redactor Stateless secret stripping for any string
│ └── RedactingLogger PSR-3 decorator that redacts on the way through
├── Model Readonly DTOs (DomainInfo, ContactInfo, EmailNotification, …)
├── Enum PostalType, ContactType, IpVersion, TransferOp, EmailTemplate, …
└── Exception Hierarchy keyed on EPP result codes
Exception handling
Every 2xxx response is thrown as a typed exception. Catch the specific class
you care about, fall back to EppException for everything else, and
HostmasterException for any failure originating in this library:
use Hostmaster\Ua\Exception\AuthenticationException; use Hostmaster\Ua\Exception\BillingException; use Hostmaster\Ua\Exception\ObjectExistsException; use Hostmaster\Ua\Exception\ObjectNotFoundException; use Hostmaster\Ua\Exception\EppException; use Hostmaster\Ua\Exception\ConnectionException; try { $client->login(); $client->domain->create($req); } catch (AuthenticationException $e) { // 2200/2501 — bad credentials } catch (ObjectExistsException $e) { // 2302 — domain already registered } catch (BillingException $e) { // 2104 — registrar balance is negative } catch (EppException $e) { // any other 2xxx — inspect $e->resultCode, $e->reasons, $e->svTRID } catch (ConnectionException $e) { // network/TLS failure }
The exception classes correspond to result-code groups defined in
src/Protocol/ResultCode.php and
mapped in src/Exception/EppException.php.
Logging & secret redaction
Pass any PSR-3 LoggerInterface as the second argument to new Client(...).
Wire frames are logged at debug level. Sensitive fields (<pw>,
<newPW>, <domain:pw>, <contact:pw>, <authInfo>, <ext>) are
redacted by default before they ever reach the logger — your handler
sees <pw>*****</pw>, while your application code still receives the real
password as a string when you build a request or read a response.
$client = new Client($config, new \Monolog\Logger('epp'));
Standalone redaction (no logger needed)
Redactor is framework-agnostic — call it from anywhere you want to store
or display EPP XML safely (DB columns, support tickets, audit trails):
use Hostmaster\Ua\Logging\Redactor; $db->insert('epp_audit', [ 'trid' => $svTRID, 'request' => Redactor::redact($requestXml), 'response' => Redactor::redact($responseXml), ]);
Add custom rules for your own extension fields:
$customPatterns = array_merge(Redactor::defaultPatterns(), [ '/(<myext:secret>)([^<]*)(<\/myext:secret>)/u', ]); $safe = Redactor::withPatterns($xml, $customPatterns);
PSR-3 wrapper
Wrap any PSR-3 logger so the rest of your code doesn't have to think about redaction:
use Hostmaster\Ua\Logging\RedactingLogger; $logger = new RedactingLogger(new \Monolog\Logger('epp')); $client = new Client($config, $logger); $logger->info('paying invoice', ['xml' => $rawXml]); // 'xml' value is sanitised on its way out
The wrapper redacts the message string and every string-valued context field, recursively. Non-string context values pass through untouched.
Polling messages
while (($msg = $client->poll->request()) !== null) { echo "[{$msg->id}] {$msg->message}\n"; // $msg->response gives access to <resData> / <extension> for typed parsing $client->poll->acknowledge($msg->id); }
Email notifications
The registry mirrors poll messages over email (From: notify@epp.hostmaster.ua, X-Notifier: UAEPP-NOTIFY, X-Template: …).
EmailNotificationParser converts a raw RFC 5322 message — or already-split
headers + body — into an EmailNotification value object with the operation,
object type, name, and ROID extracted from the subject:
use Hostmaster\Ua\Notification\EmailNotificationParser; use Hostmaster\Ua\Enum\EmailTemplate; $notification = EmailNotificationParser::parseRaw($rawEmail); if ($notification->template === EmailTemplate::ObjectNew && $notification->objectType === 'DOMAIN') { $repo->markCreated($notification->objectName, $notification->roid); } // Or when your MTA already gave you an array of headers (e.g. from a webhook): $n = EmailNotificationParser::fromHeaders($headersArray, $body); // Sanity check before parsing: if (EmailNotificationParser::isRegistryNotification($headersArray)) { // … process … }
EmailTemplate covers every documented X-Template value (object
created/changed/deleted, transfer requested/completed/cancelled/rejected,
RGP, pending delete, host unlinked, contract events, balance reports,
DNSSEC checks, etc.). Unknown values fall back to EmailTemplate::Unknown
so the parser stays forward-compatible.
Hostmaster.ua extensions
uaepp:license— passed viaDomainCreate::$license/DomainUpdate::$licenseuaepp:deleteNS confirm="yes"—HostService::delete($name, force: true)rgp:restore op="request"—DomainService::restoreRequest($name)
Examples
The examples/ directory contains a runnable script for every
EPP command, mirroring the structure of the official Hostmaster.ua
documentation. See examples/README.md for a one-to-one
mapping of registry doc pages to library calls.
export UAEPP_USER=my-registrar export UAEPP_PASS=secret php examples/session.php php examples/domain.php php examples/contact.php php examples/host.php php examples/poll.php php examples/balance.php php examples/notifications-email.php php examples/logging-redaction.php php examples/error-handling.php php examples/end-to-end.php
Development
composer install
composer test
composer phpstan
composer cs-fix
License
MIT — see LICENSE.