* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file cfdixml/src/XSLTParser.php * \ingroup cfdixml * \brief Clase para procesar y analizar documentos XSLT del SAT */ class XSLTParser { private $xsltDoc; private $templates = []; private $parametros = []; public function __construct($xsltPath) { $this->xsltDoc = new DOMDocument(); $this->xsltDoc->load($xsltPath); $this->parseXSLT(); } /** * Parses the XSLT document to extract templates and their parameters. * * This method iterates through all 'template' elements in the XSLT document, * extracts the 'match' attribute, and calls parseTemplate() for each valid template. * * @return void */ private function parseXSLT() { // Obtener todos los templates $templates = $this->xsltDoc->getElementsByTagName('template'); foreach ($templates as $template) { $match = $template->getAttribute('match'); if ($match) { $this->parseTemplate($template, $match); } } } /** * Parses a single XSLT template to extract its parameters. * * This function analyzes a given XSLT template, looking for calls to "Requerido" and "Opcional" templates. * It extracts the parameters from these calls and stores them with information about whether they are required or optional. * * @param DOMElement $template The XSLT template element to be parsed. * @param string $match The value of the 'match' attribute of the template. * * @return void This function does not return a value, but updates the internal $parametros property. */ private function parseTemplate($template, $match) { // Ignorar templates generales if ($match === '/' || $match === '*') { return; } $params = []; // Buscar llamadas a templates "Requerido" y "Opcional" $calls = $template->getElementsByTagName('call-template'); foreach ($calls as $call) { $templateName = $call->getAttribute('name'); if ($templateName === 'Requerido' || $templateName === 'Opcional') { // Obtener el parámetro del valor $withParam = $call->getElementsByTagName('with-param')->item(0); if ($withParam) { $select = $withParam->getAttribute('select'); // Extraer el nombre del atributo de la expresión XPath $attrName = $this->extractAttributeName($select); if ($attrName) { $params[] = [ 'nombre' => $attrName, 'requerido' => ($templateName === 'Requerido') ]; } } } } // Si encontramos parámetros, los guardamos if (!empty($params)) { $nodeName = str_replace(['cfdi:', '/'], '', $match); $this->parametros[$nodeName] = $params; } } /** * Extracts the attribute name from an XPath expression. * * This function attempts to extract the attribute name from XPath expressions * that follow the pattern './@AttributeName'. * * @param string $select The XPath expression to parse. * * @return string|null Returns the extracted attribute name if found, or null if not found. */ private function extractAttributeName($select) { // Extraer nombre del atributo de expresiones como ./@AttributeName if (preg_match('/\/@([a-zA-Z0-9]+)/', $select, $matches)) { return $matches[1]; } return null; } /** * Infers the data type of a parameter based on its name. * * This function attempts to determine the appropriate data type for a given parameter * by comparing its name against a predefined list of common parameter names and their * associated types. If the parameter name is not found in the list, it defaults to 'string'. * * @param string $nombreParametro The name of the parameter for which to infer the type. * * @return string The inferred data type of the parameter. Possible values are 'decimal', * 'dateTime', 'string', or any other type defined in the $tiposComunes array. * Returns 'string' if the parameter name is not found in the predefined list. */ private function inferirTipo($nombreParametro) { $tiposComunes = [ 'Version' => 'string', 'Serie' => 'string', 'Folio' => 'string', 'Fecha' => 'dateTime', 'SubTotal' => 'decimal', 'Total' => 'decimal', 'Descuento' => 'decimal', 'TipoCambio' => 'decimal', 'Moneda' => 'string', 'Rfc' => 'string', 'Nombre' => 'string', 'RegimenFiscal' => 'string', 'DomicilioFiscalReceptor' => 'string', 'UsoCFDI' => 'string', 'TipoDeComprobante' => 'string', 'MetodoPago' => 'string', 'FormaPago' => 'string', 'LugarExpedicion' => 'string' ]; return $tiposComunes[$nombreParametro] ?? 'string'; } /** * Generates a structured representation of parameters for all nodes. * * This function iterates through all parsed parameters and creates a * structured array that includes information about each parameter's * requirement status and inferred data type. * * @return array An associative array where keys are node names and values are * arrays of parameters. Each parameter is represented as an * associative array with 'requerido' and 'tipo' keys. */ public function generarEstructuraParametros() { $estructura = []; foreach ($this->parametros as $nodo => $params) { $estructura[$nodo] = []; // Si es un nodo Concepto, usamos una estructura específica if ($nodo === 'Concepto') { $estructura[$nodo] = [ 'ClaveProdServ' => ['requerido' => 1, 'tipo' => 'string'], 'NoIdentificacion' => ['requerido' => 0, 'tipo' => 'string'], 'Cantidad' => ['requerido' => 1, 'tipo' => 'decimal'], 'ClaveUnidad' => ['requerido' => 1, 'tipo' => 'string'], 'Unidad' => ['requerido' => 0, 'tipo' => 'string'], 'Descripcion' => ['requerido' => 1, 'tipo' => 'string'], 'ValorUnitario' => ['requerido' => 1, 'tipo' => 'decimal'], 'Importe' => ['requerido' => 1, 'tipo' => 'decimal'], 'Descuento' => ['requerido' => 0, 'tipo' => 'decimal'], 'ObjetoImp' => ['requerido' => 1, 'tipo' => 'string'] ]; } // Para otros nodos, procesar normalmente else { foreach ($params as $param) { $estructura[$nodo][$param['nombre']] = [ 'requerido' => $param['requerido'], 'tipo' => $this->inferirTipo($param['nombre']) ]; } } } // Agregar estructura de Impuestos separada $estructura['Impuestos'] = [ 'Base' => ['requerido' => 1, 'tipo' => 'decimal'], 'Impuesto' => ['requerido' => 1, 'tipo' => 'string'], 'TipoFactor' => ['requerido' => 1, 'tipo' => 'string'], 'TasaOCuota' => ['requerido' => 1, 'tipo' => 'decimal'], 'Importe' => ['requerido' => 1, 'tipo' => 'decimal'] ]; return $estructura; } }