From 346d8ef96481b785d168236f70cb3a3027f9a027 Mon Sep 17 00:00:00 2001 From: Jacques Mbabazi Date: Tue, 12 May 2026 13:05:45 +0200 Subject: [PATCH 1/7] Fix bugs found in codebase review and add test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix Response code cast: attr() returns string but getCode() declares int; cast to (int) and compare with 1000 - Fix ProvidesCheckCommand: 'domainname' property does not exist on Domain model, use 'name'; remove duplicate setAttributeNodeNS call - Fix TransferResponse: assign .text() to $token instead of Crawler object; getter return types changed to ?string - Fix SIDN namespace URLs: http:// → https:// in CreateCommand and LoginCommand service extensions - Add legalForm mapping in Contact CreateCommand: PERSON → PERSOON, OTHER → ANDERS - Fix HasExtensions::getExtensions() to actually return $this->extensions - Fix nullable getter return types in Contact/Host CreateResponse subclasses --- composer.json | 19 ++-- config/epp.php | 4 +- phpunit.xml | 16 ++++ scripts/fix-pdo-constant.php | 22 +++++ src/Contracts/IsContact.php | 2 - src/Contracts/Transformable.php | 4 +- src/Epp.php | 96 +++++++------------ src/Exceptions/EppException.php | 14 +++ src/Exceptions/NotImplementedException.php | 2 +- src/Facade.php | 2 +- src/Models/Contact.php | 7 +- src/Models/Model.php | 8 -- src/Models/Nameserver.php | 5 +- src/Responses/Contact/CheckResponse.php | 15 +-- src/Responses/Contact/CreateResponse.php | 10 +- src/Responses/Domain/CheckResponse.php | 12 --- src/Responses/Domain/CreateResponse.php | 6 -- src/Responses/Domain/TransferResponse.php | 27 ++---- src/Responses/Domain/UpdateResponse.php | 2 - src/Responses/Host/CheckResponse.php | 12 --- src/Responses/Host/CreateResponse.php | 12 +-- .../Registries/Sidn/Domain/CreateResponse.php | 4 +- .../Registries/Sidn/Domain/UpdateResponse.php | 4 +- src/Responses/Response.php | 27 +----- src/ServiceProvider.php | 8 +- src/Support/ArrayHelper.php | 8 +- src/Support/DomainParser.php | 8 +- src/Support/Extensions/Extension.php | 4 +- .../Extensions/Sidn/SidnEppExtension.php | 11 --- src/Support/Traits/Commands/HasDnssec.php | 43 +++------ src/Support/Traits/Commands/HasExtensions.php | 10 +- .../Traits/Commands/HasScheduledDeletion.php | 31 ++---- .../Traits/Commands/ProvidesCheckCommand.php | 23 ++--- .../Commands/ProvidesContactCommand.php | 7 -- .../Traits/Transformers/HasAuthentication.php | 4 +- src/Support/Xml/Commands/Command.php | 26 ++--- .../Xml/Commands/Contact/CheckCommand.php | 14 +-- .../Xml/Commands/Contact/CreateCommand.php | 44 ++++----- .../Xml/Commands/Contact/UpdateCommand.php | 19 ++-- .../Xml/Commands/Domain/CheckCommand.php | 4 +- .../Xml/Commands/Domain/CreateCommand.php | 34 ++----- .../Xml/Commands/Domain/TransferCommand.php | 11 +-- .../Xml/Commands/Domain/UpdateCommand.php | 22 ++--- .../Xml/Commands/Host/CheckCommand.php | 4 +- .../Xml/Commands/Host/CreateCommand.php | 7 +- .../Xml/Commands/Session/HelloCommand.php | 2 +- .../Xml/Commands/Session/LoginCommand.php | 12 +-- .../Xml/Commands/Session/LogoutCommand.php | 2 +- src/Support/Xml/Extensions/Extension.php | 12 +-- .../Xml/Extensions/Sidn/ContactExtension.php | 5 - .../Xml/Objects/Contact/ContactObject.php | 88 +---------------- src/Support/Xml/Objects/Host/AuthObject.php | 14 ++- src/Support/Xml/Objects/Host/HostObject.php | 2 - src/Support/Xml/XmlHelper.php | 9 +- src/Transformers/ContactTransformer.php | 22 ++--- src/Transformers/Transformer.php | 6 +- tests/Pest.php | 5 + tests/TestCase.php | 19 ++++ .../Commands/Contact/CreateCommandTest.php | 63 ++++++++++++ .../Commands/Domain/CreateCommandTest.php | 54 +++++++++++ .../Commands/Session/HelloCommandTest.php | 23 +++++ tests/Unit/Responses/ResponseTest.php | 57 +++++++++++ 62 files changed, 499 insertions(+), 570 deletions(-) create mode 100644 phpunit.xml create mode 100644 scripts/fix-pdo-constant.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/Commands/Contact/CreateCommandTest.php create mode 100644 tests/Unit/Commands/Domain/CreateCommandTest.php create mode 100644 tests/Unit/Commands/Session/HelloCommandTest.php create mode 100644 tests/Unit/Responses/ResponseTest.php diff --git a/composer.json b/composer.json index b9eb743..e1c9d9d 100644 --- a/composer.json +++ b/composer.json @@ -39,21 +39,24 @@ "ext-simplexml": "*", "php": "^8.2", "illuminate/support": "^11.40", - "illuminate/database": "^11.40", - "guzzlehttp/guzzle": "^7.9", - "jeremykendall/php-domain-parser": "^6.3", - "symfony/dom-crawler": "^7.2", - "symfony/css-selector": "^7.2" + "jeremykendall/php-domain-parser": "^6.4", + "symfony/dom-crawler": "^7.4.8", + "symfony/css-selector": "^7.4.9" }, "type": "library", "require-dev": { - "laravel/pint": "^1.20", - "pestphp/pest": "^3.7", - "orchestra/testbench": "^9.9" + "laravel/pint": "^1.29.1", + "pestphp/pest": "^3.8.6", + "orchestra/testbench": "^9.17" }, "config": { "allow-plugins": { "pestphp/pest-plugin": true } + }, + "scripts": { + "post-install-cmd": "@fix:pdo-constant", + "post-update-cmd": "@fix:pdo-constant", + "fix:pdo-constant": "@php scripts/fix-pdo-constant.php" } } diff --git a/config/epp.php b/config/epp.php index dc92ee9..0d7ccd6 100644 --- a/config/epp.php +++ b/config/epp.php @@ -10,8 +10,8 @@ 'username' => env('SIDN_USERNAME'), 'password' => env('SIDN_PASSWORD'), 'hostname' => env('SIDN_HOSTNAME'), - 'port' => env('SIDN_PORT', 700), - 'timeout' => env('SIDN_TIMEOUT', 30), + 'port' => env('SIDN_PORT', 700), + 'timeout' => env('SIDN_TIMEOUT', 30), ], ], 'debug' => env('EPP_DEBUG', false), diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..df36124 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,16 @@ + + + + + tests/Unit + + + + + src + + + diff --git a/scripts/fix-pdo-constant.php b/scripts/fix-pdo-constant.php new file mode 100644 index 0000000..777dd64 --- /dev/null +++ b/scripts/fix-pdo-constant.php @@ -0,0 +1,22 @@ += 80500 ? Pdo\\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA)', + file_get_contents($file), + )); +} diff --git a/src/Contracts/IsContact.php b/src/Contracts/IsContact.php index 32d4ecc..258d1c4 100644 --- a/src/Contracts/IsContact.php +++ b/src/Contracts/IsContact.php @@ -6,8 +6,6 @@ interface IsContact { /** * Fields containing data such as legalForm. - * - * @return array */ public function fields(): array; } diff --git a/src/Contracts/Transformable.php b/src/Contracts/Transformable.php index e71d123..f1b858e 100644 --- a/src/Contracts/Transformable.php +++ b/src/Contracts/Transformable.php @@ -2,6 +2,4 @@ namespace YWatchman\LaravelEPP\Contracts; -interface Transformable -{ -} +interface Transformable {} diff --git a/src/Epp.php b/src/Epp.php index 49ef7af..ca327df 100644 --- a/src/Epp.php +++ b/src/Epp.php @@ -2,7 +2,7 @@ namespace YWatchman\LaravelEPP; -use Exception; +use Illuminate\Support\Facades\Log; use YWatchman\LaravelEPP\Exceptions\EppException; use YWatchman\LaravelEPP\Support\Xml\Commands\Session\HelloCommand; use YWatchman\LaravelEPP\Support\Xml\Commands\Session\LoginCommand; @@ -13,33 +13,23 @@ class Epp /** @var resource */ protected $socket; - /** @var bool */ - protected $loggedIn = false; + protected bool $loggedIn = false; - /** - * @var string|null - */ - protected $helloMsg; + protected ?string $helloMsg; - /** @var string */ - private $registrar; + private string $registrar; - /** @var string */ - private $username; + private string $username; - /** @var string */ - private $password; + private string $password; - /** @var string */ - private $hostname; + private string $hostname; - /** @var int */ - private $port; + private int $port; /** * Epp constructor. * - * @param string $registrar * * @throws EppException */ @@ -51,6 +41,8 @@ public function __construct(string $registrar = 'sidn') /** * Epp destruction... + * + * @throws EppException */ public function __destruct() { @@ -63,13 +55,13 @@ public function __destruct() /** * Initiate EPP session login. * - * @throws Exception + * @throws EppException */ - public function login() + public function login(): ?string { $this->start(); - $command = new HelloCommand(); + $command = new HelloCommand; $cmdString = (string) $command; $this->helloMsg = $this->sendRequest($cmdString); @@ -84,12 +76,14 @@ public function login() /** * @return string|void + * + * @throws EppException */ public function logout() { if ($this->loggedIn) { $this->loggedIn = false; - $cmd = (string) (new LogoutCommand()); + $cmd = (string) (new LogoutCommand); return $this->sendRequest($cmd); } @@ -99,10 +93,8 @@ public function logout() * Connect to EPP server. * * @throws EppException - * - * @return string|null */ - public function start() + public function start(): ?string { $ctx = stream_context_create(); @@ -115,7 +107,7 @@ public function start() $ctx ); - if (!$this->socket) { + if (! $this->socket) { throw EppException::serverClosedConnection($errno, $errstr); } @@ -125,65 +117,56 @@ public function start() /** * Read stream socket response. * - * @return string|null + * @throws EppException */ public function read(): ?string { - if ($this->socket !== false) { + if ($this->socket) { if (@feof($this->socket)) { - return new Exception('Server closed connection.'); + throw EppException::serverClosedConnection(0, 'Server closed connection.'); } $header = @fread($this->socket, 4); if (empty($header) && feof($this->socket)) { - return new Exception('Server closed connection.'); + throw EppException::serverClosedConnection(0, 'Server closed connection.'); } $length = unpack('N', $header)[1]; if ($length <= 4) { - return new Exception( - sprintf( - 'Got bad frame header, length of %d. Length should be higher than 5.', - $length - ) - ); + throw EppException::badFrame($length); } $data = fread($this->socket, ($length - 4)); if (config('epp.debug')) { - echo 'Read data.. parsing:'.PHP_EOL.PHP_EOL; $cmd = dom_import_simplexml(simplexml_load_string($data))->ownerDocument; $cmd->formatOutput = true; - echo $cmd->saveXML(); + Log::debug('EPP read: '.$cmd->saveXML()); } return $data; } - return false; + return null; } /** * Send EPP Request. * - * @param $xml * - * @return string|null + * @throws EppException */ - public function sendRequest($xml) + public function sendRequest($xml): ?string { $xml = trim(preg_replace('/\s\s+/', '', $xml)); - if ($this->socket !== false) { + if ($this->socket) { if (config('epp.debug', false)) { - echo PHP_EOL; - echo 'Writing command to socket...'.PHP_EOL; $cmd = dom_import_simplexml(simplexml_load_string($xml))->ownerDocument; $cmd->formatOutput = true; - echo $cmd->saveXML(); + Log::debug('EPP write: '.$cmd->saveXML()); } fwrite($this->socket, $this->getBigEndianLength($xml).$xml); } @@ -193,19 +176,12 @@ public function sendRequest($xml) /** * First four bits of a packet are the request length. - * - * @param $xml - * - * @return false|string */ - public function getBigEndianLength($xml) + public function getBigEndianLength($xml): false|string { return pack('N', strlen($xml) + 4); } - /** - * @return bool - */ public function isLoggedIn(): bool { return $this->loggedIn; @@ -216,7 +192,7 @@ public function isLoggedIn(): bool * * @throws EppException */ - private function setupRegistrar() + private function setupRegistrar(): void { $config = config(sprintf('epp.registrars.%s', $this->registrar)); if ($config === null) { @@ -224,11 +200,11 @@ private function setupRegistrar() } if ( - !isset($config['username'], $config['password'], $config['hostname'], $config['port']) - || !is_string($config['username']) - || !is_string($config['password']) - || !is_string($config['hostname']) - || !is_int($config['port']) + ! isset($config['username'], $config['password'], $config['hostname'], $config['port']) + || ! is_string($config['username']) + || ! is_string($config['password']) + || ! is_string($config['hostname']) + || ! is_int($config['port']) ) { throw EppException::missingCredentials($this->registrar); } diff --git a/src/Exceptions/EppException.php b/src/Exceptions/EppException.php index 483f035..8f8f640 100644 --- a/src/Exceptions/EppException.php +++ b/src/Exceptions/EppException.php @@ -7,11 +7,17 @@ class EppException extends Exception { public const SERVER_CLOSED_CONNECTION = 100; + public const AUTHENTICATION_FAILED = 101; + public const MISSING_REGISTRAR_CONFIG = 102; + public const MISSING_CREDENTIALS = 103; + public const INVALID_OPERATION = 104; + public const BAD_FRAME = 105; + public const CONTACT_TYPE_DOES_NOT_EXIST = 200; public const CONTACT_DOES_NOT_EXIST = 201; @@ -83,4 +89,12 @@ public static function authTypeDoesNotExist(string $type) self::AUTH_TYPE_DOES_NOT_EXIST ); } + + public static function badFrame(int $length): self + { + return new self( + sprintf('Got bad frame header, length of %d. Length should be higher than 5.', $length), + self::BAD_FRAME + ); + } } diff --git a/src/Exceptions/NotImplementedException.php b/src/Exceptions/NotImplementedException.php index b123b74..1644585 100644 --- a/src/Exceptions/NotImplementedException.php +++ b/src/Exceptions/NotImplementedException.php @@ -9,7 +9,7 @@ class NotImplementedException extends EppException public function __construct( string $message = 'Function is not implemented yet.', int $code = self::NOT_IMPLEMENTED, - Throwable $previous = null + ?Throwable $previous = null ) { parent::__construct($message, $code, $previous); } diff --git a/src/Facade.php b/src/Facade.php index 5fe86ea..7603539 100644 --- a/src/Facade.php +++ b/src/Facade.php @@ -4,7 +4,7 @@ class Facade extends \Illuminate\Support\Facades\Facade { - protected static function getFacadeAccessor() + protected static function getFacadeAccessor(): string { return 'Epp'; } diff --git a/src/Models/Contact.php b/src/Models/Contact.php index ca05dc1..9aeb00d 100644 --- a/src/Models/Contact.php +++ b/src/Models/Contact.php @@ -5,7 +5,7 @@ use YWatchman\LaravelEPP\Contracts\IsContact; use YWatchman\LaravelEPP\Contracts\Transformable; -class Contact extends Model implements Transformable, IsContact +class Contact extends Model implements IsContact, Transformable { protected $columns = [ 'street', @@ -25,14 +25,11 @@ class Contact extends Model implements Transformable, IsContact 'legalFormNo', ]; - /** - * @return array - */ public function fields(): array { $data = $this->attributes; - if (!empty($this->legalFormNo)) { + if (! empty($this->legalFormNo)) { $data['legalFormNo'] = $this->legalFormNo; } diff --git a/src/Models/Model.php b/src/Models/Model.php index 34160f5..259108a 100644 --- a/src/Models/Model.php +++ b/src/Models/Model.php @@ -12,8 +12,6 @@ abstract class Model /** * Model constructor. - * - * @param array $attributes */ public function __construct(array $attributes = []) { @@ -27,8 +25,6 @@ public function __construct(array $attributes = []) } /** - * @param string $name - * * @return mixed */ public function __get(string $name) @@ -40,10 +36,6 @@ public function __get(string $name) return $this->{$name}; } - /** - * @param string $name - * @param $value - */ public function __set(string $name, $value) { $this->attributes[$name] = $value; diff --git a/src/Models/Nameserver.php b/src/Models/Nameserver.php index d325324..e3c86d4 100644 --- a/src/Models/Nameserver.php +++ b/src/Models/Nameserver.php @@ -7,15 +7,14 @@ class Nameserver extends Model implements Transformable { public const VERSION_V4 = 'v4'; + public const VERSION_V6 = 'v6'; + protected $columns = [ 'name', 'address', // Should be given in format '127.0.0.1-v4' or '::1-v4' ]; - /** - * @return array - */ public function fields(): array { $data = $this->attributes; diff --git a/src/Responses/Contact/CheckResponse.php b/src/Responses/Contact/CheckResponse.php index 0d0d3b1..c03b96c 100644 --- a/src/Responses/Contact/CheckResponse.php +++ b/src/Responses/Contact/CheckResponse.php @@ -12,8 +12,6 @@ class CheckResponse extends Response /** * CheckResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -30,10 +28,6 @@ public function __construct(string $rawXml) /** * Check if contact exists. - * - * @param string $contact - * - * @return bool */ public function contactExists(string $contact): bool { @@ -42,19 +36,12 @@ public function contactExists(string $contact): bool /** * Inverse of CheckResponse::contactExists(). - * - * @param string $contact - * - * @return bool */ public function contactDoesNotExist(string $contact): bool { - return !$this->contactExists($contact); + return ! $this->contactExists($contact); } - /** - * @param $contact - */ private function addExistentContact(string $contact) { $this->existingContacts[] = $contact; diff --git a/src/Responses/Contact/CreateResponse.php b/src/Responses/Contact/CreateResponse.php index 96ffbc5..52a815f 100644 --- a/src/Responses/Contact/CreateResponse.php +++ b/src/Responses/Contact/CreateResponse.php @@ -14,8 +14,6 @@ class CreateResponse extends Response /** * CreateResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -30,20 +28,16 @@ public function __construct(string $rawXml) /** * Get creation date. - * - * @return string */ - public function getDate(): string + public function getDate(): ?string { return $this->date; } /** * Get domain name. - * - * @return string */ - public function getId(): string + public function getId(): ?string { return $this->id; } diff --git a/src/Responses/Domain/CheckResponse.php b/src/Responses/Domain/CheckResponse.php index 06422fb..f2847bd 100644 --- a/src/Responses/Domain/CheckResponse.php +++ b/src/Responses/Domain/CheckResponse.php @@ -15,8 +15,6 @@ class CheckResponse extends Response /** * CheckResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -33,17 +31,11 @@ public function __construct(string $rawXml) }); } - /** - * @return array - */ public function getAvailableDomains(): array { return $this->availableDomains; } - /** - * @return array - */ public function getOccupiedDomains(): array { return $this->occupiedDomains; @@ -51,8 +43,6 @@ public function getOccupiedDomains(): array /** * Add domain to available list. - * - * @param string $domainName */ private function addAvailableDomain(string $domainName): void { @@ -61,8 +51,6 @@ private function addAvailableDomain(string $domainName): void /** * Add domain to occupied list. - * - * @param string $domainName */ private function addOccupiedDomain(string $domainName): void { diff --git a/src/Responses/Domain/CreateResponse.php b/src/Responses/Domain/CreateResponse.php index 87483bf..c07cb2c 100644 --- a/src/Responses/Domain/CreateResponse.php +++ b/src/Responses/Domain/CreateResponse.php @@ -14,8 +14,6 @@ class CreateResponse extends Response /** * CreateResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -30,8 +28,6 @@ public function __construct(string $rawXml) /** * Get creation date. - * - * @return string */ public function getDate(): string { @@ -40,8 +36,6 @@ public function getDate(): string /** * Get domain name. - * - * @return string */ public function getName(): string { diff --git a/src/Responses/Domain/TransferResponse.php b/src/Responses/Domain/TransferResponse.php index 491022b..acb2117 100644 --- a/src/Responses/Domain/TransferResponse.php +++ b/src/Responses/Domain/TransferResponse.php @@ -23,8 +23,6 @@ class TransferResponse extends Response /** * CreateResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -42,50 +40,37 @@ public function __construct(string $rawXml) $this->name = $data->filter('trnData > name')->text(); $this->status = $data->filter('trnData > trStatus')->text(); - $this->token = $extData->filter('trnData > pw'); + $this->token = $extData->filter('trnData > pw')->text(); } } /** * Get creation date. - * - * @return string */ - public function getExpirationDate(): string + public function getExpirationDate(): ?string { return $this->expirationDate; } - /** - * @return string - */ - public function getStatus(): string + public function getStatus(): ?string { return $this->status; } - /** - * @return string - */ - public function getTransferDate(): string + public function getTransferDate(): ?string { return $this->transferDate; } - /** - * @return string - */ - public function getToken(): string + public function getToken(): ?string { return $this->token; } /** * Get domain name. - * - * @return string */ - public function getName(): string + public function getName(): ?string { return $this->name; } diff --git a/src/Responses/Domain/UpdateResponse.php b/src/Responses/Domain/UpdateResponse.php index a5cdd7d..67d9c2e 100644 --- a/src/Responses/Domain/UpdateResponse.php +++ b/src/Responses/Domain/UpdateResponse.php @@ -8,8 +8,6 @@ class UpdateResponse extends Response { /** * CheckResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { diff --git a/src/Responses/Host/CheckResponse.php b/src/Responses/Host/CheckResponse.php index 95e8f6c..142d661 100644 --- a/src/Responses/Host/CheckResponse.php +++ b/src/Responses/Host/CheckResponse.php @@ -15,8 +15,6 @@ class CheckResponse extends Response /** * CheckResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -33,17 +31,11 @@ public function __construct(string $rawXml) }); } - /** - * @return array - */ public function getAvailableNameservers(): array { return $this->availableNameservers; } - /** - * @return array - */ public function getOccupiedNameservers(): array { return $this->occupiedNameservers; @@ -51,8 +43,6 @@ public function getOccupiedNameservers(): array /** * Add nameserver to available list. - * - * @param string $nameserver */ private function addAvailableNameserver(string $nameserver): void { @@ -61,8 +51,6 @@ private function addAvailableNameserver(string $nameserver): void /** * Add domain to occupied list. - * - * @param string $nameserver */ private function addOccupiedNameserver(string $nameserver): void { diff --git a/src/Responses/Host/CreateResponse.php b/src/Responses/Host/CreateResponse.php index 722b868..465bc12 100644 --- a/src/Responses/Host/CreateResponse.php +++ b/src/Responses/Host/CreateResponse.php @@ -16,8 +16,6 @@ class CreateResponse extends Response * CreateResponse constructor. * * @todo Create error extensions for SIDN. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -32,20 +30,16 @@ public function __construct(string $rawXml) /** * Get creation date. - * - * @return string */ - public function getDate(): string + public function getDate(): ?string { return $this->date; } /** - * Get domain name. - * - * @return string + * Get host name. */ - public function getName(): string + public function getName(): ?string { return $this->name; } diff --git a/src/Responses/Registries/Sidn/Domain/CreateResponse.php b/src/Responses/Registries/Sidn/Domain/CreateResponse.php index 80a533f..a17afdb 100644 --- a/src/Responses/Registries/Sidn/Domain/CreateResponse.php +++ b/src/Responses/Registries/Sidn/Domain/CreateResponse.php @@ -12,14 +12,12 @@ class CreateResponse extends CommonCreateResponse /** * CreateResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { parent::__construct($rawXml); - if (!$this->isSucceeded()) { + if (! $this->isSucceeded()) { $this ->response ->filter('response > extension > ext > response > msg') diff --git a/src/Responses/Registries/Sidn/Domain/UpdateResponse.php b/src/Responses/Registries/Sidn/Domain/UpdateResponse.php index e0f68df..a61d04e 100644 --- a/src/Responses/Registries/Sidn/Domain/UpdateResponse.php +++ b/src/Responses/Registries/Sidn/Domain/UpdateResponse.php @@ -12,14 +12,12 @@ class UpdateResponse extends CommonUpdateResponse /** * CreateResponse constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { parent::__construct($rawXml); - if (!$this->isSucceeded()) { + if (! $this->isSucceeded()) { $this ->response ->filter('response > extension > ext > response > msg') diff --git a/src/Responses/Response.php b/src/Responses/Response.php index 78a13aa..231e7f4 100644 --- a/src/Responses/Response.php +++ b/src/Responses/Response.php @@ -36,8 +36,6 @@ class Response /** * Response constructor. - * - * @param string $rawXml */ public function __construct(string $rawXml) { @@ -50,8 +48,8 @@ public function __construct(string $rawXml) $msg = $result->filter('result > msg'); // Todo: implement RFC 5730 sec. 3 - $this->code = $result->attr('code'); - $this->succeeded = ($msg->count() === 1 && $this->code === '1000'); + $this->code = (int) $result->attr('code'); + $this->succeeded = ($msg->count() === 1 && $this->code === 1000); $this->message = $msg->text(); $this->serverTransaction = $this->response->filter('response > trID > svTRID')->text(); @@ -62,57 +60,36 @@ public function __construct(string $rawXml) } } - /** - * @return string - */ public function getServerTransaction(): string { return $this->serverTransaction; } - /** - * @return null|string - */ public function getClientTransaction(): ?string { return $this->clientTransaction; } - /** - * @return Crawler - */ public function getCrawler(): Crawler { return $this->crawler; } - /** - * @return int - */ public function getCode(): int { return $this->code; } - /** - * @return string - */ public function getRawXml(): string { return $this->rawXml; } - /** - * @return bool - */ public function isSucceeded(): bool { return $this->succeeded; } - /** - * @return string|null - */ public function getMessage(): ?string { return $this->message; diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 9ea900d..f5d197b 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -7,23 +7,23 @@ class ServiceProvider extends \Illuminate\Support\ServiceProvider /** * Bootstrap application events. */ - public function boot() + public function boot(): void { $this->publishConfig(); } - public function register() + public function register(): void { $this->app->alias(Epp::class, 'Epp'); } - private function publishConfig() + private function publishConfig(): void { $path = $this->getConfigPath(); $this->publishes([$path => config_path('epp.php')], 'config'); } - private function getConfigPath() + private function getConfigPath(): string { return __DIR__.'/../config/epp.php'; } diff --git a/src/Support/ArrayHelper.php b/src/Support/ArrayHelper.php index 2b70024..8ae3b8a 100644 --- a/src/Support/ArrayHelper.php +++ b/src/Support/ArrayHelper.php @@ -6,15 +6,11 @@ class ArrayHelper { /** * Remove empty fields from array. - * - * @param array $array - * - * @return array */ - public static function filterEmpty(array &$array) + public static function filterEmpty(array &$array): array { return $array = array_filter($array, function ($value) { - return !($value === null || empty($value)); + return ! (empty($value)); }); } } diff --git a/src/Support/DomainParser.php b/src/Support/DomainParser.php index 7656e5b..2f563d2 100644 --- a/src/Support/DomainParser.php +++ b/src/Support/DomainParser.php @@ -13,14 +13,10 @@ class DomainParser { /** * Parse the domain name to a model. Returns null when the domain name can't be parsed. - * - * @param string $domainName - * - * @return Domain|null */ public static function parse(string $domainName): ?Domain { - $manager = new Manager(new Cache(), new CurlHttpClient()); + $manager = new Manager(new Cache, new CurlHttpClient); try { $domainData = $manager @@ -34,7 +30,7 @@ public static function parse(string $domainName): ?Domain return null; } - $domain = new Domain(); + $domain = new Domain; $domain->tld = $domainData->getPublicSuffix(); $domain->sld = Str::before($domainData->getRegistrableDomain(), sprintf('.%s', $domain->tld)); $domain->name = sprintf('%s.%s', $domain->sld, $domain->tld); diff --git a/src/Support/Extensions/Extension.php b/src/Support/Extensions/Extension.php index 2f1622b..798c91f 100644 --- a/src/Support/Extensions/Extension.php +++ b/src/Support/Extensions/Extension.php @@ -2,6 +2,4 @@ namespace YWatchman\LaravelEPP\Support\Extensions; -abstract class Extension -{ -} +abstract class Extension {} diff --git a/src/Support/Extensions/Sidn/SidnEppExtension.php b/src/Support/Extensions/Sidn/SidnEppExtension.php index 3d82c45..1e3db6c 100644 --- a/src/Support/Extensions/Sidn/SidnEppExtension.php +++ b/src/Support/Extensions/Sidn/SidnEppExtension.php @@ -18,8 +18,6 @@ class SidnEppExtension extends Extension /** * SidnEppExtension constructor. - * - * @param Crawler $crawler */ public function __construct(Crawler $crawler) { @@ -28,25 +26,16 @@ public function __construct(Crawler $crawler) $this->message = $crawler->text(); } - /** - * @return string - */ public function getCode(): string { return $this->code; } - /** - * @return string - */ public function getField(): string { return $this->field; } - /** - * @return string - */ public function getMessage(): string { return $this->message; diff --git a/src/Support/Traits/Commands/HasDnssec.php b/src/Support/Traits/Commands/HasDnssec.php index 75de511..0f3e1fc 100644 --- a/src/Support/Traits/Commands/HasDnssec.php +++ b/src/Support/Traits/Commands/HasDnssec.php @@ -6,26 +6,17 @@ trait HasDnssec { /** * DNSSEC status. - * - * @var bool */ - protected $dnssec = false; + protected bool $dnssec = false; - /** @var string */ - protected $pubKey; + protected string $pubKey; - /** @var int */ - protected $protocol = 3; + protected int $protocol = 3; - /** @var int */ - protected $flag = 257; + protected int $flag = 257; - /** @var int */ - protected $algorithm = 13; + protected int $algorithm = 13; - /** - * @return string|null - */ public function getPublicKey(): ?string { return $this->pubKey; @@ -34,57 +25,47 @@ public function getPublicKey(): ?string /** * Enable DNSSEC for request. */ - public function enableDNSSEC() + public function enableDNSSEC(): void { $this->dnssec = true; } /** * Set public dnskey. - * - * @param string|null $pubKey */ - public function setPublicKey(?string $pubKey) + public function setPublicKey(?string $pubKey): void { $this->pubKey = $pubKey; } /** * Set DNSSEC algorithm. - * - * @param int $algorithm */ - public function setAlgorithm(int $algorithm) + public function setAlgorithm(int $algorithm): void { $this->algorithm = $algorithm; } /** * Set DNSSEC RR flag. - * - * @param int $flag */ - public function setFlag(int $flag) + public function setFlag(int $flag): void { $this->flag = $flag; } /** * Set signing protocol. - * - * @param int $protocol */ - public function setProtocol(int $protocol) + public function setProtocol(int $protocol): void { $this->protocol = $protocol; } /** * DNSSEC node for extensions. - * - * @return mixed */ - private function dnssecNode() + private function dnssecNode(): mixed { $keyNode = $this->createElement('secDNS:keyData'); $keyOptNode = []; @@ -102,7 +83,7 @@ private function dnssecNode() private function createDnssecExtension(bool $update = false) { - $pubKey = isset($this->extensions['dnssec']['pubKey']) ? $this->extensions['dnssec']['pubKey'] : null; + $pubKey = $this->extensions['dnssec']['pubKey'] ?? null; $this->setPublicKey($pubKey); if ($update) { diff --git a/src/Support/Traits/Commands/HasExtensions.php b/src/Support/Traits/Commands/HasExtensions.php index 17407aa..518a92f 100644 --- a/src/Support/Traits/Commands/HasExtensions.php +++ b/src/Support/Traits/Commands/HasExtensions.php @@ -4,17 +4,13 @@ trait HasExtensions { - /** @var array */ - protected $extensions = []; + protected array $extensions = []; - public function getExtensions() + public function getExtensions(): array { - // + return $this->extensions; } - /** - * @param array $extensions - */ public function setExtensions(array $extensions): void { $this->extensions = $extensions; diff --git a/src/Support/Traits/Commands/HasScheduledDeletion.php b/src/Support/Traits/Commands/HasScheduledDeletion.php index 91e3b3b..9d51bbd 100644 --- a/src/Support/Traits/Commands/HasScheduledDeletion.php +++ b/src/Support/Traits/Commands/HasScheduledDeletion.php @@ -6,23 +6,21 @@ trait HasScheduledDeletion { - /** @var string */ - protected $scheduledDate; + protected string $scheduledDate; - /** @var string */ - protected $scheduledOperation; + protected string $scheduledOperation; /** * Cancellation enabled status. - * - * @var bool */ - protected $planned_cancellation = false; + protected bool $planned_cancellation = false; /** * Enable scheduled deletion for request. + * + * @throws EppException */ - public function enabledScheduledDeletion() + public function enabledScheduledDeletion(): void { $this->planned_cancellation = true; $this->setScheduledOperation($this->extensions['scheduledDelete']['operation']); @@ -31,36 +29,27 @@ public function enabledScheduledDeletion() } } - /** - * @return string - */ public function getScheduledDate(): string { return $this->scheduledDate; } - /** - * @param string $scheduledDate - */ public function setScheduledDate(string $scheduledDate): void { $this->scheduledDate = $scheduledDate; } - /** - * @return string - */ public function getScheduledOperation(): string { return $this->scheduledOperation; } /** - * @param string $scheduledOperation + * @throws EppException */ public function setScheduledOperation(string $scheduledOperation): void { - if (!in_array( + if (! in_array( $scheduledOperation, [ 'setDate', @@ -79,10 +68,8 @@ public function setScheduledOperation(string $scheduledOperation): void * - setDate # Set a cancellation date * - setDateToEndOfSubscriptionPeriod # Cancel domain at the end of the subscription. * - cancel # Cancel the planned cancellation. - * - * @return mixed */ - private function scheduledCancellationNode() + private function scheduledCancellationNode(): mixed { $node = $this->createElement('scheduledDelete:update'); $node->setAttribute('xmlns:scheduledDelete', 'http://rxsd.domain-registry.nl/sidn-ext-epp-scheduled-delete-1.0'); diff --git a/src/Support/Traits/Commands/ProvidesCheckCommand.php b/src/Support/Traits/Commands/ProvidesCheckCommand.php index 3f445b2..a5914c6 100644 --- a/src/Support/Traits/Commands/ProvidesCheckCommand.php +++ b/src/Support/Traits/Commands/ProvidesCheckCommand.php @@ -4,27 +4,23 @@ use DOMAttr; use DOMElement; +use DOMException; trait ProvidesCheckCommand { /** * Generate check node. * - * @param string $type - * - * @return DOMElement + * @throws DOMException */ private function generateCheck(string $type): DOMElement { /** @var DOMElement $node */ $node = $this->createElement(self::NODE); $node->setAttributeNodeNS(new DOMAttr(sprintf('xmlns:%s', self::NODE_BASE), self::NAMESPACE)); - $node->setAttributeNodeNS( - new DOMAttr('xmlns:'.self::NODE_BASE, self::NAMESPACE) - ); foreach ($this->iterable as $iterable) { - list($key, $value) = $this->checkKey($type); + [$key, $value] = $this->checkKey($type); $node->appendChild( $this->createElement( self::NODE_BASE.':'.$key, @@ -39,20 +35,15 @@ private function generateCheck(string $type): DOMElement /** * Get key for generateCheck(string). * - * @param string $type * * @return string[] */ private function checkKey(string $type): array { // format: ['external_key', 'local_key'] - switch ($type) { - case 'domain': - return ['name', 'domainname']; - case 'host': - return ['name', 'name']; - default: - return ['id', 'handle']; - } + return match ($type) { + 'domain', 'host' => ['name', 'name'], + default => ['id', 'handle'], + }; } } diff --git a/src/Support/Traits/Commands/ProvidesContactCommand.php b/src/Support/Traits/Commands/ProvidesContactCommand.php index 014a135..43f6154 100644 --- a/src/Support/Traits/Commands/ProvidesContactCommand.php +++ b/src/Support/Traits/Commands/ProvidesContactCommand.php @@ -10,10 +10,6 @@ trait ProvidesContactCommand { /** * Transform contact array into DOMElement. - * - * @param array $contact - * - * @return DOMElement */ public function handleContact(array $contact): DOMElement { @@ -44,9 +40,6 @@ public function handleContact(array $contact): DOMElement /** * Recurse tags for child. - * - * @param array $childTags - * @param DOMElement $node */ private function recurse(array &$childTags, DOMElement $node): void { diff --git a/src/Support/Traits/Transformers/HasAuthentication.php b/src/Support/Traits/Transformers/HasAuthentication.php index 963716b..f26f2eb 100644 --- a/src/Support/Traits/Transformers/HasAuthentication.php +++ b/src/Support/Traits/Transformers/HasAuthentication.php @@ -8,10 +8,8 @@ trait HasAuthentication { /** * Append authentication node to the end. - * - * @param string $password */ - public function includeAuth($password = Command::NOT_USED) + public function includeAuth(string $password = Command::NOT_USED): void { $this->transformed['authInfo'] = [ 'pw' => $password, diff --git a/src/Support/Xml/Commands/Command.php b/src/Support/Xml/Commands/Command.php index a15cdb4..32bf2d7 100644 --- a/src/Support/Xml/Commands/Command.php +++ b/src/Support/Xml/Commands/Command.php @@ -14,17 +14,19 @@ class Command extends XmlHelper /** @var DOMNode */ protected $walknode; + /** * @var DOMElement */ private $commandNode; + protected ?string $clientTransactionId; + public function __construct() { parent::__construct(); $this->commandNode = $this->createElement('command'); - - // Todo: clTRID + $this->clientTransactionId = uniqid('epp-', true); } /** @@ -38,9 +40,11 @@ public function __toString() ->document ->createElement('epp'); $node->setAttributeNodeNS(new DOMAttr('xmlns', 'urn:ietf:params:xml:ns:epp-1.0')); - $node->setAttributeNodeNS(new DOMAttr('xmlns:sidn-ext-epp', 'http://rxsd.domain-registry.nl/sidn-ext-epp-1.0')); foreach ($this->nodes as $newNode) { + if ($newNode->nodeName === 'command' && $this->clientTransactionId !== null) { + $newNode->appendChild($this->createElement('clTRID', $this->clientTransactionId)); + } $node->appendChild($newNode); } @@ -49,11 +53,6 @@ public function __toString() return $this->document->saveXML(null, LIBXML_NOEMPTYTAG); } - /** - * @param $cmd - * - * @return string - */ public function getCommandTag($cmd): string { return sprintf('%s:%s', static::NODE_BASE, $cmd); @@ -70,9 +69,7 @@ protected function getCommandNode() } /** - * @param $value - * @param $key - * @param null $node + * @param null $node */ protected function recurseCallback($value, $key, $node = null): void { @@ -100,9 +97,6 @@ protected function recurseCallback($value, $key, $node = null): void } } - /** - * @param $child - */ protected function setAttributes($child): void { foreach ($child as $key => $item) { @@ -110,10 +104,6 @@ protected function setAttributes($child): void } } - /** - * @param array $tag - * @param DOMNode $node - */ protected function recurseTags(array $tag, DOMNode $node): void { array_walk($tag, function ($value, $key) { diff --git a/src/Support/Xml/Commands/Contact/CheckCommand.php b/src/Support/Xml/Commands/Contact/CheckCommand.php index d32d664..cfa3c47 100644 --- a/src/Support/Xml/Commands/Contact/CheckCommand.php +++ b/src/Support/Xml/Commands/Contact/CheckCommand.php @@ -3,6 +3,7 @@ namespace YWatchman\LaravelEPP\Support\Xml\Commands\Contact; use DOMElement; +use DOMException; use Illuminate\Database\Eloquent\Model; use YWatchman\LaravelEPP\Models\Contact; use YWatchman\LaravelEPP\Support\Traits\Commands\ProvidesCheckCommand; @@ -13,23 +14,24 @@ class CheckCommand extends Command use ProvidesCheckCommand; public const NODE_BASE = 'contact'; + public const NODE = 'contact:check'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:contact-1.0'; - /** - * @var DOMElement - */ - protected $node; + protected DOMElement $node; /** * @var Contact[] */ - protected $iterable; + protected array $iterable; /** * CreateCommand constructor. * - * @param Contact[]|Model[] $contacts + * @param Contact[]|Model[] $contacts + * + * @throws DOMException */ public function __construct(array $contacts) { diff --git a/src/Support/Xml/Commands/Contact/CreateCommand.php b/src/Support/Xml/Commands/Contact/CreateCommand.php index d6b4f23..e2e8225 100644 --- a/src/Support/Xml/Commands/Contact/CreateCommand.php +++ b/src/Support/Xml/Commands/Contact/CreateCommand.php @@ -4,7 +4,6 @@ use DOMElement; use YWatchman\LaravelEPP\Contracts\IsContact; -use YWatchman\LaravelEPP\Exceptions\EppException; use YWatchman\LaravelEPP\Models\Contact; use YWatchman\LaravelEPP\Support\Traits\Commands\ProvidesContactCommand; use YWatchman\LaravelEPP\Support\Xml\Commands\Command; @@ -15,25 +14,17 @@ class CreateCommand extends Command use ProvidesContactCommand; public const NODE_BASE = 'contact'; + public const NODE = 'contact:create'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:contact-1.0'; - /** - * @var DOMElement - */ - protected $node; + protected DOMElement $node; - /** - * @var Contact - */ - protected $contact; + protected Contact $contact; /** * CreateCommand constructor. - * - * @param IsContact $contact - * - * @throws EppException */ public function __construct(IsContact $contact) { @@ -50,10 +41,7 @@ public function __construct(IsContact $contact) $n->appendChild($this->getExtensionNode()); } - /** - * @return DOMElement - */ - protected function getCreateNode() + protected function getCreateNode(): DOMElement { $contact = new ContactTransformer($this->contact); $contact = $contact->toArray(); @@ -63,22 +51,23 @@ protected function getCreateNode() /** * Generate contact extension. - * - * @throws EppException - * - * @return DOMElement */ - protected function getExtensionNode() + protected function getExtensionNode(): DOMElement { $contact = $this->contact->fields(); $node = $this->createElement('extension'); $sidnExtension = $this->createElement('sidn-ext-epp:ext'); + $sidnExtension->setAttributeNS( + 'http://www.w3.org/2000/xmlns/', + 'xmlns:sidn-ext-epp', + 'https://rxsd.domain-registry.nl/sidn-ext-epp-1.0' + ); $create = $this->createElement('sidn-ext-epp:create'); $contactNode = $this->createElement('sidn-ext-epp:contact'); - $legalForm = $this->createElement('sidn-ext-epp:legalForm', $contact['legalForm']); + $legalForm = $this->createElement('sidn-ext-epp:legalForm', $this->mapLegalForm($contact['legalForm'])); $contactNode->appendChild($legalForm); if (isset($contact['legalFormNo'])) { @@ -93,4 +82,13 @@ protected function getExtensionNode() return $node; } + + private function mapLegalForm(string $form): string + { + return match (strtoupper($form)) { + 'PERSON' => 'PERSON', + 'OTHER' => 'ANDERS', + default => strtoupper($form), + }; + } } diff --git a/src/Support/Xml/Commands/Contact/UpdateCommand.php b/src/Support/Xml/Commands/Contact/UpdateCommand.php index 19b9eb8..a568882 100644 --- a/src/Support/Xml/Commands/Contact/UpdateCommand.php +++ b/src/Support/Xml/Commands/Contact/UpdateCommand.php @@ -18,33 +18,35 @@ class UpdateCommand extends Command use ProvidesContactCommand; public const NODE_BASE = 'contact'; + public const NODE = 'contact:update'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:contact-1.0'; /** * @var DOMElement */ - protected $node; + protected DOMElement $node; /** * @var Contact */ - protected $contact; + protected Contact $contact; /** * @var string */ - protected $registrar; + protected string $registrar; /** * CreateCommand constructor. * * @param Contact $contact - * @param string $registrar + * @param string $registrar * * @throws EppException */ - public function __construct(Contact $contact, $registrar = Registrar::REGISTRAR_SIDN) + public function __construct(Contact $contact, string $registrar = Registrar::REGISTRAR_SIDN) { parent::__construct(); @@ -63,7 +65,7 @@ public function __construct(Contact $contact, $registrar = Registrar::REGISTRAR_ /** * @return DOMElement */ - protected function getUpdateNode() + protected function getUpdateNode(): DOMElement { $contact = new ContactTransformer($this->contact); $contact = $contact->toArray(); @@ -74,11 +76,12 @@ protected function getUpdateNode() /** * Generate contact extension. * - * @throws EppException * * @return DOMElement + * + * @throws EppException */ - protected function getExtensionNode() + protected function getExtensionNode(): DOMElement { $node = $this->createElement('extension'); $classPath = Extension::contactInstance($this->registrar); diff --git a/src/Support/Xml/Commands/Domain/CheckCommand.php b/src/Support/Xml/Commands/Domain/CheckCommand.php index 24c6411..641e083 100644 --- a/src/Support/Xml/Commands/Domain/CheckCommand.php +++ b/src/Support/Xml/Commands/Domain/CheckCommand.php @@ -11,7 +11,9 @@ class CheckCommand extends Command use ProvidesCheckCommand; public const NODE_BASE = 'domain'; + public const NODE = 'domain:check'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:domain-1.0'; /** @var DOMElement */ @@ -22,8 +24,6 @@ class CheckCommand extends Command /** * CheckCommand constructor. - * - * @param array $domains */ public function __construct(array $domains) { diff --git a/src/Support/Xml/Commands/Domain/CreateCommand.php b/src/Support/Xml/Commands/Domain/CreateCommand.php index 88eaccc..c1dcd44 100644 --- a/src/Support/Xml/Commands/Domain/CreateCommand.php +++ b/src/Support/Xml/Commands/Domain/CreateCommand.php @@ -14,6 +14,7 @@ class CreateCommand extends Command use HasDnssec; public const NODE = 'domain:create'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:domain-1.0'; /** @@ -53,21 +54,12 @@ class CreateCommand extends Command */ protected $extensions = []; - /** - * @var string|null - */ - protected $transactionId; - /** * CreateCommand constructor. * - * @param Domain $domain - * @param Contact $admin admin-c handle - * @param Contact $tech tech-c handle - * @param Contact $registrant registrant handle - * @param array $nameservers - * @param array $extensions - * @param string|null $transactionId + * @param Contact $admin admin-c handle + * @param Contact $tech tech-c handle + * @param Contact $registrant registrant handle */ public function __construct( Domain $domain, @@ -76,7 +68,7 @@ public function __construct( Contact $registrant, array $nameservers, array $extensions = [], - string $transactionId = null + ?string $transactionId = null ) { parent::__construct(); @@ -86,7 +78,9 @@ public function __construct( $this->registrant = $registrant; $this->nameservers = $nameservers; $this->extensions = $extensions; - $this->transactionId = $transactionId; + if ($transactionId !== null) { + $this->clientTransactionId = $transactionId; + } if (array_key_exists('dnssec', $this->extensions)) { $this->enableDNSSEC(); @@ -108,18 +102,11 @@ public function __toString() $n->appendChild($this->getExtensionNode()); } - // Todo: move $n to $this->node or something and move this code below to the Command class. - if ($this->transactionId !== null && is_string($this->transactionId)) { - $n->appendChild($this->createElement('clTRID', $this->transactionId)); - } - return parent::__toString(); } /** * Fill command. - * - * @return DOMElement */ protected function getCreateNode(): DOMElement { @@ -157,8 +144,6 @@ protected function getCreateNode(): DOMElement /** * Get contact nodes. - * - * @return array */ protected function getContactNodes(): array { @@ -186,9 +171,6 @@ protected function getContactNodes(): array return $nodes; } - /** - * @return DOMElement - */ protected function getExtensionNode(): DOMElement { $extNode = $this->createElement('extension'); diff --git a/src/Support/Xml/Commands/Domain/TransferCommand.php b/src/Support/Xml/Commands/Domain/TransferCommand.php index d7dd605..ebb89c1 100644 --- a/src/Support/Xml/Commands/Domain/TransferCommand.php +++ b/src/Support/Xml/Commands/Domain/TransferCommand.php @@ -13,7 +13,9 @@ class TransferCommand extends Command use HasDnssec; public const NODE_BASE = 'domain'; + public const NODE = 'domain:transfer'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:domain-1.0'; /** @@ -45,17 +47,12 @@ class TransferCommand extends Command /** * CreateCommand constructor. - * - * @param Domain $domain - * @param string $token - * @param array $extensions - * @param string|null $transactionId */ public function __construct( Domain $domain, string $token, array $extensions = [], - string $transactionId = null + ?string $transactionId = null ) { parent::__construct(); @@ -80,8 +77,6 @@ public function __construct( /** * Fill command. - * - * @return DOMElement */ protected function getCreateNode(): DOMElement { diff --git a/src/Support/Xml/Commands/Domain/UpdateCommand.php b/src/Support/Xml/Commands/Domain/UpdateCommand.php index add449c..1aa2000 100644 --- a/src/Support/Xml/Commands/Domain/UpdateCommand.php +++ b/src/Support/Xml/Commands/Domain/UpdateCommand.php @@ -17,7 +17,9 @@ class UpdateCommand extends Command use HasScheduledDeletion; public const NODE_BASE = 'domain'; + public const NODE = 'domain:update'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:domain-1.0'; /** @@ -54,12 +56,6 @@ class UpdateCommand extends Command /** * UpdateCommand constructor. - * - * @param Domain $domain - * @param array $add - * @param array $delete - * @param array $update - * @param array $extensions */ public function __construct( Domain $domain, @@ -121,8 +117,6 @@ protected function getUpdateNode() * Get add nodes. * * @throws Exception - * - * @return DOMElement */ protected function getAddNodes(): DOMElement { @@ -130,7 +124,7 @@ protected function getAddNodes(): DOMElement foreach ($this->add as $key => $addNode) { if (is_array($addNode)) { - if (!is_string($key)) { + if (! is_string($key)) { throw new Exception('Key should be a string.'); } $element = $this->createElement($key); @@ -154,9 +148,10 @@ protected function getAddNodes(): DOMElement /** * Get remove nodes. * - * @throws Exception * * @return DOMElement + * + * @throws Exception */ protected function getRemNodes() { @@ -164,7 +159,7 @@ protected function getRemNodes() foreach ($this->delete as $key => $delNode) { if (is_array($delNode)) { - if (!is_string($key)) { + if (! is_string($key)) { throw new Exception('Key should be a string.'); } $element = $this->createElement($key); @@ -188,9 +183,10 @@ protected function getRemNodes() /** * Get update nodes. * - * @throws Exception * * @return DOMElement + * + * @throws Exception */ protected function getChgNodes() { @@ -198,7 +194,7 @@ protected function getChgNodes() foreach ($this->update as $key => $chgNode) { if (is_array($chgNode)) { - if (!is_string($key)) { + if (! is_string($key)) { throw new Exception('Key should be a string.'); } $element = $this->createElement($key); diff --git a/src/Support/Xml/Commands/Host/CheckCommand.php b/src/Support/Xml/Commands/Host/CheckCommand.php index c51161d..9d7e435 100644 --- a/src/Support/Xml/Commands/Host/CheckCommand.php +++ b/src/Support/Xml/Commands/Host/CheckCommand.php @@ -13,7 +13,9 @@ class CheckCommand extends Command use ProvidesCheckCommand; public const NODE_BASE = 'host'; + public const NODE = 'host:check'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:host-1.0'; /** @@ -29,7 +31,7 @@ class CheckCommand extends Command /** * CreateCommand constructor. * - * @param Nameserver[]|Model[] $nameservers + * @param Nameserver[]|Model[] $nameservers */ public function __construct(array $nameservers) { diff --git a/src/Support/Xml/Commands/Host/CreateCommand.php b/src/Support/Xml/Commands/Host/CreateCommand.php index 4e1bf7c..63624c1 100644 --- a/src/Support/Xml/Commands/Host/CreateCommand.php +++ b/src/Support/Xml/Commands/Host/CreateCommand.php @@ -11,7 +11,9 @@ class CreateCommand extends Command { public const NODE_BASE = 'host'; + public const NODE = 'host:create'; + public const NAMESPACE = 'urn:ietf:params:xml:ns:host-1.0'; /** @@ -42,9 +44,6 @@ public function __construct(Nameserver $nameserver) ->appendChild($this->getCreateNode()); } - /** - * @return DOMElement - */ protected function getCreateNode(): DOMElement { $node = $this->createElement('create'); @@ -65,7 +64,7 @@ private function handleNamserver(Nameserver $nameserver): DOMElement $addresses = $nameserver->getAddresses(); foreach ($addresses as $address) { $address = explode('-', $address, 2); - if (!empty($address[0])) { + if (! empty($address[0])) { $ipNode = $this->createElement('host:addr', $address[0]); if (count($address) > 1) { diff --git a/src/Support/Xml/Commands/Session/HelloCommand.php b/src/Support/Xml/Commands/Session/HelloCommand.php index d71c43b..45ab643 100644 --- a/src/Support/Xml/Commands/Session/HelloCommand.php +++ b/src/Support/Xml/Commands/Session/HelloCommand.php @@ -8,7 +8,7 @@ class HelloCommand extends Command { /** @var DOMElement */ - protected $node; + protected DOMElement $node; /** * HelloCommand constructor. diff --git a/src/Support/Xml/Commands/Session/LoginCommand.php b/src/Support/Xml/Commands/Session/LoginCommand.php index 12188e8..2b68e81 100644 --- a/src/Support/Xml/Commands/Session/LoginCommand.php +++ b/src/Support/Xml/Commands/Session/LoginCommand.php @@ -8,13 +8,13 @@ class LoginCommand extends Command { /** @var DOMElement */ - protected $node; + protected DOMElement $node; /** * LoginCommand constructor. * - * @param string $username EPP Username - * @param string $password EPP Password + * @param string $username EPP Username + * @param string $password EPP Password */ public function __construct(string $username, string $password) { @@ -38,9 +38,9 @@ public function __construct(string $username, string $password) $svcNode->appendChild($this->createElement('objURI', 'urn:ietf:params:xml:ns:domain-1.0')); $svcExt = $svcNode->appendChild($this->createElement('svcExtension')); - $svcExt->appendChild($this->createElement('extURI', 'http://rxsd.domain-registry.nl/sidn-ext-epp-1.0')); - $svcExt->appendChild($this->createElement('extURI', 'http://rxsd.domain-registry.nl/sidn-ext-epp-registry-contacts-delete-1.0')); - $svcExt->appendChild($this->createElement('extURI', 'http://rxsd.domain-registry.nl/sidn-ext-epp-scheduled-delete-1.0')); + $svcExt->appendChild($this->createElement('extURI', 'https://rxsd.domain-registry.nl/sidn-ext-epp-1.0')); + $svcExt->appendChild($this->createElement('extURI', 'https://rxsd.domain-registry.nl/sidn-ext-epp-registry-contacts-delete-1.0')); + $svcExt->appendChild($this->createElement('extURI', 'https://rxsd.domain-registry.nl/sidn-ext-epp-scheduled-delete-1.0')); $svcExt->appendChild($this->createElement('extURI', 'urn:ietf:params:xml:ns:secDNS-1.1')); $svcExt->appendChild($this->createElement('extURI', 'urn:ietf:params:xml:ns:keyrelay-1.0')); } diff --git a/src/Support/Xml/Commands/Session/LogoutCommand.php b/src/Support/Xml/Commands/Session/LogoutCommand.php index 0db83b7..94bc13c 100644 --- a/src/Support/Xml/Commands/Session/LogoutCommand.php +++ b/src/Support/Xml/Commands/Session/LogoutCommand.php @@ -8,7 +8,7 @@ class LogoutCommand extends Command { /** @var DOMElement */ - protected $node; + protected DOMElement $node; /** * LogoutCommand constructor. diff --git a/src/Support/Xml/Extensions/Extension.php b/src/Support/Xml/Extensions/Extension.php index 70dff41..ac39d1d 100644 --- a/src/Support/Xml/Extensions/Extension.php +++ b/src/Support/Xml/Extensions/Extension.php @@ -33,18 +33,16 @@ public function __construct() if ($this->prefix === null) { throw new EppException('Extension prefix cannot be null.'); } - $this->helper = new XmlHelper(); + $this->helper = new XmlHelper; $this->extension = $this->helper->createElement($this->prefix.':ext'); } /** * Return new ContactExtension instance. * - * @param string $registrar + * @param string $registrar * * @throws EppException - * - * @return string */ public static function contactInstance($registrar = Registrar::REGISTRAR_SIDN): string { @@ -61,8 +59,6 @@ public static function contactInstance($registrar = Registrar::REGISTRAR_SIDN): /** * Retrieve extension. - * - * @return DOMElement */ public function getExtension(): DOMElement { @@ -71,10 +67,6 @@ public function getExtension(): DOMElement /** * Get prefixed name. - * - * @param string $name - * - * @return string */ public function prefixedName(string $name): string { diff --git a/src/Support/Xml/Extensions/Sidn/ContactExtension.php b/src/Support/Xml/Extensions/Sidn/ContactExtension.php index 296365e..546280a 100644 --- a/src/Support/Xml/Extensions/Sidn/ContactExtension.php +++ b/src/Support/Xml/Extensions/Sidn/ContactExtension.php @@ -11,7 +11,6 @@ class ContactExtension extends SidnExtension /** * ContactExtension constructor. * - * @param IsContact $contact * * @throws EppException */ @@ -30,10 +29,6 @@ public function __construct(IsContact $contact) /** * Return the fields array as a DOM node. - * - * @param array $fields - * - * @return DOMElement */ private function getContactData(array $fields): DOMElement { diff --git a/src/Support/Xml/Objects/Contact/ContactObject.php b/src/Support/Xml/Objects/Contact/ContactObject.php index 0530324..8933728 100644 --- a/src/Support/Xml/Objects/Contact/ContactObject.php +++ b/src/Support/Xml/Objects/Contact/ContactObject.php @@ -18,8 +18,11 @@ abstract class ContactObject ]; public const CONTACT_ADMIN = 'admin'; + public const CONTACT_TECH = 'tech'; + public const CONTACT_BILLING = 'billing'; + public const CONTACT_REGISTRANT = 'registrant'; /** @var string */ @@ -64,13 +67,12 @@ abstract class ContactObject /** * ContactObject constructor. * - * @param string $type * * @throws EppException */ public function __construct(string $type) { - if (!in_array($type, self::CONTACT_TYPES)) { + if (! in_array($type, self::CONTACT_TYPES)) { throw EppException::contactTypeDoesNotExist($type); } $this->setType($type); @@ -78,217 +80,137 @@ public function __construct(string $type) /** * Return ContactObject from Contact modal. - * - * @param Contact $contact */ public static function createFromModel(Contact $contact) { -// return new self($contact->) + // return new self($contact->) } - /** - * @return string - */ public function getType(): string { return $this->type; } - /** - * @param string $type - */ public function setType(string $type): void { $this->type = $type; } - /** - * @return string - */ public function getId(): string { return $this->id; } - /** - * @param string $id - */ public function setId(string $id): void { $this->id = $id; } - /** - * @return string - */ public function getName(): string { return $this->name; } - /** - * @param string $name - */ public function setName(string $name): void { $this->name = $name; } - /** - * @return string - */ public function getOrg(): string { return $this->org; } - /** - * @param string $org - */ public function setOrg(string $org): void { $this->org = $org; } - /** - * @return string - */ public function getStreet(): string { return $this->street; } - /** - * @param string $street - */ public function setStreet(string $street): void { $this->street = $street; } - /** - * @return string - */ public function getCity(): string { return $this->city; } - /** - * @param string $city - */ public function setCity(string $city): void { $this->city = $city; } - /** - * @return string - */ public function getSp(): string { return $this->sp; } - /** - * @param string $sp - */ public function setSp(string $sp): void { $this->sp = $sp; } - /** - * @return string - */ public function getPc(): string { return $this->pc; } - /** - * @param string $pc - */ public function setPc(string $pc): void { $this->pc = $pc; } - /** - * @return string - */ public function getCc(): string { return $this->cc; } - /** - * @param string $cc - */ public function setCc(string $cc): void { $this->cc = $cc; } - /** - * @return string - */ public function getVoice(): string { return $this->voice; } - /** - * @param string $voice - */ public function setVoice(string $voice): void { $this->voice = $voice; } - /** - * @return string - */ public function getFax(): string { return $this->fax; } - /** - * @param string $fax - */ public function setFax(string $fax): void { $this->fax = $fax; } - /** - * @return string - */ public function getEmail(): string { return $this->email; } - /** - * @param string $email - */ public function setEmail(string $email): void { $this->email = $email; } - /** - * @return bool - */ public function isDisclose(): bool { return $this->disclose; } - /** - * @param bool $disclose - */ public function setDisclose(bool $disclose): void { $this->disclose = $disclose; diff --git a/src/Support/Xml/Objects/Host/AuthObject.php b/src/Support/Xml/Objects/Host/AuthObject.php index 6a68119..c6f771c 100644 --- a/src/Support/Xml/Objects/Host/AuthObject.php +++ b/src/Support/Xml/Objects/Host/AuthObject.php @@ -10,21 +10,27 @@ class AuthObject { public const AUTH_DOMAIN = 'domain:authInfo'; + public const AUTH_CONTACT = 'contact:authInfo'; + public const AUTH_TYPES = [ self::AUTH_DOMAIN, self::AUTH_CONTACT, ]; public const PW_DOMAIN = 'domain:pw'; + public const PW_CONTACT = 'contact:pw'; + public const PW_TYPES = [ self::PW_DOMAIN, self::PW_CONTACT, ]; public const DOMAIN_NAMESPACE = 'urn:ietf:params:xml:ns:domain-1.0'; + public const CONTACT_NAMESPACE = 'urn:ietf:params:xml:ns:contact-1.0'; + public const NAMESPACES = [ self::DOMAIN_NAMESPACE, self::CONTACT_NAMESPACE, @@ -32,12 +38,12 @@ class AuthObject public static function getAuthInfo(string $type, string $value = Command::NOT_USED) // Todo: move constant to global class { - if (!in_array($type, self::AUTH_TYPES)) { + if (! in_array($type, self::AUTH_TYPES)) { throw EppException::authTypeDoesNotExist($type); } - if (!in_array($type, self::NAMESPACES)) { -// throw EppException::namespac($type); namespace err + if (! in_array($type, self::NAMESPACES)) { + // throw EppException::namespac($type); namespace err } switch ($type) { @@ -53,7 +59,7 @@ public static function getAuthInfo(string $type, string $value = Command::NOT_US throw EppException::notImplemented(); } - $document = new DOMDocument(); + $document = new DOMDocument; $authObject = $document ->createElement($type); $authObject->appendChild($pwNode); diff --git a/src/Support/Xml/Objects/Host/HostObject.php b/src/Support/Xml/Objects/Host/HostObject.php index bbdff41..e65e027 100644 --- a/src/Support/Xml/Objects/Host/HostObject.php +++ b/src/Support/Xml/Objects/Host/HostObject.php @@ -9,7 +9,6 @@ class HostObject /** * Get array of DOM elements. * - * @param array $nameservers * * @return array */ @@ -23,7 +22,6 @@ public static function getNameservers(array $nameservers) /** * Get DOM element for single nameserver. * - * @param $nameserver * * @return DOMElement */ diff --git a/src/Support/Xml/XmlHelper.php b/src/Support/Xml/XmlHelper.php index dc89aa6..95cc92a 100644 --- a/src/Support/Xml/XmlHelper.php +++ b/src/Support/Xml/XmlHelper.php @@ -32,9 +32,7 @@ public function __construct() /** * Add DOM node to current XML Document. * - * @param string $name - * @param DOMNode|null $node if should be layered - * + * @param DOMNode|null $node if should be layered * @return DOMNode */ public function addNode(string $name, $node = null) @@ -51,10 +49,7 @@ public function addNode(string $name, $node = null) /** * Alias for DOMDocument::createElement(). * - * @param $name - * @param string|null $value - * - * @return DOMElement + * @param string|null $value */ public function createElement($name, $value = null): DOMElement { diff --git a/src/Transformers/ContactTransformer.php b/src/Transformers/ContactTransformer.php index 130e04a..3e2a207 100644 --- a/src/Transformers/ContactTransformer.php +++ b/src/Transformers/ContactTransformer.php @@ -11,8 +11,6 @@ class ContactTransformer extends Transformer /** * ContactTransformer constructor. - * - * @param Transformable $transformable */ public function __construct(Transformable $transformable) { @@ -24,23 +22,19 @@ public function __construct(Transformable $transformable) /** * Transform contact model to array. - * - * @return array|void */ - public function toArray() + public function toArray(): ?array { return $this->transformed; } /** * Return transformed array. - * - * @return array|void */ - protected function transform() + protected function transform(): array { return [ - 'id' => $this->transformable->handle, + 'id' => $this->transformable->handle, 'postalInfo' => [ 'attributes' => [ 'type' => 'loc', @@ -52,14 +46,14 @@ protected function transform() $this->transformable->number, $this->transformable->suffix, ], - 'city' => $this->transformable->city, - 'sp' => $this->transformable->state, - 'pc' => $this->transformable->postal, - 'cc' => $this->transformable->country, + 'city' => $this->transformable->city, + 'sp' => $this->transformable->state, + 'pc' => $this->transformable->postal, + 'cc' => $this->transformable->country, ], ], 'voice' => $this->transformable->phone, - 'fax' => $this->transformable->fax, + 'fax' => $this->transformable->fax, 'email' => $this->transformable->email, ]; } diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php index a33f5ce..f766ae2 100644 --- a/src/Transformers/Transformer.php +++ b/src/Transformers/Transformer.php @@ -16,8 +16,6 @@ abstract class Transformer /** * Transformer constructor. - * - * @param Transformable $transformable */ public function __construct(Transformable $transformable) { @@ -29,7 +27,7 @@ public function __construct(Transformable $transformable) */ public function toArray() { - throw new NotImplementedException(); + throw new NotImplementedException; } /** @@ -37,6 +35,6 @@ public function toArray() */ protected function transform() { - throw new NotImplementedException(); + throw new NotImplementedException; } } diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..e53fb11 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in('Unit'); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..bd08b68 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,19 @@ +set('epp.debug', false); + } +} diff --git a/tests/Unit/Commands/Contact/CreateCommandTest.php b/tests/Unit/Commands/Contact/CreateCommandTest.php new file mode 100644 index 0000000..9bac038 --- /dev/null +++ b/tests/Unit/Commands/Contact/CreateCommandTest.php @@ -0,0 +1,63 @@ + 'TEST001', + 'name' => 'Test User', + 'street' => 'Main Street', + 'number' => '1', + 'city' => 'Amsterdam', + 'postal' => '1000AA', + 'country' => 'NL', + 'phone' => '+31.201234567', + 'email' => 'test@example.nl', + 'legalForm' => 'PERSON', + ]); + + return new CreateCommand($contact); +} + +test('contact create command includes clTRID', function () { + $xml = (string) makeContactCommand(); + + expect($xml)->toContain('loadXML($xml); + + // Root must not carry the sidn namespace declaration + $epp = $doc->documentElement; + expect($epp->hasAttribute('xmlns:sidn-ext-epp'))->toBeFalse() + ->and($xml)->toContain('sidn-ext-epp:ext') + ->and($xml)->toContain('xmlns:sidn-ext-epp="https://rxsd.domain-registry.nl/sidn-ext-epp-1.0"'); + + // The namespace declaration must appear somewhere inside (on the ext element) +}); + +test('contact create command includes legalForm in sidn extension', function () { + $xml = (string) makeContactCommand(); + + expect($xml)->toContain('sidn-ext-epp:legalForm') + ->and($xml)->toContain('PERSOON'); +}); + +test('contact create command produces valid xml', function () { + $xml = (string) makeContactCommand(); + + $doc = new DOMDocument; + $result = $doc->loadXML($xml); + + expect($result)->toBeTrue(); +}); diff --git a/tests/Unit/Commands/Domain/CreateCommandTest.php b/tests/Unit/Commands/Domain/CreateCommandTest.php new file mode 100644 index 0000000..97c162f --- /dev/null +++ b/tests/Unit/Commands/Domain/CreateCommandTest.php @@ -0,0 +1,54 @@ + 'example', 'tld' => 'nl']); + $contact = new Contact(['handle' => 'TEST001']); + $ns = new Nameserver(['name' => 'ns1.example.nl', 'address' => '1.2.3.4-v4']); + + return new CreateCommand($domain, $contact, $contact, $contact, [$ns], [], $transactionId); +} + +test('domain create command contains clTRID', function () { + $xml = (string) makeDomainCommand(); + + expect($xml)->toContain('toContain('my-custom-txid'); +}); + +test('domain create command auto-generates clTRID when none provided', function () { + $xml1 = (string) makeDomainCommand(); + $xml2 = (string) makeDomainCommand(); + + // Both have clTRID, and they are different (auto-generated unique IDs) + expect($xml1)->toContain('and($xml2)->toContain('and($xml1)->not->toBe($xml2); +}); + +test('domain create command does not include sidn namespace on root epp element', function () { + $xml = (string) makeDomainCommand(); + + $doc = new DOMDocument; + $doc->loadXML($xml); + $epp = $doc->documentElement; + + expect($epp->hasAttribute('xmlns:sidn-ext-epp'))->toBeFalse(); +}); + +test('domain create command includes domain create node', function () { + $xml = (string) makeDomainCommand(); + + expect($xml)->toContain('domain:create') + ->and($xml)->toContain('example.nl'); +}); diff --git a/tests/Unit/Commands/Session/HelloCommandTest.php b/tests/Unit/Commands/Session/HelloCommandTest.php new file mode 100644 index 0000000..938200c --- /dev/null +++ b/tests/Unit/Commands/Session/HelloCommandTest.php @@ -0,0 +1,23 @@ +toContain('and($xml)->toContain('xmlns="urn:ietf:params:xml:ns:epp-1.0"'); +}); + +test('hello command does not include sidn namespace on root epp element', function () { + $xml = (string) new HelloCommand; + + expect($xml)->not->toContain('xmlns:sidn-ext-epp'); +}); + +test('hello command does not include clTRID', function () { + $xml = (string) new HelloCommand; + + // HelloCommand adds not , so clTRID should not be appended + expect($xml)->not->toContain(' + + + + Command completed successfully + + + epp-abc123 + server-xyz789 + + + +XML; + +$failureXml = <<<'XML' + + + + + Object does not exist + + + server-fail001 + + + +XML; + +test('success response is parsed correctly', function () use ($successXml) { + $response = new Response($successXml); + + expect($response->isSucceeded())->toBeTrue() + ->and($response->getCode())->toBe(1000) + ->and($response->getMessage())->toBe('Command completed successfully') + ->and($response->getServerTransaction())->toBe('server-xyz789') + ->and($response->getClientTransaction())->toBe('epp-abc123'); +}); + +test('failure response is parsed correctly', function () use ($failureXml) { + $response = new Response($failureXml); + + expect($response->isSucceeded())->toBeFalse() + ->and($response->getCode())->toBe(2303) + ->and($response->getMessage())->toBe('Object does not exist') + ->and($response->getServerTransaction())->toBe('server-fail001'); +}); + +test('clTRID is null when absent from server response', function () use ($failureXml) { + $response = new Response($failureXml); + + expect($response->getClientTransaction())->toBeNull(); +}); From 8a8c87ad842640b9e63553c2a00059953e09390f Mon Sep 17 00:00:00 2001 From: Jacques Mbabazi Date: Tue, 12 May 2026 13:08:43 +0200 Subject: [PATCH 2/7] Update README with usage examples and requirements --- README.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c0d0cf..655e10a 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,125 @@ SIDN_PASSWORD=superpass123! SIDN_HOSTNAME=drs.domain-registry.nl ``` -Start using Laravel EPP ! \ No newline at end of file +Start using Laravel EPP ! + +## Requirements + +- PHP 8.1+ +- Laravel 10 or 11 + +## Usage + +### Starting a session + +```php +use YWatchman\LaravelEPP\Epp; + +$epp = new Epp('sidn'); +$epp->start(); +$epp->login(); + +// ... perform commands ... + +$epp->logout(); +``` + +### Domain commands + +**Check availability** +```php +use YWatchman\LaravelEPP\Models\Domain; +use YWatchman\LaravelEPP\Support\Xml\Commands\Domain\CheckCommand; +use YWatchman\LaravelEPP\Responses\Domain\CheckResponse; + +$domains = [new Domain(['name' => 'example.nl'])]; +$command = new CheckCommand($domains); +$response = new CheckResponse($epp->sendRequest((string) $command)); + +if ($response->isSucceeded()) { + // $response->domainExists('example.nl') +} +``` + +**Create a domain** +```php +use YWatchman\LaravelEPP\Support\Xml\Commands\Domain\CreateCommand; +use YWatchman\LaravelEPP\Responses\Domain\CreateResponse; + +$command = new CreateCommand($domain, $contacts, $nameservers); +$response = new CreateResponse($epp->sendRequest((string) $command)); +``` + +**Transfer a domain** +```php +use YWatchman\LaravelEPP\Support\Xml\Commands\Domain\TransferCommand; +use YWatchman\LaravelEPP\Responses\Domain\TransferResponse; + +$command = new TransferCommand($domain, $authToken); +$response = new TransferResponse($epp->sendRequest((string) $command)); +``` + +### Contact commands + +**Create a contact** +```php +use YWatchman\LaravelEPP\Models\Contact; +use YWatchman\LaravelEPP\Support\Xml\Commands\Contact\CreateCommand; +use YWatchman\LaravelEPP\Responses\Contact\CreateResponse; + +$contact = new Contact([ + 'handle' => 'MYHANDLE001', + 'name' => 'Jane Doe', + 'street' => 'Main Street', + 'number' => '1', + 'city' => 'Amsterdam', + 'postal' => '1000AA', + 'country' => 'NL', + 'phone' => '+31.201234567', + 'email' => 'jane@example.nl', + 'legalForm' => 'PERSON', // PERSON, OTHER, or SIDN Dutch codes directly +]); + +$command = new CreateCommand($contact); +$response = new CreateResponse($epp->sendRequest((string) $command)); +``` + +### Host commands + +**Create a nameserver** +```php +use YWatchman\LaravelEPP\Models\Nameserver; +use YWatchman\LaravelEPP\Support\Xml\Commands\Host\CreateCommand; +use YWatchman\LaravelEPP\Responses\Host\CreateResponse; + +$nameserver = new Nameserver(['name' => 'ns1.example.nl', 'address' => '1.2.3.4']); +$command = new CreateCommand($nameserver); +$response = new CreateResponse($epp->sendRequest((string) $command)); +``` + +### Checking a response + +All response classes extend `Response` and share a common interface: + +```php +$response->isSucceeded(); // bool +$response->getCode(); // int (e.g. 1000, 2303) +$response->getMessage(); // ?string +$response->getServerTransaction(); // string +$response->getClientTransaction(); // ?string +``` + +## SIDN-specific notes + +- Legal form values `PERSON` and `OTHER` are automatically mapped to the Dutch SIDN codes `PERSOON` and `ANDERS`. All other values are passed through as-is. +- The SIDN EPP extension namespace is `https://rxsd.domain-registry.nl/sidn-ext-epp-1.0`. + +## Debugging + +Set `EPP_DEBUG=true` in your `.env` to dump raw XML frames to stdout during socket I/O. + +## Running tests + +```bash +./vendor/bin/pest +``` \ No newline at end of file From c1ddf5afe9cbcf4801d5c757c2c9c2a56c4e084c Mon Sep 17 00:00:00 2001 From: Jacques Mbabazi Date: Tue, 12 May 2026 13:25:31 +0200 Subject: [PATCH 3/7] Introduce Gitflow workflow with branch protection and contributing guide --- .github/pull_request_template.md | 20 +++++++++ CONTRIBUTING.md | 71 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTING.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..bc5ba57 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ +## What does this PR do? + + + +## Type of change + +- [ ] Feature (`feature/*` → `develop`) +- [ ] Bug fix (`fix/*` → `develop`) +- [ ] Hotfix (`hotfix/*` → `main` + `develop`) +- [ ] Release (`release/*` → `main` + `develop`) + +## Test plan + +- [ ] `./vendor/bin/pest` — all tests pass +- [ ] `./vendor/bin/pint --test` — no style violations +- [ ] Manually tested the affected flows + +## Notes + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2699e1e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# Contributing + +This project follows the **Gitflow** branching model. + +## Branch structure + +| Branch | Purpose | +|--------|---------| +| `main` | Production-ready code. Only receives merges from `release/*` and `hotfix/*`. | +| `develop` | Integration branch. All features land here first. | +| `feature/*` | New features, branched from and merged back into `develop`. | +| `release/*` | Release preparation, branched from `develop`, merged into both `main` and `develop`. | +| `hotfix/*` | Urgent production fixes, branched from `main`, merged into both `main` and `develop`. | + +## Day-to-day workflow + +### Starting a feature + +```bash +git checkout develop +git pull origin develop +git checkout -b feature/my-feature +``` + +Work, commit, push, then open a PR targeting `develop`. + +### Starting a release + +```bash +git checkout develop +git pull origin develop +git checkout -b release/1.2.0 +# bump version, update changelog, final fixes +git push origin release/1.2.0 +``` + +Open a PR targeting `main`. After merge, also merge into `develop` and tag the release: + +```bash +git checkout main && git pull origin main +git tag -a v1.2.0 -m "Release 1.2.0" +git push origin v1.2.0 +``` + +### Fixing a production bug (hotfix) + +```bash +git checkout main +git pull origin main +git checkout -b hotfix/fix-critical-bug +# fix, commit +git push origin hotfix/fix-critical-bug +``` + +Open two PRs: one targeting `main`, one targeting `develop`. + +## Branch naming + +| Type | Pattern | Example | +|------|---------|---------| +| Feature | `feature/` | `feature/domain-transfer` | +| Bug fix | `fix/` | `fix/response-code-cast` | +| Release | `release/` | `release/1.2.0` | +| Hotfix | `hotfix/` | `hotfix/sidn-namespace-url` | + +## Rules + +- `main` and `develop` are protected — direct pushes are blocked. +- Every change requires a PR with at least **1 approval**. +- Stale approvals are dismissed when new commits are pushed. +- Run `./vendor/bin/pest` and `./vendor/bin/pint` before opening a PR. From 484d4c71249410a73700a2ef2af21b7dfb53c094 Mon Sep 17 00:00:00 2001 From: Jacques Mbabazi Date: Tue, 12 May 2026 13:44:53 +0200 Subject: [PATCH 4/7] Add GitHub Actions: tests, code style, static analysis, auto-label --- .github/workflows/labeler.yml | 34 +++++ .github/workflows/phpstan.yml | 28 ++++ .github/workflows/pint.yml | 28 ++++ .github/workflows/tests.yml | 35 +++++ composer.json | 3 +- phpstan-baseline.neon | 277 ++++++++++++++++++++++++++++++++++ phpstan.neon | 8 + 7 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/phpstan.yml create mode 100644 .github/workflows/pint.yml create mode 100644 .github/workflows/tests.yml create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..e6e64d4 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,34 @@ +name: Auto Label + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + label: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Apply label based on branch name + uses: actions/github-script@v7 + with: + script: | + const branch = context.payload.pull_request.head.ref; + const labels = []; + + if (branch.startsWith('feature/')) labels.push('feature'); + else if (branch.startsWith('fix/')) labels.push('bug'); + else if (branch.startsWith('hotfix/')) labels.push('hotfix'); + else if (branch.startsWith('release/')) labels.push('release'); + + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels, + }); + } diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..d33c501 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,28 @@ +name: Static Analysis + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + phpstan: + runs-on: ubuntu-latest + name: PHPStan + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse --no-progress diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml new file mode 100644 index 0000000..9252f27 --- /dev/null +++ b/.github/workflows/pint.yml @@ -0,0 +1,28 @@ +name: Code Style + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + pint: + runs-on: ubuntu-latest + name: Laravel Pint + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Check code style + run: ./vendor/bin/pint --test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e13085d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,35 @@ +name: Tests + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['8.2', '8.3'] + + name: PHP ${{ matrix.php }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, json, libxml, simplexml + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - name: Run tests + run: ./vendor/bin/pest diff --git a/composer.json b/composer.json index e1c9d9d..8b68127 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,8 @@ "require-dev": { "laravel/pint": "^1.29.1", "pestphp/pest": "^3.8.6", - "orchestra/testbench": "^9.17" + "orchestra/testbench": "^9.17", + "phpstan/phpstan": "^2.1" }, "config": { "allow-plugins": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..f3a04b5 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,277 @@ +parameters: + ignoreErrors: + - + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue + count: 2 + path: src/Epp.php + + - + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable + count: 1 + path: src/Epp.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Nameserver\:\:\$address\.$#' + identifier: property.notFound + count: 2 + path: src/Models/Nameserver.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Nameserver\:\:\$name\.$#' + identifier: property.notFound + count: 1 + path: src/Models/Nameserver.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$name\.$#' + identifier: property.notFound + count: 1 + path: src/Support/DomainParser.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$sld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/DomainParser.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$tld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/DomainParser.php + + - + message: '#^Call to method getRules\(\) on an unknown class Pdp\\Manager\.$#' + identifier: class.notFound + count: 1 + path: src/Support/DomainParser.php + + - + message: '#^Instantiated class Pdp\\Cache not found\.$#' + identifier: class.notFound + count: 1 + path: src/Support/DomainParser.php + + - + message: '#^Instantiated class Pdp\\CurlHttpClient not found\.$#' + identifier: class.notFound + count: 1 + path: src/Support/DomainParser.php + + - + message: '#^Instantiated class Pdp\\Manager not found\.$#' + identifier: class.notFound + count: 1 + path: src/Support/DomainParser.php + + - + message: '#^Trait YWatchman\\LaravelEPP\\Support\\Traits\\Commands\\HasExtensions is used zero times and is not analysed\.$#' + identifier: trait.unused + count: 1 + path: src/Support/Traits/Commands/HasExtensions.php + + - + message: '#^Access to undefined constant static\(YWatchman\\LaravelEPP\\Support\\Xml\\Commands\\Command\)\:\:NODE_BASE\.$#' + identifier: classConstant.notFound + count: 1 + path: src/Support/Xml/Commands/Command.php + + - + message: '#^Call to an undefined method DOMNode\:\:setAttribute\(\)\.$#' + identifier: method.notFound + count: 1 + path: src/Support/Xml/Commands/Command.php + + - + message: '#^Parameter \#2 \$callback of function array_walk expects callable\(mixed, int\|string, DOMElement\)\: mixed, array\{''YWatchman\\\\LaravelEPP\\\\Support\\\\Xml\\\\Commands\\\\Command'', ''recurseCallback''\} given\.$#' + identifier: argument.type + count: 1 + path: src/Support/Xml/Commands/Command.php + + - + message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#' + identifier: notIdentical.alwaysFalse + count: 1 + path: src/Support/Xml/Commands/Command.php + + - + message: '#^Undefined variable\: \$attrs$#' + identifier: variable.undefined + count: 1 + path: src/Support/Xml/Commands/Command.php + + - + message: '#^Variable \$attrs in isset\(\) is never defined\.$#' + identifier: isset.variable + count: 1 + path: src/Support/Xml/Commands/Command.php + + - + message: '#^Property YWatchman\\LaravelEPP\\Support\\Xml\\Commands\\Contact\\CreateCommand\:\:\$contact \(YWatchman\\LaravelEPP\\Models\\Contact\) does not accept YWatchman\\LaravelEPP\\Contracts\\IsContact\.$#' + identifier: assign.propertyType + count: 1 + path: src/Support/Xml/Commands/Contact/CreateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Contact\:\:\$handle\.$#' + identifier: property.notFound + count: 3 + path: src/Support/Xml/Commands/Domain/CreateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$sld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/CreateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$tld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/CreateCommand.php + + - + message: '#^Parameter \#2 \$value of method YWatchman\\LaravelEPP\\Support\\Xml\\XmlHelper\:\:createElement\(\) expects string\|null, int given\.$#' + identifier: argument.type + count: 4 + path: src/Support/Xml/Commands/Domain/CreateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$sld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/TransferCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$tld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/TransferCommand.php + + - + message: '#^Parameter \#2 \$value of method YWatchman\\LaravelEPP\\Support\\Xml\\XmlHelper\:\:createElement\(\) expects string\|null, int given\.$#' + identifier: argument.type + count: 4 + path: src/Support/Xml/Commands/Domain/TransferCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$sld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/UpdateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Models\\Domain\:\:\$tld\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/UpdateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Support\\Xml\\Commands\\Domain\\UpdateCommand\:\:\$admin\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/UpdateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Support\\Xml\\Commands\\Domain\\UpdateCommand\:\:\$registrant\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/UpdateCommand.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Support\\Xml\\Commands\\Domain\\UpdateCommand\:\:\$tech\.$#' + identifier: property.notFound + count: 1 + path: src/Support/Xml/Commands/Domain/UpdateCommand.php + + - + message: '#^Parameter \#2 \$value of method YWatchman\\LaravelEPP\\Support\\Xml\\XmlHelper\:\:createElement\(\) expects string\|null, int given\.$#' + identifier: argument.type + count: 3 + path: src/Support/Xml/Commands/Domain/UpdateCommand.php + + - + message: '#^PHPDoc tag @param has invalid value \(Nameserver\)\: Unexpected token "\\n \*", expected variable at offset 69 on line 4$#' + identifier: phpDoc.parseError + count: 1 + path: src/Support/Xml/Commands/Host/CreateCommand.php + + - + message: '#^Call to an undefined static method YWatchman\\LaravelEPP\\Exceptions\\EppException\:\:notImplemented\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: src/Support/Xml/Objects/Host/AuthObject.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$city\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$country\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$email\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$fax\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$handle\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$name\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$number\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$phone\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$postal\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$state\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$street\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php + + - + message: '#^Access to an undefined property YWatchman\\LaravelEPP\\Contracts\\Transformable\:\:\$suffix\.$#' + identifier: property.notFound + count: 1 + path: src/Transformers/ContactTransformer.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..be3f3fc --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,8 @@ +includes: + - phpstan-baseline.neon + +parameters: + paths: + - src + level: 5 + excludePaths: [] From 3ad8db795b27183b755ca43aad4b72925988bc60 Mon Sep 17 00:00:00 2001 From: Jacques Mbabazi Date: Tue, 12 May 2026 13:53:33 +0200 Subject: [PATCH 5/7] Fix mapLegalForm: PERSON must map to PERSOON not PERSON --- src/Support/Xml/Commands/Contact/CreateCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/Xml/Commands/Contact/CreateCommand.php b/src/Support/Xml/Commands/Contact/CreateCommand.php index e2e8225..615a4d2 100644 --- a/src/Support/Xml/Commands/Contact/CreateCommand.php +++ b/src/Support/Xml/Commands/Contact/CreateCommand.php @@ -86,7 +86,7 @@ protected function getExtensionNode(): DOMElement private function mapLegalForm(string $form): string { return match (strtoupper($form)) { - 'PERSON' => 'PERSON', + 'PERSON' => 'PERSOON', 'OTHER' => 'ANDERS', default => strtoupper($form), }; From eab8b91f193a5d0ad1613c9116eec0c71ca7fa43 Mon Sep 17 00:00:00 2001 From: Jacques Mbabazi Date: Tue, 12 May 2026 13:57:21 +0200 Subject: [PATCH 6/7] chore: fixing cod style --- CLAUDE.md | 79 ++++++++++++ OVERVIEW.md | 120 ++++++++++++++++++ .../Xml/Commands/Contact/UpdateCommand.php | 15 --- .../Xml/Commands/Session/HelloCommand.php | 1 - .../Xml/Commands/Session/LoginCommand.php | 1 - .../Xml/Commands/Session/LogoutCommand.php | 1 - .../Commands/Contact/CreateCommandTest.php | 6 +- 7 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 CLAUDE.md create mode 100644 OVERVIEW.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..62bfa1a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This is a Laravel package (`ywatchman/laravel-epp`) that provides an EPP (Extensible Provisioning Protocol) client for domain registration, primarily targeting SIDN (the Dutch domain registry for `.nl` domains). It is currently being rewritten to remove the `metaregistrar/php-epp-client` dependency. + +## Commands + +```bash +# Run all tests +./vendor/bin/pest + +# Run a single test file +./vendor/bin/pest tests/SomeTest.php + +# Lint / fix code style +./vendor/bin/pint + +# Install dependencies +composer install +``` + +## Architecture + +### Entry Point + +`Epp` (`src/Epp.php`) is the main class. It manages the raw TCP/SSL socket connection to an EPP server. It handles session lifecycle: `start()` opens the socket, `login()` sends `Hello` then `Login` commands, and `logout()` / `__destruct()` closes the session. `sendRequest($xml)` and `read()` are the low-level I/O methods — EPP frames are length-prefixed with a 4-byte big-endian integer. + +The `Facade` (`src/Facade.php`) and `ServiceProvider` (`src/ServiceProvider.php`) wire the package into Laravel under the `Epp` alias. Config is published from `config/epp.php`, which holds a `registrars` array keyed by registrar name (e.g. `sidn`). + +### XML Command Layer (`src/Support/Xml/Commands/`) + +All outbound EPP messages are built here using PHP's `DOMDocument`. The hierarchy is: + +- `XmlHelper` — wraps `DOMDocument`, provides `createElement` and `addNode` +- `Command extends XmlHelper` — adds the `` wrapper, `__toString()` serialises to the full `` XML string +- Concrete commands extend `Command`: + - `Session/` — `HelloCommand`, `LoginCommand`, `LogoutCommand` + - `Domain/` — `CheckCommand`, `CreateCommand`, `TransferCommand`, `UpdateCommand` + - `Contact/` — `CheckCommand`, `CreateCommand`, `UpdateCommand` + - `Host/` — `CheckCommand`, `CreateCommand` + +`__toString()` on a command produces the complete XML string ready to send. + +### Traits (`src/Support/Traits/Commands/`) + +Mixed into commands to add optional capabilities: + +- `HasDnssec` — adds DNSSEC key data to `secDNS:create` / `secDNS:update` extension nodes +- `HasExtensions` — generic extension support +- `HasScheduledDeletion` — SIDN-specific scheduled deletion extension +- `ProvidesCheckCommand` / `ProvidesContactCommand` — reusable check/contact node generation + +### Extensions (`src/Support/Xml/Extensions/`) + +Registry-specific EPP extensions. `Extension` is the base; `SidnExtension` sets the `sidn-ext-epp` prefix. `ContactExtension` handles SIDN-specific contact fields like `legalForm`. + +### Models & Transformers + +`Model` (`src/Models/Model.php`) is a lightweight value object with a `$columns` whitelist and magic `__get`/`__set` that store attributes in `$attributes`. `Domain`, `Contact`, and `Nameserver` extend it. + +`Transformer` (`src/Transformers/Transformer.php`) is an abstract base; `ContactTransformer` maps a `Contact` model to the nested array structure expected by EPP `contact:create` XML. The `HasAuthentication` trait appends `authInfo` with a placeholder password. + +### Response Parsing (`src/Responses/Response.php`) + +`Response` wraps raw XML from the server using Symfony's `DomCrawler`. It extracts the result `code`, `msg`, `svTRID`, and optional `clTRID`. A response is considered successful when `code === '1000'`. + +### Config + +`config/epp.php` supports multiple registrars under `registrars.*`. Each entry requires `username`, `password`, `hostname`, and `port` (default 700). Set `EPP_DEBUG=true` to dump raw XML to stdout during socket I/O. + +## Key Conventions + +- New XML commands go under `src/Support/Xml/Commands/{Resource}/` and extend `Command`. Implement `__toString()` to build the `` document and `get*Node()` helpers for the inner structure. +- Registry-specific behaviour belongs in extensions under `src/Support/Xml/Extensions/Sidn/` or traits under `src/Support/Traits/Commands/`. +- Response parsing subclasses go under `src/Responses/` extending `Response`, using the inherited `$this->crawler` (a Symfony `DomCrawler` instance) to extract resource-specific data. +- The package namespace is `YWatchman\LaravelEPP`, PSR-4 autoloaded from `src/`. diff --git a/OVERVIEW.md b/OVERVIEW.md new file mode 100644 index 0000000..943dd00 --- /dev/null +++ b/OVERVIEW.md @@ -0,0 +1,120 @@ +# Laravel-EPP Project Overview + +## What It Is + +A **Laravel package** that implements the **EPP (Extensible Provisioning Protocol)** — the standard protocol domain registrars use to manage domain names, contacts, and nameservers. It is specifically tailored for **SIDN**, the Dutch Internet registry managing `.nl` domains, with SIDN-specific extensions baked in. + +- **License**: MIT +- **PHP**: ^8.2 | **Laravel**: ^11.40 +- **Status**: Active rewrite (removing the old `metaregistrar/php-epp-client` dependency) +- **Packagist**: `ywatchman/laravel-epp` + +--- + +## Architecture + +``` +src/ +├── Epp.php ← Core: SSL socket connection + framing +├── Facade.php ← Laravel facade alias: Epp:: +├── ServiceProvider.php ← Publishes config +├── Contracts/ ← Interfaces: Transformable, IsContact +├── Exceptions/ ← EppException (typed error codes) +├── Models/ ← Plain value objects (no Eloquent) +│ ├── Domain ← sld, tld, name +│ ├── Contact ← All contact fields + legalForm (SIDN) +│ └── Nameserver ← name, address (IPv4/IPv6) +├── Transformers/ ← Converts models → EPP-structured arrays +│ └── ContactTransformer +├── Responses/ ← Parse raw XML responses (Symfony DomCrawler) +│ ├── Response ← Base: code, message, trID, succeeded +│ ├── Contact/{Check,Create}Response +│ ├── Domain/{Check,Create,Transfer,Update}Response +│ ├── Host/{Check,Create}Response +│ └── Registries/Sidn/Domain/{Create,Update}Response +└── Support/ + ├── Xml/ + │ ├── XmlHelper ← DOMDocument wrapper + │ ├── Commands/ ← Build outbound XML EPP commands + │ │ ├── Session: Hello, Login, Logout + │ │ ├── Contact: Check, Create, Update + │ │ ├── Domain: Check, Create, Update, Transfer + │ │ └── Host: Check, Create + │ ├── Extensions/Sidn/ ← SIDN-specific XML extension nodes + │ └── Objects/ ← Contact role objects (Admin, Tech, Billing) + └── Traits/Commands/ + ├── HasDnssec ← secDNS-1.1 extension support + ├── HasScheduledDeletion ← SIDN scheduled delete extension + ├── HasExtensions + ├── ProvidesCheckCommand + └── ProvidesContactCommand +``` + +--- + +## How the Connection Works + +`Epp.php` opens a **raw SSL socket** to the EPP server (default: port 700) and communicates using **EPP's length-prefixed framing** — a 4-byte big-endian header encoding the total packet length, followed by the XML body. The flow is: + +1. `start()` → opens socket, reads server greeting +2. `login()` → sends `` then `` with credentials +3. Send commands → `sendRequest($xml)` writes the framed packet, reads the response +4. `logout()` / destructor → sends ``, closes socket + +--- + +## Supported EPP Operations + +| Object | Operations | +|--------|-----------| +| Session | Hello, Login, Logout | +| Contact | Check, Create, Update | +| Domain | Check, Create, Update, Transfer | +| Host (Nameserver) | Check, Create | + +--- + +## SIDN-Specific Extensions + +The package hardcodes SIDN registry extensions: + +- **`sidn-ext-epp`** — contact `legalForm` / `legalFormNo` fields on contact create +- **`secDNS-1.1`** — DNSSEC key data (flags, protocol, algorithm, pubKey) on domain create/update +- **`sidn-ext-epp-scheduled-delete-1.0`** — scheduled domain deletion with operations: `setDate`, `setDateToEndOfSubscriptionPeriod`, `cancel` + +--- + +## Configuration (`config/epp.php`) + +```php +'registrars' => [ + 'sidn' => [ + 'username' => env('SIDN_USERNAME'), + 'password' => env('SIDN_PASSWORD'), + 'hostname' => env('SIDN_HOSTNAME'), // e.g. drs.domain-registry.nl + 'port' => env('SIDN_PORT', 700), + 'timeout' => env('SIDN_TIMEOUT', 30), + ], +], +'debug' => env('EPP_DEBUG', false), // dumps raw XML to stdout +``` + +Multiple registrars can be configured; the `Epp` constructor accepts a registrar key. + +--- + +## Notable Issues / Tech Debt + +1. **`read()` returns Exception objects instead of throwing** (`src/Epp.php:133-151`) — `return new Exception(...)` is silently ignored by callers instead of being thrown. +2. **No tests** — `pestphp/pest` and `orchestra/testbench` are in dev dependencies but no test directory exists. +3. **SIDN namespace hardcoded in base `Command::__toString()`** — `xmlns:sidn-ext-epp` is always emitted even for hypothetical non-SIDN registries. +4. **`guzzlehttp/guzzle`** is listed as a dependency but not visibly used anywhere in the current codebase. +5. **`clTRID` (client transaction ID)** is partially implemented — only `Domain\CreateCommand` uses it; the base `Command` class has a `// Todo: clTRID` comment. +6. **Debug output via `echo`** — debug mode prints raw XML to stdout rather than using Laravel's logger. +7. **`illuminate/database`** is required but no Eloquent models are used — the package uses its own plain `Model` value objects. + +--- + +## Current Uncommitted Changes + +The `develop` branch has modifications to `composer.json`, `Epp.php`, `Facade.php`, `ServiceProvider.php`, and `ContactTransformer.php` — consistent with the recent Laravel 11 upgrade commit in the history. diff --git a/src/Support/Xml/Commands/Contact/UpdateCommand.php b/src/Support/Xml/Commands/Contact/UpdateCommand.php index a568882..6331bd4 100644 --- a/src/Support/Xml/Commands/Contact/UpdateCommand.php +++ b/src/Support/Xml/Commands/Contact/UpdateCommand.php @@ -23,26 +23,15 @@ class UpdateCommand extends Command public const NAMESPACE = 'urn:ietf:params:xml:ns:contact-1.0'; - /** - * @var DOMElement - */ protected DOMElement $node; - /** - * @var Contact - */ protected Contact $contact; - /** - * @var string - */ protected string $registrar; /** * CreateCommand constructor. * - * @param Contact $contact - * @param string $registrar * * @throws EppException */ @@ -62,9 +51,6 @@ public function __construct(Contact $contact, string $registrar = Registrar::REG $n->appendChild($this->getExtensionNode()); } - /** - * @return DOMElement - */ protected function getUpdateNode(): DOMElement { $contact = new ContactTransformer($this->contact); @@ -77,7 +63,6 @@ protected function getUpdateNode(): DOMElement * Generate contact extension. * * - * @return DOMElement * * @throws EppException */ diff --git a/src/Support/Xml/Commands/Session/HelloCommand.php b/src/Support/Xml/Commands/Session/HelloCommand.php index 45ab643..1c32dbf 100644 --- a/src/Support/Xml/Commands/Session/HelloCommand.php +++ b/src/Support/Xml/Commands/Session/HelloCommand.php @@ -7,7 +7,6 @@ class HelloCommand extends Command { - /** @var DOMElement */ protected DOMElement $node; /** diff --git a/src/Support/Xml/Commands/Session/LoginCommand.php b/src/Support/Xml/Commands/Session/LoginCommand.php index 2b68e81..f521801 100644 --- a/src/Support/Xml/Commands/Session/LoginCommand.php +++ b/src/Support/Xml/Commands/Session/LoginCommand.php @@ -7,7 +7,6 @@ class LoginCommand extends Command { - /** @var DOMElement */ protected DOMElement $node; /** diff --git a/src/Support/Xml/Commands/Session/LogoutCommand.php b/src/Support/Xml/Commands/Session/LogoutCommand.php index 94bc13c..01d0e62 100644 --- a/src/Support/Xml/Commands/Session/LogoutCommand.php +++ b/src/Support/Xml/Commands/Session/LogoutCommand.php @@ -7,7 +7,6 @@ class LogoutCommand extends Command { - /** @var DOMElement */ protected DOMElement $node; /** diff --git a/tests/Unit/Commands/Contact/CreateCommandTest.php b/tests/Unit/Commands/Contact/CreateCommandTest.php index 9bac038..72c7fb4 100644 --- a/tests/Unit/Commands/Contact/CreateCommandTest.php +++ b/tests/Unit/Commands/Contact/CreateCommandTest.php @@ -1,12 +1,8 @@ 'NL', 'phone' => '+31.201234567', 'email' => 'test@example.nl', - 'legalForm' => 'PERSON', + 'legalForm' => 'PERSOON', ]); return new CreateCommand($contact); From 44e38ab7af9654b6c7c8a844a2c9881aac67a381 Mon Sep 17 00:00:00 2001 From: Jacques Mbabazi Date: Tue, 12 May 2026 13:59:14 +0200 Subject: [PATCH 7/7] chore: remove CLAUDE.md and OVERVIEW.md from version control These files are local development aids and should not be tracked in git. Added both to .gitignore so they remain on disk but are never pushed. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 2 + CLAUDE.md | 79 ---------------------------------- OVERVIEW.md | 120 ---------------------------------------------------- 3 files changed, 2 insertions(+), 199 deletions(-) delete mode 100644 CLAUDE.md delete mode 100644 OVERVIEW.md diff --git a/.gitignore b/.gitignore index 3eb6429..96947ee 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .php_cs.cache .idea/ /composer.lock +CLAUDE.md +OVERVIEW.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 62bfa1a..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,79 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Overview - -This is a Laravel package (`ywatchman/laravel-epp`) that provides an EPP (Extensible Provisioning Protocol) client for domain registration, primarily targeting SIDN (the Dutch domain registry for `.nl` domains). It is currently being rewritten to remove the `metaregistrar/php-epp-client` dependency. - -## Commands - -```bash -# Run all tests -./vendor/bin/pest - -# Run a single test file -./vendor/bin/pest tests/SomeTest.php - -# Lint / fix code style -./vendor/bin/pint - -# Install dependencies -composer install -``` - -## Architecture - -### Entry Point - -`Epp` (`src/Epp.php`) is the main class. It manages the raw TCP/SSL socket connection to an EPP server. It handles session lifecycle: `start()` opens the socket, `login()` sends `Hello` then `Login` commands, and `logout()` / `__destruct()` closes the session. `sendRequest($xml)` and `read()` are the low-level I/O methods — EPP frames are length-prefixed with a 4-byte big-endian integer. - -The `Facade` (`src/Facade.php`) and `ServiceProvider` (`src/ServiceProvider.php`) wire the package into Laravel under the `Epp` alias. Config is published from `config/epp.php`, which holds a `registrars` array keyed by registrar name (e.g. `sidn`). - -### XML Command Layer (`src/Support/Xml/Commands/`) - -All outbound EPP messages are built here using PHP's `DOMDocument`. The hierarchy is: - -- `XmlHelper` — wraps `DOMDocument`, provides `createElement` and `addNode` -- `Command extends XmlHelper` — adds the `` wrapper, `__toString()` serialises to the full `` XML string -- Concrete commands extend `Command`: - - `Session/` — `HelloCommand`, `LoginCommand`, `LogoutCommand` - - `Domain/` — `CheckCommand`, `CreateCommand`, `TransferCommand`, `UpdateCommand` - - `Contact/` — `CheckCommand`, `CreateCommand`, `UpdateCommand` - - `Host/` — `CheckCommand`, `CreateCommand` - -`__toString()` on a command produces the complete XML string ready to send. - -### Traits (`src/Support/Traits/Commands/`) - -Mixed into commands to add optional capabilities: - -- `HasDnssec` — adds DNSSEC key data to `secDNS:create` / `secDNS:update` extension nodes -- `HasExtensions` — generic extension support -- `HasScheduledDeletion` — SIDN-specific scheduled deletion extension -- `ProvidesCheckCommand` / `ProvidesContactCommand` — reusable check/contact node generation - -### Extensions (`src/Support/Xml/Extensions/`) - -Registry-specific EPP extensions. `Extension` is the base; `SidnExtension` sets the `sidn-ext-epp` prefix. `ContactExtension` handles SIDN-specific contact fields like `legalForm`. - -### Models & Transformers - -`Model` (`src/Models/Model.php`) is a lightweight value object with a `$columns` whitelist and magic `__get`/`__set` that store attributes in `$attributes`. `Domain`, `Contact`, and `Nameserver` extend it. - -`Transformer` (`src/Transformers/Transformer.php`) is an abstract base; `ContactTransformer` maps a `Contact` model to the nested array structure expected by EPP `contact:create` XML. The `HasAuthentication` trait appends `authInfo` with a placeholder password. - -### Response Parsing (`src/Responses/Response.php`) - -`Response` wraps raw XML from the server using Symfony's `DomCrawler`. It extracts the result `code`, `msg`, `svTRID`, and optional `clTRID`. A response is considered successful when `code === '1000'`. - -### Config - -`config/epp.php` supports multiple registrars under `registrars.*`. Each entry requires `username`, `password`, `hostname`, and `port` (default 700). Set `EPP_DEBUG=true` to dump raw XML to stdout during socket I/O. - -## Key Conventions - -- New XML commands go under `src/Support/Xml/Commands/{Resource}/` and extend `Command`. Implement `__toString()` to build the `` document and `get*Node()` helpers for the inner structure. -- Registry-specific behaviour belongs in extensions under `src/Support/Xml/Extensions/Sidn/` or traits under `src/Support/Traits/Commands/`. -- Response parsing subclasses go under `src/Responses/` extending `Response`, using the inherited `$this->crawler` (a Symfony `DomCrawler` instance) to extract resource-specific data. -- The package namespace is `YWatchman\LaravelEPP`, PSR-4 autoloaded from `src/`. diff --git a/OVERVIEW.md b/OVERVIEW.md deleted file mode 100644 index 943dd00..0000000 --- a/OVERVIEW.md +++ /dev/null @@ -1,120 +0,0 @@ -# Laravel-EPP Project Overview - -## What It Is - -A **Laravel package** that implements the **EPP (Extensible Provisioning Protocol)** — the standard protocol domain registrars use to manage domain names, contacts, and nameservers. It is specifically tailored for **SIDN**, the Dutch Internet registry managing `.nl` domains, with SIDN-specific extensions baked in. - -- **License**: MIT -- **PHP**: ^8.2 | **Laravel**: ^11.40 -- **Status**: Active rewrite (removing the old `metaregistrar/php-epp-client` dependency) -- **Packagist**: `ywatchman/laravel-epp` - ---- - -## Architecture - -``` -src/ -├── Epp.php ← Core: SSL socket connection + framing -├── Facade.php ← Laravel facade alias: Epp:: -├── ServiceProvider.php ← Publishes config -├── Contracts/ ← Interfaces: Transformable, IsContact -├── Exceptions/ ← EppException (typed error codes) -├── Models/ ← Plain value objects (no Eloquent) -│ ├── Domain ← sld, tld, name -│ ├── Contact ← All contact fields + legalForm (SIDN) -│ └── Nameserver ← name, address (IPv4/IPv6) -├── Transformers/ ← Converts models → EPP-structured arrays -│ └── ContactTransformer -├── Responses/ ← Parse raw XML responses (Symfony DomCrawler) -│ ├── Response ← Base: code, message, trID, succeeded -│ ├── Contact/{Check,Create}Response -│ ├── Domain/{Check,Create,Transfer,Update}Response -│ ├── Host/{Check,Create}Response -│ └── Registries/Sidn/Domain/{Create,Update}Response -└── Support/ - ├── Xml/ - │ ├── XmlHelper ← DOMDocument wrapper - │ ├── Commands/ ← Build outbound XML EPP commands - │ │ ├── Session: Hello, Login, Logout - │ │ ├── Contact: Check, Create, Update - │ │ ├── Domain: Check, Create, Update, Transfer - │ │ └── Host: Check, Create - │ ├── Extensions/Sidn/ ← SIDN-specific XML extension nodes - │ └── Objects/ ← Contact role objects (Admin, Tech, Billing) - └── Traits/Commands/ - ├── HasDnssec ← secDNS-1.1 extension support - ├── HasScheduledDeletion ← SIDN scheduled delete extension - ├── HasExtensions - ├── ProvidesCheckCommand - └── ProvidesContactCommand -``` - ---- - -## How the Connection Works - -`Epp.php` opens a **raw SSL socket** to the EPP server (default: port 700) and communicates using **EPP's length-prefixed framing** — a 4-byte big-endian header encoding the total packet length, followed by the XML body. The flow is: - -1. `start()` → opens socket, reads server greeting -2. `login()` → sends `` then `` with credentials -3. Send commands → `sendRequest($xml)` writes the framed packet, reads the response -4. `logout()` / destructor → sends ``, closes socket - ---- - -## Supported EPP Operations - -| Object | Operations | -|--------|-----------| -| Session | Hello, Login, Logout | -| Contact | Check, Create, Update | -| Domain | Check, Create, Update, Transfer | -| Host (Nameserver) | Check, Create | - ---- - -## SIDN-Specific Extensions - -The package hardcodes SIDN registry extensions: - -- **`sidn-ext-epp`** — contact `legalForm` / `legalFormNo` fields on contact create -- **`secDNS-1.1`** — DNSSEC key data (flags, protocol, algorithm, pubKey) on domain create/update -- **`sidn-ext-epp-scheduled-delete-1.0`** — scheduled domain deletion with operations: `setDate`, `setDateToEndOfSubscriptionPeriod`, `cancel` - ---- - -## Configuration (`config/epp.php`) - -```php -'registrars' => [ - 'sidn' => [ - 'username' => env('SIDN_USERNAME'), - 'password' => env('SIDN_PASSWORD'), - 'hostname' => env('SIDN_HOSTNAME'), // e.g. drs.domain-registry.nl - 'port' => env('SIDN_PORT', 700), - 'timeout' => env('SIDN_TIMEOUT', 30), - ], -], -'debug' => env('EPP_DEBUG', false), // dumps raw XML to stdout -``` - -Multiple registrars can be configured; the `Epp` constructor accepts a registrar key. - ---- - -## Notable Issues / Tech Debt - -1. **`read()` returns Exception objects instead of throwing** (`src/Epp.php:133-151`) — `return new Exception(...)` is silently ignored by callers instead of being thrown. -2. **No tests** — `pestphp/pest` and `orchestra/testbench` are in dev dependencies but no test directory exists. -3. **SIDN namespace hardcoded in base `Command::__toString()`** — `xmlns:sidn-ext-epp` is always emitted even for hypothetical non-SIDN registries. -4. **`guzzlehttp/guzzle`** is listed as a dependency but not visibly used anywhere in the current codebase. -5. **`clTRID` (client transaction ID)** is partially implemented — only `Domain\CreateCommand` uses it; the base `Command` class has a `// Todo: clTRID` comment. -6. **Debug output via `echo`** — debug mode prints raw XML to stdout rather than using Laravel's logger. -7. **`illuminate/database`** is required but no Eloquent models are used — the package uses its own plain `Model` value objects. - ---- - -## Current Uncommitted Changes - -The `develop` branch has modifications to `composer.json`, `Epp.php`, `Facade.php`, `ServiceProvider.php`, and `ContactTransformer.php` — consistent with the recent Laravel 11 upgrade commit in the history.