endeken-com /
ofx-php-parser
| 1 | <?php |
||||
| 2 | |||||
| 3 | namespace Endeken\OFX; |
||||
| 4 | |||||
| 5 | use DateTime; |
||||
| 6 | use DateTimeZone; |
||||
| 7 | use Exception; |
||||
| 8 | use SimpleXMLElement; |
||||
| 9 | |||||
| 10 | /** |
||||
| 11 | * Class OFX |
||||
| 12 | * |
||||
| 13 | * This class provides functions to parse OFX data and convert it into an associative array or appropriate objects. |
||||
| 14 | */ |
||||
| 15 | class OFX |
||||
| 16 | { |
||||
| 17 | /** |
||||
| 18 | * Parse OFX data and return an associative array with the parsed information. |
||||
| 19 | * |
||||
| 20 | * @param string $ofxData The OFX data as a string. |
||||
| 21 | * @return OFXData|null An associative array with the parsed information or false on failure. |
||||
| 22 | * @throws Exception |
||||
| 23 | */ |
||||
| 24 | public static function parse(string $ofxData): null|OFXData |
||||
| 25 | { |
||||
| 26 | |||||
| 27 | // Check if SimpleXML object was created successfully |
||||
| 28 | |||||
| 29 | $xml = OFXUtils::normalizeOfx($ofxData); |
||||
| 30 | if ($xml === false) { |
||||
| 31 | return null; |
||||
| 32 | } |
||||
| 33 | |||||
| 34 | $signOn = self::parseSignOn($xml->SIGNONMSGSRSV1->SONRS); |
||||
| 35 | $accountInfo = self::parseAccountInfo($xml->SIGNUPMSGSRSV1->ACCTINFOTRNRS); |
||||
|
0 ignored issues
–
show
|
|||||
| 36 | $bankAccounts = []; |
||||
| 37 | |||||
| 38 | if (isset($xml->BANKMSGSRSV1)) { |
||||
| 39 | foreach ($xml->BANKMSGSRSV1->STMTTRNRS as $accountStatement) { |
||||
| 40 | foreach ($accountStatement->STMTRS as $statementResponse) { |
||||
| 41 | $bankAccounts[] = self::parseBankAccount($accountStatement->TRNUID, $statementResponse); |
||||
|
0 ignored issues
–
show
It seems like
$statementResponse can also be of type null; however, parameter $xml of Endeken\OFX\OFX::parseBankAccount() does only seem to accept SimpleXMLElement, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 42 | } |
||||
| 43 | } |
||||
| 44 | } elseif (isset($xml->CREDITCARDMSGSRSV1)) { |
||||
| 45 | $bankAccounts[] = self::parseCreditAccount($xml->TRNUID, $xml->CREDITCARDMSGSRSV1->CCSTMTTRNRS); |
||||
| 46 | } |
||||
| 47 | return new OFXData($signOn, $accountInfo, $bankAccounts); |
||||
| 48 | } |
||||
| 49 | |||||
| 50 | /** |
||||
| 51 | * @param SimpleXMLElement $xml |
||||
| 52 | * @return SignOn |
||||
| 53 | * @throws Exception |
||||
| 54 | */ |
||||
| 55 | protected static function parseSignOn(SimpleXMLElement $xml): SignOn |
||||
| 56 | { |
||||
| 57 | $status = self::parseStatus($xml->STATUS); |
||||
| 58 | $dateTime = self::parseDate($xml->DTSERVER); |
||||
| 59 | $language = $xml->LANGUAGE; |
||||
| 60 | $institute = self::parseInstitute($xml->FI); |
||||
| 61 | return new SignOn($status, $dateTime, $language, $institute); |
||||
| 62 | } |
||||
| 63 | |||||
| 64 | protected static function parseInstitute(SimpleXMLElement $xml): Institute |
||||
| 65 | { |
||||
| 66 | $name = (string) $xml->ORG; |
||||
| 67 | $id = (string) $xml->FID; |
||||
| 68 | return new Institute($id, $name); |
||||
| 69 | } |
||||
| 70 | |||||
| 71 | /** |
||||
| 72 | * @param SimpleXMLElement $xml |
||||
| 73 | * @return Status |
||||
| 74 | */ |
||||
| 75 | protected static function parseStatus(SimpleXMLElement $xml): Status |
||||
| 76 | { |
||||
| 77 | $code = (string) $xml->STATUS->CODE; |
||||
| 78 | $severity = (string) $xml->STATUS->SEVERITY; |
||||
| 79 | $message = (string) $xml->STATUS->MESSAGE; |
||||
| 80 | return new Status($code, $severity, $message); |
||||
| 81 | } |
||||
| 82 | |||||
| 83 | /** |
||||
| 84 | * Parse a date string and return a formatted date. |
||||
| 85 | * |
||||
| 86 | * @param string $dateString The date string to parse. |
||||
| 87 | * @return DateTime The formatted date. |
||||
| 88 | * @throws Exception |
||||
| 89 | */ |
||||
| 90 | protected static function parseDate(string $dateString): DateTime |
||||
| 91 | { |
||||
| 92 | $dateString = explode('.', $dateString)[0]; |
||||
| 93 | // Extract the numeric part of the offset (e.g., -5 from [-5:EST]) |
||||
| 94 | // Also deal with some OFX data where the date string contains the offset, but not the |
||||
| 95 | // timezone abbreviation, ie. 20240501094851[-8] |
||||
| 96 | preg_match('/([-+]\d+)(:)?(\w+)?/', $dateString, $matches); |
||||
| 97 | |||||
| 98 | if (count($matches) >= 2) { |
||||
| 99 | $offset = $matches[1]; |
||||
| 100 | $timezoneAbbreviation = $matches[3] ?? null; |
||||
| 101 | |||||
| 102 | // Remove the offset with brackets and timezone abbreviation from the date string |
||||
| 103 | $dateStringWithoutOffset = preg_replace('/[-+]\d+:\w+/', '', $dateString); |
||||
| 104 | |||||
| 105 | // Remove brackets and timezone abbreviation |
||||
| 106 | $dateStringWithoutOffset = str_replace(['[', ']', ':' . $timezoneAbbreviation], '', $dateStringWithoutOffset); |
||||
| 107 | |||||
| 108 | // Create a DateTime object with the appropriate timezone offset |
||||
| 109 | $dateTime = new DateTime($dateStringWithoutOffset, new DateTimeZone("GMT$offset")); |
||||
| 110 | (null === $timezoneAbbreviation) ?: $dateTime->setTimezone(new DateTimeZone($timezoneAbbreviation)); |
||||
| 111 | |||||
| 112 | } else { |
||||
| 113 | // Handle cases where the date format doesn't match expectations |
||||
| 114 | // You might want to log an error or throw an exception depending on your needs |
||||
| 115 | $dateTime = new DateTime($dateString); |
||||
| 116 | } |
||||
| 117 | return $dateTime; |
||||
| 118 | } |
||||
| 119 | |||||
| 120 | /** |
||||
| 121 | * @throws Exception |
||||
| 122 | */ |
||||
| 123 | private static function parseBankAccount(string $uuid, SimpleXMLElement $xml): BankAccount |
||||
| 124 | { |
||||
| 125 | $accountNumber = $xml->BANKACCTFROM->ACCTID ?? 'N/A'; |
||||
| 126 | $accountType = $xml->BANKACCTFROM->ACCTTYPE ?? 'N/A'; |
||||
| 127 | $agencyNumber = $xml->BANKACCTFROM->BRANCHID ?? 'N/A'; |
||||
| 128 | $routingNumber = $xml->BANKACCTFROM->BANKID ?? 'N/A'; |
||||
| 129 | $balance = $xml->LEDGERBAL->BALAMT ?? 'N/A'; |
||||
| 130 | $balanceDate = (null !== $xml->LEDGERBAL->DTASOF) ? self::parseDate($xml->LEDGERBAL->DTASOF) : new DateTime(); |
||||
| 131 | $statement = self::parseStatement($xml); |
||||
| 132 | return new BankAccount( |
||||
| 133 | $accountNumber, |
||||
| 134 | $accountType, |
||||
| 135 | $agencyNumber, |
||||
| 136 | $routingNumber, |
||||
| 137 | $balance, |
||||
| 138 | $balanceDate, |
||||
| 139 | $uuid, |
||||
| 140 | $statement, |
||||
| 141 | ); |
||||
| 142 | } |
||||
| 143 | |||||
| 144 | /** |
||||
| 145 | * @throws Exception |
||||
| 146 | */ |
||||
| 147 | private static function parseCreditAccount(string $uuid, SimpleXMLElement $xml): BankAccount |
||||
| 148 | { |
||||
| 149 | $nodeName = 'CCACCTFROM'; |
||||
| 150 | if (!isset($xml->CCSTMTRS->$nodeName)) { |
||||
| 151 | $nodeName = 'BANKACCTFROM'; |
||||
| 152 | } |
||||
| 153 | |||||
| 154 | $accountNumber = $xml->CCSTMTRS->$nodeName->ACCTID; |
||||
| 155 | $accountType = $xml->CCSTMTRS->$nodeName->ACCTTYPE; |
||||
| 156 | $agencyNumber = $xml->CCSTMTRS->$nodeName->BRANCHID; |
||||
| 157 | $routingNumber = $xml->CCSTMTRS->$nodeName->BANKID; |
||||
| 158 | $balance = $xml->CCSTMTRS->LEDGERBAL->BALAMT; |
||||
| 159 | $balanceDate = self::parseDate($xml->CCSTMTRS->LEDGERBAL->DTASOF); |
||||
| 160 | $statement = self::parseStatement($xml->CCSTMTRS); |
||||
| 161 | return new BankAccount( |
||||
| 162 | $accountNumber, |
||||
| 163 | $accountType, |
||||
| 164 | $agencyNumber, |
||||
| 165 | $routingNumber, |
||||
| 166 | $balance, |
||||
| 167 | $balanceDate, |
||||
| 168 | $uuid, |
||||
| 169 | $statement, |
||||
| 170 | ); |
||||
| 171 | } |
||||
| 172 | |||||
| 173 | /** |
||||
| 174 | * @throws Exception |
||||
| 175 | */ |
||||
| 176 | private static function parseStatement(SimpleXMLElement $xml): Statement |
||||
| 177 | { |
||||
| 178 | $currency = $xml->CURDEF; |
||||
| 179 | $startDate = self::parseDate($xml->BANKTRANLIST->DTSTART); |
||||
| 180 | $endDate = self::parseDate($xml->BANKTRANLIST->DTEND); |
||||
| 181 | $transactions = []; |
||||
| 182 | foreach ($xml->BANKTRANLIST->STMTTRN as $transactionXml) { |
||||
| 183 | $type = (string) $transactionXml->TRNTYPE; |
||||
| 184 | $date = self::parseDate($transactionXml->DTPOSTED); |
||||
| 185 | $userInitiatedDate = null; |
||||
| 186 | if (!empty((string) $transactionXml->DTUSER)) { |
||||
| 187 | $userInitiatedDate = self::parseDate($transactionXml->DTUSER); |
||||
| 188 | } |
||||
| 189 | $amount = (float) $transactionXml->TRNAMT; |
||||
| 190 | $uniqueId = (string) $transactionXml->FITID; |
||||
| 191 | $name = (string) $transactionXml->NAME; |
||||
| 192 | $memo = (string) $transactionXml->MEMO; |
||||
| 193 | $sic = $transactionXml->SIC; |
||||
| 194 | $checkNumber = $transactionXml->CHECKNUM; |
||||
| 195 | $transactions[] = new Transaction( |
||||
| 196 | $type, |
||||
| 197 | $amount, |
||||
| 198 | $date, |
||||
| 199 | $userInitiatedDate, |
||||
| 200 | $uniqueId, |
||||
| 201 | $name, |
||||
| 202 | $memo, |
||||
| 203 | $sic, |
||||
| 204 | $checkNumber, |
||||
| 205 | ); |
||||
| 206 | } |
||||
| 207 | return new Statement($currency, $transactions, $startDate, $endDate); |
||||
| 208 | } |
||||
| 209 | |||||
| 210 | private static function parseAccountInfo(SimpleXMLElement $xml = null): array|null |
||||
| 211 | { |
||||
| 212 | if ($xml === null || !isset($xml->ACCTINFO)) { |
||||
| 213 | return null; |
||||
| 214 | } |
||||
| 215 | $accounts = []; |
||||
| 216 | foreach ($xml->ACCTINFO as $account) { |
||||
| 217 | $accounts[] = new AccountInfo( |
||||
| 218 | (string)$account->DESC, |
||||
| 219 | (string)$account->ACCTID |
||||
| 220 | ); |
||||
| 221 | } |
||||
| 222 | |||||
| 223 | return $accounts; |
||||
| 224 | } |
||||
| 225 | } |
||||
| 226 |
This check looks for function or method calls that always return null and whose return value is assigned to a variable.
The method
getObject()can return nothing but null, so it makes no sense to assign that value to a variable.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.