318 lines
13 KiB
PHP
318 lines
13 KiB
PHP
<?php
|
|
|
|
namespace KupShop\FeedGeneratorBundle;
|
|
|
|
use KupShop\FeedGeneratorBundle\Configuration\Configuration;
|
|
use KupShop\FeedGeneratorBundle\Configuration\V8BuildingVisitor;
|
|
use KupShop\FeedGeneratorBundle\Expressions\IExpressionObject;
|
|
use Symfony\Component\Serializer\Serializer;
|
|
use Symfony\Component\Serializer\SerializerInterface;
|
|
|
|
class V8FeedGenerator
|
|
{
|
|
/** @var Configuration */
|
|
protected $configuration;
|
|
|
|
/** @var Serializer */
|
|
protected $serializer;
|
|
|
|
protected \V8Js $v8js;
|
|
|
|
protected string $js;
|
|
|
|
/** @var V8BuildingVisitor */
|
|
protected $jsBuilder;
|
|
|
|
/** @var IExpressionObject */
|
|
protected $wrapper;
|
|
|
|
/** @var ContextPropertiesLocator */
|
|
protected $contextPropertiesLocator;
|
|
|
|
/** @var bool */
|
|
protected $throwErrors = false;
|
|
|
|
protected bool $isPreview = false;
|
|
|
|
/** @var callable */
|
|
protected $buildItemCallback;
|
|
|
|
protected $indent = false;
|
|
|
|
protected $report = [];
|
|
|
|
public function __construct(
|
|
SerializerInterface $serializer,
|
|
V8BuildingVisitor $jsBuilder,
|
|
ContextPropertiesLocator $contextPropertiesLocator,
|
|
) {
|
|
$this->serializer = $serializer;
|
|
$this->jsBuilder = $jsBuilder;
|
|
$this->contextPropertiesLocator = $contextPropertiesLocator;
|
|
}
|
|
|
|
public function setIndent(bool $indent): self
|
|
{
|
|
$this->indent = $indent;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setThrowErrors(bool $throwErrors = true): void
|
|
{
|
|
$this->throwErrors = $throwErrors;
|
|
}
|
|
|
|
public function setIsPreview(bool $isPreview = true): void
|
|
{
|
|
$this->isPreview = $isPreview;
|
|
}
|
|
|
|
public function setWrapper(IExpressionObject $wrapper): self
|
|
{
|
|
$this->wrapper = $wrapper;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setBuildItemCallback(?callable $callback = null): void
|
|
{
|
|
$this->buildItemCallback = $callback;
|
|
}
|
|
|
|
public function getConfiguration(): Configuration
|
|
{
|
|
return $this->configuration;
|
|
}
|
|
|
|
public function setConfiguration(?string $jsonConfiguration)
|
|
{
|
|
// deserialize configuration from JSON string to Configuration object
|
|
$this->configuration = $configuration = $this->serializer->deserialize($jsonConfiguration ?? '{}', Configuration::class, 'json');
|
|
|
|
$this->jsBuilder->setIndent($this->indent);
|
|
$this->jsBuilder->initialize($this->wrapper);
|
|
$this->configuration->accept($this->jsBuilder);
|
|
$this->js = $js = $this->jsBuilder->getOutput();
|
|
|
|
// enable array access for MultiWrapper and ObjectMultiWrapper
|
|
ini_set('v8js.use_array_access', true);
|
|
|
|
// convert PHP DateTime to js Date
|
|
ini_set('v8js.use_date', true);
|
|
|
|
$properties = [];
|
|
$methods = [];
|
|
foreach ($this->wrapper->getRawFieldsAndMethods() as $item) {
|
|
if ($item['method']) {
|
|
$methods[$item['name']] = '';
|
|
} else {
|
|
$properties[$item['name']] = '';
|
|
}
|
|
}
|
|
|
|
$this->v8js = $v8js = new \V8Js(null, $properties);
|
|
$v8js->__isPreview = $this->isPreview;
|
|
$v8js->__properties = $properties;
|
|
$v8js->__methods = $methods;
|
|
$v8js->__buildItemCallback = $this->buildItemCallback;
|
|
|
|
// add common context properties
|
|
$contextPropertiesNames = array_keys($this->contextPropertiesLocator->getContexts());
|
|
$v8js->__contextProperties = array_combine(
|
|
$contextPropertiesNames,
|
|
array_fill(0, count($contextPropertiesNames), null)
|
|
);
|
|
$v8js->__getContextPropertyService = function ($name) {
|
|
return $this->contextPropertiesLocator->getServiceByContextName($name);
|
|
};
|
|
|
|
// add source expression to error message for report
|
|
$v8js->__transformJSError = function ($exception, $message, $stack) {
|
|
if (!empty($message)) {
|
|
// probably js error: has filled message
|
|
return $this->transformExceptionOrStackIntoMesssage(null, $message, $stack);
|
|
} elseif ($exception instanceof \Throwable) {
|
|
return $exception->getMessage();
|
|
} else {
|
|
return $exception;
|
|
}
|
|
};
|
|
|
|
try {
|
|
if (getVal('v8debug')) {
|
|
print_r($js.PHP_EOL.'print(__buildXML());'.PHP_EOL);
|
|
exit;
|
|
}
|
|
|
|
$v8js->executeString(file_get_contents(__DIR__.'/Expressions/v8feed.XMLWriter.js'), 'XMLWriter');
|
|
$v8js->executeString(file_get_contents(__DIR__.'/Expressions/v8feed.dayjs.min.js'), 'V8Functions');
|
|
$v8js->executeString(file_get_contents(__DIR__.'/Expressions/v8feed.functions.js'), 'V8Functions');
|
|
$v8js->executeString($js, 'configuration');
|
|
} catch (\V8JsScriptException $e) {
|
|
throw $this->transformException($e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @throws \Throwable
|
|
*/
|
|
public function transform(\Generator $items): void
|
|
{
|
|
$this->transformInternal($items, function (): void {
|
|
// __runBuild is set to true, so whole feed will be rendered using `v8feed.transform.js`
|
|
$this->v8js->__runBuild = true;
|
|
try {
|
|
$this->v8js->executeString(
|
|
file_get_contents(__DIR__.'/Expressions/v8feed.transform.js'),
|
|
'',
|
|
\V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS
|
|
);
|
|
} catch (\V8JsScriptException $e) {
|
|
throw $this->transformException($e);
|
|
}
|
|
});
|
|
}
|
|
|
|
public function transformWithGenerator(\Generator $items): \Generator
|
|
{
|
|
return $this->transformInternal($items, function (): \Generator {
|
|
try {
|
|
// just include code so we can use it
|
|
$this->v8js->executeString(
|
|
file_get_contents(__DIR__.'/Expressions/v8feed.transform.js'),
|
|
'',
|
|
\V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS
|
|
);
|
|
|
|
// render feed using `__buildBatch` so we can yield it from generator
|
|
while (true) {
|
|
ob_start();
|
|
$done = $this->v8js->executeString('__buildBatch(100);', '', \V8Js::FLAG_PROPAGATE_PHP_EXCEPTIONS);
|
|
$xml = ob_get_contents();
|
|
ob_end_clean();
|
|
|
|
if (!empty($xml)) {
|
|
yield $xml;
|
|
}
|
|
|
|
if ($done) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->v8js->executeString('__updateReport();');
|
|
} catch (\V8JsScriptException $e) {
|
|
throw $this->transformException($e);
|
|
}
|
|
});
|
|
}
|
|
|
|
public function getReport(): array
|
|
{
|
|
return $this->report;
|
|
}
|
|
|
|
protected function transformInternal(\Generator $items, callable $fn): mixed
|
|
{
|
|
$time_start = getScriptTime();
|
|
|
|
$this->v8js->__updateReport = function ($newReport) use ($time_start) {
|
|
$report = (array) $newReport;
|
|
$report['errors'] = (array) $report['errors']; // V8 passes it as stdClass, but we expect PHP's associative array
|
|
$report['reportCounters'] = (array) $report['reportCounters']; // V8 passes it as stdClass, but we expect PHP's associative array
|
|
$report['time_generate'] = getScriptTime() - $time_start;
|
|
$report['peak_memory'] = memory_get_peak_usage(true); // in bytes
|
|
|
|
$this->report = $report;
|
|
};
|
|
|
|
$this->v8js->__throwErrors = (bool) $this->throwErrors;
|
|
$this->v8js->__isDevelopment = (bool) isDevelopment();
|
|
|
|
$this->v8js->__contextWrapper = $this->wrapper;
|
|
$this->v8js->__getWrapperProp = function ($prop) {
|
|
return $this->wrapper[$prop];
|
|
};
|
|
|
|
$this->v8js->__items = $items;
|
|
|
|
return $fn();
|
|
}
|
|
|
|
protected function transformException(\Exception $exception): \Exception
|
|
{
|
|
if ($exception instanceof \V8JsScriptException) {
|
|
$exception = new V8ErrorException($this->transformExceptionOrStackIntoMesssage($exception), 0, $exception);
|
|
}
|
|
|
|
return $exception;
|
|
}
|
|
|
|
protected function transformExceptionOrStackIntoMesssage(?\V8JsScriptException $exception = null, ?string $description = null, ?string $stack = null): string
|
|
{
|
|
// when V8Js::compileString() error appear after __buildXML() is called, the exception doesn't contain relevant source line
|
|
// this finds the proper source line if possible
|
|
// (.*)\s+at configuration:(\d+):(\d*)\s+at __buildXML \(configuration:\d*:\d*\)\s*at <anonymous>:\d*:\d*(?:\s*in: throw error;)?
|
|
// (.*)\s+[^\n]*\s+at __buildXML \(configuration:(\d+):(\d*)\)\s*at <anonymous>:\d*:\d*(?:\s*in: throw error;)?
|
|
// (.*)\s+at configuration:(\d+):(\d*)\s+at __buildXML \(configuration:\d*:\d*\)\s*at V8Js::compileString\(\):\d*:\d*(?:\s*in: throw error;)?
|
|
// (.*)\s+[^\n]*\s+at __buildXML \(configuration:(\d+):(\d*)\)\s*at V8Js::compileString\(\):\d*:\d*(?:\s*in: throw error;)?
|
|
if (preg_match('/(.*)\\s+at configuration:(\\d+):(\\d*)\\s+at __buildXML \\(configuration:\\d*:\\d*\\)\\s*at <anonymous>:\\d*:\\d*(?:\\s*in: throw error;)?/', $exception ? $exception->getJsTrace() : $stack, $matches)
|
|
|| preg_match('/(.*)\\s+[^\\n]*\\s+at __buildXML \\(configuration:(\\d+):(\\d*)\\)\\s*at <anonymous>:\\d*:\\d*(?:\\s*in: throw error;)?/', $stack, $matches)
|
|
|| preg_match('/(.*)\\s+at configuration:(\\d+):(\\d*)\\s+at __buildXML \\(configuration:\\d*:\\d*\\)\\s*at V8Js::compileString\\(\\):\\d*:\\d*(?:\\s*in: throw error;)?/', $exception ? $exception->getJsTrace() : $stack, $matches)
|
|
|| preg_match('/(.*)\\s+[^\\n]*\\s+at __buildXML \\(configuration:(\\d+):(\\d*)\\)\\s*at V8Js::compileString\\(\\):\\d*:\\d*(?:\\s*in: throw error;)?/', $stack, $matches)
|
|
) {
|
|
$lineNumber = $matches[2] - 1; // rows will be indexed from 0
|
|
// explode built js configuration
|
|
$tmp = explode(PHP_EOL, $this->js.PHP_EOL);
|
|
$rawExpression = trim($tmp[$lineNumber] ?? '');
|
|
$errorDescription = "{$matches[1]} on line {$lineNumber}:{$matches[3]}";
|
|
} elseif ($exception instanceof \V8JsScriptException) {
|
|
// otherwise get source line directly from the exception
|
|
$rawExpression = $exception ? $exception->getJsSourceLine() : $stack;
|
|
$lineNumber = $exception->getJsLineNumber();
|
|
$rawExpression .= PHP_EOL.' ['.$exception->getJsLineNumber().', '.$exception->getJsStartColumn().']';
|
|
$errorDescription = $exception ? $exception->getJsTrace() : $description;
|
|
if ($exception->getJsFileName() === 'undefined') {
|
|
// for ExternalFeed error when source file has no root element or other failures during read
|
|
$lines = explode(PHP_EOL, $exception->getMessage());
|
|
$mainMsg = str_replace('/vagrant/home/engine/bundles/KupShop', '...', $lines[0]);
|
|
$mainMsg = str_replace('/vagrant/home', '...', $mainMsg);
|
|
$errorDescription = $mainMsg.';;;'.PHP_EOL.' '.$errorDescription;
|
|
if (mb_strpos($mainMsg, 'XMLReader::read()')) {
|
|
$errorDescription = 'Failed to read source XML file!'.PHP_EOL.' '.$errorDescription;
|
|
}
|
|
}
|
|
} else {
|
|
return $description;
|
|
}
|
|
|
|
// try to extract exact original expression and block type
|
|
$regexesForExtraction = [
|
|
'/if \\(\\(function\\(\\){\\s+return (.*);\\s+}\\)\\(\\)\\) {/' => ' Condition', // if \(\(function\(\){\s+return (.*);\s+}\)\(\)\) {
|
|
'/__xmlWriter\\.text\\(\\(function\\(\\){\\s+return (.*);\\s+}\\)\\(\\)\\);/' => ' Expression', // __xmlWriter\.text\(\(function\(\){\s+return (.*);\s+}\)\(\)\);
|
|
'/for \\(const \\[.*?\\] of Object\\.entries\\(\\(function\\(\\){\\s+return (.*);\\s+}\\)\\(\\)\\)\\) {/' => ' Cycle expression', // for \(const \[.*?\] of Object\.entries\(\(function\(\){\s+return (.*);\s+}\)\(\)\)\) {
|
|
];
|
|
$extractedExpression = null;
|
|
$blockName = '';
|
|
if ($lineNumber > 1) {
|
|
foreach ($regexesForExtraction as $regex => $tmpBlockName) {
|
|
if (preg_match($regex, ($tmp[$lineNumber - 1] ?? '').($tmp[$lineNumber] ?? '').($tmp[$lineNumber + 1] ?? ''), $matches)) {
|
|
$extractedExpression = $matches[1] ?? '';
|
|
$blockName = $tmpBlockName;
|
|
break;
|
|
}
|
|
}
|
|
if ($blockName === '') {
|
|
// try Multiline
|
|
// original regex __xmlWriter\.text\(\(function\(\){\s+[\S\s]*
|
|
if (preg_match('/__xmlWriter\\.text\\(\\(function\\(\\){\\s+[\\S\\s]*/', ($tmp[$lineNumber - 1] ?? '').($tmp[$lineNumber] ?? ''))) {
|
|
$blockName = ' Multiline';
|
|
}
|
|
}
|
|
}
|
|
|
|
return $errorDescription.' in'.$blockName.': '.($extractedExpression ?? $rawExpression);
|
|
}
|
|
}
|