From fe61da24ff6834adce39ebb153fa72775cd66767 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 6 Nov 2023 16:22:45 +1100 Subject: [PATCH] Cleanup for parsing field stacks --- app/Services/Template/TemplateService.php | 287 ++++++++++++++++------ 1 file changed, 206 insertions(+), 81 deletions(-) diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index bc2558b91f8c..13f599d8619c 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -13,6 +13,7 @@ namespace App\Services\Template; use App\Models\Quote; use App\Utils\Number; +use Twig\Error\Error; use App\Models\Client; use App\Models\Credit; use App\Models\Design; @@ -23,8 +24,12 @@ use App\Models\Payment; use App\Models\Project; use App\Utils\HtmlEngine; use League\Fractal\Manager; +use Twig\Error\LoaderError; +use Twig\Error\SyntaxError; +use Twig\Error\RuntimeError; use App\Models\PurchaseOrder; use App\Utils\VendorHtmlEngine; +use Twig\Sandbox\SecurityError; use App\Models\RecurringInvoice; use App\Utils\PaymentHtmlEngine; use App\Utils\Traits\MakesDates; @@ -40,8 +45,9 @@ use League\Fractal\Serializer\ArraySerializer; class TemplateService { - use MakesDates, PdfMaker; - + use MakesDates; + use PdfMaker; + private \DomDocument $document; public \Twig\Environment $twig; @@ -69,7 +75,7 @@ class TemplateService $this->template = $template; $this->init(); } - + /** * Boot Dom Document * @@ -78,7 +84,6 @@ class TemplateService private function init(): self { - $this->commonmark = new CommonMarkConverter([ 'allow_unsafe_links' => false, ]); @@ -96,22 +101,22 @@ class TemplateService $this->twig->addExtension(new \Twig\Extension\DebugExtension()); $function = new \Twig\TwigFunction('img', function ($string, $style = '') { - return ''; + return ''; }); $this->twig->addFunction($function); $filter = new \Twig\TwigFilter('sum', function (array $array, string $column) { return array_sum(array_column($array, $column)); }); - + $this->twig->addFilter($filter); return $this; } - + /** * Iterate through all of the - * ninja nodes + * ninja nodes, and field stacks * * @param array $data - the payload to be passed into the template * @return self @@ -127,7 +132,13 @@ class TemplateService return $this; } - + + /** + * Initialized a set of HTMLEngine variables + * + * @param array | Collection $data + * @return self + */ private function processVariables($data): self { $this->variables = $this->resolveHtmlEngine($data); @@ -135,6 +146,11 @@ class TemplateService return $this; } + /** + * Returns a Mock Template + * + * @return self + */ public function mock(): self { $tm = new TemplateMock($this->company); @@ -151,7 +167,7 @@ class TemplateService return $this; } - + /** * Returns the HTML as string * @@ -161,13 +177,13 @@ class TemplateService { return $this->compiled_html; } - + /** * Returns the PDF string * - * @return mixed + * @return string */ - public function getPdf(): mixed + public function getPdf(): string { if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') { @@ -179,7 +195,7 @@ class TemplateService return $pdf; } - + /** * Get the parsed data * @@ -189,11 +205,11 @@ class TemplateService { return $this->data; } - + /** * Process data variables * - * @param mixed $data + * @param array | Collection $data * @return self */ public function processData($data): self @@ -216,24 +232,24 @@ class TemplateService $contents = $this->document->getElementsByTagName('ninja'); foreach ($contents as $content) { - + $template = $content->ownerDocument->saveHTML($content); try { $template = $this->twig->createTemplate(html_entity_decode($template)); - } catch(\Twig\Error\SyntaxError $e) { + } catch(SyntaxError $e) { nlog($e->getMessage()); throw ($e); - } catch(\Twig\Error\Error $e) { - nlog("error = " .$e->getMessage()); + } catch(Error $e) { + nlog("error = " . $e->getMessage()); throw ($e); - } catch(\Twig\Error\RuntimeError $e) { - nlog("runtime = " .$e->getMessage()); + } catch(RuntimeError $e) { + nlog("runtime = " . $e->getMessage()); throw ($e); - } catch(\Twig\Error\LoaderError $e) { + } catch(LoaderError $e) { nlog("loader = " . $e->getMessage()); throw ($e); - } catch(\Twig\Error\SecurityError $e) { + } catch(SecurityError $e) { nlog("security = " . $e->getMessage()); throw ($e); } @@ -242,7 +258,7 @@ class TemplateService $f = $this->document->createDocumentFragment(); $f->appendXML(html_entity_decode($template)); - + $replacements[] = $f; } @@ -256,7 +272,7 @@ class TemplateService return $this; } - + /** * Parses all variables in the document * @@ -279,7 +295,7 @@ class TemplateService return $this; } - + /** * Saves the document and updates the compiled string. * @@ -314,7 +330,7 @@ class TemplateService return $this; } - + /** * Inject the template components * manually @@ -340,12 +356,13 @@ class TemplateService * Resolves the labels and values needed to replace the string * holders in the template. * + * @param array $data * @return array */ private function resolveHtmlEngine(array $data): array { return collect($data)->map(function ($value, $key) { - + $processed = []; if(in_array($key, ['tasks','projects','aging']) || !$value->first()) { @@ -364,13 +381,20 @@ class TemplateService 'aging' => $processed = [], default => $processed = [], }; - + return $processed; })->toArray(); } + /** + * Pre Processes the Data Blocks into + * Twig consumables + * + * @param array | Collection $data + * @return array + */ private function preProcessDataBlocks($data): array { return collect($data)->map(function ($value, $key) { @@ -394,6 +418,12 @@ class TemplateService })->toArray(); } + /** + * Process Invoices into consumable form for Twig templates + * + * @param array | Collection $invoices + * @return array + */ public function processInvoices($invoices): array { $invoices = collect($invoices) @@ -448,7 +478,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $invoice->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $invoice->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $invoice->custom_surcharge_tax4, - 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client): (array) [], + 'line_items' => $invoice->line_items ? $this->padLineItems($invoice->line_items, $invoice->client) : (array) [], 'reminder1_sent' => $this->translateDate($invoice->reminder1_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder2_sent' => $this->translateDate($invoice->reminder2_sent, $invoice->client->date_format(), $invoice->client->locale()), 'reminder3_sent' => $this->translateDate($invoice->reminder3_sent, $invoice->client->date_format(), $invoice->client->locale()), @@ -472,6 +502,13 @@ class TemplateService } + /** + * Pads Line Items with raw and formatted content + * + * @param array $items + * @param mixed $client + * @return array + */ public function padLineItems(array $items, Client $client): array { return collect($items)->map(function ($item) use ($client) { @@ -484,11 +521,11 @@ class TemplateService $item->product_cost_raw = $item->product_cost ?? 0; $item->cost = Number::formatMoney($item->cost_raw, $client); - + if($item->is_amount_discount) { $item->discount = Number::formatMoney($item->discount_raw, $client); } - + $item->line_total = Number::formatMoney($item->line_total_raw, $client); $item->gross_line_total = Number::formatMoney($item->gross_line_total_raw, $client); $item->tax_amount = Number::formatMoney($item->tax_amount_raw, $client); @@ -499,11 +536,17 @@ class TemplateService })->toArray(); } + /** + * Transforms a Payment into consumable for twig + * + * @param Payment $payment + * @return array + */ private function transformPayment(Payment $payment): array { $data = []; - + $this->payment = $payment; $credits = $payment->credits->map(function ($credit) use ($payment) { @@ -625,6 +668,12 @@ class TemplateService } + /** + * @todo refactor + * + * @param mixed $quotes + * @return array + */ public function processQuotes($quotes): array { $it = new QuoteTransformer(); @@ -638,18 +687,18 @@ class TemplateService $resources['data'][$key]['client'] = $resource['client']['data'] ?? []; $resources['data'][$key]['client']['contacts'] = $resource['client']['data']['contacts']['data'] ?? []; - + } return $resources['data']; } - + /** * Pushes credits through the appropriate transformer * and builds any required relationships * - * @param mixed $credits + * @param array | Collection $credits * @return array */ public function processCredits($credits): array @@ -658,7 +707,7 @@ class TemplateService ->map(function ($credit) { $this->entity = $credit; - + return [ 'amount' => Number::formatMoney($credit->amount, $credit->client), 'balance' => Number::formatMoney($credit->balance, $credit->client), @@ -699,7 +748,7 @@ class TemplateService 'custom_surcharge_tax2' => (bool) $credit->custom_surcharge_tax2, 'custom_surcharge_tax3' => (bool) $credit->custom_surcharge_tax3, 'custom_surcharge_tax4' => (bool) $credit->custom_surcharge_tax4, - 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client): (array) [], + 'line_items' => $credit->line_items ? $this->padLineItems($credit->line_items, $credit->client) : (array) [], 'reminder1_sent' => $this->translateDate($credit->reminder1_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder2_sent' => $this->translateDate($credit->reminder2_sent, $credit->client->date_format(), $credit->client->locale()), 'reminder3_sent' => $this->translateDate($credit->reminder3_sent, $credit->client->date_format(), $credit->client->locale()), @@ -723,12 +772,10 @@ class TemplateService } - - /** * Pushes payments through the appropriate transformer * - * @param mixed $payments + * @param array | Collection $payments * @return array */ public function processPayments($payments): array @@ -737,12 +784,17 @@ class TemplateService $payments = collect($payments)->map(function ($payment) { return $this->transformPayment($payment); })->toArray(); - - return $payments; + return $payments; } + /** + * @todo refactor + * + * @param mixed $tasks + * @return array + */ public function processTasks($tasks): array { $it = new TaskTransformer(); @@ -757,7 +809,7 @@ class TemplateService $resources['data'][$key]['client']['contacts'] = $resource['client']['data']['contacts']['data'] ?? []; $resources['data'][$key]['project'] = $resource['project']['data'] ?? []; $resources['data'][$key]['invoice'] = $resource['invoice'] ?? []; - + } return $resources['data']; @@ -765,6 +817,12 @@ class TemplateService } + /** + * @todo refactor + * + * @param mixed $projects + * @return array + */ public function processProjects($projects): array { @@ -778,9 +836,15 @@ class TemplateService } + /** + * @todo refactor + * + * @param mixed $purchase_orders + * @return array + */ public function processPurchaseOrders($purchase_orders): array { - + $it = new PurchaseOrderTransformer(); $it->setDefaultIncludes(['vendor','expense']); $manager = new Manager(); @@ -791,27 +855,45 @@ class TemplateService } + /** + * Set Company + * + * @param mixed $company + * @return self + */ public function setCompany(Company $company): self { $this->company = $company; - + return $this; } + /** + * Get Company + * + * @return Company + */ public function getCompany(): Company { return $this->company; } + /** + * Setter that allows external variables to override the + * resolved ones from this class + * + * @param mixed $variables + * @return self + */ public function overrideVariables($variables): self { $this->variables = $variables; - + return $this; } - + /** - * Parses and finds any stacks to replace + * Parses and finds any field stacks to inject into the DOM Document * * @return self */ @@ -829,14 +911,14 @@ class TemplateService collect($stacks)->filter(function ($stack) { $exists = $this->document->getElementById($stack) ?? false; return $exists ? ['stack' => $stack, 'labels' => $exists->getAttribute('labels')] : false; - })->each(function ($stack){ + })->each(function ($stack) { $this->parseStack($stack); }); return $this; } - + /** * Injects field stacks into Template * @@ -846,7 +928,7 @@ class TemplateService private function parseStack(string $stack): self { - match($stack['stack']){ + match($stack['stack']) { 'entity-details' => $this->entityDetails($stack['labels'] == 'true'), 'client-details' => $this->clientDetails($stack['labels'] == 'true'), 'vendor-details' => $this->vendorDetails($stack['labels'] == 'true'), @@ -860,27 +942,35 @@ class TemplateService return $this; } + /** + * Inject the Company Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ private function companyDetails(bool $include_labels): self { $var_set = $this->getVarSet(); - $company_details = + $company_details = collect($this->company->settings->pdf_variables->company_details) - ->filter(function ($variable) use($var_set) { + ->filter(function ($variable) use ($var_set) { return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); }) - ->map(function ($variable) { - return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]]; + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_details-' . substr($variable, 1)]]; + }); })->toArray(); $company_details = $include_labels ? $this->labelledFieldStack($company_details) : $company_details; - + $this->updateElementProperties('company-details', $company_details); return $this; } - private function companyAddress(): self + private function companyAddress(bool $include_labels = false): self { $var_set = $this->getVarSet(); @@ -890,19 +980,30 @@ class TemplateService ->filter(function ($variable) use ($var_set) { return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); }) - ->map(function ($variable) { - return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_address-' . substr($variable, 1)]]; + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'company_address-' . substr($variable, 1)]]; + }); })->toArray(); + $company_address = $include_labels ? $this->labelledFieldStack($company_address) : $company_address; + $this->updateElementProperties('company-address', $company_address); return $this; } - private function shippingDetails(): self + /** + * Injects the Shipping Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function shippingDetails(bool $include_labels = false): self { - if(!$this->entity->client) + if(!$this->entity->client) { return $this; + } $this->client = $this->entity->client; @@ -920,7 +1021,7 @@ class TemplateService ]; $shipping_address = - collect($shipping_address)->filter(function ($address){ + collect($shipping_address)->filter(function ($address) { return isset($address['content']) && !empty($address['content']); })->toArray(); @@ -929,7 +1030,13 @@ class TemplateService return $this; } - private function clientDetails(): self + /** + * Injects the Client Details into the DOM Document + * + * @param bool $include_labels + * @return self + */ + private function clientDetails(bool $include_labels = false): self { $var_set = $this->getVarSet(); @@ -938,21 +1045,31 @@ class TemplateService ->filter(function ($variable) use ($var_set) { return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); }) - ->map(function ($variable) { - return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'client_details-' . substr($variable, 1)]]; + ->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'client_details-' . substr($variable, 1)]]; + }); })->toArray(); + $client_details = $include_labels ? $this->labelledFieldStack($client_details) : $client_details; $this->updateElementProperties('client-details', $client_details); return $this; } + /** + * Resolves the entity. + * + * Only required for resolving the entity-details stack + * + * @return string + */ private function resolveEntity(): string { $entity_string = ''; - match($this->entity){ + match($this->entity) { ($this->entity instanceof Invoice) => $entity_string = 'invoice', ($this->entity instanceof Quote) => $entity_string = 'quote', ($this->entity instanceof Credit) => $entity_string = 'credit', @@ -964,7 +1081,7 @@ class TemplateService return $entity_string; } - + /** * Returns the variable array by first key, if it exists * @@ -974,7 +1091,7 @@ class TemplateService { return array_key_exists(array_key_first($this->variables), $this->variables) ? $this->variables[array_key_first($this->variables)] : $this->variables; } - + /** * Injects the entity details to the DOM document * @@ -996,7 +1113,7 @@ class TemplateService return $this; } - + /** * Generates the field stacks with labels * @@ -1005,7 +1122,7 @@ class TemplateService */ private function labelledFieldStack(array $variables): array { - + $elements = []; foreach ($variables as $variable) { @@ -1028,10 +1145,16 @@ class TemplateService } return $elements; - + } - private function vendorDetails(): self + /** + * Inject Vendor Details into DOM Document + * + * @param bool $include_labels + * @return self + */ + private function vendorDetails(bool $include_labels = false): self { $var_set = $this->getVarSet(); @@ -1040,11 +1163,13 @@ class TemplateService collect($this->company->settings->pdf_variables->vendor_details) ->filter(function ($variable) use ($var_set) { return isset($var_set['values'][$variable]) && !empty($var_set['values'][$variable]); - }) - ->map(function ($variable) { - return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]]; + })->when(!$include_labels, function ($collection) { + return $collection->map(function ($variable) { + return ['element' => 'p', 'content' => $variable, 'show_empty' => false, 'properties' => ['data-ref' => 'vendor_details-' . substr($variable, 1)]]; + }); })->toArray(); + $vendor_details = $include_labels ? $this->labelledFieldStack($vendor_details) : $vendor_details; $this->updateElementProperties('vendor-details', $vendor_details); @@ -1096,8 +1221,8 @@ class TemplateService public function updateElementProperties(string $element_id, array $elements): self { $node = $this->document->getElementById($element_id); - - $this->createElementContent($node, $elements); + + $this->createElementContent($node, $elements); return $this; } @@ -1119,7 +1244,7 @@ class TemplateService } - public function createElementContent($element, $children) :self + public function createElementContent($element, $children): self { foreach ($children as $child) { @@ -1140,7 +1265,7 @@ class TemplateService $contains_html = preg_match('#(?<=<)\w+(?=[^<]*?>)#', $child['content'], $m) != 0; } - + if ($contains_html) { // If the element contains the HTML, we gonna display it as is. Backend is going to // encode it for us, preventing any errors on the processing stage.