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 :\d*:\d*(?:\s*in: throw error;)? // (.*)\s+[^\n]*\s+at __buildXML \(configuration:(\d+):(\d*)\)\s*at :\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 :\\d*:\\d*(?:\\s*in: throw error;)?/', $exception ? $exception->getJsTrace() : $stack, $matches) || preg_match('/(.*)\\s+[^\\n]*\\s+at __buildXML \\(configuration:(\\d+):(\\d*)\\)\\s*at :\\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); } }