Skip to content

Parsing

use Silarhi\Caf\Parser\PaymentSlipParser;
$parser = new PaymentSlipParser();
$paymentSlip = $parser->parse($content);

parse() takes the content of the file as a string: read it from disk, an upload or a remote storage, the parser never opens files. It keeps no state between calls, so one instance can parse any number of files.

The parser looks for lines of 121 dashes. The rows are the lines between the 2nd and the 3rd of them, and a 4th must follow (the total row sits between the 3rd and the 4th). Without these four borders, parse() throws Input CAF LA44 could not be parsed.

Each line between the 2nd and the 3rd borders is a row, split on : into exactly ten parts: the eight columns, plus what comes before the first : and after the last one, which are ignored. Every value is trimmed. A row with another number of parts, a blank line between two rows or an empty table throws CAF Row n°<row> could not be parsed, counting rows from 1 at the first row of the table. Blank lines before the first row or after the last one are ignored.

Large files are fine: the table is located without regular expressions, and the test suite parses 10,000 rows.

Value Format Result
Row period (DATE DEBUT, DATE FIN) m Y (PaymentSlipParser::DATE_FORMAT), e.g. 01 2026 The 1st of the month at midnight
Processing and payment dates d m Y (PaymentSlipParser::FULL_DATE_FORMAT), e.g. 05 02 2026 That day at midnight

Dates are DateTimeImmutable instances in the default timezone of PHP; the getters declare DateTimeInterface. Each line gets its own instances, even when rows share the same month. Several spaces between the parts of a processing or payment date are accepted (29 02 2024).

Invalid or out-of-range dates throw a ParseException instead of rolling over: 13 2021, 00 2022 or AB 2022 in a row, 31 02 2021 or 29 02 2021 in the header. See error handling.

Amounts are returned as float, in the rows and in the total. Spaces, no-break spaces (U+00A0) and narrow no-break spaces (U+202F) are thousands separators. Then:

  • a single comma or dot is the decimal separator: 1298,50 and 1298.50 are both 1298.5
  • with both, the last one is the decimal separator and the other one groups the digits by 3: French (1.298,50) and English (1,298.50) formats give the same result
  • a comma or dot repeated without decimals groups the digits by 3: 12.345.678 or 12,345,678
In the file Result
272 272.0
1298,00 1298.0
1 298,50 1298.5
1.298,50 1298.5
1,298.50 1298.5
1298.50 1298.5
12 345 678,90 12345678.9
12.345.678 12345678.0
-12,50 -12.5

Any other amount throws a ParseException instead of returning a wrong value: an empty amount, letters, the same separator for thousands and decimals (1.298.50), digits not grouped by 3 (12.34,50) or a trailing minus sign (12,50-). See error handling. A blank total (TOTAL : :) is not an error: getTotalAmount() returns null, like a missing one.

Each metadata field is read on its own, from its label in the header (see LA44ZZ files). A field the parser does not find stays null, and the rows are still parsed. With only the table, every field but the total is null:

$paymentSlip->getProcessingDate(); // null
$paymentSlip->getPaymentDate(); // null
$paymentSlip->getCafName(); // null
$paymentSlip->getCafAddress(); // null
$paymentSlip->getRecipientName(); // null
$paymentSlip->getRecipientAddress(); // null
$paymentSlip->getReference(); // null
$paymentSlip->getBic(); // null
$paymentSlip->getIban(); // null
$paymentSlip->getTotalAmount(); // 600.0
count($paymentSlip->getLines()); // 3
$line = $paymentSlip->getLines()[0];
$line->getReference(); // ""
$line->getBeneficiaryReference(); // "1234567 A"
$line->getBeneficiaryName(); // "MME EXEMPLE ALICE"
$line->getStartDate(); // DateTimeImmutable '2026-01-01 00:00:00'
$line->getEndDate(); // DateTimeImmutable '2026-01-01 00:00:00'
$line->getGrossAmount(); // 312.0
$line->getDeduction(); // 0.0
$line->getNetAmount(); // 312.0
$line = $paymentSlip->getLines()[1];
$line->getReference(); // "0000000000000"
$line->getBeneficiaryReference(); // "2345678 B"
$line->getBeneficiaryName(); // "MR EXEMPLE BRUNO"
$line->getStartDate(); // DateTimeImmutable '2025-12-01 00:00:00'
$line->getEndDate(); // DateTimeImmutable '2026-01-01 00:00:00'
$line->getGrossAmount(); // 225.5
$line->getDeduction(); // 25.5
$line->getNetAmount(); // 200.0
$line = $paymentSlip->getLines()[2];
$line->getReference(); // ""
$line->getBeneficiaryReference(); // "3456789 C"
$line->getBeneficiaryName(); // "MME EXEMPLE CHLOE"
$line->getStartDate(); // DateTimeImmutable '2026-01-01 00:00:00'
$line->getEndDate(); // DateTimeImmutable '2026-01-01 00:00:00'
$line->getGrossAmount(); // 88.0
$line->getDeduction(); // 0.0
$line->getNetAmount(); // 88.0
Field Read from Accepts, e.g.
getCafName() CAISSE D'ALLOCATIONS FAMILIALES, then the next line up to its right column: uppercase letters, spaces, ' and - DE L'AIN, DE SEINE-SAINT-DENIS
getCafAddress() The text before NUMERO DE PRODUIT on its line (street), then the text before TYPE DE TRAITEMENT starting with a 5-digit postal code (city) 8 AVENUE DES CANUTS, PLACE DE LA GARE, 42000 SAINT ETIENNE
getRecipientName() The first line indented by 20 spaces or more that starts with a letter: letters, digits, spaces, -, ., ' and & SCI L'ETOILE, DUPONT & FILS, Mme Dupont
getRecipientAddress() The end of the NM REFERENCE line (street), then the next line starting with a 5-digit postal code (city) LIEU-DIT LES PINS, 42000 SAINT-ETIENNE CEDEX 2

The CAF address is set only when both its street and its city are found. The patterns come from sample slips: a header laid out differently gives null for that field.

Windows (\r\n) and Unix (\n) line endings give the same result.