mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 13:12:50 +01:00
template service
This commit is contained in:
parent
2e08fe1ff6
commit
ad41e6dc93
@ -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()
|
||||
|
235
app/Services/Template/TemplateService.php
Normal file
235
app/Services/Template/TemplateService.php
Normal file
@ -0,0 +1,235 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\Template;
|
||||
|
||||
use App\Models\Design;
|
||||
|
||||
|
||||
class TemplateService
|
||||
{
|
||||
|
||||
private \DomDocument $document;
|
||||
|
||||
private string $compiled_html = '';
|
||||
|
||||
public function __construct(public Design $template)
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
@ -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 = '
|
||||
|
||||
<ninja>
|
||||
$company.name
|
||||
<table class="min-w-full text-left text-sm font-light">
|
||||
<thead class="border-b font-medium dark:border-neutral-500">
|
||||
<tr class="text-sm leading-normal">
|
||||
<th scope="col" class="px-6 py-4">Item #</th>
|
||||
<th scope="col" class="px-6 py-4">Description</th>
|
||||
<th scope="col" class="px-6 py-4">Ordered</th>
|
||||
<th scope="col" class="px-6 py-4">Delivered</th>
|
||||
<th scope="col" class="px-6 py-4">Outstanding</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in entity.line_items|filter(item => item.type_id == "1") %}
|
||||
<tr class="border-b dark:border-neutral-500">
|
||||
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.product_key }}</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.notes }}</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.quantity }}</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 font-medium">{{ item.quantity }}</td>
|
||||
<td class="whitespace-nowrap px-6 py-4 font-medium">0</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</ninja>
|
||||
|
||||
';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user