field value * 'Firstname' => 'John', * * // Hierarchical/nested fields in dot notation * 'Address.Street' => 'Some Street', * 'Address.City' => 'Any City', * * // Multi value fields * 'Pets' => ['Cat', 'Mouse'], * ] * ``` * * This will result in the following XML structure (header/footer omitted): * * ``` * * John * * * * Some Street * * * Any City * * * * Cat * Mouse * * ``` * * @author Tomas Holy * @author Michael Härtl * @license http://www.opensource.org/licenses/MIT */ class XfdfFile extends File { // XFDF file header private const XFDF_HEADER = << FDF; // XFDF file footer private const XFDF_FOOTER = << FDF; /** * Constructor * * * @param array $data the form data as name => value * @param string|null $suffix the optional suffix for the tmp file * @param string|null $prefix the optional prefix for the tmp file. If null * 'php_tmpfile_' is used. * @param string|null $directory directory where the file should be * created. Autodetected if not provided. * @param string|null $encoding of the data. Default is 'UTF-8'. */ public function __construct( array $data, ?string $suffix = null, ?string $prefix = null, ?string $directory = null, ?string $encoding = 'UTF-8', ) { if ($directory === null) { $directory = self::getTempDir(); } if ($suffix === null) { $suffix = '.xfdf'; } if ($prefix === null) { $prefix = 'php_pdftk_xfdf_'; } parent::__construct('', $suffix, $prefix, $directory); $fields = $this->parseData($data, $encoding); $this->writeXml($fields); } /** * Parses an array of key/value data into a nested array structure. * * The data may use keys in dot notation (#55). Values can also be arrays in * case of multi value fields (#148). To make both distinguishable in the * result array keys that represent field names are prefixed with `_`. This * also allows for numeric field names (#260). * * For example an array like this: * * ``` * [ * 'a' => 'value a', * 'b.x' => 'value b.x', * 'b.y' => 'value b.y', * * 'c.0' => 'val c.0', * 'c.1' => 'val c.1', * * 'd' => ['m1', 'm2'], * ] * ``` * * Will become: * * ``` * [ * '_a' => 'value a', * '_b' => [ * '_x' => 'value b.x', * '_y' => 'value b.y', * ], * '_c' => [ * '_0' => 'value c.0', * '_1' => 'value c.1', * ], * '_d' => [ * // notice the missing underscore in the keys * 0 => 'm1', * 1 => 'm2', * ], * ] * * @param mixed $data the data to parse * @param string the encoding of the data * @param null|string $encoding * * @return array the result array in UTF-8 encoding with dot keys converted * to nested arrays */ protected function parseData($data, ?string $encoding): array { $willConvert = $encoding !== 'UTF-8' && function_exists('mb_convert_encoding'); $result = array(); foreach ($data as $key => $value) { $key = (string) $key; if ($willConvert) { $key = mb_convert_encoding($key, 'UTF-8', $encoding); $value = mb_convert_encoding($value, 'UTF-8', $encoding); } if (strpos($key, '.') === false) { $result['_' . $key] = $value; } else { /** @var array $target */ $target = &$result; $keyParts = explode('.', $key); $lastPart = array_pop($keyParts); foreach ($keyParts as $part) { if (!isset($target['_' . $part])) { $target['_' . $part] = array(); } /** @var array $target */ $target = &$target['_' . $part]; } $target['_' . $lastPart] = $value; } } return $result; } /** * Write the given fields to an XML file * * @param array $fields the fields in a nested array structure */ protected function writeXml(array $fields): void { // Use fwrite, since file_put_contents() messes around with character encoding $fp = fopen($this->_fileName, 'w'); fwrite($fp, self::XFDF_HEADER); $this->writeFields($fp, $fields); fwrite($fp, self::XFDF_FOOTER); fclose($fp); } /** * Write the fields to the given filepointer * * @param resource $fp * @param mixed[] $fields an array of field values as returned by * `parseData()`. */ protected function writeFields($fp, array $fields): void { foreach ($fields as $key => $value) { $key = $this->xmlEncode(substr($key, 1)); fwrite($fp, "\n"); if (!is_array($value)) { $value = array($value); } if (array_key_exists(0, $value)) { // Numeric keys: single or multi-value field foreach ($value as $val) { $val = $this->xmlEncode($val); fwrite($fp, "$val\n"); } } else { // String keys: nested/hierarchical fields $this->writeFields($fp, $value); } fwrite($fp, "\n"); } } /** * @param string|null $value the value to encode * @return string|null the value correctly encoded for use in a XML document */ protected function xmlEncode(?string $value): ?string { if ($value === null) { return null; } return defined('ENT_XML1') ? htmlspecialchars($value, ENT_XML1, 'UTF-8') : htmlspecialchars($value); } }