Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Alidns 20150109 Laravel Package

alibabacloud/alidns-20150109

Official Alibaba Cloud Alidns (DNS) SDK for PHP, generated from OpenAPI for the 2015-01-09 API. Manage domains, DNS records, and related Alibaba Cloud DNS operations from PHP applications via Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install

composer require alibabacloud/alidns-20150109

This pulls in the Tea/Darabonja runtime (alibabacloud/darabonja, alibabacloud/tea-utils, alibabacloud/tea). There is no Laravel service provider and no facade — you wire it yourself.

2. Credentials + config

Use a dedicated RAM user for DNS (never your account-level AK), and put it in config/services.php:

// config/services.php
'alidns' => [
    'key'      => env('ALIDNS_ACCESS_KEY_ID'),
    'secret'   => env('ALIDNS_ACCESS_KEY_SECRET'),
    'endpoint' => env('ALIDNS_ENDPOINT', 'alidns.cn-hangzhou.aliyuncs.com'),
],

Minimal RAM policy (AliyunDNSFullAccess is too broad for a tenant-subdomain feature):

{
  "Version": "1",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["alidns:Describe*", "alidns:AddDomainRecord", "alidns:UpdateDomainRecord", "alidns:DeleteDomainRecord", "alidns:SetDomainRecordStatus"],
    "Resource": "acs:alidns:*:*:domain/example.com"
  }]
}

Note: the ATI agent-registration actions added in 4.6.1 (CreateAtiAgentRegisterInfo*, DescribeAtiAgentRegisterInfo*, VerifyAtiAgentDnsRecords, …) are not covered by alidns:Describe*/alidns:*DomainRecord*. If you adopt that surface, the RAM policy above will silently 403 you — add the specific alidns:CreateAtiAgentRegisterInfo, alidns:DescribeAtiAgentRegisterInfo, alidns:VerifyAtiAgentDnsRecords actions explicitly.

3. Bind a single client in the container

// app/Providers/AppServiceProvider.php
use AlibabaCloud\SDK\Alidns\V20150109\Alidns;
use Darabonja\OpenApi\Models\Config;

$this->app->singleton(Alidns::class, fn () => new Alidns(new Config([
    'accessKeyId'     => config('services.alidns.key'),
    'accessKeySecret' => config('services.alidns.secret'),
    'endpoint'        => config('services.alidns.endpoint'),
])));

4. First call — a smoke test that lists the zones you can see

use AlibabaCloud\SDK\Alidns\V20150109\Models\DescribeDomainsRequest;

$response = app(Alidns::class)->describeDomains(
    new DescribeDomainsRequest(['pageSize' => 20])
);

foreach ($response->body->domains->domain as $domain) {
    dump($domain->domainName, $domain->recordCount);
}

