├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── cfdi-to-json.php ├── check-current-max-occurs-paths.bash └── max-occurs-paths.php ├── composer.json ├── docs ├── CHANGELOG.md ├── SEMVER.md └── TODO.md ├── infection.json.dist └── src ├── CfdiToDataNode.php ├── Factory.php ├── JsonConverter.php ├── Nodes ├── Children.php ├── KeysCounter.php └── Node.php ├── UnboundedOccursPaths.json ├── UnboundedOccursPaths.php └── XsdMaxOccurs ├── Downloader.php ├── DownloaderInterface.php ├── Finder.php ├── FinderInterface.php └── XsdMaxOccursFromNsRegistry.php /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Código de Conducta Convenido para Contribuyentes 2 | 3 | ## Nuestro compromiso 4 | 5 | Nosotros, como miembros, contribuyentes y administradores nos comprometemos a hacer de la participación en nuestra comunidad una experiencia libre de acoso para todo el mundo, independientemente de la edad, dimensión corporal, minusvalía visible o invisible, etnicidad, características sexuales, identidad y expresión de género, nivel de experiencia, educación, nivel socioeconómico, nacionalidad, apariencia personal, raza, religión, o identidad u orientación sexual. 6 | 7 | Nos comprometemos a actuar e interactuar de maneras que contribuyan a una comunidad abierta, acogedora, diversa, inclusiva y sana. 8 | 9 | ## Nuestros estándares 10 | 11 | Ejemplos de comportamiento que contribuyen a crear un ambiente positivo para nuestra comunidad: 12 | 13 | * Demostrar empatía y amabilidad ante otras personas 14 | * Respeto a diferentes opiniones, puntos de vista y experiencias 15 | * Dar y aceptar adecuadamente retroalimentación constructiva 16 | * Aceptar la responsabilidad y disculparse ante quienes se vean afectados por nuestros errores, aprendiendo de la experiencia 17 | * Centrarse en lo que sea mejor no solo para nosotros como individuos, sino para la comunidad en general 18 | 19 | Ejemplos de comportamiento inaceptable: 20 | 21 | * El uso de lenguaje o imágenes sexualizadas, y aproximaciones o atenciones sexuales de cualquier tipo 22 | * Comentarios despectivos (_trolling_), insultantes o derogatorios, y ataques personales o políticos 23 | * El acoso en público o privado 24 | * Publicar información privada de otras personas, tales como direcciones físicas o de correo electrónico, sin su permiso explícito 25 | * Otras conductas que puedan ser razonablemente consideradas como inapropiadas en un entorno profesional 26 | 27 | ## Aplicación de las responsabilidades 28 | 29 | Los administradores de la comunidad son responsables de aclarar y hacer cumplir nuestros estándares de comportamiento aceptable y tomarán acciones apropiadas y correctivas de forma justa en respuesta a cualquier comportamiento que consideren inapropiado, amenazante, ofensivo o dañino. 30 | 31 | Los administradores de la comunidad tendrán el derecho y la responsabilidad de eliminar, editar o rechazar comentarios, _commits_, código, ediciones de páginas de wiki, _issues_ y otras contribuciones que no se alineen con este Código de Conducta, y comunicarán las razones para sus decisiones de moderación cuando sea apropiado. 32 | 33 | ## Alcance 34 | 35 | Este código de conducta aplica tanto a espacios del proyecto como a espacios públicos donde un individuo esté en representación del proyecto o comunidad. Ejemplos de esto incluyen el uso de la cuenta oficial de correo electrónico, publicaciones a través de las redes sociales oficiales, o presentaciones con personas designadas en eventos en línea o no. 36 | 37 | ## Aplicación 38 | 39 | Instancias de comportamiento abusivo, acosador o inaceptable de otro modo podrán ser reportadas a los administradores de la comunidad responsables del cumplimiento a través de [coc@phpcfdi.com](). Todas las quejas serán evaluadas e investigadas de una manera puntual y justa. 40 | 41 | Todos los administradores de la comunidad están obligados a respetar la privacidad y la seguridad de quienes reporten incidentes. 42 | 43 | ## Guías de Aplicación 44 | 45 | Los administradores de la comunidad seguirán estas Guías de Impacto en la Comunidad para determinar las consecuencias de cualquier acción que juzguen como un incumplimiento de este Código de Conducta: 46 | 47 | ### 1. Corrección 48 | 49 | **Impacto en la Comunidad**: El uso de lenguaje inapropiado u otro comportamiento considerado no profesional o no acogedor en la comunidad. 50 | 51 | **Consecuencia**: Un aviso escrito y privado por parte de los administradores de la comunidad, proporcionando claridad alrededor de la naturaleza de este incumplimiento y una explicación de por qué el comportamiento es inaceptable. Una disculpa pública podría ser solicitada. 52 | 53 | ### 2. Aviso 54 | 55 | **Impacto en la Comunidad**: Un incumplimiento causado por un único incidente o por una cadena de acciones. 56 | 57 | **Consecuencia**: Un aviso con consecuencias por comportamiento prolongado. No se interactúa con las personas involucradas, incluyendo interacción no solicitada con quienes se encuentran aplicando el Código de Conducta, por un periodo especificado de tiempo. Esto incluye evitar las interacciones en espacios de la comunidad, así como a través de canales externos como las redes sociales. Incumplir estos términos puede conducir a una expulsión temporal o permanente. 58 | 59 | ### 3. Expulsión temporal 60 | 61 | **Impacto en la Comunidad**: Una serie de incumplimientos de los estándares de la comunidad, incluyendo comportamiento inapropiado continuo. 62 | 63 | **Consecuencia**: Una expulsión temporal de cualquier forma de interacción o comunicación pública con la comunidad durante un intervalo de tiempo especificado. No se permite interactuar de manera pública o privada con las personas involucradas, incluyendo interacciones no solicitadas con quienes se encuentran aplicando el Código de Conducta, durante este periodo. Incumplir estos términos puede conducir a una expulsión permanente. 64 | 65 | ### 4. Expulsión permanente 66 | 67 | **Impacto en la Comunidad**: Demostrar un patrón sistemático de incumplimientos de los estándares de la comunidad, incluyendo conductas inapropiadas prolongadas en el tiempo, acoso de individuos, o agresiones o menosprecio a grupos de individuos. 68 | 69 | **Consecuencia**: Una expulsión permanente de cualquier tipo de interacción pública con la comunidad del proyecto. 70 | 71 | ## Atribución 72 | 73 | Este Código de Conducta es una adaptación del [Contributor Covenant](https://www.contributor-covenant.org), versión 2.0, disponible en . 74 | 75 | Las Guías de Impacto en la Comunidad están inspiradas en la [escalera de aplicación del código de conducta de Mozilla](https://github.com/mozilla/diversity). 76 | 77 | Para respuestas a las preguntas frecuentes de este código de conducta, consulta las FAQ en . Hay traducciones disponibles en . 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribuciones 2 | 3 | Las contribuciones son bienvenidas. Aceptamos *Pull Requests* en el [repositorio GitHub][project]. 4 | 5 | Este proyecto se apega al siguiente [Código de Conducta][coc]. 6 | Al participar en este proyecto y en su comunidad, deberás seguir este código. 7 | 8 | ## Miembros del equipo 9 | 10 | * [phpCfdi][] - Organización que mantiene el proyecto. 11 | * [Contribuidores][contributors]. 12 | 13 | ## Canales de comunicación 14 | 15 | Puedes encontrar ayuda y comentar asuntos relacionados con este proyecto en estos lugares: 16 | 17 | * Comunidad Discord: 18 | * GitHub Issues: 19 | 20 | ## Reportar Bugs 21 | 22 | Publica los *Bugs* en la sección [GitHub Issues][issues] del proyecto. 23 | 24 | Sigue las recomendaciones generales de [phpCfdi][] para reportar problemas 25 | . 26 | 27 | Cuando se reporte un *Bug*, por favor incluye la mayor información posible para reproducir el problema, preferentemente 28 | con ejemplos de código o cualquier otra información técnica que nos pueda ayudar a identificar el caso. 29 | 30 | **Recuerda no incluir contraseñas, información personal o confidencial.** 31 | 32 | ## Corrección de Bugs 33 | 34 | Apreciamos mucho los *Pull Request* para corregir Bugs. 35 | 36 | Si encuentras un reporte de Bug y te gustaría solucionarlo siéntete libre de hacerlo. 37 | Sigue las directrices de "Agregar nuevas funcionalidades" a continuación. 38 | 39 | ## Agregar nuevas funcionalidades 40 | 41 | Si tienes una idea para una nueva funcionalidad revisa primero que existan discusiones o *Pull Requests* 42 | en donde ya se esté trabajando en la funcionalidad. 43 | 44 | Antes de trabajar en la nueva característica, utiliza los "Canales de comunicación" mencionados 45 | anteriormente para platicar acerca de tu idea. Si dialogas tus ideas con la comunidad y los 46 | mantenedores del proyecto, podrás ahorrar mucho esfuerzo de desarrollo y prevenir que tu 47 | *Pull Request* sea rechazado. No nos gusta rechazar contribuciones, pero algunas características 48 | o la forma de desarrollarlas puede que no estén alineadas con el proyecto. 49 | 50 | Considera las siguientes directrices: 51 | 52 | * Usa una rama única que se desprenda de la rama principal. 53 | No mezcles dos diferentes funcionalidades en una misma rama o *Pull Request*. 54 | * Describe claramente y en detalle los cambios que hiciste. 55 | * **Escribe pruebas** para la funcionalidad que deseas agregar. 56 | * **Asegúrate que las pruebas pasan** antes de enviar tu contribución. 57 | Usamos integración continua donde se hace esta verificación, pero es mucho mejor si lo pruebas localmente. 58 | * Intenta enviar una historia coherente, entenderemos cómo cambia el código si los *commits* tienen significado. 59 | * La documentación es parte del proyecto. 60 | Realiza los cambios en los archivos de ayuda para que reflejen los cambios en el código. 61 | 62 | ## Proceso de construcción 63 | 64 | ```shell 65 | # Actualiza tus dependencias 66 | composer update 67 | phive update 68 | 69 | # Verificación de estilo de código 70 | composer dev:check-style 71 | 72 | # Corrección de estilo de código 73 | composer dev:fix-style 74 | 75 | # Ejecución de pruebas 76 | composer dev:test 77 | 78 | # Ejecución todo en uno: corregir estilo, verificar estilo y correr pruebas 79 | composer dev:build 80 | 81 | # Opcional: correr las pruebas de mutación 82 | composer dev:infection 83 | ``` 84 | 85 | ## Ejecutar GitHub Actions localmente 86 | 87 | Puedes utilizar la herramienta [`act`](https://github.com/nektos/act) para ejecutar las GitHub Actions localmente. 88 | Según [`actions/setup-php-action`](https://github.com/marketplace/actions/setup-php-action#local-testing-setup) 89 | puedes ejecutar el siguiente comando para revisar los flujos de trabajo localmente: 90 | 91 | ```shell 92 | act -P ubuntu-latest=shivammathur/node:latest 93 | ``` 94 | 95 | [phpCfdi]: https://github.com/phpcfdi/ 96 | [project]: https://github.com/phpcfdi/cfdi-to-json 97 | [contributors]: https://github.com/phpcfdi/cfdi-to-json/graphs/contributors 98 | [coc]: https://github.com/phpcfdi/cfdi-to-json/blob/main/CODE_OF_CONDUCT.md 99 | [issues]: https://github.com/phpcfdi/cfdi-to-json/issues 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 - 2024 PhpCfdi https://www.phpcfdi.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpcfdi/cfdi-to-json 2 | 3 | [![Source Code][badge-source]][source] 4 | [![Packagist PHP Version Support][badge-php-version]][php-version] 5 | [![Discord][badge-discord]][discord] 6 | [![Latest Version][badge-release]][release] 7 | [![Software License][badge-license]][license] 8 | [![Build Status][badge-build]][build] 9 | [![Reliability][badge-reliability]][reliability] 10 | [![Maintainability][badge-maintainability]][maintainability] 11 | [![Code Coverage][badge-coverage]][coverage] 12 | [![Violations][badge-violations]][violations] 13 | [![Total Downloads][badge-downloads]][downloads] 14 | 15 | > Herramienta para convertir archivos CFDI a JSON 16 | 17 | ## Acerca de `phpcfdi/cfdi-to-json` 18 | 19 | Esta es una herramienta que sigue sus propias convenciones para convertir los archivos de CFDI (XML de SAT) 20 | a formato JSON. 21 | 22 | Algunas de las convenciones que se siguen son: 23 | 24 | - Los elementos son objetos que contienen su valor, los atributos y sus elementos hijos. 25 | - Los elementos que pueden aparecer más de una vez, son manejados como arreglos. 26 | - La librería guarda un registro interno de los elementos que pueden aparecer más de una vez. 27 | 28 | ## Instalación 29 | 30 | Usa [composer](https://getcomposer.org/) 31 | 32 | ```shell 33 | composer require phpcfdi/cfdi-to-json 34 | ``` 35 | 36 | ## Uso básico 37 | 38 | ### Convirtiendo de CFDI (string) a JSON (string) 39 | 40 | ```php 41 | createConverter(); 63 | $rootNode = $dataConverter->convertXmlDocument($document); 64 | $array = $rootNode->toArray(); 65 | 66 | var_export($array); 67 | ``` 68 | 69 | ### Ejemplo de salida 70 | 71 | Note que: 72 | - `Emisor` parece una propiedad más del objeto principal, pero el contenido es un objeto y no una cadena de caracteres. 73 | - `Concepto` contiene un arreglo de objetos, cada uno es una representación de un nodo concepto. 74 | - `Traslado` contiene un arreglo a pesar de que solo contenga un objeto, se conoce que es múltiple. 75 | - `Complemento` es un arreglo a pesar de lo definido en el Anexo 20 porque el XSD dice que puede tener múltiples apariciones. 76 | 77 | ```json 78 | { 79 | "Certificado": "MIIGH...imAyX", 80 | "CondicionesDePago": "CONTADO", 81 | "Fecha": "2018-01-12T08:15:01", 82 | "Folio": "11541", 83 | "FormaPago": "04", 84 | "LugarExpedicion": "76802", 85 | "MetodoPago": "PUE", 86 | "Moneda": "MXN", 87 | "NoCertificado": "00001000000401220451", 88 | "Sello": "Xt7tK...gdg==", 89 | "Serie": "H", 90 | "SubTotal": "1709.12", 91 | "TipoDeComprobante": "I", 92 | "Total": "2010.01", 93 | "Version": "3.3", 94 | "xsi:schemaLocation": "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd http://www.sat.gob.mx/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd", 95 | "Emisor": { 96 | "Nombre": "PROMOTORA OTIR SA DE CV", 97 | "RegimenFiscal": "601", 98 | "Rfc": "POT9207213D6" 99 | }, 100 | "Receptor": { 101 | "Nombre": "DAY INTERNATIONAL DE MEXICO SA DE CV", 102 | "Rfc": "DIM8701081LA", 103 | "UsoCFDI": "G03" 104 | }, 105 | "Conceptos": { 106 | "Concepto": [ 107 | { 108 | "Cantidad": "2.00", 109 | "ClaveProdServ": "90111501", 110 | "ClaveUnidad": "E48", 111 | "Descripcion": "Paquete", 112 | "Importe": "1355.67", 113 | "Unidad": "UNIDAD DE SERVICIO", 114 | "ValorUnitario": "677.83", 115 | "Impuestos": { 116 | "Traslados": { 117 | "Traslado": [ 118 | { 119 | "Base": "1355.67", 120 | "Importe": "216.91", 121 | "Impuesto": "002", 122 | "TasaOCuota": "0.160000", 123 | "TipoFactor": "Tasa" 124 | } 125 | ] 126 | } 127 | } 128 | }, 129 | { 130 | "Cantidad": "1.00", 131 | "ClaveProdServ": "90101501", 132 | "ClaveUnidad": "E48", 133 | "Descripcion": "Restaurante", 134 | "Importe": "353.45", 135 | "Unidad": "UNIDAD DE SERVICIO", 136 | "ValorUnitario": "353.45", 137 | "Impuestos": { 138 | "Traslados": { 139 | "Traslado": [ 140 | { 141 | "Base": "353.45", 142 | "Importe": "56.55", 143 | "Impuesto": "002", 144 | "TasaOCuota": "0.160000", 145 | "TipoFactor": "Tasa" 146 | } 147 | ] 148 | } 149 | } 150 | } 151 | ] 152 | }, 153 | "Impuestos": { 154 | "TotalImpuestosTrasladados": "273.46", 155 | "Traslados": { 156 | "Traslado": [ 157 | { 158 | "Importe": "273.46", 159 | "Impuesto": "002", 160 | "TasaOCuota": "0.160000", 161 | "TipoFactor": "Tasa" 162 | } 163 | ] 164 | } 165 | }, 166 | "Complemento": [ 167 | { 168 | "ImpuestosLocales": { 169 | "TotaldeRetenciones": "0.00", 170 | "TotaldeTraslados": "27.43", 171 | "version": "1.0", 172 | "TrasladosLocales": [ 173 | { 174 | "ImpLocTrasladado": "IH", 175 | "Importe": "27.43", 176 | "TasadeTraslado": "2.50" 177 | } 178 | ] 179 | }, 180 | "TimbreFiscalDigital": { 181 | "FechaTimbrado": "2018-01-12T08:17:54", 182 | "NoCertificadoSAT": "00001000000406258094", 183 | "RfcProvCertif": "DCD090706E42", 184 | "SelloCFD": "Xt7tK...gdg==", 185 | "SelloSAT": "IRy7w...6Zg==", 186 | "UUID": "CEE4BE01-ADFA-4DEB-8421-ADD60F0BEDAC", 187 | "Version": "1.1", 188 | "xsi:schemaLocation": "http://www.sat.gob.mx/TimbreFiscalDigital http://www.sat.gob.mx/sitio_internet/cfd/TimbreFiscalDigital/TimbreFiscalDigitalv11.xsd" 189 | } 190 | } 191 | ] 192 | } 193 | ``` 194 | 195 | ## Funcionamiento interno 196 | 197 | La conversión parte de un objeto `DOMDocument` que es recorrido nodo a nodo y en cada transformación genera 198 | un objeto de tipo `Nodes\Node` que contiene sus propiedades básicas de nombre, ruta, valor de texto, atributos e hijos. 199 | Los hijos (`Nodes\Children`) son una colección de nodos `Nodes\Node`. 200 | 201 | Al momento de exportar a un arreglo `Nodes\Node::toArray()` es cuando se resuelve si los nodos deben agregarse como 202 | llaves directas a objetos, o bien, como arreglos de objetos. 203 | 204 | ### Elementos con múltiples apariciones 205 | 206 | Para detectar los elementos con múltiples apariciones esta librería contiene un archivo `src/UnboundedOccursPaths.json` 207 | con el listado de rutas de elementos que pueden aparecer más de una vez. 208 | 209 | Este listado se puede generar utilizando el archivo `bin/max-occurs-paths.php`, que descargará el registro de espacios 210 | de nombres del SAT de PhpCfdi [`phpcfdi/sat-ns-registry`](https://github.com/phpcfdi/sat-ns-registry) así como todos 211 | los archivos XSD para interpretar las rutas que contienen `maxOccurs="unbounded"`. 212 | 213 | Desde 2021-03-22 se ha agregado un evento desde `phpcfdi/sat-ns-registry` para que notifique a este mismo repositorio 214 | que el registro de espacios de nombres cambió. 215 | 216 | ### Nodos con texto 217 | 218 | El texto o valor que contenga algún nodo XML es exportado a una llave de cadena vacía en el JSON resultante. 219 | Por ejemplo, el siguiente XML: 220 | 221 | ```xml 222 | 223 | 3 224 | 225 | ``` 226 | 227 | Genera el siguiente JSON: 228 | 229 | ```json 230 | { 231 | "orderIdentification": { 232 | "referenceIdentification": { 233 | "": "3", 234 | "type": "ON" 235 | } 236 | } 237 | } 238 | ``` 239 | 240 | ## Soporte 241 | 242 | Puedes obtener soporte abriendo un ticket en Github. 243 | 244 | Adicionalmente, esta librería pertenece a la comunidad [PhpCfdi](https://www.phpcfdi.com), así que puedes usar los 245 | mismos canales de comunicación para obtener ayuda de algún miembro de la comunidad. 246 | 247 | ## Compatibilidad 248 | 249 | Esta librería se mantendrá compatible con al menos la versión con 250 | [soporte activo de PHP](https://www.php.net/supported-versions.php) más reciente. 251 | 252 | También utilizamos [Versionado Semántico 2.0.0](docs/SEMVER.md) por lo que puedes usar esta librería 253 | sin temor a romper tu aplicación. 254 | 255 | ## Contribuciones 256 | 257 | Las contribuciones con bienvenidas. Por favor, revisa [CONTRIBUTING][] para más detalles 258 | y recuerda revisar el archivo de tareas pendientes [TODO][] y el archivo [CHANGELOG][]. 259 | 260 | ## Copyright and License 261 | 262 | The `phpcfdi/cfdi-to-json` library is copyright © [PhpCfdi](https://www.phpcfdi.com/) 263 | and licensed for use under the MIT License (MIT). Please see [LICENSE][] for more information. 264 | 265 | [contributing]: https://github.com/phpcfdi/cfdi-to-json/blob/main/CONTRIBUTING.md 266 | [changelog]: https://github.com/phpcfdi/cfdi-to-json/blob/main/docs/CHANGELOG.md 267 | [todo]: https://github.com/phpcfdi/cfdi-to-json/blob/main/docs/TODO.md 268 | 269 | [source]: https://github.com/phpcfdi/cfdi-to-json 270 | [php-version]: https://packagist.org/packages/phpcfdi/cfdi-to-json 271 | [discord]: https://discord.gg/aFGYXvX 272 | [release]: https://github.com/phpcfdi/cfdi-to-json/releases 273 | [license]: https://github.com/phpcfdi/cfdi-to-json/blob/main/LICENSE 274 | [build]: https://github.com/phpcfdi/cfdi-to-json/actions/workflows/build.yml?query=branch:main 275 | [reliability]:https://sonarcloud.io/component_measures?id=phpcfdi_cfdi-to-json&metric=Reliability 276 | [maintainability]: https://sonarcloud.io/component_measures?id=phpcfdi_cfdi-to-json&metric=Maintainability 277 | [coverage]: https://sonarcloud.io/component_measures?id=phpcfdi_cfdi-to-json&metric=Coverage 278 | [violations]: https://sonarcloud.io/project/issues?id=phpcfdi_cfdi-to-json&resolved=false 279 | [downloads]: https://packagist.org/packages/phpcfdi/cfdi-to-json 280 | 281 | [badge-source]: https://img.shields.io/badge/source-phpcfdi/cfdi--to--json-blue?logo=github 282 | [badge-discord]: https://img.shields.io/discord/459860554090283019?logo=discord 283 | [badge-php-version]: https://img.shields.io/packagist/php-v/phpcfdi/cfdi-to-json?logo=php 284 | [badge-release]: https://img.shields.io/github/release/phpcfdi/cfdi-to-json?logo=git 285 | [badge-license]: https://img.shields.io/github/license/phpcfdi/cfdi-to-json?logo=open-source-initiative 286 | [badge-build]: https://img.shields.io/github/actions/workflow/status/phpcfdi/cfdi-to-json/build.yml?branch=main&logo=github-actions 287 | [badge-reliability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_cfdi-to-json&metric=reliability_rating 288 | [badge-maintainability]: https://sonarcloud.io/api/project_badges/measure?project=phpcfdi_cfdi-to-json&metric=sqale_rating 289 | [badge-coverage]: https://img.shields.io/sonar/coverage/phpcfdi_cfdi-to-json/main?logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io 290 | [badge-violations]: https://img.shields.io/sonar/violations/phpcfdi_cfdi-to-json/main?format=long&logo=sonarcloud&server=https%3A%2F%2Fsonarcloud.io 291 | [badge-downloads]: https://img.shields.io/packagist/dt/phpcfdi/cfdi-to-json?logo=packagist 292 | -------------------------------------------------------------------------------- /bin/cfdi-to-json.php: -------------------------------------------------------------------------------- 1 | loadXML($contents); 15 | 16 | $factory = new Factory(); 17 | $converter = $factory->createConverter(); 18 | $node = $converter->convertXmlDocument($document); 19 | 20 | echo json_encode($node->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); 21 | return 0; 22 | }, ...$argv)); 23 | -------------------------------------------------------------------------------- /bin/check-current-max-occurs-paths.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash -e 2 | 3 | TEMPFILE="$(mktemp)" 4 | BINPATH="$(dirname $0)" 5 | 6 | echo "Creating UnboundedOccursPaths.json on $TEMPFILE" 7 | php "${BINPATH}/max-occurs-paths.php" > "$TEMPFILE" 8 | 9 | echo "Comparing to current UnboundedOccursPaths.json" 10 | diff -u -b -B "${BINPATH}/../src/UnboundedOccursPaths.json" "$TEMPFILE" 11 | 12 | echo "OK: Files match" 13 | rm "$TEMPFILE" 14 | -------------------------------------------------------------------------------- /bin/max-occurs-paths.php: -------------------------------------------------------------------------------- 1 | commandName = $commandName; 19 | $this->arguments = $arguments; 20 | } 21 | 22 | public function __invoke(): int 23 | { 24 | try { 25 | if ([] !== array_intersect($this->arguments, ['-h', '--help'])) { 26 | $this->printHelp(); 27 | return 0; 28 | } 29 | 30 | $app = new XsdMaxOccursFromNsRegistry(); 31 | echo json_encode($app->obtainPaths(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n"; 32 | 33 | return 0; 34 | } catch (Throwable $exception) { 35 | file_put_contents('php://stderr', $exception->getMessage() . PHP_EOL, FILE_APPEND); 36 | return $exception->getCode() ?: 1; 37 | } 38 | } 39 | 40 | public function printHelp(): void 41 | { 42 | echo <<commandName} [-h|--help] 48 | 49 | Common usage: 50 | php {$this->commandName} > src/UnboundedOccursPaths.json 51 | 52 | This file is part of https://github.com/phpcfdi/cfdi-to-json project 53 | 54 | HELP; 55 | } 56 | })); 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpcfdi/cfdi-to-json", 3 | "description": "Convert CFDI to JSON", 4 | "keywords": ["cfdi", "json", "mexico", "sat"], 5 | "homepage": "https://github.com/phpcfdi/cfdi-to-json", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Carlos C Soto", 10 | "email": "eclipxe13@gmail.com", 11 | "homepage": "https://github.com/phpcfdi/cfdi-to-json" 12 | } 13 | ], 14 | "support": { 15 | "source": "https://github.com/phpcfdi/cfdi-to-json", 16 | "issues": "https://github.com/phpcfdi/cfdi-to-json/issues" 17 | }, 18 | "prefer-stable": true, 19 | "config": { 20 | "optimize-autoloader": true, 21 | "preferred-install": { 22 | "*": "dist" 23 | } 24 | }, 25 | "require": { 26 | "php": ">=7.3", 27 | "ext-dom": "*", 28 | "ext-json": "*" 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^9.5" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "PhpCfdi\\CfdiToJson\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "PhpCfdi\\CfdiToJson\\Tests\\": "tests/" 41 | } 42 | }, 43 | "scripts": { 44 | "dev:build": ["@dev:fix-style", "@dev:test"], 45 | "dev:check-style": [ 46 | "@php tools/php-cs-fixer fix --dry-run --verbose", 47 | "@php tools/phpcs --colors -sp" 48 | ], 49 | "dev:fix-style": [ 50 | "@php tools/php-cs-fixer fix --verbose", 51 | "@php tools/phpcbf --colors -sp" 52 | ], 53 | "dev:test": [ 54 | "@dev:check-style", 55 | "@php vendor/bin/phpunit --testdox --verbose --stop-on-failure", 56 | "@php tools/phpstan analyse --no-progress", 57 | "@dev:infection" 58 | ], 59 | "dev:coverage": [ 60 | "@php -dzend_extension=xdebug.so -dxdebug.mode=coverage vendor/bin/phpunit --verbose --coverage-html build/coverage/html/" 61 | ], 62 | "dev:infection": [ 63 | "@php tools/infection --no-progress --no-interaction --show-mutations" 64 | ] 65 | }, 66 | "scripts-descriptions": { 67 | "dev:build": "DEV: run dev:fix-style and dev:tests, run before pull request", 68 | "dev:check-style": "DEV: search for code style errors using php-cs-fixer and phpcs", 69 | "dev:fix-style": "DEV: fix code style errors using php-cs-fixer and phpcbf", 70 | "dev:test": "DEV: run dev:check-style, phpunit and phpstan", 71 | "dev:coverage": "DEV: run phpunit with xdebug and storage coverage in build/coverage/html/", 72 | "dev:infection": "DEV: run mutation tests using infection" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## Acerca de SemVer 4 | 5 | Usamos [Versionado Semántico 2.0.0](SEMVER.md) por lo que puedes usar esta librería sin temor a romper tu aplicación. 6 | 7 | Pueden aparecer cambios no liberados que se integran a la rama principal, pero no ameritan una nueva liberación de 8 | versión aunque sí su incorporación en la rama principal de trabajo, generalmente se tratan de cambios en el desarrollo. 9 | 10 | ## Cambios no liberados en una versión 11 | 12 | ## Listado de cambios 13 | 14 | ### Versión 0.3.5 2024-07-11 15 | 16 | - Se actualiza el archivo `UnboundedOccursPaths.json` porque se incluyó el nuevo complemento *Carta Porte 3.1*. 17 | 18 | Los siguientes cambios no modifican el código fuente. 19 | 20 | - Se actualiza el archivo de licencia. 21 | - En los flujos de trabajo de GitHub: 22 | - Se agrega PHP 8.3 a la matriz de pruebas. 23 | - Se ejecutan los trabajos en PHP 8.3. 24 | - Se utilizan las acciones de GitHub versión 4. 25 | - Se actualizan las herramientas de desarrollo. 26 | 27 | ### Versión 0.3.4 2023-10-22 28 | 29 | - Se actualiza el archivo `UnboundedOccursPaths.json` porque se incluyó el nuevo complemento *Carta Porte 3.0*. 30 | 31 | ### Versión 0.3.3 2022-06-14 32 | 33 | Cuando se busca abrir un archivo para obtener las rutas sin límite, se lanza una excepción si no se pudo 34 | abrir el archivo, anteriormente se lanzaba un *warning* de la función `get_file_contents`. 35 | 36 | Los siguientes cambios no modifican el código fuente. 37 | 38 | - Se actualiza el archivo de licencia. 39 | - Se elimina una conversión a cadena de texto innecesaria introducida para satisfacer a PHPStan. 40 | - Se agrega una prueba para comprobar que al generar paths repetidos se devuelve un arreglo secuencial. 41 | - Se corrige la insignia de construcción en el archivo `README.md`. 42 | - En los flujos de trabajo de GitHub: 43 | - Se agrega PHP 8.2 a la matriz de pruebas. 44 | - Se ejecutan los trabajos en PHP 8.2. 45 | - Se agrega la habilidad de ejecutar un flujo de trabajo a petición. 46 | - Se sustituye la directiva `::set-output` con `$GITHUB_OUTPUT`. 47 | - Se utilizan las acciones de GitHub versión 3. 48 | - Se corrige la configuración de SonarCloud. 49 | - Se actualizan las herramientas de desarrollo. 50 | 51 | ### Versión 0.3.2 2022-10-01 52 | 53 | Permite la lectura del contenido de texto de los nodos, esto es porque el "Complemento Detallista" 54 | usa este tipo de estructura. Estos contenidos se consideran como espacios en blanco colapsados. 55 | 56 | Gracias `@gam04` por tu contribución. 57 | 58 | #### Cambios al entorno de desarrollo 59 | 60 | - Se actualizan las herramientas de desarrollo. 61 | - Se actualiza el flujo de trabajo de integración continua: 62 | - Se utilizan las GitHub Actions versión 3. 63 | - Se corren los procesos en PHP 8.1. 64 | - Se elimina la dependencia de `composer` donde no se usa. 65 | - Se actualiza el archivo de configuración de `php-cs-fixer`. 66 | - Se agrega la configuración en Git para que los finales de línea solo sean `LF`. 67 | - Se integra el proyecto a [SonarCloud](https://sonarcloud.io/code?id=phpcfdi_cfdi-to-json). 68 | - Se elimina la integración con Scrutinizer CI a favor de SonarCloud. ¡Gracias Scrutinizer CI!. 69 | 70 | ### Versión 0.3.1 2022-04-04 71 | 72 | La herramienta PHPStan detectó un posible error de mal uso de la propiedad `DOMElement::localName` donde 73 | puede ser de los tipos `string` o `null`, pero solo se consideraba `string`. 74 | 75 | La herramienta PHPStan detectó un posible error de mal uso de la propiedad `DOMElement::parentNode` donde 76 | se verifica que la propiedad ahora sea de tipo `DOMElement`. 77 | 78 | ### Versión 0.3.0 2022-03-16 79 | 80 | Se ha descubierto un error en donde dos especificaciones de esquemas del SAT pueden chocar 81 | y en una definición tener nodos que no son múltiples y en otra versión que sí lo son. 82 | Por ejemplo, en CFDI 3.3 el nodo `CfdiRelacionados` solo puede aparecer 1 vez, 83 | mientras que en CFDI 4.0 su número de apariciones es ilimitado. 84 | 85 | Se corrige esta situación cambiando la forma de generar las rutas del archivo leído 86 | y cambiando las rutas extraídas de los archivos XSD. En ambos casos ahora se antepone 87 | el espacio de nombres XML, por ejemplo: `{http://www.sat.gob.mx/cfd/4}/Comprobante/CfdiRelacionados`. 88 | 89 | De igual forma, ahora el archivo `UnboundedOccursPaths.json` solo contiene entradas únicas y ordenadas. 90 | De esta forma la búsqueda de una coincidencia es mucho más rápida al usar las llaves de un arreglo, 91 | y será más fácil entender los cambios que ocurran en el archivo. 92 | 93 | Además, se le ha dado mantenimiento al proyecto actualizando los archivos de desarrollo, 94 | dependencias de las herramientas de desarrollo, flujo de trabajo de integración continua, 95 | licencia (feliz 2022) y probando la compatibilidad con PHP 8.1. 96 | 97 | ### Versión 0.2.2 2021-11-18 98 | 99 | - Se actualiza el archivo `UnboundedOccursPaths.json` porque se incluyó el nuevo complemento `CartaPorte 2.0`. 100 | 101 | ### Versión 0.2.1 2021-05-17 102 | 103 | - Se actualiza el archivo `UnboundedOccursPaths.json` porque se incluyó el nuevo complemento `CartaPorte`. 104 | 105 | Cambios en desarrollo 106 | 107 | - Se actualizó la herramienta `php-cs-fixer` a `^3.0`. 108 | - Se actualizó el archivo de configuración de PHPUnit a uno más apegado al recomendado. 109 | - Se agrega a GitHub Actions un flujo de trabajo de construcción del proyecto. 110 | - Se agrega a GitHub Actions un flujo de trabajo de actualización y PR desde `phpcfdi/sat-ns-registry`. 111 | - Se elimina la integración con Travis-CI. Gracias. 112 | 113 | ### Versión 0.2.0 2021-03-22 114 | 115 | - Se extrae la lógica del conteo de hijos de `Nodes\Children` a `Nodes\KeysCounter`. 116 | - Se corrigen los test y las llamadas de `file_get_contents`. 117 | - Conseguir el 100% de testeo. 118 | - Agregar a Travis-CI la comprobación de que el archivo `src/UnboundedOccursPaths.json` no ha cambiado. 119 | - Usar `phive` para las herramientas de desarrollo. 120 | - Se agrega `infection` para correr pruebas de mutación. No es mandatorio por el momento. 121 | 122 | ### Versión 0.1.0 2021-02-02 ¡Feliz cumpleaños Dany! 123 | 124 | - Primera liberación para su uso público. 125 | -------------------------------------------------------------------------------- /docs/SEMVER.md: -------------------------------------------------------------------------------- 1 | # SEMVER 2 | 3 | Respetamos el estándar [Versionado Semántico 2.0.0](https://semver.org/lang/es/). 4 | 5 | En resumen, [SemVer](https://semver.org/) es un sistema de versiones de tres componentes `X.Y.Z` 6 | que nombraremos así: ` Breaking . Feature . Fix `, donde: 7 | 8 | - `Breaking`: Rompe la compatibilidad de código con versiones anteriores. 9 | - `Feature`: Agrega una nueva característica que es compatible con lo anterior. 10 | - `Fix`: Incluye algún cambio (generalmente correcciones) que no agregan nueva funcionalidad. 11 | 12 | ## Composer 13 | 14 | La herramienta [Composer](https://getcomposer.org/) es un gestor de dependencias en proyectos para PHP. 15 | Este gestor usa las [reglas](https://getcomposer.org/doc/articles/versions.md) 16 | de versionado semántico para instalar y actualizar paquetes. 17 | 18 | Te recomendamos instalar dependencias de librerías (no frameworks) con *Caret Version Range*. 19 | Por ejemplo: `"vendor/package": "^2.5"`. 20 | 21 | Esto significa que: 22 | 23 | - no debe actualizar a versiones `3.x.x` 24 | - no debe utilizar ninguna versión menor a `2.5.0` 25 | 26 | ## Versiones 0.x.y no rompe compatibilidad 27 | 28 | Las versiones que inician con cero, por ejemplo `0.y.z`, no se ajustan a las reglas de versionado. 29 | Se considera que estas versiones son previas a la madurez del proyecto y por lo tanto 30 | introducen cambios sin previo aviso. 31 | 32 | ## `@internal` no rompe compatibilidad 33 | 34 | Si la librería contiene elementos marcados como `@internal` significa que no deben ser utilizados 35 | por tu código. Son partes de código internos de la librería. 36 | Por lo tanto, no se consideran breaking changes. 37 | 38 | Cuando un elemento es `@internal`, dicho elemento: 39 | 40 | - no debe ser una entrada (parámetro) 41 | - no debe ser una salida (retorno) 42 | - no debe exponer funcionalidades en los objetos públicos (rasgos) 43 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # phpcfdi/cfdi-to-json Lista de tareas pendientes 2 | 3 | ## Ideas, por favor, levante un ticket para discutirlas 4 | 5 | - ¿Los elementos `Comprobante/Complemento` deberían colapsarse? 6 | 7 | - Hacer que `infection` se ejecute con un mínimo requerido en la integración contínua. 8 | 9 | - Rehacer la construcción de `PhpCfdi\CfdiToJson\Nodes\Node` para que `$value` no sea opcional. 10 | Se puso de esta forma para no romper la compatiblidad con implementaciones actuales. 11 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "build\/infection.log" 9 | }, 10 | "mutators": { 11 | "@default": true 12 | }, 13 | "initialTestsPhpOptions": "-dzend_extension=xdebug.so -dxdebug.mode=coverage" 14 | } 15 | -------------------------------------------------------------------------------- /src/CfdiToDataNode.php: -------------------------------------------------------------------------------- 1 | unboundedOccursPaths = $unboundedOccursPaths; 22 | } 23 | 24 | public function getUnboundedOccursPaths(): UnboundedOccursPaths 25 | { 26 | return $this->unboundedOccursPaths; 27 | } 28 | 29 | public function convertXmlContent(string $xmlContents): Nodes\Node 30 | { 31 | $document = new DOMDocument(); 32 | $document->loadXML($xmlContents); 33 | return $this->convertXmlDocument($document); 34 | } 35 | 36 | public function convertXmlDocument(DOMDocument $document): Nodes\Node 37 | { 38 | if (null === $document->documentElement) { 39 | throw new InvalidArgumentException('The DOMDocument does not have a root element'); 40 | } 41 | return $this->convertElementoToDataNode($document->documentElement); 42 | } 43 | 44 | private function convertElementoToDataNode(DOMElement $element): Nodes\Node 45 | { 46 | $path = $this->buildPathForElement($element); 47 | $value = $this->extractValue($element); 48 | 49 | // children to internal struct 50 | $convertionChildren = new Nodes\Children($this->unboundedOccursPaths); 51 | foreach ($element->childNodes as $childElement) { 52 | if ($childElement instanceof DOMElement) { 53 | $convertionChildren->append( 54 | $this->convertElementoToDataNode($childElement), 55 | ); 56 | } 57 | } 58 | 59 | return new Nodes\Node( 60 | strval($element->localName), 61 | $path, 62 | $this->obtainAttributes($element), 63 | $convertionChildren, 64 | $value 65 | ); 66 | } 67 | 68 | /** 69 | * @param DOMElement $element 70 | * @return array 71 | */ 72 | private function obtainAttributes(DOMElement $element): array 73 | { 74 | /** 75 | * phpstan does not recognize that DOMElement::attributes cannot be null 76 | * @phpstan-var DOMNamedNodeMap $elementAttributes 77 | */ 78 | $elementAttributes = $element->attributes; 79 | 80 | $attributes = []; 81 | foreach ($elementAttributes as $attribute) { 82 | $attributes[$attribute->nodeName] = $attribute->value; 83 | } 84 | return $attributes; 85 | } 86 | 87 | private function buildPathForElement(DOMElement $element): string 88 | { 89 | $namespace = $element->namespaceURI ?: ''; 90 | $parentsStack = []; 91 | 92 | for ($current = $element; null !== $current; $current = $current->parentNode) { 93 | if ($namespace !== $current->namespaceURI) { 94 | break; 95 | } 96 | 97 | $parentsStack[] = $current->localName; 98 | } 99 | 100 | return sprintf('{%s}/%s', $namespace, implode('/', array_reverse($parentsStack))); 101 | } 102 | 103 | private function extractValue(DOMElement $element): string 104 | { 105 | $values = []; 106 | foreach ($element->childNodes as $childElement) { 107 | if (! $childElement instanceof DOMText) { 108 | continue; 109 | } 110 | $values[] = $childElement->wholeText; 111 | } 112 | return (string) preg_replace(['/\s+/', '/^ +/', '/ +$/'], [' ', '', ''], implode('', $values)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | unboundedOccursPaths = $unboundedOccursPaths ?? $this->createDefaultUnboundedOccursPaths(); 18 | } 19 | 20 | public function createConverter(): CfdiToDataNode 21 | { 22 | $unboundedOccursPaths = $this->getUnboundedOccursPaths(); 23 | return new CfdiToDataNode($unboundedOccursPaths); 24 | } 25 | 26 | public function getUnboundedOccursPaths(): UnboundedOccursPaths 27 | { 28 | return $this->unboundedOccursPaths; 29 | } 30 | 31 | public function createDefaultUnboundedOccursPaths(): UnboundedOccursPaths 32 | { 33 | return $this->createUnboundedOccursPathsUsingJsonFile(__DIR__ . '/UnboundedOccursPaths.json'); 34 | } 35 | 36 | public function createUnboundedOccursPathsUsingJsonFile(string $sourceFile): UnboundedOccursPaths 37 | { 38 | /** @noinspection PhpUsageOfSilenceOperatorInspection */ 39 | $contents = @file_get_contents($sourceFile); 40 | if (false === $contents) { 41 | throw new LogicException("Unable to open file $sourceFile"); 42 | } 43 | 44 | try { 45 | $unboundedOccursPaths = $this->createUnboundedOccursPathsUsingJsonSource($contents); 46 | } catch (JsonException | LogicException $exception) { 47 | throw new LogicException("The file $sourceFile has invalid contents", 0, $exception); 48 | } 49 | 50 | return $unboundedOccursPaths; 51 | } 52 | 53 | /** 54 | * @param string $contents 55 | * @return UnboundedOccursPaths 56 | * @throws JsonException|LogicException 57 | */ 58 | public function createUnboundedOccursPathsUsingJsonSource(string $contents): UnboundedOccursPaths 59 | { 60 | $sourcePaths = json_decode($contents, true, 512, JSON_THROW_ON_ERROR); 61 | 62 | if (! is_array($sourcePaths)) { 63 | throw new LogicException('JSON does not contains an array of entries'); 64 | } 65 | 66 | foreach ($sourcePaths as $index => $sourcePath) { 67 | if (! is_string($sourcePath)) { 68 | throw new LogicException("JSON does not contains a string on index $index"); 69 | } 70 | } 71 | 72 | return new UnboundedOccursPaths(...$sourcePaths); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/JsonConverter.php: -------------------------------------------------------------------------------- 1 | createConverter(); 37 | $dataNode = $converter->convertXmlContent($cfdi); 38 | return $dataNode->toArray(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Nodes/Children.php: -------------------------------------------------------------------------------- 1 | unboundedOccursPaths = $unboundedOccursPaths; 23 | $this->keysCounter = new KeysCounter(); 24 | } 25 | 26 | public function append(Node $child): void 27 | { 28 | $this->children[] = $child; 29 | $this->keysCounter->register($child->getKey()); 30 | } 31 | 32 | public function isChildrenMultiple(Node $child): bool 33 | { 34 | return $this->keysCounter->hasMany($child->getKey()) 35 | || $this->unboundedOccursPaths->match($child->getPath()); 36 | } 37 | 38 | /** 39 | * @return array 40 | * @phpstan-ignore-next-line 41 | */ 42 | public function toArray(): array 43 | { 44 | $children = []; 45 | 46 | foreach ($this->children as $item) { 47 | if ($this->isChildrenMultiple($item)) { 48 | $children[$item->getKey()][] = $item->toArray(); 49 | } else { 50 | $children[$item->getKey()] = $item->toArray(); 51 | } 52 | } 53 | 54 | return $children; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Nodes/KeysCounter.php: -------------------------------------------------------------------------------- 1 | */ 10 | private $counts; 11 | 12 | public function register(string $key): void 13 | { 14 | $this->counts[$key] = $this->get($key) + 1; 15 | } 16 | 17 | public function get(string $key): int 18 | { 19 | return $this->counts[$key] ?? 0; 20 | } 21 | 22 | public function hasMany(string $key): bool 23 | { 24 | return $this->get($key) > 1; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Nodes/Node.php: -------------------------------------------------------------------------------- 1 | */ 19 | private $attributes; 20 | 21 | /** @var string */ 22 | private $value; 23 | 24 | /** 25 | * @param string $key 26 | * @param string $path 27 | * @param array $attributes 28 | * @param Children $children 29 | * @param string $value 30 | */ 31 | public function __construct(string $key, string $path, array $attributes, Children $children, string $value = '') 32 | { 33 | $this->key = $key; 34 | $this->path = $path; 35 | $this->attributes = $attributes; 36 | $this->children = $children; 37 | $this->value = $value; 38 | } 39 | 40 | public function getKey(): string 41 | { 42 | return $this->key; 43 | } 44 | 45 | public function getPath(): string 46 | { 47 | return $this->path; 48 | } 49 | 50 | public function getValue(): string 51 | { 52 | return $this->value; 53 | } 54 | 55 | /** 56 | * @return array 57 | * @phpstan-ignore-next-line 58 | */ 59 | public function toArray(): array 60 | { 61 | $textArray = ('' !== $this->getValue()) ? ['' => $this->getValue()] : []; 62 | return $textArray + $this->attributes + $this->children->toArray(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/UnboundedOccursPaths.json: -------------------------------------------------------------------------------- 1 | [ 2 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/FiguraTransporte/TiposFigura", 3 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/FiguraTransporte/TiposFigura/PartesTransporte", 4 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/Mercancia", 5 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/Mercancia/CantidadTransporta", 6 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/Mercancia/GuiasIdentificacion", 7 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/Mercancia/Pedimentos", 8 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/TransporteFerroviario/Carro", 9 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/TransporteFerroviario/Carro/Contenedor", 10 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/TransporteFerroviario/DerechosDePaso", 11 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Mercancias/TransporteMaritimo/Contenedor", 12 | "{http://www.sat.gob.mx/CartaPorte20}/CartaPorte/Ubicaciones/Ubicacion", 13 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/FiguraTransporte/TiposFigura", 14 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/FiguraTransporte/TiposFigura/PartesTransporte", 15 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/Mercancia", 16 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/Mercancia/CantidadTransporta", 17 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/Mercancia/DocumentacionAduanera", 18 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/Mercancia/GuiasIdentificacion", 19 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/TransporteFerroviario/Carro", 20 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/TransporteFerroviario/Carro/Contenedor", 21 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/TransporteFerroviario/DerechosDePaso", 22 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Mercancias/TransporteMaritimo/Contenedor", 23 | "{http://www.sat.gob.mx/CartaPorte30}/CartaPorte/Ubicaciones/Ubicacion", 24 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/FiguraTransporte/TiposFigura", 25 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/FiguraTransporte/TiposFigura/PartesTransporte", 26 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/Mercancia", 27 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/Mercancia/CantidadTransporta", 28 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/Mercancia/DocumentacionAduanera", 29 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/Mercancia/GuiasIdentificacion", 30 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/TransporteFerroviario/Carro", 31 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/TransporteFerroviario/Carro/Contenedor", 32 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/TransporteFerroviario/DerechosDePaso", 33 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Mercancias/TransporteMaritimo/Contenedor", 34 | "{http://www.sat.gob.mx/CartaPorte31}/CartaPorte/Ubicaciones/Ubicacion", 35 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/FiguraTransporte/Arrendatario", 36 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/FiguraTransporte/Notificado", 37 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/FiguraTransporte/Operadores", 38 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/FiguraTransporte/Operadores/Operador", 39 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/FiguraTransporte/Propietario", 40 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/Mercancias/Mercancia", 41 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/Mercancias/Mercancia/CantidadTransporta", 42 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/Mercancias/TransporteFerroviario/Carro", 43 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/Mercancias/TransporteFerroviario/Carro/Contenedor", 44 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/Mercancias/TransporteFerroviario/DerechosDePaso", 45 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/Mercancias/TransporteMaritimo/Contenedor", 46 | "{http://www.sat.gob.mx/CartaPorte}/CartaPorte/Ubicaciones/Ubicacion", 47 | "{http://www.sat.gob.mx/ComercioExterior11}/ComercioExterior/Destinatario", 48 | "{http://www.sat.gob.mx/ComercioExterior11}/ComercioExterior/Destinatario/Domicilio", 49 | "{http://www.sat.gob.mx/ComercioExterior11}/ComercioExterior/Mercancias/Mercancia", 50 | "{http://www.sat.gob.mx/ComercioExterior11}/ComercioExterior/Mercancias/Mercancia/DescripcionesEspecificas", 51 | "{http://www.sat.gob.mx/ComercioExterior11}/ComercioExterior/Propietario", 52 | "{http://www.sat.gob.mx/ComercioExterior20}/ComercioExterior/Destinatario", 53 | "{http://www.sat.gob.mx/ComercioExterior20}/ComercioExterior/Destinatario/Domicilio", 54 | "{http://www.sat.gob.mx/ComercioExterior20}/ComercioExterior/Mercancias/Mercancia", 55 | "{http://www.sat.gob.mx/ComercioExterior20}/ComercioExterior/Mercancias/Mercancia/DescripcionesEspecificas", 56 | "{http://www.sat.gob.mx/ComercioExterior20}/ComercioExterior/Propietario", 57 | "{http://www.sat.gob.mx/ComercioExterior}/ComercioExterior/Mercancias/Mercancia", 58 | "{http://www.sat.gob.mx/ComercioExterior}/ComercioExterior/Mercancias/Mercancia/DescripcionesEspecificas", 59 | "{http://www.sat.gob.mx/ConsumoDeCombustibles11}/ConsumoDeCombustibles/Conceptos/ConceptoConsumoDeCombustibles", 60 | "{http://www.sat.gob.mx/ConsumoDeCombustibles11}/ConsumoDeCombustibles/Conceptos/ConceptoConsumoDeCombustibles/Determinados/Determinado", 61 | "{http://www.sat.gob.mx/EstadoDeCuentaCombustible12}/EstadoDeCuentaCombustible/Conceptos/ConceptoEstadoDeCuentaCombustible", 62 | "{http://www.sat.gob.mx/EstadoDeCuentaCombustible12}/EstadoDeCuentaCombustible/Conceptos/ConceptoEstadoDeCuentaCombustible/Traslados/Traslado", 63 | "{http://www.sat.gob.mx/EstadoDeCuentaCombustible}/EstadoDeCuentaCombustible/Conceptos/ConceptoEstadoDeCuentaCombustible", 64 | "{http://www.sat.gob.mx/EstadoDeCuentaCombustible}/EstadoDeCuentaCombustible/Conceptos/ConceptoEstadoDeCuentaCombustible/Traslados/Traslado", 65 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion", 66 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion/Actividades", 67 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion/Actividades/SubActividades", 68 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion/Actividades/SubActividades/Tareas", 69 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion/CentroCostos", 70 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion/CentroCostos/Yacimientos", 71 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion/CentroCostos/Yacimientos/Pozos", 72 | "{http://www.sat.gob.mx/GastosHidrocarburos10}/GastosHidrocarburos/Erogacion/DocumentoRelacionado", 73 | "{http://www.sat.gob.mx/IngresosHidrocarburos10}/IngresosHidrocarburos/DocumentoRelacionado", 74 | "{http://www.sat.gob.mx/Pagos20}/Pagos/Pago", 75 | "{http://www.sat.gob.mx/Pagos20}/Pagos/Pago/DoctoRelacionado", 76 | "{http://www.sat.gob.mx/Pagos20}/Pagos/Pago/DoctoRelacionado/ImpuestosDR/RetencionesDR/RetencionDR", 77 | "{http://www.sat.gob.mx/Pagos20}/Pagos/Pago/DoctoRelacionado/ImpuestosDR/TrasladosDR/TrasladoDR", 78 | "{http://www.sat.gob.mx/Pagos20}/Pagos/Pago/ImpuestosP/RetencionesP/RetencionP", 79 | "{http://www.sat.gob.mx/Pagos20}/Pagos/Pago/ImpuestosP/TrasladosP/TrasladoP", 80 | "{http://www.sat.gob.mx/Pagos}/Pagos/Pago", 81 | "{http://www.sat.gob.mx/Pagos}/Pagos/Pago/DoctoRelacionado", 82 | "{http://www.sat.gob.mx/Pagos}/Pagos/Pago/Impuestos", 83 | "{http://www.sat.gob.mx/Pagos}/Pagos/Pago/Impuestos/Retenciones/Retencion", 84 | "{http://www.sat.gob.mx/Pagos}/Pagos/Pago/Impuestos/Traslados/Traslado", 85 | "{http://www.sat.gob.mx/aerolineas}/Aerolineas/OtrosCargos/Cargo", 86 | "{http://www.sat.gob.mx/cfd/2}/Comprobante/Conceptos/Concepto", 87 | "{http://www.sat.gob.mx/cfd/2}/Comprobante/Conceptos/Concepto/InformacionAduanera", 88 | "{http://www.sat.gob.mx/cfd/2}/Comprobante/Conceptos/Concepto/Parte", 89 | "{http://www.sat.gob.mx/cfd/2}/Comprobante/Conceptos/Concepto/Parte/InformacionAduanera", 90 | "{http://www.sat.gob.mx/cfd/2}/Comprobante/Emisor/RegimenFiscal", 91 | "{http://www.sat.gob.mx/cfd/2}/Comprobante/Impuestos/Retenciones/Retencion", 92 | "{http://www.sat.gob.mx/cfd/2}/Comprobante/Impuestos/Traslados/Traslado", 93 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/CfdiRelacionados/CfdiRelacionado", 94 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Complemento", 95 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Conceptos/Concepto", 96 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Conceptos/Concepto/Impuestos/Retenciones/Retencion", 97 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Conceptos/Concepto/Impuestos/Traslados/Traslado", 98 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Conceptos/Concepto/InformacionAduanera", 99 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Conceptos/Concepto/Parte", 100 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Conceptos/Concepto/Parte/InformacionAduanera", 101 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Emisor/RegimenFiscal", 102 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Impuestos/Retenciones/Retencion", 103 | "{http://www.sat.gob.mx/cfd/3}/Comprobante/Impuestos/Traslados/Traslado", 104 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/CfdiRelacionados", 105 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/CfdiRelacionados/CfdiRelacionado", 106 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Conceptos/Concepto", 107 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Conceptos/Concepto/CuentaPredial", 108 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Conceptos/Concepto/Impuestos/Retenciones/Retencion", 109 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Conceptos/Concepto/Impuestos/Traslados/Traslado", 110 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Conceptos/Concepto/InformacionAduanera", 111 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Conceptos/Concepto/Parte", 112 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Conceptos/Concepto/Parte/InformacionAduanera", 113 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Impuestos/Retenciones/Retencion", 114 | "{http://www.sat.gob.mx/cfd/4}/Comprobante/Impuestos/Traslados/Traslado", 115 | "{http://www.sat.gob.mx/consumodecombustibles}/ConsumoDeCombustibles/Conceptos/ConceptoConsumoDeCombustibles", 116 | "{http://www.sat.gob.mx/consumodecombustibles}/ConsumoDeCombustibles/Conceptos/ConceptoConsumoDeCombustibles/Determinados/Determinado", 117 | "{http://www.sat.gob.mx/detallista}/detallista/Customs", 118 | "{http://www.sat.gob.mx/detallista}/detallista/TotalAllowanceCharge", 119 | "{http://www.sat.gob.mx/detallista}/detallista/lineItem", 120 | "{http://www.sat.gob.mx/detallista}/detallista/lineItem/Customs", 121 | "{http://www.sat.gob.mx/detallista}/detallista/lineItem/aditionalQuantity", 122 | "{http://www.sat.gob.mx/detallista}/detallista/lineItem/alternateTradeItemIdentification", 123 | "{http://www.sat.gob.mx/detallista}/detallista/shipTo/nameAndAddress/city", 124 | "{http://www.sat.gob.mx/detallista}/detallista/shipTo/nameAndAddress/name", 125 | "{http://www.sat.gob.mx/detallista}/detallista/shipTo/nameAndAddress/postalCode", 126 | "{http://www.sat.gob.mx/detallista}/detallista/shipTo/nameAndAddress/streetAddressOne", 127 | "{http://www.sat.gob.mx/ecb}/EstadoDeCuentaBancario/Movimientos/MovimientoECB", 128 | "{http://www.sat.gob.mx/ecb}/EstadoDeCuentaBancario/Movimientos/MovimientoECBFiscal", 129 | "{http://www.sat.gob.mx/ecc}/EstadoDeCuentaCombustible/Conceptos/ConceptoEstadoDeCuentaCombustible", 130 | "{http://www.sat.gob.mx/ecc}/EstadoDeCuentaCombustible/Conceptos/ConceptoEstadoDeCuentaCombustible/Traslados/Traslado", 131 | "{http://www.sat.gob.mx/esquemas/retencionpago/1/PlataformasTecnologicas10}/ServiciosPlataformasTecnologicas/Servicios/DetallesDelServicio", 132 | "{http://www.sat.gob.mx/esquemas/retencionpago/1}/Retenciones/Totales/ImpRetenidos", 133 | "{http://www.sat.gob.mx/esquemas/retencionpago/2}/Retenciones/Totales/ImpRetenidos", 134 | "{http://www.sat.gob.mx/implocal}/ImpuestosLocales/RetencionesLocales", 135 | "{http://www.sat.gob.mx/implocal}/ImpuestosLocales/TrasladosLocales", 136 | "{http://www.sat.gob.mx/ine}/INE/Entidad/Contabilidad", 137 | "{http://www.sat.gob.mx/leyendasFiscales}/LeyendasFiscales/Leyenda", 138 | "{http://www.sat.gob.mx/nomina12}/Nomina/Deducciones/Deduccion", 139 | "{http://www.sat.gob.mx/nomina12}/Nomina/Incapacidades/Incapacidad", 140 | "{http://www.sat.gob.mx/nomina12}/Nomina/OtrosPagos/OtroPago", 141 | "{http://www.sat.gob.mx/nomina12}/Nomina/Percepciones/Percepcion", 142 | "{http://www.sat.gob.mx/nomina12}/Nomina/Percepciones/Percepcion/HorasExtra", 143 | "{http://www.sat.gob.mx/nomina12}/Nomina/Receptor/SubContratacion", 144 | "{http://www.sat.gob.mx/nomina}/Nomina/Deducciones/Deduccion", 145 | "{http://www.sat.gob.mx/nomina}/Nomina/HorasExtras/HorasExtra", 146 | "{http://www.sat.gob.mx/nomina}/Nomina/Incapacidades/Incapacidad", 147 | "{http://www.sat.gob.mx/nomina}/Nomina/Percepciones/Percepcion", 148 | "{http://www.sat.gob.mx/notariospublicos}/NotariosPublicos/DatosAdquiriente/DatosAdquirientesCopSC/DatosAdquirienteCopSC", 149 | "{http://www.sat.gob.mx/notariospublicos}/NotariosPublicos/DatosEnajenante/DatosEnajenantesCopSC/DatosEnajenanteCopSC", 150 | "{http://www.sat.gob.mx/notariospublicos}/NotariosPublicos/DescInmuebles/DescInmueble", 151 | "{http://www.sat.gob.mx/renovacionysustitucionvehiculos}/renovacionysustitucionvehiculos/DecretoRenovVehicular/VehiculosUsadosEnajenadoPermAlFab", 152 | "{http://www.sat.gob.mx/spei}/Complemento_SPEI/SPEI_Tercero", 153 | "{http://www.sat.gob.mx/terceros}/PorCuentadeTerceros/Impuestos/Retenciones/Retencion", 154 | "{http://www.sat.gob.mx/terceros}/PorCuentadeTerceros/Impuestos/Traslados/Traslado", 155 | "{http://www.sat.gob.mx/terceros}/PorCuentadeTerceros/Parte", 156 | "{http://www.sat.gob.mx/terceros}/PorCuentadeTerceros/Parte/InformacionAduanera", 157 | "{http://www.sat.gob.mx/valesdedespensa}/ValesDeDespensa/Conceptos/Concepto", 158 | "{http://www.sat.gob.mx/vehiculousado}/VehiculoUsado/InformacionAduanera", 159 | "{http://www.sat.gob.mx/ventavehiculos}/VentaVehiculos/InformacionAduanera", 160 | "{http://www.sat.gob.mx/ventavehiculos}/VentaVehiculos/Parte", 161 | "{http://www.sat.gob.mx/ventavehiculos}/VentaVehiculos/Parte/InformacionAduanera", 162 | "{}/Comprobante/Conceptos/", 163 | "{}/Comprobante/Impuestos/Retenciones/Retencion", 164 | "{}/Comprobante/Impuestos/Traslados/Traslado" 165 | ] 166 | -------------------------------------------------------------------------------- /src/UnboundedOccursPaths.php: -------------------------------------------------------------------------------- 1 | */ 10 | private $paths; 11 | 12 | public function __construct(string ...$paths) 13 | { 14 | $this->paths = array_flip($paths); 15 | } 16 | 17 | public function match(string $path): bool 18 | { 19 | return array_key_exists($path, $this->paths); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/XsdMaxOccurs/Downloader.php: -------------------------------------------------------------------------------- 1 | loadXML($xsdContents); 24 | 25 | $this->targetNamespace = $this->findTargetNamespace($document); 26 | 27 | return array_merge( 28 | $this->obtainPathsForXPathQuery($document, '//x:element[@maxOccurs="unbounded"]'), 29 | $this->obtainPathsForXPathQuery($document, '//x:sequence[@maxOccurs="unbounded"]/x:element'), 30 | $this->obtainPathsForXPathQuery($document, '//x:choice[@maxOccurs="unbounded"]/x:element'), 31 | ); 32 | } 33 | 34 | /** 35 | * @param DOMDocument $document 36 | * @param string $query 37 | * @return string[] 38 | */ 39 | private function obtainPathsForXPathQuery(DOMDocument $document, string $query): array 40 | { 41 | $paths = []; 42 | $xpath = new DOMXPath($document); 43 | $xpath->registerNamespace('x', self::NS_XMLSCHEMA); 44 | $nodes = $xpath->query($query) ?: new DOMNodeList(); 45 | foreach ($nodes as $node) { 46 | if ($node instanceof DOMElement) { 47 | $paths[] = $this->obtainPathForElement($node); 48 | } 49 | } 50 | return $paths; 51 | } 52 | 53 | private function obtainPathForElement(DOMElement $xsElement): string 54 | { 55 | $pathItems = []; 56 | 57 | while (null !== $xsElement) { 58 | $pathItems[] = $xsElement->getAttribute('name'); 59 | $xsElement = $this->findParentElement($xsElement); 60 | } 61 | 62 | return sprintf('{%s}/%s', $this->targetNamespace, implode('/', array_reverse($pathItems))); 63 | } 64 | 65 | private function findParentElement(DOMElement $node): ?DOMElement 66 | { 67 | for ($node = $node->parentNode; $node instanceof DOMElement; $node = $node->parentNode) { 68 | if ('element' !== $node->localName || self::NS_XMLSCHEMA !== $node->namespaceURI) { 69 | continue; 70 | } 71 | return $node; 72 | } 73 | return null; 74 | } 75 | 76 | private function findTargetNamespace(DOMDocument $document): string 77 | { 78 | $xpath = new DOMXPath($document); 79 | $xpath->registerNamespace('x', self::NS_XMLSCHEMA); 80 | /** @var DOMNodeList $targets */ 81 | $targets = $xpath->query('/x:schema/@targetNamespace') ?: new DOMNodeList(); 82 | /** @var DOMAttr|null $firstTarget */ 83 | $firstTarget = $targets->item(0); 84 | return $firstTarget->value ?? ''; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/XsdMaxOccurs/FinderInterface.php: -------------------------------------------------------------------------------- 1 | registryUrl = $registryUrl; 29 | $this->downloader = $downloader ?? new Downloader(); 30 | $this->finder = $finder ?? new Finder(); 31 | } 32 | 33 | public function getRegistryUrl(): string 34 | { 35 | return $this->registryUrl; 36 | } 37 | 38 | public function getFinder(): FinderInterface 39 | { 40 | return $this->finder; 41 | } 42 | 43 | /** @return string[] */ 44 | public function obtainPaths(): array 45 | { 46 | $registryContents = $this->downloadUrl($this->getRegistryUrl()); 47 | $entries = json_decode($registryContents, true, JSON_THROW_ON_ERROR); 48 | if (! is_array($entries)) { 49 | throw new RuntimeException('Unexpected registry structure, root entry is not an array'); 50 | } 51 | 52 | return $this->obtainPathsFromEntries($entries); 53 | } 54 | 55 | /** 56 | * @param mixed[] $entries 57 | * @return string[] 58 | */ 59 | private function obtainPathsFromEntries(array $entries): array 60 | { 61 | $paths = []; 62 | foreach ($entries as $index => $entry) { 63 | $paths[] = $this->obtainPathsFromEntry($index, $entry); 64 | } 65 | if ([] === $paths) { 66 | return []; 67 | } 68 | 69 | $entries = array_merge(...$paths); 70 | sort($entries); 71 | $entries = array_values(array_unique($entries)); 72 | 73 | return $entries; 74 | } 75 | 76 | /** 77 | * @param int|string $index 78 | * @param mixed $entry 79 | * @return string[] 80 | */ 81 | private function obtainPathsFromEntry($index, $entry): array 82 | { 83 | if (! is_array($entry)) { 84 | throw new RuntimeException("Unexpected registry structure, entry $index is not an array"); 85 | } 86 | if (! isset($entry['xsd']) || ! is_string($entry['xsd'])) { 87 | throw new RuntimeException("Unexpected registry structure, entry $index does not contains xsd key"); 88 | } 89 | $xsdContents = $this->downloadUrl($entry['xsd']); 90 | return $this->getFinder()->obtainPathsFromXsdContents($xsdContents); 91 | } 92 | 93 | public function downloadUrl(string $url): string 94 | { 95 | return $this->downloader->get($url); 96 | } 97 | } 98 | --------------------------------------------------------------------------------