From ad41e6dc9376d63644d22f30a3ad1bce60b56bb9 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 22 Sep 2023 16:14:25 +1000 Subject: [PATCH] template service --- .../Requests/Design/UpdateDesignRequest.php | 5 +- app/Services/Template/TemplateService.php | 235 ++++++++++++++++++ tests/Feature/Template/TemplateTest.php | 110 +++++++- 3 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 app/Services/Template/TemplateService.php diff --git a/app/Http/Requests/Design/UpdateDesignRequest.php b/app/Http/Requests/Design/UpdateDesignRequest.php index 5e643562bf..9c6c48fa51 100644 --- a/app/Http/Requests/Design/UpdateDesignRequest.php +++ b/app/Http/Requests/Design/UpdateDesignRequest.php @@ -25,7 +25,10 @@ class UpdateDesignRequest extends Request */ public function authorize() : bool { - return auth()->user()->isAdmin(); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->isAdmin(); } public function rules() diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php new file mode 100644 index 0000000000..8b4bfc07fd --- /dev/null +++ b/app/Services/Template/TemplateService.php @@ -0,0 +1,235 @@ +template = $template; + $this->init(); + } + + /** + * Boot Dom Document + * + * @return self + */ + private function init(): self + { + $this->document = new \DOMDocument(); + $this->document->validateOnParse = true; + + return $this; + } + + /** + * Iterate through all of the + * ninja nodes + * + * @param array $data - the payload to be passed into the template + * @return self + */ + private function build(array $data): self + { + $this->compose() + ->parseNinjaBlocks($data) + ->parseVariables(); + + return $this; + } + + public function getHtml(): string + { + return $this->compiled_html; + } + /** + * Parses all Ninja tags in the document + * + * @param array $data + * + * @return self + */ + private function parseNinjaBlocks(array $data): self + { + $data = $this->preProcessDataBlocks($data); + $replacements = []; + + $contents = $this->document->getElementsByTagName('ninja'); + + foreach ($contents as $content) { + + $template = $content->ownerDocument->saveHTML($content); + + $loader = new \Twig\Loader\FilesystemLoader(storage_path()); + $twig = new \Twig\Environment($loader); + + $string_extension = new \Twig\Extension\StringLoaderExtension(); + $twig->addExtension($string_extension); + + $template = $twig->createTemplate(html_entity_decode($template)); + $template = $template->render($data); + + $f = $this->document->createDocumentFragment(); + $f->appendXML($template); + $replacements[] = $f; + + } + + foreach($contents as $key => $content) { + $content->parentNode->replaceChild($replacements[$key], $content); + } + + $this->save(); + + return $this; + + } + + /** + * Parses all variables in the document + * + * @return self + */ + private function parseVariables(): self + { + $variables = $this->resolveHtmlEngine(); + + $html = strtr($this->getHtml(), $variables['labels']); + $html = strtr($html, $variables['values']); + + @$this->document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); + $this->save(); + + return $this; + } + + /** + * Saves the document and updates the compiled string. + * + * @return self + */ + private function save(): self + { + $this->compiled_html = str_replace('%24', '$', $this->document->saveHTML()); + + return $this; + } + + /** + * compose + * + * @return self + */ + private function compose(): self + { + $html = ''; + $html .= $this->template->design->includes; + $html .= $this->template->design->header; + $html .= $this->template->design->body; + $html .= $this->template->design->footer; + + @$this->document->loadHTML($html); + + return $this; + + } + + /** + * Resolves the labels and values needed to replace the string + * holders in the template. + * + * @return array + */ + private function resolveHtmlEngine(): array + { + + } + + private function preProcessDataBlocks($data): array + { + return collect($data)->map(function ($key, $value){ + + $processed[$key] = []; + + match ($key) { + 'invoices' => $processed[$key] = $this->processInvoices($value), + 'quotes' => $processed[$key] = $this->processQuotes($value), + 'credits' => $processed[$key] = $this->processCredits($value), + 'payments' => $processed[$key] = $this->processPayments($value), + 'tasks' => $processed[$key] = $this->processTasks($value), + 'projects' => $processed[$key] = $this->processProjects($value), + 'purchase_orders' => $processed[$key] = $this->processPurchaseOrders($value), + }; + + return $processed; + + })->toArray(); + } + + private function processInvoices($invoices): array + { + return $invoices->map(function ($invoice){ + + })->toArray(); + } + + private function processQuotes($quotes): array + { + return $quotes->map(function ($quote){ + + })->toArray(); + } + + private function processCredits($credits): array + { + return $credits->map(function ($credit){ + + })->toArray(); + } + + private function processPayments($payments): array + { + return $payments->map(function ($payment){ + + })->toArray(); + } + + private function processTasks($tasks): array + { + return $tasks->map(function ($task){ + + })->toArray(); + } + + private function processProjects($projects): array + { + return $projects->map(function ($project){ + + })->toArray(); + } + + private function processPurchaseOrders($purchase_orders): array + { + return $purchase_orders->map(function ($purchase_order){ + + })->toArray(); + } +} \ No newline at end of file diff --git a/tests/Feature/Template/TemplateTest.php b/tests/Feature/Template/TemplateTest.php index 043d774790..688d20c9af 100644 --- a/tests/Feature/Template/TemplateTest.php +++ b/tests/Feature/Template/TemplateTest.php @@ -11,17 +11,20 @@ namespace Tests\Feature\Template; -use App\Models\Design; use Tests\TestCase; use App\Utils\Ninja; +use App\Models\Design; +use App\Models\Invoice; use App\Utils\HtmlEngine; use Tests\MockAccountData; use App\Services\PdfMaker\PdfMaker; use Illuminate\Support\Facades\App; -use Illuminate\Routing\Middleware\ThrottleRequests; -use Illuminate\Foundation\Testing\DatabaseTransactions; +use App\Jobs\Entity\CreateEntityPdf; +use App\Services\PdfMaker\Design as DesignMaker; use App\Services\PdfMaker\Design as PdfDesignModel; use App\Services\PdfMaker\Design as PdfMakerDesign; +use Illuminate\Routing\Middleware\ThrottleRequests; +use Illuminate\Foundation\Testing\DatabaseTransactions; /** * @test @@ -32,6 +35,36 @@ class TemplateTest extends TestCase use DatabaseTransactions; use MockAccountData; + private string $body = ' + + + $company.name + + + + + + + + + + + + {% for item in entity.line_items|filter(item => item.type_id == "1") %} + + + + + + + + {% endfor %} + +
Item #DescriptionOrderedDeliveredOutstanding
{{ item.product_key }}{{ item.notes }}{{ item.quantity }}{{ item.quantity }}0
+
+ + '; + protected function setUp() :void { parent::setUp(); @@ -44,6 +77,59 @@ class TemplateTest extends TestCase } + public function testTimingOnCleanDesign() + { + $design_model = Design::find(2); + + $replicated_design = $design_model->replicate(); + $design = $replicated_design->design; + $design->body .= $this->body; + $replicated_design->design = $design; + $replicated_design->is_custom = true; + $replicated_design->save(); + + $entity_obj = \App\Models\Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'status_id' => Invoice::STATUS_SENT, + 'design_id' => $replicated_design->id, + ]); + + $i = \App\Models\InvoiceInvitation::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'invoice_id' => $entity_obj->id, + 'client_contact_id' => $this->client->contacts->first()->id, + ]); + + $start = microtime(true); + + $pdf = (new CreateEntityPdf($i))->handle(); + + $end = microtime(true); + + $this->assertNotNull($pdf); + + nlog("Twig + PDF Gen Time: " . $end-$start); + + + } + + + public function testStaticPdfGeneration() + { + $start = microtime(true); + + $pdf = (new CreateEntityPdf($this->invoice->invitations->first()))->handle(); + + $end = microtime(true); + + $this->assertNotNull($pdf); + + nlog("Plain PDF Gen Time: " . $end-$start); + } + public function testTemplateGeneration() { $entity_obj = $this->invoice; @@ -60,7 +146,7 @@ class TemplateTest extends TestCase $design_object = new \stdClass; $design_object->includes = ''; $design_object->header = ''; - $design_object->body = ''; + $design_object->body = $this->body; $design_object->product = ''; $design_object->task = ''; $design_object->footer = ''; @@ -69,6 +155,8 @@ class TemplateTest extends TestCase $design->save(); + $start = microtime(true); + App::forgetInstance('translator'); $t = app('translator'); App::setLocale($entity_obj->client->locale()); @@ -76,9 +164,6 @@ class TemplateTest extends TestCase $html = new HtmlEngine($entity_obj->invitations()->first()); - /** @var \App\Models\Design $design */ - $design = \App\Models\Design::withTrashed()->find($entity_obj->design_id); - $options = [ 'custom_partials' => json_decode(json_encode($design->design), true), ]; @@ -106,7 +191,18 @@ class TemplateTest extends TestCase ]; $maker = new PdfMaker($state); + $maker + ->design($template) + ->build(); + $html = $maker->getCompiledHTML(true); + $end = microtime(true); + + $this->assertNotNull($html); + $this->assertStringContainsStringIgnoringCase($this->company->settings->name, $html); + + nlog("Twig Solo Gen Time: ". $end - $start); } + } \ No newline at end of file