Refactor Custom Invoice/Estimate PDF Templates (#277)

* Add utility class for managing templates

* Register custom pdf template views location

* Update the make:template command to make use of PdfTemplateUtils

* Update PDF invoice/estimate template controllers

* Register pdf_templates filesystem disk

* Remove unused leftovers

* Reformat with pint
This commit is contained in:
Darko Gjorgjijoski
2025-01-13 01:20:13 +01:00
committed by GitHub
parent 12d9d6c801
commit d862ee05e9
11 changed files with 221 additions and 53 deletions

View File

@@ -2,8 +2,11 @@
namespace App\Console\Commands;
use App\Space\PdfTemplateUtils;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class CreateTemplateCommand extends Command
{
@@ -37,25 +40,53 @@ class CreateTemplateCommand extends Command
public function handle(): int
{
$templateName = $this->argument('name');
$type = $this->option('type');
$templateType = $this->option('type');
if (! $type) {
$type = $this->choice('Create a template for?', ['invoice', 'estimate']);
if (! $templateType) {
$templateType = $this->choice('Create a template for?', ['invoice', 'estimate']);
}
if (Storage::disk('views')->exists("/app/pdf/{$type}/{$templateName}.blade.php")) {
if (PdfTemplateUtils::customTemplateFileExists($templateType, sprintf('%s.blade.php', $templateName))) {
$this->info('Template with given name already exists.');
return 0;
return self::INVALID;
}
Storage::disk('views')->copy("/app/pdf/{$type}/{$type}1.blade.php", "/app/pdf/{$type}/{$templateName}.blade.php");
copy(resource_path("static/img/PDF/{$type}1.png"), resource_path("static/img/PDF/{$templateName}.png"));
if (! PdfTemplateUtils::toCustomTemplateMarkupFile(
Str::replace(
sprintf('app.pdf.%s', $templateType),
sprintf('pdf_templates::%s', $templateType),
Storage::disk('views')->get("/app/pdf/{$templateType}/{$templateType}1.blade.php"),
),
$templateType,
$templateName
)) {
$this->error(sprintf('Unable to create %s template.', ucfirst($templateType)));
$path = resource_path("views/app/pdf/{$type}/{$templateName}.blade.php");
$type = ucfirst($type);
$this->info("{$type} Template created successfully at ".$path);
return self::FAILURE;
}
return 0;
PdfTemplateUtils::toCustomTemplateImageFile(
File::get(resource_path("static/img/PDF/{$templateType}1.png")),
$templateType,
$templateName,
);
if (! PdfTemplateUtils::customTemplateFileExists($templateType, 'partials/table.blade.php')) {
PdfTemplateUtils::toCustomTemplateFile(
Storage::disk('views')->get("/app/pdf/{$templateType}/partials/table.blade.php"),
$templateType,
'partials/table.blade.php'
);
}
$this->info(
sprintf('%s Template created successfully at %s',
ucfirst($templateType),
PdfTemplateUtils::getCustomTemplateFilePath($templateType, sprintf('%s.blade.php', $templateName))
)
);
return self::SUCCESS;
}
}

View File

@@ -4,6 +4,8 @@ namespace App\Http\Controllers\V1\Admin\Estimate;
use App\Http\Controllers\Controller;
use App\Models\Estimate;
use App\Space\PdfTemplateUtils;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EstimateTemplatesController extends Controller
@@ -11,13 +13,13 @@ class EstimateTemplatesController extends Controller
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
* @return JsonResponse
*/
public function __invoke(Request $request)
{
$this->authorize('viewAny', Estimate::class);
$estimateTemplates = Estimate::estimateTemplates();
$estimateTemplates = PdfTemplateUtils::getFormattedTemplates('estimate');
return response()->json([
'estimateTemplates' => $estimateTemplates,

View File

@@ -4,6 +4,9 @@ namespace App\Http\Controllers\V1\Admin\Invoice;
use App\Http\Controllers\Controller;
use App\Models\Invoice;
use App\Space\PdfTemplateUtils;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class InvoiceTemplatesController extends Controller
@@ -11,13 +14,16 @@ class InvoiceTemplatesController extends Controller
/**
* Handle the incoming request.
*
* @return \Illuminate\Http\Response
*
* @return JsonResponse
*
* @throws AuthorizationException
*/
public function __invoke(Request $request)
{
$this->authorize('viewAny', Invoice::class);
$invoiceTemplates = Invoice::invoiceTemplates();
$invoiceTemplates = PdfTemplateUtils::getFormattedTemplates('invoice');
return response()->json([
'invoiceTemplates' => $invoiceTemplates,

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use App;
use App\Mail\SendEstimateMail;
use App\Services\SerialNumberFormatter;
use App\Space\PdfTemplateUtils;
use App\Traits\GeneratesPdfTrait;
use App\Traits\HasCustomFieldsTrait;
use Barryvdh\DomPDF\Facade\Pdf as PDF;
@@ -14,8 +15,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\Str;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@@ -424,11 +423,14 @@ class Estimate extends Model implements HasMedia
'taxes' => $taxes,
]);
$template = PdfTemplateUtils::findFormattedTemplate('estimate', $estimateTemplate, '');
$templatePath = $template['custom'] ? sprintf('pdf_templates::estimate.%s', $estimateTemplate) : sprintf('app.pdf.estimate.%s', $estimateTemplate);
if (request()->has('preview')) {
return view('app.pdf.estimate.'.$estimateTemplate);
return view($templatePath);
}
return PDF::loadView('app.pdf.estimate.'.$estimateTemplate);
return PDF::loadView($templatePath);
}
public function getCompanyAddress()
@@ -499,27 +501,13 @@ class Estimate extends Model implements HasMedia
];
}
public static function estimateTemplates()
{
$templates = Storage::disk('views')->files('/app/pdf/estimate');
$estimateTemplates = [];
foreach ($templates as $key => $template) {
$templateName = Str::before(basename($template), '.blade.php');
$estimateTemplates[$key]['name'] = $templateName;
$estimateTemplates[$key]['path'] = Vite::asset('resources/static/img/PDF/'.$templateName.'.png');
}
return $estimateTemplates;
}
public function getInvoiceTemplateName()
{
$templateName = Str::replace('estimate', 'invoice', $this->template_name);
$name = [];
foreach (Invoice::invoiceTemplates() as $template) {
foreach (PdfTemplateUtils::getFormattedTemplates('invoice') as $template) {
$name[] = $template['name'];
}

View File

@@ -5,7 +5,7 @@ namespace App\Models;
use App;
use App\Mail\SendInvoiceMail;
use App\Services\SerialNumberFormatter;
use App\Space\ImageUtils;
use App\Space\PdfTemplateUtils;
use App\Traits\GeneratesPdfTrait;
use App\Traits\HasCustomFieldsTrait;
use Barryvdh\DomPDF\Facade\Pdf as PDF;
@@ -15,8 +15,6 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Nwidart\Modules\Facades\Module;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@@ -582,11 +580,14 @@ class Invoice extends Model implements HasMedia
'taxes' => $taxes,
]);
$template = PdfTemplateUtils::findFormattedTemplate('invoice', $invoiceTemplate, '');
$templatePath = $template['custom'] ? sprintf('pdf_templates::invoice.%s', $invoiceTemplate) : sprintf('app.pdf.invoice.%s', $invoiceTemplate);
if (request()->has('preview')) {
return view('app.pdf.invoice.'.$invoiceTemplate);
return view($templatePath);
}
return PDF::loadView('app.pdf.invoice.'.$invoiceTemplate);
return PDF::loadView($templatePath);
}
public function getEmailAttachmentSetting()
@@ -657,20 +658,6 @@ class Invoice extends Model implements HasMedia
];
}
public static function invoiceTemplates()
{
$templates = Storage::disk('views')->files('/app/pdf/invoice');
$invoiceTemplates = [];
foreach ($templates as $key => $template) {
$templateName = Str::before(basename($template), '.blade.php');
$invoiceTemplates[$key]['name'] = $templateName;
$invoiceTemplates[$key]['path'] = ImageUtils::toBase64Src(resource_path('static/img/PDF/'.$templateName.'.png'));
}
return $invoiceTemplates;
}
public function addInvoicePayment($amount)
{
$this->due_amount += $amount;

View File

@@ -25,6 +25,7 @@ use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
use Silber\Bouncer\Database\Models as BouncerModels;
use Silber\Bouncer\Database\Role;
use View;
class AppServiceProvider extends ServiceProvider
{
@@ -58,6 +59,8 @@ class AppServiceProvider extends ServiceProvider
Gate::policy(Role::class, RolePolicy::class);
View::addNamespace('pdf_templates', storage_path('app/templates/pdf'));
$this->bootAuth();
$this->bootBroadcast();

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Space;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class PdfTemplateUtils
{
/**
* Find the formatted template
*
* @param string $imageFormat
* @return array|null
*/
public static function findFormattedTemplate($templateType, $templateName, $imageFormat = 'base64')
{
foreach (array_reverse(self::getFormattedTemplates($templateType, $imageFormat)) as $formattedTemplate) {
if ($formattedTemplate['name'] === $templateName) {
return $formattedTemplate;
}
}
return null;
}
/**
* Return the available formatted template paths
*
* @param string $imageFormat
* @return array|array[]
*/
public static function getFormattedTemplates($templateType, $imageFormat = 'base64')
{
$files_native = array_map(function ($file) {
return [
'path' => $file,
'custom' => false,
];
}, Storage::disk('views')->files(sprintf('/app/pdf/%s', $templateType)));
$files_custom = array_map(function ($file) {
return [
'path' => $file,
'custom' => true,
];
}, Storage::disk('pdf_templates')->files(sprintf('/%s', $templateType)));
$files = array_merge($files_native, $files_custom);
$files = array_filter($files, function ($file) {
return Str::endsWith($file['path'], '.blade.php');
});
return array_map(function ($file) use ($templateType, $imageFormat) {
$templateName = Str::before(basename($file['path']), '.blade.php');
if ($file['custom']) {
$imagePath = self::getCustomTemplateFilePath($templateType, sprintf('%s.png', $templateName));
$isCustomTemplate = true;
} else {
$imagePath = resource_path('static/img/PDF/'.$templateName.'.png');
$isCustomTemplate = false;
}
if (empty($imageFormat)) {
$imageValue = '';
} elseif ($imageFormat == 'path') {
$imageValue = $imagePath;
} else {
$imageValue = File::exists($imagePath) ? ImageUtils::toBase64Src($imagePath) : '';
}
return [
'name' => $templateName,
'path' => $imageValue,
'custom' => $isCustomTemplate,
];
}, $files);
}
/**
* Returns custom template path
*
* @param string $fileName
*/
public static function getCustomTemplateFilePath($templateType, $fileName = ''): string
{
$path = ! empty($fileName) ? sprintf('/%s/%s', $templateType, $fileName) : sprintf('/%s/', $templateType);
return Storage::disk('pdf_templates')->path($path);
}
/**
* Check if custom template exists.
*
* @param $templateName
* @return string
*/
public static function customTemplateFileExists($templateType, $fileName)
{
return Storage::disk('pdf_templates')->exists(sprintf('/%s/%s', $templateType, $fileName));
}
/**
* Save template markup file
*
* @return bool|string
*/
public static function toCustomTemplateMarkupFile($contents, $templateType, $templateName)
{
return self::toCustomTemplateFile($contents, $templateType, $templateName.'.blade.php');
}
/**
* Save template image file
*
*
* @return bool|string
*/
public static function toCustomTemplateImageFile($contents, $templateType, $templateName, $imageType = 'png')
{
return self::toCustomTemplateFile($contents, $templateType, $templateName.'.'.$imageType);
}
/**
* Save file contents into a template file of specific template type.
*
*
* @return bool|string
*/
public static function toCustomTemplateFile($contents, $templateType, $fileName)
{
return Storage::disk('pdf_templates')->put(
sprintf('/%s/%s', $templateType, $fileName),
$contents
);
}
}

View File

@@ -55,6 +55,11 @@ return [
'driver' => 'local',
'root' => resource_path('views'),
],
'pdf_templates' => [
'driver' => 'local',
'root' => storage_path('app/templates/pdf'),
],
],
];

View File

@@ -1,3 +1,4 @@
*
!public/
!templates/
!.gitignore

3
storage/app/templates/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!pdf/
!.gitignore

2
storage/app/templates/pdf/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore