|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +/* |
| 6 | + * This file belongs to the package "TYPO3 Fluid". |
| 7 | + * See LICENSE.txt that was shipped with this package. |
| 8 | + */ |
| 9 | + |
| 10 | +namespace TYPO3Fluid\Fluid\Schema; |
| 11 | + |
| 12 | +/** |
| 13 | + * @internal |
| 14 | + */ |
| 15 | +final class SchemaGenerator |
| 16 | +{ |
| 17 | + /** |
| 18 | + * @param ViewHelperMetadata[] $viewHelpers |
| 19 | + */ |
| 20 | + public function generate(string $xmlNamespace, array $viewHelpers): \SimpleXMLElement |
| 21 | + { |
| 22 | + $file = $this->createXmlRootElement($xmlNamespace); |
| 23 | + foreach ($viewHelpers as $metadata) { |
| 24 | + $xsdElement = $file->addChild('xsd:element'); |
| 25 | + |
| 26 | + $xsdElement->addAttribute('name', $metadata->tagName); |
| 27 | + |
| 28 | + $documentation = $metadata->documentation; |
| 29 | + // Add deprecation information to ViewHelper documentation |
| 30 | + if (isset($metadata->docTags['@deprecated'])) { |
| 31 | + $documentation .= "\n@deprecated " . $metadata->docTags['@deprecated']; |
| 32 | + } |
| 33 | + $documentation = trim($documentation); |
| 34 | + |
| 35 | + // Add documentation to xml |
| 36 | + if ($documentation !== '') { |
| 37 | + $xsdAnnotation = $xsdElement->addChild('xsd:annotation'); |
| 38 | + $xsdDocumentation = $xsdAnnotation->addChild('xsd:documentation'); |
| 39 | + $this->appendWithCdata($xsdDocumentation, $documentation); |
| 40 | + } |
| 41 | + |
| 42 | + $xsdComplexType = $xsdElement->addChild('xsd:complexType'); |
| 43 | + |
| 44 | + // Allow text as well as subelements |
| 45 | + $xsdComplexType->addAttribute('mixed', 'true'); |
| 46 | + |
| 47 | + // Allow a sequence of arbitrary subelements of any type |
| 48 | + $xsdSequence = $xsdComplexType->addChild('xsd:sequence'); |
| 49 | + $xsdAny = $xsdSequence->addChild('xsd:any'); |
| 50 | + $xsdAny->addAttribute('minOccurs', '0'); |
| 51 | + |
| 52 | + // Add argument definitions to xml |
| 53 | + foreach ($metadata->argumentDefinitions as $argumentDefinition) { |
| 54 | + $default = $argumentDefinition->getDefaultValue(); |
| 55 | + $type = $argumentDefinition->getType(); |
| 56 | + |
| 57 | + $xsdAttribute = $xsdComplexType->addChild('xsd:attribute'); |
| 58 | + $xsdAttribute->addAttribute('type', $this->convertPhpTypeToXsdType($type)); |
| 59 | + $xsdAttribute->addAttribute('name', $argumentDefinition->getName()); |
| 60 | + if ($argumentDefinition->isRequired()) { |
| 61 | + $xsdAttribute->addAttribute('use', 'required'); |
| 62 | + } else { |
| 63 | + $xsdAttribute->addAttribute('default', $this->createFluidRepresentation($default)); |
| 64 | + } |
| 65 | + |
| 66 | + // Add PHP type to documentation text |
| 67 | + // TODO check if there is a better field for this |
| 68 | + $documentation = $argumentDefinition->getDescription(); |
| 69 | + $documentation .= "\n@type $type"; |
| 70 | + $documentation = trim($documentation); |
| 71 | + |
| 72 | + // Add documentation for argument to xml |
| 73 | + $xsdAnnotation = $xsdAttribute->addChild('xsd:annotation'); |
| 74 | + $xsdDocumentation = $xsdAnnotation->addChild('xsd:documentation'); |
| 75 | + $this->appendWithCdata($xsdDocumentation, $documentation); |
| 76 | + } |
| 77 | + |
| 78 | + if ($metadata->allowsArbitraryArguments) { |
| 79 | + $xsdComplexType->addChild('xsd:anyAttribute'); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + return $file; |
| 84 | + } |
| 85 | + |
| 86 | + private function appendWithCdata(\SimpleXMLElement $parent, string $text): \SimpleXMLElement |
| 87 | + { |
| 88 | + $parentDomNode = dom_import_simplexml($parent); |
| 89 | + $parentDomNode->appendChild($parentDomNode->ownerDocument->createCDATASection($text)); |
| 90 | + return simplexml_import_dom($parentDomNode); |
| 91 | + } |
| 92 | + |
| 93 | + private function createXmlRootElement(string $targetNamespace): \SimpleXMLElement |
| 94 | + { |
| 95 | + return new \SimpleXMLElement( |
| 96 | + '<?xml version="1.0" encoding="UTF-8"?><xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="' . $targetNamespace . '"></xsd:schema>', |
| 97 | + ); |
| 98 | + } |
| 99 | + |
| 100 | + private function convertPhpTypeToXsdType(string $type): string |
| 101 | + { |
| 102 | + switch ($type) { |
| 103 | + case 'integer': |
| 104 | + return 'xsd:integer'; |
| 105 | + case 'float': |
| 106 | + return 'xsd:float'; |
| 107 | + case 'double': |
| 108 | + return 'xsd:double'; |
| 109 | + case 'boolean': |
| 110 | + case 'bool': |
| 111 | + return 'xsd:boolean'; |
| 112 | + case 'string': |
| 113 | + return 'xsd:string'; |
| 114 | + case 'array': |
| 115 | + case 'mixed': |
| 116 | + default: |
| 117 | + return 'xsd:anySimpleType'; |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + private function createFluidRepresentation(mixed $input, bool $isRoot = true): string |
| 122 | + { |
| 123 | + if (is_array($input)) { |
| 124 | + $fluidArray = []; |
| 125 | + foreach ($input as $key => $value) { |
| 126 | + $fluidArray[] = $this->createFluidRepresentation($key, false) . ': ' . $this->createFluidRepresentation($value, false); |
| 127 | + } |
| 128 | + return '{' . implode(', ', $fluidArray) . '}'; |
| 129 | + } |
| 130 | + |
| 131 | + if (is_string($input) && !$isRoot) { |
| 132 | + return "'" . addcslashes($input, "'") . "'"; |
| 133 | + } |
| 134 | + |
| 135 | + if (is_bool($input)) { |
| 136 | + return ($input) ? 'true' : 'false'; |
| 137 | + } |
| 138 | + |
| 139 | + if (is_null($input)) { |
| 140 | + return 'NULL'; |
| 141 | + } |
| 142 | + |
| 143 | + // Generally, this wouldn't be correct, since it's not the correct representation, |
| 144 | + // but in the context of XSD files we merely need to provide *any* string representation |
| 145 | + if (is_object($input)) { |
| 146 | + return ''; |
| 147 | + } |
| 148 | + |
| 149 | + return (string)$input; |
| 150 | + } |
| 151 | +} |
0 commit comments