Where to look first in the source

  • src/Alidns.php — every API is a public method; each has a xxx() and xxxWithOptions() variant. This is the real table of contents.
  • src/Models/ — ~250+ generated request/response classes. The request class constructor param array is the API documentation: property names there are the exact wire parameter names.
  • New in 4.6.1: a cluster of *AtiAgentRegisterInfo* / VerifyAtiAgentDnsRecords methods and models. grep -n "AtiAgent" src/Alidns.php is the fastest way to see the whole agent-registration surface (agent sub-hosts, domain mode, trust level, managed ACME challenge records). It's effectively a second product bundled into the same client.
  • Ignore src/Models/*ResponseBody* class trees until you need them; dump $response->body->toMap() first and read the shape from real data.

Best first use case: a scheduled DDNS updater or an internal "provision a subdomain for a new tenant" action. Both are 30 lines and immediately teach you the request → response → error-code loop.

Implementation Patterns

Wrap it behind your own interface (non-negotiable for testing)

The SDK makes real HTTP calls and its response objects are deep generated graphs. Never let Alidns leak into controllers or domain code.

// app/Services/Dns/DnsProvider.php
interface DnsProvider
{
    public function records(string $zone, array $filters = []): array;
    public function upsert(string $zone, string $rr, string $type, string $value, int $ttl = 600): string;
    public function delete(string $recordId): void;
}
// app/Services/Dns/AlidnsProvider.php
class AlidnsProvider implements DnsProvider
{
    public function __construct(private Alidns $client) {}

    public function records(string $zone, array $filters = []): array
    {
        $out = [];
        $page = 1;

        do {
            $res = $this->client->describeDomainRecords(new DescribeDomainRecordsRequest([
                'domainName' => $zone,
                'pageNumber' => $page,
                'pageSize'   => 500,
                ...$filters, // RRKeyWord, type, line, status ...
            ]));

            foreach ($res->body->domainRecords->record ?? [] as $r) {
                $out[] = [
                    'id'    => $r->recordId,
                    'rr'    => $r->RR,
                    'type'  => $r->type,
                    'value' => $r->value,
                    'ttl'   => $r->TTL,
                    'line'  => $r->line,
                    'status'=> $r->status,
                ];
            }
        } while (++$page <= ceil(($res->body->totalCount ?? 0) / 500));

        return $out;
    }
}

The mapped array is what you cache, log, and assert on in tests. Bind it in the provider: $this->app->bind(DnsProvider::class, AlidnsProvider::class);

Upsert, not create-or-fail

AddDomainRecord throws DomainRecordDuplicate when RR+type+value already match. For a "make sure this record exists" flow (ACME challenges, tenant subdomains, DDNS), do read-then-decide:

public function upsert(string $zone, string $rr, string $type, string $value, int $ttl = 600): string
{
    $existing = collect($this->records($zone, ['RRKeyWord' => $rr, 'type' => $type]));

    if ($hit = $existing->firstWhere('value', $value)) {
        // TTL drift is the common case — Update requires ALL fields, incl. value
        if ((int) $hit['ttl'] !== $ttl) {
            $this->client->updateDomainRecord(new UpdateDomainRecordRequest([
                'recordId' => $hit['id'],
                'RR'       => $rr,
                'type'     => $type,
                'value'    => $value,
                'TTL'      => $ttl,
            ]));
        }
        return $hit['id'];
    }

    $res = $this->client->addDomainRecord(new AddDomainRecordRequest([
        'domainName' => $zone,
        'RR'         => $rr,
        'type'       => $type,
        'value'      => $value,
        'TTL'        => $ttl,
    ]));

    return $res->body->recordId;
}

DDNS on a schedule

// app/Console/Commands/UpdateDynamicDns.php
protected $signature = 'dns:ddns {zone} {rr=home}';

public function handle(DnsProvider $dns): int
{
    $ip = trim(Http::get('https://api.ipify.org')->body());
    if (! filter_var($ip, FILTER_VALIDATE_IP)) {
        return self::FAILURE;
    }

    $dns->upsert($this->argument('zone'), $this->argument('rr'), 'A', $ip, 600);
    $this->info("{$this->argument('rr')} → {$ip}");

    return self::SUCCESS;
}

Register $schedule->command(...)->everyFiveMinutes()->withoutOverlapping();

DNS-01 ACME / certificate issuance (hand-rolled)

$token = Str::random(43);
$id = $dns->upsert($zone, '_acme-challenge', 'TXT', $token, 60);

try {
    $this->acme->validate($domain, $token); // shell out to acme.sh / lego
} finally {
    // Do NOT delete immediately; some CAs re-query. Delay via a queued job.
    DeleteAcmeChallenge::dispatch($id)->delay(now()->addMinutes(10));
}

Create the TXT record with a low TTL before you need it if you can — a 600s TTL slows every renewal.

The managed ACME / agent-registration path (4.6.1+)

4.6.1 changed CreateAtiAgentRegisterInfoAcmeChallengeRecord to return Body.Records, and added VerifyAtiAgentDnsRecords. That means the challenge-record lifecycle can now be driven by the API instead of you guessing RR names and TXT values:

use AlibabaCloud\SDK\Alidns\V20150109\Models\CreateAtiAgentRegisterInfoAcmeChallengeRecordRequest;
use AlibabaCloud\SDK\Alidns\V20150109\Models\VerifyAtiAgentDnsRecordsRequest;

$created = $client->createAtiAgentRegisterInfoAcmeChallengeRecord(
    new CreateAtiAgentRegisterInfoAcmeChallengeRecordRequest([
        'DomainName' => 'example.com',
        // remaining params: read the constructor docblock — mixed-case, verbatim
    ])
);

// 4.6.1: publish these, then verify. Guard: Records is absent on older payloads.
foreach ($created->body->records ?? [] as $record) {
    Log::info('acme record', ['rr' => $record->RR ?? null, 'value' => $record->value ?? null]);
}

// After DNS has propagated:
$client->verifyAtiAgentDnsRecords(new VerifyAtiAgentDnsRecordsRequest([
    'TrustLevel' => $level,
]));

Practical stance: keep your hand-rolled _acme-challenge upsert as the default for cert-manager / acme.sh workflows (it's provider-agnostic and you already test it behind the interface). Reach for the ATI flow when you're specifically registering an agent (a host/sub-domain under AgentSubHost) and want Alibaba to own the challenge-record bookkeeping and the trust assessment. Wrap it as a second interface (AgentRegistrar) rather than bolting it onto DnsProvider — it has its own state machine (DomainMode, TrustLevel) that DNS records don't share.

interface AgentRegistrar
{
    public function register(string $zone, string $agentSubHost, string $domainMode): string;
    /** @return array{trustLevel: string, domainMode: string, agentSubHost: string} */
    public function status(string $domain): array;
    public function verify(string $domain): void;
}

