Pull to refresh

Простейшая генерация odt файла из существующего

Reading time 9 min
Views 11K
Как-то раз передо мной стояла задача реализовать генерацию договоров для клиентов с нашего корпоративного сайта.
Сначала задача была решена просто ужасно — был заготовлен html шаблон договора, а пользователю выдавалась конвертация шаблона в pdf. Само собой это выливалось в кучу неудобств, в том числе, если требовалось что-то поменять в договоре.

Следующим решением было генерация odt документа. Это позволило редактировать документ нашим менеджерам независимо от сайта и программистов.
Полностью генерировать с нуля смысла нет. Почему бы не поработать с уже имеющимся файлом (отредактированным в OpenOffice) и просто заменить в нем необходимые элементы?

Этим мы с вами и займемся.

Но для начала…
UPD! Вторая статья с исправлением ошибок этой — habrahabr.ru/blogs/php/87254
Пару комментариев:
— Мы сильно ограничим нашу задачу изменением данных только в тексте документа, а также только текстовых переменных.
— Для решения данной задачи лично я использовал SimpleXML, ZIPArchive. Никто Вам не запрещает пользоваться другими инструментами.
— То, что описано в статье — упрощенный и урезанный пример, а не готовый инструмент.

Создание шаблона в OpenOffice:
Создаем обычный документ odt и в нужных местах вставляем пользовательские переменные:
Меню «Вставка» -> Поля -> Дополнительно
Вкладка «Переменные»
Выбираем «поле пользователя» и добавляем/вставляем поля как показано на изображении.


Сохраняем наш файл. В моем примере это файл test.odt.

Загружаем файл на сервер:
ODT как и любой ODF файл, как многим наверно известно, является обычным ZIP архивом.

upload.php
<?php
 
//путь к временному архиву
$tmpfile='upload/temp.zip';
 
//сохраняем полученный документ
if (isset($_FILES['document']) and move_uploaded_file($_FILES['document']['tmp_name'], $tmpfile)) {
 
// функция удаления директории
    function deleteDirectory($dir) {
        if (!file_exists($dir)) return true;
        if (!is_dir($dir) || is_link($dir)) return unlink($dir);
        foreach (scandir($dir) as $item) {
            if ($item == '.' || $item == '..') continue;
            if (!deleteDirectory($dir . "/" . $item)) {
                chmod($dir . "/" . $item, 0777);
                if (!deleteDirectory($dir . "/" . $item)) return false;
            };
        }
        return rmdir($dir);
    }
 
    // удаляем директорию с содержимым документа и создаем заново
    // при желании, можно перемещать старую версию куда-либо, сделав тем самым версионность документа
    deleteDirectory('doc/');
    mkdir('doc/');
 
 
    // извлекаем архив
    $zip = new ZipArchive;
    if ($zip->open($tmpfile) === TRUE) {
    // Сохраняем пути к файлам в нужной последовательности
    // Это нам понадобится в будущем.
    // Например, по требованию формата odf , файл mimetype должен быть первым в архиве.
        $files=array();
        for($i = 0; $i < $zip->numFiles; $i++) {
            $files[]=$zip->getNameIndex($i);
        }
        file_put_contents("doc.list",implode("\n",$files));
 
        //извлекаем
        $zip->extractTo('doc/');
        $zip->close();
    } else {
        die("zip error");
    }
 
    unlink ($tmpfile);
 
    $print='Файл успешно загружен';
}
else {
    $print='
      <form action="" method="post" enctype="multipart/form-data">
      <input type="file" name="document"><br>
      <input type="submit" value="Загрузить"><br>
      </form>'
;
}
print '<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Загрузка документа</title>
</head>
<body>
'
.$print.'
</body>
</html>'
;
 
?>


При успешной загрузке мы получаем папку с содержимым odt файла и doc.list со списком файлов.

Отдаем измененный файл пользователю:
Нам нужно заменить значения пользовательских полей и сжать все обратно в архив.

download.php

<?php
 
//путь к временному файлу
$tmpfile='download/doc.odt';
//файл, который будем отдавать
$outname='zayavlenie.odt';
 
 
//удаляем старый файл
unlink($tmpfile);
 
 
//создаем новый архив
$zip = new ZipArchive;
if ($zip->open($tmpfile,ZIPARCHIVE::CREATE) === TRUE) {
//проходимся по структуре нашего архива
    $files=file('doc.list');
    foreach ($files as $filename) {
        $filename=trim($filename);
 
        //если директория - добавляем ее
        if (is_dir('doc/'.$filename)) {
            $zip->addEmptyDir($filename);
        }
        //иначе добавляем файл
        else {
 
        //если нужный файл, то проводим в нем подстановку пользовательских полей
            if ($filename=="content.xml") {
 
            //значения полей
                $vars=array(
                    'ФИО'=>'Иванова И.И.',
                    'Дата'=>date('d.m.Y'),
                    'Планета'=>'Юпитер'
                );
 
                //создаем объект simplexml
                $xml = new SimpleXMLElement(file_get_contents('doc/'.$filename));
 
                //получаем заранее нужные namespace
                $ns=$xml->getNamespaces(true);
 
                // две переменные, необходимые для доступа к элементам xml и к атрибутам
                $usr="user-field-decls";
                $str="string-value";
 
                //проверяем есть ли в файле пользовательские поля
                if ($fields=$xml->children($ns["office"])->body->text->children($ns["text"])->$usr) {
                //если есть, пробегаемся по ним и заменяем их атрибут string-value на новый
                    foreach ($fields->children($ns["text"]) as  $field) {
 
                        if (isset($vars[(string)$field->attributes($ns["text"])->name])) {
                            $field->attributes($ns["office"])->$str = $vars[(string)$field->attributes($ns["text"])->name];
                        }
                    }
 
                }
                //добавляем в архив
                $zip->addFromString($filename, $xml->asXML());
            }
            else {
            //добавляем в архив из файла
                $zip->addFile('doc/'.$filename,$filename );
            }
        }
    }
 
 
    $zip->close();
} else {
    die("zip error");
}
 
//очищаем буфер и выдаем файл
ob_clean();
 
header('Content-Disposition: attachment; filename="'.$outname.'"');
header('Content-type: application/odt');
print file_get_contents($tmpfile);
 
?>
 


Вуаля.

Важные замечания и ссылки:
— В примере я использовал только текстовые поля, но Вы можете использовать также и другие типы полей.
— В ODT есть также возможность использоваться условные элементы (например часть текста показывается или не показывается в зависимости от условия — например значения пользовательского поля)
— В примере я менял значения полей только в content.xml. Но поля могут использоваться и в других файлах, например в styles.xml находятся колонтитулы.
спецификация ODF (точнее Open Document)
— Как и всё в мире — пример можно оптимизировать. Например, если требуется менять только content.xml, то никто не запрещает подготовить заранее архив, а при запросе пользователя заменять/добавлять в него этот файл.

Скачать исходник целиком
Tags:
Hubs:
+31
Comments 23
Comments Comments 23

Articles