NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Certificates for Laravel

Issuing certificates

Issuance goes through the Certificates facade, backed by CertificatesManager. issueIfMissing() returns the active registry record for a domain, or issues a new one:

use RoundlyConsulting\Certificates\Facades\Certificates;

$certificate = Certificates::issueIfMissing('app.example.com');

$certificate->status;            // RoundlyConsulting\Certificates\Enums\CertificateStatus::Issued
$certificate->expires_at;        // CarbonImmutable|null
$certificate->expiresWithin(14); // bool
$certificate->daysUntilExpiry(); // int|null

Issuing from a DTO

issue() always provisions. It takes an IssueCertificateData — the same input the builder, the certificates:issue command and HasCertificates use:

use RoundlyConsulting\Certificates\DataTransferObjects\IssueCertificateData;
use RoundlyConsulting\Certificates\Facades\Certificates;

// Fully specified issuance, recorded in the registry.
$certificate = Certificates::issue(new IssueCertificateData(
    domain: 'shop.example.com',
    driver: 'acme',
    validForDays: 90,
    meta: ['plan' => 'pro'],
    owner: $tenant,
));

// Shorthands.
Certificates::issue(IssueCertificateData::make('shop.example.com'));
Certificates::issue(IssueCertificateData::makeForDomains(['example.com', 'www.example.com']));

What happens on issue

  • Domains are lowercased and de-duplicated — hostnames are case-insensitive — and each is checked with the ValidDomain rule. An invalid one throws InvalidDomainException before anything is written.
  • The certificate’s own provisioning lock is taken before the row is touched. If another process holds it, issue() throws ProvisioningInProgressException and the row is left as it was.
  • The registry row for (driver, name) is created or updated with status requested, the domain list, meta and owner, and CertificateRequested fires. A row pruned earlier is revived as a fresh registration.
  • The provider provisions the certificate — generateMany() for several domains when it supports SANs, generate() otherwise — and its status report is read.
  • Reported issued: the row becomes issued with the reported expiry, issuer, serial and fingerprint (validForDays, default 90, fills in only when the driver reports no expiry) and CertificateIssued fires.
  • Reported still in progress (cert-manager issues asynchronously): the row stays requested until certificates:sync records the outcome.
  • Reported failed, expired or revoked — or the provider threw: the row becomes failed with last_error set, CertificateFailed fires and the exception is rethrown. Either way the certificate’s cached status report is dropped.

Provisioning without the registry

generate() is the lower-level call. With the registry table present it runs issue() — returning false instead of throwing when the certificate is already being provisioned; without it, it provisions straight through the provider under the same per-certificate lock:

// true once it ran; false while another process is provisioning
// this certificate (its per-certificate lock is held).
Certificates::generate('shop.example.com'); // bool

Reading

Certificates::get();                         // Collection<int, RemoteCertificate> from the active driver
Certificates::exists('app.example.com');     // bool
Certificates::find('app.example.com');       // ?Models\Certificate (registry lookup)
Certificates::find('app.example.com', 'acme'); // ...limited to one driver
Certificates::status('app.example.com');     // ?CertificateStatus (registry enum)
Certificates::statusReport('app.example.com'); // ?CertificateStatusReport (live, cached)
Certificates::expiring(14, 'acme');          // Collection<int, Certificate> expiring within 14 days
Certificates::driver('null');                // resolve a specific provider instance
Certificates::certificateName('app.example.com'); // "generated-tls-app-example-com"
Certificates::certificateName('*.Example.com');   // "generated-tls-wildcard-example-com"

The full method list is on The Certificates facade page.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.