Per-call runtime options (timeouts, retries)

The default read timeout is generous enough to wedge a queue worker. Use the WithOptions variant for anything user-facing:

use Darabonja\OpenApi\Models\RuntimeOptions;

$runtime = new RuntimeOptions([
    'autoretry'      => true,
    'maxAttempts'    => 3,
    'connectTimeout' => 3000,
    'readTimeout'    => 8000,
]);

$res = $client->describeDomainRecordsWithOptions($request, $runtime);

Queue the writes, cache the reads

// reads — records change rarely relative to request volume
$records = Cache::remember("dns:{$zone}", 300, fn () => $dns->records($zone));

// writes — always in a job, and bust the tag/keys after
Cache::forget("dns:{$zone}");

For multi-tenant subdomains, cache the existence check for the subdomain (a Cache::has / Redis key) rather than round-tripping DescribeDomainRecords on every registration. Same rule applies to DescribeAtiAgentRegisterInfo — it's a per-domain config read, not something to call in a loop over tenants; use ListAtiAgentRegisterInfos (which now returns TrustLevel per entry) for that.

Bulk operations

OperateBatchDomain handles up to 1000 add/delete ops in one call — use it for migrations and bulk imports instead of looping AddDomainRecord (you will hit Throttling.User well before 1000 individual calls). For large zone imports, chunk in 500s, submit, then poll DescribeBatchResultCount until status is DONE.

Test double

// tests/Fakes/FakeDnsProvider.php
class FakeDnsProvider implements DnsProvider
{
    public array $records = [];
    public function upsert(...): string { /* pure array logic */ }
}

Bind it in TestCase::setUp(). Because the SDK talks to a live endpoint with signed requests, there is no useful way to Http::fake() it — faking at the interface is the only sane path.

Gotchas and Tips

Property names are mixed-case and unforgiving. DescribeDomainRecordsRequest takes domainName (lowercase d) but RRKeyWord, RRKeyWord and type (lowercase). PHP arrays are case-sensitive: 'DomainName' => ... is silently ignored and you get a validation error from the API with no hint about which key was wrong. Copy parameter names verbatim from the constructor docblock in src/Models/. The 4.6.1 additions (AgentSubHost, DomainMode, TrustLevel) are camel-case on the wire — don't "fix" them to AgentSubhost.

UpdateDomainRecord requires RR, type, and value — not just the field you want to change. Omitting value to "only bump the TTL" either errors or, worse, blanks the record on some API paths. Always fetch the current record and resend all fields.

Errors are TeaError, and the code is a string. $e->getCode() returns 'DomainRecordDuplicate', 'Throttling.User', 'InvalidDomainName.NoExist', 'DomainRecordNotBelongToUser' — not an HTTP status. Branch on the string, and treat Throttling.* as retryable while Invalid*/DomainRecordDuplicate are not.

use AlibabaCloud\Tea\Exception\TeaError;

