Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/josemmo/Verifactu-PHP/llms.txt

Use this file to discover all available pages before exploring further.

Every data object in Verifactu-PHP — from a full billing record down to a single tax breakdown line — is an instance of a class that extends the abstract josemmo\Verifactu\Models\Model base class. This shared base exposes a single public method, validate(), that runs all constraint checks for that object and any nested child objects. When a constraint fails, validate() throws an InvalidModelException whose $violations property gives you a structured list of exactly what went wrong and where. This design means you can validate any model in isolation, or call validate() on a top-level record to recursively check the entire object graph in one shot.

The Model base class

Model is a final-method abstract class in the josemmo\Verifactu\Models namespace. Its only public method is validate():
namespace josemmo\Verifactu\Models;

use josemmo\Verifactu\Exceptions\InvalidModelException;
use Symfony\Component\Validator\Validation;

abstract class Model {
    /**
     * Validate this instance
     *
     * @throws InvalidModelException if failed to pass validation
     */
    final public function validate(): void {
        $validator = Validation::createValidatorBuilder()->enableAttributeMapping()->getValidator();
        $errors = $validator->validate($this);
        if (count($errors) > 0) {
            throw new InvalidModelException($errors);
        }
    }
}
Internally, validate() builds a Symfony Validator instance with attribute mapping enabled, meaning all constraints are declared directly as PHP 8 attributes on the model properties (e.g. #[Assert\NotBlank], #[Assert\Regex(...)], #[Assert\Callback]). No separate YAML or XML validation config files are needed. If the validator finds one or more violations it wraps them in an InvalidModelException and throws it. If validation passes, validate() returns void with no side effects.

Using validate()

Wrap any validate() call in a try/catch block and catch josemmo\Verifactu\Exceptions\InvalidModelException:
use josemmo\Verifactu\Exceptions\InvalidModelException;
use josemmo\Verifactu\Models\Records\RegistrationRecord;
use josemmo\Verifactu\Models\Records\InvoiceIdentifier;
use josemmo\Verifactu\Models\Records\InvoiceType;
use DateTimeImmutable;

$record = new RegistrationRecord();
$record->invoiceId = new InvoiceIdentifier('A00000000', 'FAC-2025-001', new DateTimeImmutable('2025-01-15'));
$record->issuerName = 'Acme Supplies, S.L.';
$record->invoiceType = InvoiceType::Simplificada;
$record->description = 'Sale of goods';
// ... set breakdown, totals, hash, etc.

try {
    $record->validate();
    echo "Record is valid.\n";
} catch (InvalidModelException $e) {
    echo "Validation failed:\n$e\n";
    // $e->violations contains the structured list — see below
}
The exception’s __toString() method renders all violations in a human-readable bullet list, which is handy for quick debugging. For programmatic handling, iterate $e->violations directly.
Always call validate() before submitting a record to the AEAT. The AEAT web service will reject malformed records and return an error response that may be harder to interpret than a local validation failure. Validating locally first saves round-trips and makes bugs much easier to diagnose.

Available models

The following concrete model classes ship with Verifactu-PHP:
ClassNamespacePurpose
ComputerSystemjosemmo\Verifactu\ModelsDescribes the SIF itself (software name, version, NIF, etc.)
RegistrationRecordjosemmo\Verifactu\Models\RecordsBilling record for a new or corrected invoice
CancellationRecordjosemmo\Verifactu\Models\RecordsBilling record for the cancellation of a prior record
BreakdownDetailsjosemmo\Verifactu\Models\RecordsSingle tax line within a registration record’s breakdown
InvoiceIdentifierjosemmo\Verifactu\Models\RecordsTuple of issuer NIF, invoice number, and issue date
FiscalIdentifierjosemmo\Verifactu\Models\RecordsSpanish fiscal identity (name + NIF) for invoice recipients
ForeignFiscalIdentifierjosemmo\Verifactu\Models\RecordsNon-Spanish fiscal identity (name, country, ID type, ID value)
AeatResponsejosemmo\Verifactu\Models\ResponsesParsed response from an AEAT web service call
All of the above extend Model and therefore expose validate().

Validation constraints

Constraints are declared as PHP 8 attributes directly on model properties. The library uses several constraint families:

Presence & length

#[Assert\NotBlank] ensures required string fields are neither null nor empty. #[Assert\Length] enforces maximum (or exact) character counts — for example, issuerName allows up to 120 characters and issuerId must be exactly 9 characters.

Format (Regex)

Monetary amounts use #[Assert\Regex(pattern: '/^-?\d{1,12}\.\d{2}$/')] to enforce the ####.## decimal format the AEAT requires. Tax rates use a similar pattern restricted to non-negative values.

Nested objects (Valid)

#[Assert\Valid] on a property causes validate() to recurse into that nested model (e.g. invoiceId, breakdown[]), so a single top-level validate() call checks the entire object graph.

Business rules (Callback)

#[Assert\Callback] methods implement cross-field logic that simple attribute constraints cannot express — including hash integrity, tax amount cross-checks, recipient rules by invoice type, and corrective invoice details.
Notable business-rule callbacks include:
  • validateHash — verifies that $record->hash === $record->calculateHash(), catching any mismatch between the stored hash and the record’s current field values.
  • validateTotals — sums all BreakdownDetails->taxAmount values and checks they equal totalTaxAmount; also checks totalAmount against the sum of base amounts and tax amounts (with ±€0.02 rounding tolerance).
  • validateRecipients — enforces that simplified invoices (InvoiceType::Simplificada, InvoiceType::R5) must have no recipients, while all other invoice types require at least one.
  • validateCorrectiveDetails — ensures corrective invoices carry a correctiveType and that non-corrective invoices do not.
  • validateOperationType — ensures taxRate and taxAmount are present for subject operations and absent for non-subject or exempt ones.

Reading violation details

InvalidModelException exposes a public readonly property $violations of type Symfony\Component\Validator\ConstraintViolationListInterface. You can iterate it to access each violation’s property path and message:
use josemmo\Verifactu\Exceptions\InvalidModelException;

try {
    $record->validate();
} catch (InvalidModelException $e) {
    foreach ($e->violations as $violation) {
        echo sprintf(
            "[%s] %s\n",
            $violation->getPropertyPath(),
            $violation->getMessage()
        );
    }
}
Each $violation is a Symfony\Component\Validator\ConstraintViolation instance. Useful methods:
MethodReturns
getPropertyPath()Dot-notation path to the failing field, e.g. breakdown[0].taxAmount
getMessage()Human-readable description of the failure
getInvalidValue()The actual value that failed the constraint
getRoot()The top-level model object that validate() was called on

Example: catching a tax amount mismatch

The following BreakdownDetails deliberately sets taxAmount to an incorrect value. The validateTaxAmount callback detects the mismatch and validate() throws with a descriptive message:
use josemmo\Verifactu\Exceptions\InvalidModelException;
use josemmo\Verifactu\Models\Records\BreakdownDetails;
use josemmo\Verifactu\Models\Records\OperationType;
use josemmo\Verifactu\Models\Records\RegimeType;
use josemmo\Verifactu\Models\Records\TaxType;

$details = new BreakdownDetails();
$details->taxType      = TaxType::IVA;
$details->regimeType   = RegimeType::C01;
$details->operationType = OperationType::Subject;
$details->baseAmount   = '100.00';
$details->taxRate      = '10.00';
$details->taxAmount    = '12.34'; // ← incorrect: should be 10.00

try {
    $details->validate();
} catch (InvalidModelException $e) {
    foreach ($e->violations as $violation) {
        echo sprintf("[%s] %s\n", $violation->getPropertyPath(), $violation->getMessage());
        // [taxAmount] Expected amount of 10.00, got 12.34
    }
}
The validator computes the expected tax amount from baseAmount × taxRate / 100 (with a ±€0.02 rounding tolerance) and reports the discrepancy, pointing directly at the taxAmount property path.

Build docs developers (and LLMs) love