Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Usage
use PHPStamp\Templator;
use PHPStamp\Document\WordDocument;

$cachePath = 'path/to/writable/directory/';
$cachePath = 'path/to/writable/directory';
$templator = new Templator($cachePath);

// Enable debug mode to re-generate template with every render call.
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="Performance">
<directory>tests/Performance</directory>
</testsuite>
</testsuites>
<php>
<ini name="memory_limit" value="512M"/>
Expand Down
37 changes: 35 additions & 2 deletions src/PHPStamp/Document/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public function __construct($documentPath)
*/
public function extract($to, $overwrite)
{
$filePath = $to.$this->getDocumentName().'/'.$this->getContentPath();
$filePath = $this->composeExtractPath($to);

if (!file_exists($filePath) || $overwrite === true) {
$zip = new \ZipArchive();
Expand All @@ -67,14 +67,47 @@ public function extract($to, $overwrite)
throw new InvalidArgumentException('Can`t open archive "'.$this->documentPath.'", code "'.$code.'" returned.');
}

if ($zip->extractTo($to.$this->documentName, $this->getContentPath()) === false) {
if ($zip->extractTo($this->composeExtractDirectory($to), $this->getContentPath()) === false) {
throw new InvalidArgumentException('Destination not reachable.');
}
}

return $filePath;
}

/**
* @param string $to
*
* @return string
*/
private function composeExtractDirectory($to)
{
return rtrim($to, '/\\').DIRECTORY_SEPARATOR.$this->generateCacheKey();
}

/**
* @param string $to
*
* @return string
*/
public function composeExtractPath($to)
{
return $this->composeExtractDirectory($to).DIRECTORY_SEPARATOR.$this->getContentPath();
}

/**
* Generate document cache key.
*/
public function generateCacheKey()
{
$path = realpath($this->documentPath);
if ($path === false) {
$path = $this->documentPath;
}

return hash('sha256', $path);
}

/**
* Get MD5 hash to detect original document update.
*/
Expand Down
16 changes: 16 additions & 0 deletions src/PHPStamp/Document/DocumentInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ public function __construct($documentPath);
*/
public function extract($to, $overwrite);

/**
* Compose extracted content file path.
*
* @param string $to
*
* @return string
*/
public function composeExtractPath($to);

/**
* Generate document cache key.
*
* @return string
*/
public function generateCacheKey();

/**
* Get document file hash.
*
Expand Down
2 changes: 1 addition & 1 deletion src/PHPStamp/Document/WordDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class WordDocument extends Document
*/
public static function getContentPath()
{
return 'word/document.xml';
return 'word'.DIRECTORY_SEPARATOR.'document.xml';
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/PHPStamp/Processor/Lexer.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,6 @@ public function getInputBetweenPosition(int $position, int $length): string
/** @var string $input */
$input = $reflectionProperty->getValue($this);

return mb_substr($input, $position, $length);
return substr($input, $position, $length);
}
}
18 changes: 16 additions & 2 deletions src/PHPStamp/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,28 @@ public function buildFile()

if (copy($this->document->getDocumentPath(), $tempArchive) === true) {
$zip = new \ZipArchive();
$zip->open($tempArchive);
$code = $zip->open($tempArchive);
if ($code !== true) {
unlink($tempArchive);

throw new TempException('Cannot open temp archive, code "'.$code.'" returned.');
}

$content = $this->output->saveXML();
if ($content === false) {
$zip->close();
unlink($tempArchive);

throw new XmlException('Print XML error');
}

$zip->addFromString($this->document->getContentPath(), $content);
if ($zip->addFromString($this->document->getContentPath(), $content) !== true) {
$zip->close();
unlink($tempArchive);

throw new TempException('Cannot write document content to temp archive.');
}

$zip->close();

return $tempArchive;
Expand Down
7 changes: 3 additions & 4 deletions src/PHPStamp/Templator.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function __construct(string $cachePath, array $brackets = ['[[', ']]'])
throw new Exception\InvalidArgumentException('Brackets are in wrong format.');
}

$this->cachePath = $cachePath;
$this->cachePath = rtrim($cachePath, '/\\');
$this->brackets = $brackets;
}

Expand Down Expand Up @@ -176,8 +176,7 @@ private function searchAndReplace(\DOMNodeList $nodeList, DocumentInterface $doc
throw new XmlException('Some node value expected');
}

$decodedValue = utf8_decode($nodeValue);
$lexer->setInput($decodedValue);
$lexer->setInput($nodeValue);