try {
    $client->addDomainRecord($request);
} catch (TeaError $e) {
    if (str_starts_with($e->getCode(), 'Throttling')) {
        $this->release(30); // retry the job
        return;
    }
    throw $e;
}

$response->body can be null on some non-2xx paths even when no exception is thrown. Guard with ?-> or check $response->statusCode before traversing ->domainRecords->record.

Empty collections are null, not []. $body->domainRecords->record is null when a zone has no matching records. Always ?? []. This now applies to Body.Records on the ACME-challenge response too — the field only exists from 4.6.1 onward, so a cached or replayed older payload will ?? [] right past the data you wanted.

TrustLevel and DomainMode are two-way words. TrustLevel is a request parameter on DescribeAtiAgentRegisterInfoMarket and VerifyAtiAgentDnsRecords, but a response field on DescribeAtiAgentRegisterInfo. Don't read a value out of one call and feed it back into another without checking the accepted enum — the API validates and gives you a generic InvalidParameter that doesn't say which field.

Verification and registration are asynchronous. VerifyAtiAgentDnsRecords and CreateAtiAgentRegisterInfo* return an accepted state, not a settled one. Poll DescribeAtiAgentRegisterInfo for DomainMode / TrustLevel to stabilise; don't treat the create response as "ready". This is the same discipline as DNS propagation, just one layer up.

Pagination caps at 500 per page for DescribeDomainRecords (default 100). Loop on totalCount, not on "did I get less than pageSize" — records can be added mid-iteration.

@ records and empty RR. The apex record is RR => '@'. Sending '' gets you InvalidRR. Same for wildcards: '*' is valid but only one level deep (*.example.com, not *.*).

Internationalized domains must be punycode. AddDomainRecord for 例子.com fails; run idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46) first and store the ASCII form.

TTL floors. Free-plan zones reject or silently round anything below 600s; paid tiers allow 60s. Don't hardcode 60 in code that must also work on a free zone — read MinTtl from DescribeDomainInfo once and clamp.

Use SetDomainRecordStatus instead of DeleteDomainRecord for anything reversible. Disabling is instant, free of propagation drama, and keeps the record ID stable — much better for feature-flag-driven subdomains.

Two SDKs exist; don't mix them. alibabacloud/alidns-20150109 gives you AlibabaCloud\SDK\Alidns\V20150109\Alidns. The legacy monolith alibabacloud/sdk gives you AlibabaCloud::alidns() with AlibabaCloud\Alidns\... classes. If your composer.lock has both, you'll autoload the wrong one and get confusing "method not found" errors.

The 2015-01-09 API date is real — but the package is still moving. Newer Alibaba Cloud DNS features (some GTm/Cloud GTM configs, newer line types) appear under different version packages or are simply absent; 4.6.1, however, added a whole agent-registration surface to this package. So: check src/Alidns.php for the method rather than assuming, and re-run composer update alibabacloud/alidns-20150109 deliberately — if you pin tight, prod and a teammate's local can disagree about whether createAtiAgentRegisterInfoAcmeChallengeRecord exists, which surfaces as a Call to undefined method at the worst moment. If a method truly is missing, drop to AlibabaCloud\Tea\Tea::doRPCRequest or raw signed cURL.

Signature/clock skew. SignatureDoesNotMatch in production usually means the container clock drifted (NTP off) or the secret has trailing whitespace from a .env paste. InvalidAccessKeyId.NotFound after a key rotation means config cache: php artisan config:clear.

Package weight. The SDK is a few hundred generated model files, and 4.6.1 grew it further. It's runtime, not dev — but if you're running on a size-constrained Lambda, note that you'll pay for ~3–5 MB of autoload map for what is often three API calls. Wrapping it behind your DnsProvider interface makes it trivial to swap for raw signed requests later if cold-start becomes a problem.

Propagation is not the SDK's job. AddDomainRecord returning a recordId means Alibaba accepted the write, not that resolvers see it. Any test or health check asserting on resolution needs a poll loop against dig/DoH with a 60–120s budget, and ACME flows especially need that delay before validation — including the managed ATI path, which still validates over public DNS.

Log the request ID. $response->body doesn't carry it, but Describe* errors include RequestId in $e->data. Log it — Alibaba support will ask for it, and it's the only way to correlate a throttling incident.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin