Refactor existing code into classes

This commit is contained in:
Ramon Caballero 2026-04-05 17:37:57 +01:00
parent a24906280f
commit ef51985bf0
11 changed files with 218 additions and 201 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Config files:
/config/*.json
!/config/pb-dydns.example.json

View File

@ -1,144 +1,13 @@
#!/usr/bin/env php
<?php <?php
// require __DIR__ . "/PorkbunAPI.php";
// This script updates the DNS type A on porkbun.com so it is always pointing to your public IP address. require __DIR__ . "/Config.php";
// require __DIR__ . "/Logger.php";
// Dependencies: require __DIR__ . "/UpdateDnsCommand.php";
//
// - PorkbunAPI.php
// - pb-dydns.json (it will be created automatically the 1st time)
//
// How to use it:
//
// - from command line:
//
// $ php /path/to/pb-dydns.php domain_name
//
// It will retrieve all DNS records for the specificied domain_name and update the A type ones
// with your public IP address.
//
//- as a cron job, to add it to:
//
// $ crontab -e
//
// Append this line as many times as domains you want to automatically update:
//
// */10 * * * * php /path/to/pb-dydns.php domain_name > /dev/null
//
// And restart cron (I'm not sure if this is necessary):
//
// $ sudo systemctl restart cron
//
// Entries to a log file called "pb-dydns.log" will be added, to view it you can:
//
// $ cat /path/to/pb-dydns.log
//
require "PorkbunAPI.php"; $config = new Config(__DIR__ . "/pb-dydns.json");
$logger = new Logger(__DIR__ . "/pb-dydns.log");
// Make sure that pb-dydns.json exists: $cmd = new UpdateDnsCommand($config, $logger);
$config_filename = __DIR__ . "/pb-dydns.json"; $cmd->run($argv);
if (!file_exists($config_filename))
{
echo "The config file $config_filename does not exist." . PHP_EOL;
$config = json_encode(array
(
"url" => "https://porkbun.com/api/json/v3/",
"apikey" => "",
"secretapikey" => ""
), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents($config_filename, $config);
echo "It has been created but you must edit it with your API key and secret." . PHP_EOL;
echo "After that you can try again." . PHP_EOL;
exit(1);
}
// Domain name must be provided as argument.
// There should be 2 arguments as $argc includes the script name itself:
if ($argc != 2)
{
echo_to_cli("This script requires a domain name as argument:" . PHP_EOL);
echo_to_cli("php $argv[0] domain_name" . PHP_EOL);
exit(2);
}
// Take the arguments in individual variables:
$myDomain = $argv[1];
// Create an instance of PorkbunAPI:
$pbapi = new PorkbunAPI($config_filename);
// Test connection to Porkbun API in order to get the public IP:
$raw = $pbapi->ping();
$result = json_decode($raw);
if ($result === null)
{
echo_to_cli("Invalid JSON returned by Porkbun API:\n$raw\n");
exit(3);
}
if (!isset($result->status))
{
echo_to_cli("Porkbun API response missing 'status' field:\n$raw\n");
exit(3);
}
if ($result->status !== "SUCCESS")
{
echo_to_cli("Porkbun API returned an error:\n$raw\n");
exit(3);
}
if (!isset($result->yourIp))
{
echo_to_cli("Porkbun API did not return your public IP.\n");
exit(3);
}
$myIp = $result->yourIp;
echo_to_cli("Your public IP address is $myIp" . PHP_EOL);
// Retrieve all DNS records associated with user's domain:
$records = $pbapi->retrieve($myDomain);
// Discard those records that are not type "A":
$data = json_decode($records);
$filteredRecords = array_filter($data->records, function ($record)
{
return $record->type == "A";
});
$log_filename = __DIR__ . "/pb-dydns.log";
// Update the records that passed the filter with the public IP:
foreach ($filteredRecords as $record)
{
echo_to_cli("Porkbun's DNS for $record->name is pointing to $record->content... ");
if ($record->content != $myIp)
{
echo_to_cli("Let's change that... ");
$name = rtrim(strstr($record->name, $myDomain, true), ".");
$result = json_decode($pbapi->edit($myDomain, $record->id, $myIp, $name));
if ($result->status == "SUCCESS") echo_to_cli("Done!" . PHP_EOL);
$now = date('Y-m-d H:i:s');
$message = "$now: Updated DNS on $record->name from $record->content to $myIp" . PHP_EOL;
file_put_contents($log_filename, $message, FILE_APPEND);
}
else
{
echo_to_cli("Nothing needs to be changed!" . PHP_EOL);
}
}
?>

View File

@ -0,0 +1,5 @@
{
"url": "https://api.porkbun.com/api/json/v3/",
"apikey": "YOUR_API_KEY_HERE",
"secretapikey": "YOUR_SECRET_KEY_HERE"
}

View File

@ -1,29 +1,16 @@
<?php <?php
//
// Outputs a message to CLI, if this script is run from CLI.
//
function echo_to_cli(string $message)
{
if (php_sapi_name() === 'cli') echo $message;
}
// //
// Wrapper class to communicate with Porkbun API. // Wrapper class to communicate with Porkbun API.
// //
class PorkbunAPI class PorkbunAPI
{ {
private stdClass $config;
function __construct(string $config_filename) function __construct(string $config_filename)
{ {
$this->config = json_decode(file_get_contents($config_filename)); $this->config = json_decode(file_get_contents($config_filename));
$this->ch = curl_init();
}
function __destruct()
{
curl_close($this->ch);
} }
// //
@ -37,15 +24,17 @@ class PorkbunAPI
// Create the correct endpoint based on the URL: // Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "ping"; $endpoint = $this->config->url . "ping";
$ch = curl_init();
// Set some cURL options: // Set some cURL options:
curl_setopt($this->ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data: // Tell the server that the request body contains JSON data:
$headers = array(); $headers = array();
$headers[] = "Content-Type: application/json"; $headers[] = "Content-Type: application/json";
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants to be passed data in JSON format: // Porkbun API wants to be passed data in JSON format:
$json = array $json = array
@ -54,13 +43,13 @@ class PorkbunAPI
"secretapikey" => $this->config->secretapikey "secretapikey" => $this->config->secretapikey
); );
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result: // Execute cURL and return the result:
$result = curl_exec($this->ch); $result = curl_exec($ch);
if (curl_errno($this->ch)) if (curl_errno($ch))
{ {
echo_to_cli("Error: " . curl_error($this->ch)); Console::echo("Error: " . curl_error($ch));
exit; exit;
} }
@ -78,15 +67,17 @@ class PorkbunAPI
// Create the correct endpoint based on the URL: // Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "dns/create/" . $domain; $endpoint = $this->config->url . "dns/create/" . $domain;
$ch = curl_init();
// Set some cURL options: // Set some cURL options:
curl_setopt($this->ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data: // Tell the server that the request body contains JSON data:
$headers = array(); $headers = array();
$headers[] = "Content-Type: application/json"; $headers[] = "Content-Type: application/json";
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants to be passed data in JSON format: // Porkbun API wants to be passed data in JSON format:
$json = array $json = array
@ -99,13 +90,13 @@ class PorkbunAPI
"ttl" => "$ttl" "ttl" => "$ttl"
); );
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result: // Execute cURL and return the result:
$result = curl_exec($this->ch); $result = curl_exec($ch);
if (curl_errno($this->ch)) if (curl_errno($ch))
{ {
echo_to_cli("Error: " . curl_error($this->ch)); Console::echo("Error: " . curl_error($ch));
exit; exit;
} }
@ -124,15 +115,17 @@ class PorkbunAPI
$endpoint = $this->config->url . "dns/edit/" . $domain; $endpoint = $this->config->url . "dns/edit/" . $domain;
$endpoint .= "/" . $id; $endpoint .= "/" . $id;
$ch = curl_init();
// Set some cURL options: // Set some cURL options:
curl_setopt($this->ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data: // Tell the server that the request body contains JSON data:
$headers = array(); $headers = array();
$headers[] = "Content-Type: application/json"; $headers[] = "Content-Type: application/json";
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants the API key and secret in a JSON structure: // Porkbun API wants the API key and secret in a JSON structure:
$json = array $json = array
@ -144,13 +137,13 @@ class PorkbunAPI
"content" => "$content" "content" => "$content"
); );
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result: // Execute cURL and return the result:
$result = curl_exec($this->ch); $result = curl_exec($ch);
if (curl_errno($this->ch)) if (curl_errno($ch))
{ {
echo_to_cli("Error: " . curl_error($this->ch)); Console::echo("Error: " . curl_error($ch));
exit; exit;
} }
@ -163,21 +156,23 @@ class PorkbunAPI
// https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID // https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain%20or%20ID
// //
function retrieve(string $domain, string $id = null) function retrieve(string $domain, ?string $id = null)
{ {
// Create the correct endpoint based on the URL: // Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "dns/retrieve/" . $domain; $endpoint = $this->config->url . "dns/retrieve/" . $domain;
if (!is_null($id)) $endpoint .= "/" . $id; if (!is_null($id)) $endpoint .= "/" . $id;
$ch = curl_init();
// Set some cURL options: // Set some cURL options:
curl_setopt($this->ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data: // Tell the server that the request body contains JSON data:
$headers = array(); $headers = array();
$headers[] = "Content-Type: application/json"; $headers[] = "Content-Type: application/json";
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants the API key and secret in a JSON structure: // Porkbun API wants the API key and secret in a JSON structure:
$json = array $json = array
@ -186,13 +181,13 @@ class PorkbunAPI
"secretapikey" => $this->config->secretapikey "secretapikey" => $this->config->secretapikey
); );
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result: // Execute cURL and return the result:
$result = curl_exec($this->ch); $result = curl_exec($ch);
if (curl_errno($this->ch)) if (curl_errno($ch))
{ {
echo_to_cli("Error: " . curl_error($this->ch)); Console::echo("Error: " . curl_error($ch));
exit; exit;
} }
@ -205,21 +200,23 @@ class PorkbunAPI
// https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type // https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type
// //
function retrieveByNameType(string $domain, string $type, string $subdomain = null) function retrieveByNameType(string $domain, string $type, ?string $subdomain = null)
{ {
// Create the correct endpoint based on the URL: // Create the correct endpoint based on the URL:
$endpoint = $this->config->url . "dns/retrieveByNameType/" . $domain . "/" . $type; $endpoint = $this->config->url . "dns/retrieveByNameType/" . $domain . "/" . $type;
if (!is_null($subdomain)) $endpoint .= "/" . $subdomain; if (!is_null($subdomain)) $endpoint .= "/" . $subdomain;
$ch = curl_init();
// Set some cURL options: // Set some cURL options:
curl_setopt($this->ch, CURLOPT_URL, $endpoint); curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POST, 1);
// Tell the server that the request body contains JSON data: // Tell the server that the request body contains JSON data:
$headers = array(); $headers = array();
$headers[] = "Content-Type: application/json"; $headers[] = "Content-Type: application/json";
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
// Porkbun API wants the API key and secret in a JSON structure: // Porkbun API wants the API key and secret in a JSON structure:
$json = array $json = array
@ -228,22 +225,18 @@ class PorkbunAPI
"secretapikey" => $this->config->secretapikey "secretapikey" => $this->config->secretapikey
); );
curl_setopt($this->ch, CURLOPT_POSTFIELDS, json_encode($json)); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($json));
// Execute cURL and return the result: // Execute cURL and return the result:
$result = curl_exec($this->ch); $result = curl_exec($ch);
if (curl_errno($this->ch)) if (curl_errno($ch))
{ {
echo_to_cli("Error: " . curl_error($this->ch)); Console::echo("Error: " . curl_error($ch));
exit; exit;
} }
return $result; return $result;
} }
// These properties don't need to be declared as they would be dynamically created when assigned a value:
private stdClass $config;
private CurlHandle $ch;
}; };
?> ?>

