diff --git a/app/Domain/ResumeGenerator.php b/app/Domain/ResumeGenerator.php new file mode 100644 index 0000000..0639c50 --- /dev/null +++ b/app/Domain/ResumeGenerator.php @@ -0,0 +1,140 @@ +getTemplate()); + + $processor->setValue("id", $resume->id); + $processor->setValue("name", $resume->user ? $resume->user->profile->full_name : $resume->name); + + $this->fillTechnologies($processor, $resume); + $this->fillLanguages($processor, $resume); + $this->fillEducation($processor, $resume); + $this->fillProjects($processor, $resume); + + $processor->saveAs($path); + + return $path; + } + + public function getTemplate(): string + { + return resource_path("views/docx/resume_eng.docx"); + } + + protected function fillTechnologies(TemplateProcessor $processor, Resume $resume): void + { + if ($resume->technologies->count() <= 0) { + $processor->deleteBlock("technologies"); + + return; + } + + $processor->cloneBlock("technologies", 0, true, false, $this->getTechnologies($resume)); + } + + + protected function fillLanguages(TemplateProcessor $processor, Resume $resume): void + { + if ($resume->education->count() <= 0) { + $processor->deleteBlock("languages"); + + return; + } + + $processor->cloneBlock("languages", 0, true, false, $this->getLanguages($resume)); + } + + protected function fillEducation(TemplateProcessor $processor, Resume $resume): void + { + if ($resume->education->count() <= 0) { + $processor->deleteBlock("education"); + + return; + } + + $processor->cloneBlock("education", 0, true, false, $this->getEducation($resume)); + } + + protected function fillProjects(TemplateProcessor $processor, Resume $resume): void + { + if ($resume->projects->count() <= 0) { + $processor->deleteBlock("projects"); + + return; + } + + $processor->cloneBlock("projects", $resume->projects->count(), true, true); + + foreach ($resume->projects as $index => $project) { + $index += 1; + $processor->setValues($this->getProject($project, $index)); + + $processor->cloneBlock("project_technologies#{$index}", 0, true, false, $this->getProjectTechnologies($project, $index)); + + } + } + + protected function getProject(array $project, int $index): array + { + return [ + "index#{$index}" => $index, + "start_date#{$index}" => Carbon::create($project["startDate"])->toDisplayString(), + "end_date#{$index}" => Carbon::create($project["endDate"])->toDisplayString(), + "description#{$index}" => $project["description"], + "tasks#{$index}" => $project["tasks"], + ]; + } + + protected function getProjectTechnologies(array $project, int $index): array + { + $technologies = new Collection($project["technologies"] ?? []); + + return $technologies->map(fn(string $name) => [ + "technology#{$index}" => $name, + ])->all(); + } + + protected function getTechnologies(Resume $resume): array + { + return $resume->technologies->map(fn(array $technology): array => [ + "technology_name" => $technology["name"], + "technology_level" => $technology["level"], + ])->all(); + } + + protected function getLanguages(Resume $resume): array + { + return $resume->languages->map(fn(array $language): array => [ + "language_name" => $language["name"], + "language_level" => $language["level"], + ])->all(); + } + + protected function getEducation(Resume $resume): array + { + return $resume->education->map(fn(array $project, int $index): array => [ + "start_date" => Carbon::create($project["startDate"])->toDisplayString(), + "end_date" => Carbon::create($project["endDate"])->toDisplayString(), + "school" => $project["school"], + "field_of_study" => $project["fieldOfStudy"], + "degree" => $project["degree"], + ])->all(); + } +} diff --git a/app/Infrastructure/Http/Controllers/ResumeController.php b/app/Infrastructure/Http/Controllers/ResumeController.php index 9624f4b..6f4907d 100644 --- a/app/Infrastructure/Http/Controllers/ResumeController.php +++ b/app/Infrastructure/Http/Controllers/ResumeController.php @@ -6,6 +6,8 @@ namespace Toby\Infrastructure\Http\Controllers; use Illuminate\Http\RedirectResponse; use Inertia\Response; +use Symfony\Component\HttpFoundation\BinaryFileResponse as BinaryFileResponseAlias; +use Toby\Domain\ResumeGenerator; use Toby\Eloquent\Models\Resume; use Toby\Eloquent\Models\Technology; use Toby\Eloquent\Models\User; @@ -39,6 +41,15 @@ class ResumeController extends Controller ]); } + public function show(Resume $resume, ResumeGenerator $generator): BinaryFileResponseAlias + { + $path = $generator->generate($resume); + + return response() + ->download($path, "resume-{$resume->id}.docx") + ->deleteFileAfterSend(); + } + public function store(ResumeRequest $request): RedirectResponse { $resume = new Resume(); diff --git a/composer.json b/composer.json index 82145dd..9ddfe08 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "laravel/telescope": "^4.6", "laravel/tinker": "^2.5", "maatwebsite/excel": "^3.1", + "phpoffice/phpword": "^0.18.3", "rackbeat/laravel-ui-avatars": "^1.0", "spatie/laravel-google-calendar": "^3.5", "spatie/laravel-model-states": "^2.1" diff --git a/composer.lock b/composer.lock index 8dd0196..da47ef0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "414a1fc13e0731e59605248bd4e39de6", + "content-hash": "3e4ab38084e10e25b26a11dc275dd007", "packages": [ { "name": "asm89/stack-cors", @@ -1731,6 +1731,68 @@ ], "time": "2021-12-16T16:49:26+00:00" }, + { + "name": "laminas/laminas-escaper", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "58af67282db37d24e584a837a94ee55b9c7552be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/58af67282db37d24e584a837a94ee55b9c7552be", + "reference": "58af67282db37d24e584a837a94ee55b9c7552be", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "^7.4 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.26.6", + "laminas/laminas-coding-standard": "~2.3.0", + "maglnet/composer-require-checker": "^3.8.0", + "phpunit/phpunit": "^9.5.18", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.22.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-03-08T20:15:36+00:00" + }, { "name": "laravel/framework", "version": "v9.7.0", @@ -3958,6 +4020,118 @@ }, "time": "2022-02-18T12:57:07+00:00" }, + { + "name": "phpoffice/phpword", + "version": "0.18.3", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPWord.git", + "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/be0190cd5d8f95b4be08d5853b107aa4e352759a", + "reference": "be0190cd5d8f95b4be08d5853b107aa4e352759a", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "laminas/laminas-escaper": "^2.2", + "php": "^5.3.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "dompdf/dompdf": "0.8.* || 1.0.*", + "ext-gd": "*", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^2.2", + "mpdf/mpdf": "5.7.4 || 6.* || 7.* || 8.*", + "php-coveralls/php-coveralls": "1.1.0 || ^2.0", + "phploc/phploc": "2.* || 3.* || 4.* || 5.* || 6.* || 7.*", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^4.8.36 || ^7.0", + "squizlabs/php_codesniffer": "^2.9 || ^3.5", + "tecnickcom/tcpdf": "6.*" + }, + "suggest": { + "dompdf/dompdf": "Allows writing PDF", + "ext-gd2": "Allows adding images", + "ext-xmlwriter": "Allows writing OOXML and ODF", + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", + "ext-zip": "Allows writing OOXML and ODF" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "0.19-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOffice\\PhpWord\\": "src/PhpWord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Mark Baker" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com", + "homepage": "http://gabrielbull.com/" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net/blog/" + }, + { + "name": "Ivan Lanin", + "homepage": "http://ivan.lanin.org" + }, + { + "name": "Roman Syroeshko", + "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" + }, + { + "name": "Antoine de Troostembergh" + } + ], + "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", + "homepage": "http://phpoffice.github.io", + "keywords": [ + "ISO IEC 29500", + "OOXML", + "Office Open XML", + "OpenDocument", + "OpenXML", + "PhpOffice", + "PhpWord", + "Rich Text Format", + "WordprocessingML", + "doc", + "docx", + "html", + "odf", + "odt", + "office", + "pdf", + "php", + "reader", + "rtf", + "template", + "template processor", + "word", + "writer" + ], + "support": { + "issues": "https://github.com/PHPOffice/PHPWord/issues", + "source": "https://github.com/PHPOffice/PHPWord/tree/0.18.3" + }, + "time": "2022-02-17T15:40:03+00:00" + }, { "name": "phpoption/phpoption", "version": "1.8.1", diff --git a/resources/js/Pages/Resumes/Edit.vue b/resources/js/Pages/Resumes/Edit.vue index 2c3debe..33dffb5 100644 --- a/resources/js/Pages/Resumes/Edit.vue +++ b/resources/js/Pages/Resumes/Edit.vue @@ -1,5 +1,5 @@