while ($tag = $mapper->parse($lexer)) {
foreach ($tag->getFunctions() as $function) {
Expand Down Expand Up @@ -226,7 +225,7 @@ private function compareHash(DocumentInterface $document): bool
{
$overwrite = false;

$contentPath = $this->cachePath.$document->getDocumentName().'/'.$document->getContentPath();
$contentPath = $document->composeExtractPath($this->cachePath);
if (file_exists($contentPath) === true) {
$template = new \DOMDocument('1.0', 'UTF-8');
$template->load($contentPath);
Expand Down
23 changes: 22 additions & 1 deletion src/PHPStamp/XMLHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStamp;

use PHPStamp\Exception\EncodeException;
use PHPStamp\Exception\ParsingException;
use PHPStamp\Exception\XmlException;

Expand Down Expand Up @@ -218,7 +219,7 @@ public static function xmlEncode($mixed, \DOMNode $domElement, \DOMDocument $dom
$tagName = $itemName;
}

$node = $domDocument->createElement($tagName);
$node = self::createElement($domDocument, (string) $tagName);
$domElement->appendChild($node);

self::xmlEncode($mixedElement, $node, $domDocument, $itemName);
Expand All @@ -227,4 +228,24 @@ public static function xmlEncode($mixed, \DOMNode $domElement, \DOMDocument $dom
$domElement->appendChild($domDocument->createTextNode((string) $mixed));
}
}

/**
* @throws EncodeException
*/
private static function createElement(\DOMDocument $document, string $tagName): \DOMElement
{
// PHP 7.4 exposes invalid DOM names as warnings. Bridge those warnings
// into app-level exceptions until PHP 7.4 support can be removed.
set_error_handler(static function (int $severity, string $message) use ($tagName): bool {
throw new EncodeException('Invalid XML element name "'.$tagName.'": '.$message);
});

try {
return $document->createElement($tagName);
} catch (\DOMException $exception) {
throw new EncodeException('Invalid XML element name "'.$tagName.'".', 0, $exception);
} finally {
restore_error_handler();
}
}
}
16 changes: 10 additions & 6 deletions tests/BaseCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,27 @@ class BaseCase extends \PHPUnit\Framework\TestCase
{
public static function makeMockDocument(string $content, string $instance, string $filename): DocumentInterface
{
$zip = new \ZipArchive();

$dir = sys_get_temp_dir().DIRECTORY_SEPARATOR.'docx';
if (file_exists($dir) === false) {
mkdir($dir);
}

$filename = $dir.DIRECTORY_SEPARATOR.$filename;
if ($zip->open($filename, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
throw new \Exception('Cant open archive '.$filename);
return self::makeMockDocumentAt($content, $instance, $dir.DIRECTORY_SEPARATOR.$filename);
}

public static function makeMockDocumentAt(string $content, string $instance, string $path): DocumentInterface
{
$zip = new \ZipArchive();

if ($zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
throw new \Exception('Cant open archive '.$path);
}

$zip->addFromString($instance::getContentPath(), $content);
$zip->close();

/** @var Document $doc */
$doc = new $instance($filename);
$doc = new $instance($path);

return $doc;
}
Expand Down
78 changes: 78 additions & 0 deletions tests/Performance/PHPStamp/XMLHelperPerformanceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace PHPStamp\Tests\Performance\PHPStamp;

use PHPStamp\Tests\BaseCase;
use PHPStamp\XMLHelper;

class XMLHelperPerformanceTest extends BaseCase
{
public function testXmlEncodeLargeFlatArrayCompletesWithinReasonableTime(): void
{
$data = $this->makeFlatData(100000);
$document = new \DOMDocument('1.0', 'UTF-8');
$root = $document->createElement('root');
$document->appendChild($root);

$elapsed = $this->measure(static function () use ($data, $root, $document): void {
XMLHelper::xmlEncode($data, $root, $document);
});

$this->assertSame(10000, $root->childNodes->length);
$this->assertLessThan(1.0, $elapsed, sprintf('Encoding 10k flat values took %.4f seconds.', $elapsed));
}

public function testXmlEncodeElementCreationOverheadStaysBounded(): void
{
$data = $this->makeFlatData(100000);

$baselineDocument = new \DOMDocument('1.0', 'UTF-8');
$baselineRoot = $baselineDocument->createElement('root');
$baselineDocument->appendChild($baselineRoot);
$baselineElapsed = $this->measure(static function () use ($data, $baselineRoot, $baselineDocument): void {
foreach ($data as $key => $value) {
$node = $baselineDocument->createElement($key);
$baselineRoot->appendChild($node);
$node->appendChild($baselineDocument->createTextNode($value));
}
});

$document = new \DOMDocument('1.0', 'UTF-8');
$root = $document->createElement('root');
$document->appendChild($root);
$elapsed = $this->measure(static function () use ($data, $root, $document): void {
XMLHelper::xmlEncode($data, $root, $document);
});

$this->assertSame(10000, $root->childNodes->length);
$this->assertLessThan(
max(0.1, $baselineElapsed * 20),
$elapsed,
sprintf('XMLHelper took %.4fs; direct DOM baseline took %.4fs.', $elapsed, $baselineElapsed)
);
}

/**
* @return array<string,string>
*/
private function makeFlatData(int $count): array
{
$data = [];
for ($i = 0; $i < $count; ++$i) {
$data['key_'.$i] = 'value_'.$i;
}

return $data;
}

/**
* @param callable(): void $callback
*/
private function measure(callable $callback): float
{
$start = microtime(true);
$callback();

return microtime(true) - $start;
}
}
Loading
Loading