View File

View File

@ -0,0 +1,94 @@
<?php
require_once __DIR__ . '/../Api/PorkbunAPI.php';
require_once __DIR__ . '/../Config/Config.php';
require_once __DIR__ . '/../Util/Logger.php';
require_once __DIR__ . '/../Util/Console.php';
class UpdateDnsCommand
{
private Config $config;
private Logger $logger;
public function __construct(Config $config, Logger $logger)
{
$this->config = $config;
$this->logger = $logger;
}
public function run(array $argv): void
{
if (count($argv) < 2)
{
echo "Usage: pb-dydns.php <domain>\n";
exit(1);
}
$domain = $argv[1];
$api = new PorkbunAPI($this->config->get('config_filename'));
// Test connection to Porkbun API in order to get the public IP:
$raw = $api->ping();
$result = json_decode($raw);
if ($result === null)
{
Console::echo("Invalid JSON returned by Porkbun API:\n$raw\n");
exit(3);
}
if (!isset($result->status))
{
Console::echo("Porkbun API response missing 'status' field:\n$raw\n");
exit(3);
}
if ($result->status !== "SUCCESS")
{
Console::echo("Porkbun API returned an error:\n$raw\n");
exit(3);
}
if (!isset($result->yourIp))
{
Console::echo("Porkbun API did not return your public IP.\n");
exit(3);
}
$myIp = $result->yourIp;
Console::echo("Your public IP address is $myIp" . PHP_EOL);
// Retrieve all DNS records associated with user's domain:
$records = $api->retrieve($domain);
// Discard those records that are not type "A":
$data = json_decode($records);
$filteredRecords = array_filter($data->records, function ($record)
{
return $record->type == "A";
});
// Update the records that passed the filter with the public IP:
foreach ($filteredRecords as $record)
{
Console::echo("Porkbun's DNS for $record->name is pointing to $record->content... ");
if ($record->content != $myIp)
{
Console::echo("Let's change that... ");
$name = rtrim(strstr($record->name, $domain, true), ".");
$result = json_decode($api->edit($domain, $record->id, $myIp, $name));
if ($result->status == "SUCCESS") Console::echo("Done!" . PHP_EOL);
$this->logger->log("Updated DNS on $record->name from $record->content to $myIp");
}
else
{
Console::echo("Nothing needs to be changed!" . PHP_EOL);
}
}
}
}

View File

27
src/Config/Config.php Normal file
View File

@ -0,0 +1,27 @@
<?php
class Config
{
private array $data;
public function __construct(string $filename)
{
if (!file_exists($filename))
{
throw new RuntimeException("Config file not found: $filename");
}
$json = file_get_contents($filename);
$this->data = json_decode($json, true);
if ($this->data === null)
{
throw new RuntimeException("Invalid JSON in config file: $filename");
}
}
public function get(string $key): mixed
{
return $this->data[$key] ?? null;
}
}

View File

9
src/Util/Console.php Normal file
View File

@ -0,0 +1,9 @@
<?php
class Console
{
public static function echo(string $message): void
{
fwrite(STDOUT, $message . PHP_EOL);
}
}

17
src/Util/Logger.php Normal file
View File

@ -0,0 +1,17 @@
<?php
class Logger
{
private string $filename;
public function __construct(string $filename)
{
$this->filename = $filename;
}
public function log(string $message): void
{
$timestamp = date("Y-m-d H:i:s");
file_put_contents($this->filename, $timestamp . ' ' . $message . PHP_EOL, FILE_APPEND);
}
}