From 89feac93f11552389f50139bb7cd3c7cdcd2fbf6 Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Mon, 13 Jul 2026 15:59:19 +0900 Subject: [PATCH 01/12] fix: a small bug --- src/Factories/Factory.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Factories/Factory.php b/src/Factories/Factory.php index 689505d..1d0aa9e 100644 --- a/src/Factories/Factory.php +++ b/src/Factories/Factory.php @@ -47,12 +47,10 @@ public function state(array $state): static */ public function create(): array { - $record = array_values($this->definition()); + $record = $this->definition(); if (! empty($this->state)) { - foreach ($this->state as $key => $value) { - $record[$key] = $this->{$key} ?? $value; - } + $record = array_replace($record, $this->state); } return $record; From 94b9318643d0314c750e08feb053a15f3ceab6c4 Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Mon, 13 Jul 2026 15:59:42 +0900 Subject: [PATCH 02/12] fix: typo in `README.md` --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f617d6e..a81ef17 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ composer require cable8mm/waybill ## Usage -Save a waybill for pdf format: +Save a waybill in PDF format: ```php use Cable8mm\Waybill\Enums\ParcelService; @@ -34,7 +34,7 @@ Get a waybill array: ```php $waybill = Waybill::of(ParcelService::Cj) - ->toArray() + ->toArray(); ``` Save multiple waybills for pdf format: @@ -53,7 +53,7 @@ WaybillCollection::of(mpdf: $mpdf) WaybillCollection::of(mpdf: $mpdf) ->add([ Waybill::of(ParcelService::Cj, mpdf: $mpdf), - Waybill::of(ParcelService::Cj, mpdf: $mpdf), + Waybill::of(ParcelService::Cj, mpdf: $mpdf), ]) ->path(realpath(__DIR__.'/../dist')) ->save('collection.pdf'); @@ -70,10 +70,11 @@ Slicer::of(ParcelService::Cj, 1) ### How to customize -If you want to add another parcel service like UPS, you would need to make `Enums` and `Factory` class, for example: +If you want to add another parcel service like UPS, you would need to make `Enum` and `Factory` classes, for example: -1. Make `UpsFactory.php` into `src/Factories' folder. -2. Make `Enum` element into `src/Enums` folder. +1. Create `UpsFactory.php` in the `src/Factories/` folder. +2. Add an `Ups` case to the `ParcelService` enum in `src/Enums/ParcelService.php`. +3. Implement `factoryClass()`, `stub()`, and `templateArea()` methods for the new case. ### Testing From 3daec02a194c48b49f0fb456a7df10a286cc7f2d Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Mon, 13 Jul 2026 16:00:07 +0900 Subject: [PATCH 03/12] docs: improve comments --- src/Enums/ParcelService.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Enums/ParcelService.php b/src/Enums/ParcelService.php index 7929077..6865d95 100644 --- a/src/Enums/ParcelService.php +++ b/src/Enums/ParcelService.php @@ -44,13 +44,18 @@ public function stub(): string } /** - * Get area variables + * Get the template area for slicing a specific waybill from a page * - * @return array[int,int,int,int] The area variables + * Returns [offsetX, offsetY, width, height] in mm units. + * These values define the crop region when extracting a single waybill + * from a multi-waybill PDF page using mPDF's ImportPage + UseTemplate. + * + * @return array{0: int, 1: int, 2: int, 3: int} The area as [offsetX, offsetY, width, height] */ public function templateArea(): array { return match ($this) { + // CJ waybill: offset (-40mm, -46mm), size 285mm x 196mm self::Cj => [-40, -46, 285, 196], }; } From dd53f7b55f2c05be8bbf5b29ea700f3c9e1963cf Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Mon, 13 Jul 2026 16:00:26 +0900 Subject: [PATCH 04/12] test: widen test coverage --- tests/Factories/CjFactoryTest.php | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/Factories/CjFactoryTest.php b/tests/Factories/CjFactoryTest.php index 12a6408..c4760e4 100644 --- a/tests/Factories/CjFactoryTest.php +++ b/tests/Factories/CjFactoryTest.php @@ -28,4 +28,46 @@ public function test_it_create_with_state(): void $this->assertEquals('10293', $cjFactory['city']['code']); $this->assertEquals('new', $cjFactory['city']['name']); } + + public function test_it_create_with_empty_state_returns_original_definition(): void + { + $cjFactory = CjFactory::make()->state([])->create(); + + $this->assertArrayHasKey('city', $cjFactory); + $this->assertArrayHasKey('seller', $cjFactory); + $this->assertArrayHasKey('receiver', $cjFactory); + $this->assertArrayHasKey('barcode', $cjFactory); + } + + public function test_it_create_with_partial_state_override(): void + { + $cjFactory = CjFactory::make()->state(['tracking_number' => 'OVERRIDE-1234'])->create(); + + $this->assertEquals('OVERRIDE-1234', $cjFactory['tracking_number']); + // Other fields should remain from the original definition + $this->assertArrayHasKey('city', $cjFactory); + $this->assertArrayHasKey('seller', $cjFactory); + } + + public function test_it_create_without_state_returns_full_definition(): void + { + $cjFactory = CjFactory::make()->create(); + + $this->assertCount(15, $cjFactory); + $this->assertArrayHasKey('city', $cjFactory); + $this->assertArrayHasKey('region', $cjFactory); + $this->assertArrayHasKey('line_items', $cjFactory); + $this->assertArrayHasKey('seller', $cjFactory); + $this->assertArrayHasKey('receiver', $cjFactory); + $this->assertArrayHasKey('printed', $cjFactory); + $this->assertArrayHasKey('total_printed_count', $cjFactory); + $this->assertArrayHasKey('site_order_no', $cjFactory); + $this->assertArrayHasKey('tracking_number', $cjFactory); + $this->assertArrayHasKey('delivery_worker', $cjFactory); + $this->assertArrayHasKey('settlement_type', $cjFactory); + $this->assertArrayHasKey('print_date', $cjFactory); + $this->assertArrayHasKey('box_quantity', $cjFactory); + $this->assertArrayHasKey('freight_type', $cjFactory); + $this->assertArrayHasKey('barcode', $cjFactory); + } } From ebbb2197c7441e10b742352cb3c6165a0ebf56dd Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Mon, 13 Jul 2026 16:51:48 +0900 Subject: [PATCH 05/12] fix: small bugs --- src/Waybill.php | 6 +++--- src/WaybillCollection.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Waybill.php b/src/Waybill.php index 3eca224..e8eea60 100644 --- a/src/Waybill.php +++ b/src/Waybill.php @@ -20,7 +20,7 @@ class Waybill /** * The path to save the waybills */ - private string $path; + private string $path = ''; /** * Constructor @@ -50,9 +50,9 @@ public function write(?array $args = null): void if (! is_null($args)) { $data = $args; } elseif (! empty($this->state)) { - $data = $this->parcelService->factoryClass()::make()->state($this->state)->definition(); + $data = $this->parcelService->factoryClass()::make()->state($this->state)->create(); } else { - $data = $this->parcelService->factoryClass()::make()->definition(); + $data = $this->parcelService->factoryClass()::make()->create(); } $this->mpdf->WriteHTML( diff --git a/src/WaybillCollection.php b/src/WaybillCollection.php index b251e63..b872dd1 100644 --- a/src/WaybillCollection.php +++ b/src/WaybillCollection.php @@ -14,7 +14,7 @@ class WaybillCollection /** * @var string The path to save the waybills */ - private string $path; + private string $path = ''; /** * Constructor From 53cccedf8f33124de3fef536990ae5c8c317aed9 Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Mon, 13 Jul 2026 17:47:47 +0900 Subject: [PATCH 06/12] refactor: initialize a variable --- src/Factories/Factory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Factories/Factory.php b/src/Factories/Factory.php index 1d0aa9e..ee9f49f 100644 --- a/src/Factories/Factory.php +++ b/src/Factories/Factory.php @@ -7,7 +7,7 @@ abstract class Factory /** * Change key-value pairs in the definition */ - private array $state; + private array $state = []; /** * Define a factory definition for online mall companies From b471bbf6f2c2c6483c5ff98acf02b7f0a72b4cd2 Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Mon, 13 Jul 2026 17:55:51 +0900 Subject: [PATCH 07/12] chore: fix `example` of method comment --- src/Slicer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Slicer.php b/src/Slicer.php index e20bd05..b4b286e 100644 --- a/src/Slicer.php +++ b/src/Slicer.php @@ -105,7 +105,7 @@ public function download(string $path): mixed * @param int $page The page to save the waybills * @return static The method returns the Slicer instance * - * @example Slicer::of(ParcelService::Cj)->... + * @example Slicer::of(ParcelService::Cj, 1)->... */ public static function of(ParcelService $parcelService, int $page): static { From b55d475ff600c311c8ec6075b15f107a1e395f0e Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Tue, 14 Jul 2026 01:15:27 +0900 Subject: [PATCH 08/12] test: widen test coverage --- tests/Enums/ParcelServiceTest.php | 11 +++++++++++ tests/SlicerTest.php | 16 +++++++++++++++- tests/WaybillCollectionTest.php | 18 ++++++++++++++++++ tests/WaybillTest.php | 22 ++++++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/tests/Enums/ParcelServiceTest.php b/tests/Enums/ParcelServiceTest.php index caa57ec..941d036 100644 --- a/tests/Enums/ParcelServiceTest.php +++ b/tests/Enums/ParcelServiceTest.php @@ -26,4 +26,15 @@ public function test_stub(): void { $this->assertIsString(ParcelService::Cj->stub()); } + + public function test_template_area(): void + { + $area = ParcelService::Cj->templateArea(); + + $this->assertCount(4, $area); + $this->assertIsInt($area[0]); + $this->assertIsInt($area[1]); + $this->assertIsInt($area[2]); + $this->assertIsInt($area[3]); + } } diff --git a/tests/SlicerTest.php b/tests/SlicerTest.php index d3daed9..a4112e2 100644 --- a/tests/SlicerTest.php +++ b/tests/SlicerTest.php @@ -12,7 +12,7 @@ final class SlicerTest extends TestCase { - public function test_path_method(): void + public function test_source_method(): void { $slicer = Slicer::of(ParcelService::Cj, 1) ->source(realpath(__DIR__.'/../dist')); @@ -26,6 +26,20 @@ public function test_path_method(): void $this->assertStringContainsString(DIRECTORY_SEPARATOR.'dist', $source->getValue($slicer)); } + public function test_page_method(): void + { + $slicer = Slicer::of(ParcelService::Cj, 1) + ->page(3); + + $reflection = new ReflectionClass($slicer); + + $page = $reflection->getProperty('page'); + + $page->setAccessible(true); + + $this->assertEquals(3, $page->getValue($slicer)); + } + public function test_save_method(): void { $mpdf = Mpdf::instance(); diff --git a/tests/WaybillCollectionTest.php b/tests/WaybillCollectionTest.php index 50cdd67..b605932 100644 --- a/tests/WaybillCollectionTest.php +++ b/tests/WaybillCollectionTest.php @@ -100,4 +100,22 @@ public function test_save_with_chaining(): void unlink(realpath(__DIR__.'/../dist').DIRECTORY_SEPARATOR.'collection.pdf'); } + + public function test_download_method(): void + { + $mpdf = Mpdf::instance(); + + $waybillCollection = WaybillCollection::of(mpdf: $mpdf) + ->add(Waybill::of(ParcelService::Cj, mpdf: $mpdf)); + + ob_start(); + + $waybillCollection->download('collection_download.pdf'); + + $content = ob_get_contents(); + + ob_end_clean(); + + $this->assertNotEmpty($content); + } } diff --git a/tests/WaybillTest.php b/tests/WaybillTest.php index a6d1358..714eac4 100644 --- a/tests/WaybillTest.php +++ b/tests/WaybillTest.php @@ -74,4 +74,26 @@ public function test_save_on_another_way(): void unlink(realpath(__DIR__.'/../dist').DIRECTORY_SEPARATOR.'test.pdf'); } + + public function test_to_string(): void + { + $waybill = Waybill::of(ParcelService::Cj); + + $this->assertEquals('CJ택배', (string) $waybill); + } + + public function test_download(): void + { + $waybill = Waybill::of(ParcelService::Cj); + + ob_start(); + + $waybill->download('test_download.pdf'); + + $content = ob_get_contents(); + + ob_end_clean(); + + $this->assertNotEmpty($content); + } } From 8d87e4aa77662487a34135bc56cff24771dee418 Mon Sep 17 00:00:00 2001 From: cable8mm <2672043+cable8mm@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:15:52 +0000 Subject: [PATCH 09/12] Fixes coding style --- src/Collections/Waybills.php | 4 ++-- src/Enums/ParcelService.php | 4 +++- src/Slicer.php | 2 +- src/Support/Faker.php | 23 +++++++++++++++-------- src/Support/Mpdf.php | 7 +++++-- src/WaybillCollection.php | 2 +- tests/Enums/ParcelServiceTest.php | 3 ++- 7 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/Collections/Waybills.php b/src/Collections/Waybills.php index 57ffba3..4c191fd 100644 --- a/src/Collections/Waybills.php +++ b/src/Collections/Waybills.php @@ -18,7 +18,7 @@ class Waybills implements ArrayAccess, Countable, IteratorAggregate */ public function __construct( /** - * @var array Array of Waybill objects + * @var array Array of Waybill objects */ private array $container = [] ) { @@ -28,7 +28,7 @@ public function __construct( /** * Adds a Waybill object to the container * - * @param \Cable8mm\Waybill\Waybill|array $waybill a Waybill object + * @param Waybill|array $waybill a Waybill object * @return static The method returns the instance */ public function add(Waybill|array $waybill): static diff --git a/src/Enums/ParcelService.php b/src/Enums/ParcelService.php index 6865d95..127363f 100644 --- a/src/Enums/ParcelService.php +++ b/src/Enums/ParcelService.php @@ -2,6 +2,8 @@ namespace Cable8mm\Waybill\Enums; +use Cable8mm\Waybill\Factories\CjFactory; + enum ParcelService: string { /** @@ -19,7 +21,7 @@ enum ParcelService: string public function factoryClass(): string { return match ($this) { - self::Cj => \Cable8mm\Waybill\Factories\CjFactory::class, + self::Cj => CjFactory::class, }; } diff --git a/src/Slicer.php b/src/Slicer.php index b4b286e..394ec4d 100644 --- a/src/Slicer.php +++ b/src/Slicer.php @@ -63,7 +63,7 @@ public function page(int $page): static * Save the page of waybills * * @param string $path The path to save the waybills - * @param \Mpdf\Output\Destination $destination The destination + * @param Destination $destination The destination * @return mixed The method returns */ public function save(string $path, $destination = Destination::FILE): mixed diff --git a/src/Support/Faker.php b/src/Support/Faker.php index 1f79cd4..60b68a0 100644 --- a/src/Support/Faker.php +++ b/src/Support/Faker.php @@ -2,10 +2,17 @@ namespace Cable8mm\Waybill\Support; +use Bezhanov\Faker\Provider\Commerce; +use Bezhanov\Faker\Provider\Device; +use Faker\Factory; +use Faker\Generator; +use Picqer\Barcode\Renderers\PngRenderer; +use Picqer\Barcode\Types\TypeCode128; + class Faker { /** - * @var \Faker\Generator + * @var Generator */ private static $instance; @@ -13,15 +20,15 @@ class Faker * Get \Faker\Generator singleton instance * * @param ?string $locale the locale - * @return \Faker\Generator The method returns \Faker\Generator singleton instance + * @return Generator The method returns \Faker\Generator singleton instance */ - public static function shared(?string $locale = 'ko_KR'): \Faker\Generator + public static function shared(?string $locale = 'ko_KR'): Generator { if (! isset(self::$instance)) { - self::$instance = \Faker\Factory::create($locale); + self::$instance = Factory::create($locale); - self::$instance->addProvider(new \Bezhanov\Faker\Provider\Commerce(self::$instance)); - self::$instance->addProvider(new \Bezhanov\Faker\Provider\Device(self::$instance)); + self::$instance->addProvider(new Commerce(self::$instance)); + self::$instance->addProvider(new Device(self::$instance)); } return self::$instance; @@ -50,9 +57,9 @@ public function site(): string */ public function barcode(): string { - $barcode = (new \Picqer\Barcode\Types\TypeCode128)->getBarcode(self::shared()->ean13()); + $barcode = (new TypeCode128)->getBarcode(self::shared()->ean13()); - $renderer = new \Picqer\Barcode\Renderers\PngRenderer; + $renderer = new PngRenderer; return 'data:image/png;base64,'.base64_encode($renderer->render($barcode)); } diff --git a/src/Support/Mpdf.php b/src/Support/Mpdf.php index fe54e37..d16e0ef 100644 --- a/src/Support/Mpdf.php +++ b/src/Support/Mpdf.php @@ -2,17 +2,20 @@ namespace Cable8mm\Waybill\Support; +use Mpdf\Config\ConfigVariables; +use Mpdf\Config\FontVariables; + class Mpdf { public static function instance(): \Mpdf\Mpdf { $config = include __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'config.php'; - $defaultConfig = (new \Mpdf\Config\ConfigVariables)->getDefaults(); + $defaultConfig = (new ConfigVariables)->getDefaults(); $fontDirs = $defaultConfig['fontDir']; $tempDir = $defaultConfig['tempDir']; - $defaultFontConfig = (new \Mpdf\Config\FontVariables)->getDefaults(); + $defaultFontConfig = (new FontVariables)->getDefaults(); $fontData = $defaultFontConfig['fontdata']; $configGlobal = [ diff --git a/src/WaybillCollection.php b/src/WaybillCollection.php index b872dd1..dea3ca8 100644 --- a/src/WaybillCollection.php +++ b/src/WaybillCollection.php @@ -39,7 +39,7 @@ private function __construct( /** * Adds a Waybill object to the container * - * @param \Cable8mm\Waybill\Waybill|array $waybill a Waybill object + * @param Waybill|array $waybill a Waybill object * @return static The method returns the instance */ public function add(Waybill|array $waybill): static diff --git a/tests/Enums/ParcelServiceTest.php b/tests/Enums/ParcelServiceTest.php index 941d036..b438e16 100644 --- a/tests/Enums/ParcelServiceTest.php +++ b/tests/Enums/ParcelServiceTest.php @@ -3,6 +3,7 @@ namespace Cable8mm\Waybill\Tests\Enums; use Cable8mm\Waybill\Enums\ParcelService; +use Cable8mm\Waybill\Factories\CjFactory; use PHPUnit\Framework\TestCase; final class ParcelServiceTest extends TestCase @@ -19,7 +20,7 @@ public function test_value(): void public function test_factory_class(): void { - $this->assertEquals(\Cable8mm\Waybill\Factories\CjFactory::class, ParcelService::Cj->factoryClass()); + $this->assertEquals(CjFactory::class, ParcelService::Cj->factoryClass()); } public function test_stub(): void From 5eb1f4fd789f5758d62b689cb010b29d1437ef0a Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Tue, 14 Jul 2026 01:19:35 +0900 Subject: [PATCH 10/12] docs: remove `PULL_REQUEST_TEMPLATE.md` --- .github/PULL_REQUEST_TEMPLATE.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 16479cd..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,7 +0,0 @@ - From df1c729ec8439b0fc461345d6e62c2d9a5f3756e Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Tue, 14 Jul 2026 01:27:59 +0900 Subject: [PATCH 11/12] test: add `php 8.5` for testing --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 86e6554..2caf24f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.2, 8.3, 8.4] + php: [8.2, 8.3, 8.4, 8.5] name: PHP ${{ matrix.php }} From 124b8858d9e5f5a3a4ef49a7a444b9343d6c9295 Mon Sep 17 00:00:00 2001 From: Samgu Lee Date: Tue, 14 Jul 2026 01:28:26 +0900 Subject: [PATCH 12/12] docs: translate `README.md` with Korean --- AGENTS.md | 342 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 123 +++++++++++++++----- 2 files changed, 439 insertions(+), 26 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..113cd9f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,342 @@ +# Waybill — AI Agent Guide + +This document helps AI agents understand the structure and architecture of the `cable8mm/waybill` package for accurate code generation and modification. + +## Project Overview + +A PHP library that generates PDF waybills for Korean courier services (CJ Logistics). Built with Factory + Enum patterns for easy extension to new parcel services. + +- **Namespace root**: `Cable8mm\Waybill` +- **PHP version**: ^8.2 +- **PDF engine**: mpdf/mpdf ^8.0 +- **Template engine**: cable8mm/stub-template (Twig-based) +- **Faker**: fakerphp/faker + mbezhanov/faker-provider-collection +- **Barcode**: picqer/php-barcode-generator + +## Directory Structure + +``` +waybill/ +├── src/ +│ ├── Waybill.php # Main entry: create single waybill +│ ├── WaybillCollection.php # Manage multiple waybills +│ ├── Slicer.php # Extract a single waybill from a PDF page +│ ├── Collections/ +│ │ └── Waybills.php # Waybill object container (ArrayAccess, Countable, IteratorAggregate) +│ ├── Enums/ +│ │ └── ParcelService.php # Parcel service enum (core extension point) +│ ├── Factories/ +│ │ ├── Factory.php # Abstract factory (definition + state + create pattern) +│ │ └── CjFactory.php # CJ Logistics implementation +│ ├── Support/ +│ │ ├── Faker.php # Faker wrapper for Korean locale data +│ │ └── Mpdf.php # mPDF instance factory (includes Korean font config) +│ └── ... +├── stubs/ +│ └── Cj.stub # CJ waybill Twig template +├── fonts/ +│ └── NanumBarunGothic.ttf # Korean font +├── config.php # mPDF settings (font, paper, margins, watermark) +└── tests/ +``` + +## Architecture & Key Patterns + +### 1. Static Factory Method (`of()` / `make()`) + +All major classes use static factory methods: + +```php +// of() — auto-creates all dependencies +Waybill::of(ParcelService::Cj); + +// make() — create instance without mpdf, inject later +Waybill::make(ParcelService::Cj)->mpdf($mpdf); +``` + +### 2. Fluent API (Method Chaining) + +All setters return `$this;` to support chaining: + +```php +Waybill::of(ParcelService::Cj) + ->state(['seller' => ['name' => '회사명']]) + ->path('/dist') + ->save('waybill.pdf'); +``` + +### 3. Enum + Factory Extension Pattern + +To add a new parcel service, modify 3 files: + +| Step | File | Description | +| ---- | --------------------------------- | ---------------------------------------------- | +| 1 | `src/Factories/{Name}Factory.php` | Extend `Factory`, implement `definition()` | +| 2 | `src/Enums/ParcelService.php` | Add `case Name = 'value'`, implement 3 methods | +| 3 | `stubs/{Name}.stub` | Create Twig template | + +### 4. State Pattern + +`Factory::state()` overrides specific fields from `definition()`: + +```php +// Factory.php +public function create(): array +{ + $record = $this->definition(); + if (! empty($this->state)) { + $record = array_replace($record, $this->state); + } + return $record; +} +``` + +Both `Waybill::toArray()` and `Waybill::write()` internally call `Factory::create()`. + +### 5. mPDF Instances Must Be Created via Support\Mpdf Only + +```php +// CORRECT — Korean font config is applied +use Cable8mm\Waybill\Support\Mpdf; +$mpdf = Mpdf::instance(); + +// WRONG — bypasses config.php settings (no Korean font support) +$mpdf = new \Mpdf\Mpdf(); // ❌ +``` + +## Class Details + +### Waybill (`src/Waybill.php`) + +| Method | Description | +| -------------------------- | ----------------------------------- | +| `of(ParcelService, ?Mpdf)` | Static factory (auto-creates Mpdf) | +| `make(ParcelService)` | Create instance without Mpdf | +| `mpdf(Mpdf)` | Inject Mpdf instance | +| `state(array)` | Set custom data | +| `path(string)` | Set save path | +| `toArray(): array` | Return data as array | +| `save(string): mixed` | Save as PDF file | +| `download(string): mixed` | Output PDF as download stream | +| `write(?array)` | Render HTML and write to Mpdf | +| `__toString(): string` | Return service name (e.g. "CJ택배") | + +### WaybillCollection (`src/WaybillCollection.php`) + +| Method | Description | +| --------------------------- | -------------------------------- | +| `of(?Waybills, int, ?Mpdf)` | Static factory | +| `make(?Waybills, int)` | Create without Mpdf | +| `add(Waybill\|array)` | Add waybill(s) | +| `path(string)` | Set save path | +| `toArray(): array` | Return all waybill data as array | +| `save(string): mixed` | Save as PDF | +| `download(string): mixed` | Download as PDF | + +### Slicer (`src/Slicer.php`) + +| Method | Description | +| ---------------------------------- | --------------------------------- | +| `of(ParcelService, int)` | Static factory (with page number) | +| `source(string)` | Set source PDF path | +| `page(int)` | Set page number to extract | +| `save(string, Destination): mixed` | Save page | +| `download(string): mixed` | Download page | + +### ParcelService Enum (`src/Enums/ParcelService.php`) + +Methods to implement when adding a new service: + +```php +enum ParcelService: string +{ + case Cj = 'CJ택배'; + + // Returns full namespace of the Factory class + public function factoryClass(): string; + + // Returns the stub file path + public function stub(): string; + + // Returns crop region [offsetX, offsetY, width, height] for Slicer + public function templateArea(): array; +} +``` + +### Factory Abstract Class (`src/Factories/Factory.php`) + +```php +abstract class Factory +{ + abstract public function definition(): array; + public static function make(?array $state = []): static; + public function state(array $state): static; + public function create(): array; // definition() + state merge +} +``` + +### CjFactory (`src/Factories/CjFactory.php`) + +Fields returned by `definition()`: + +| Key | Type | Description | +| --------------------- | ----------------------------------------- | -------------------- | +| `city` | `array{code, name}` | City code/name | +| `region` | `array{code, name}` | Region code/name | +| `line_items` | `string[]` | Product list | +| `seller` | `array{name, phone, site, address}` | Seller info | +| `receiver` | `array{name, phone, cell_phone, address}` | Receiver info | +| `printed` | `string (Y-m-d)` | Print date | +| `total_printed_count` | `int` | Total print count | +| `site_order_no` | `string` | Order number | +| `tracking_number` | `string` | Tracking number | +| `delivery_worker` | `string` | Delivery worker name | +| `settlement_type` | `string` | Settlement type | +| `print_date` | `string (Y-m-d)` | Print date | +| `box_quantity` | `int` | Box quantity | +| `freight_type` | `string` | Freight type | +| `barcode` | `string (base64 img)` | Barcode image | + +### Mpdf Factory (`src/Support/Mpdf.php`) + +- Reads all settings from `config.php` to create mPDF instance +- Automatically registers Korean font (NanumBarunGothic) +- Applies watermark, font, paper settings + +### Faker (`src/Support/Faker.php`) + +- Generates Korean data (names, addresses, phone numbers, company names) +- Generates barcodes via `picqer/php-barcode-generator` +- Generates product names via `mbezhanov/faker-provider-collection` + +## Adding a New Parcel Service (Step-by-Step) + +Example: Adding "UPS" + +### 1. Create factory class + +```php +// src/Factories/UpsFactory.php +namespace Cable8mm\Waybill\Factories; + +use Cable8mm\Waybill\Support\Faker; + +class UpsFactory extends Factory +{ + public function definition(): array + { + return [ + // Fields tailored to UPS + 'sender' => [ + 'name' => Faker::shared()->company(), + 'phone' => Faker::shared()->phoneNumber(), + 'address' => Faker::shared()->address(), + ], + 'receiver' => [ + 'name' => Faker::shared()->name(), + 'phone' => Faker::shared()->phoneNumber(), + 'address' => Faker::shared()->address(), + ], + 'tracking_number' => Faker::shared()->bothify('1Z-????-????-????'), + // ... + ]; + } +} +``` + +### 2. Add enum case + +```php +// src/Enums/ParcelService.php +enum ParcelService: string +{ + case Cj = 'CJ택배'; + case Ups = 'UPS'; // ADD + + public function factoryClass(): string + { + return match ($this) { + self::Cj => \Cable8mm\Waybill\Factories\CjFactory::class, + self::Ups => \Cable8mm\Waybill\Factories\UpsFactory::class, // ADD + }; + } + + public function stub(): string + { + $stubPath = match ($this) { + self::Cj => realpath(...), + self::Ups => realpath(...), // ADD + }; + // ... + } + + public function templateArea(): array + { + return match ($this) { + self::Cj => [-40, -46, 285, 196], + self::Ups => [0, 0, 210, 297], // ADD (A4 size) + }; + } +} +``` + +### 3. Create stub template + +```twig +{# stubs/Ups.stub #} + +UPS Waybill + +

{{ sender.name }}

+

{{ receiver.name }} - {{ tracking_number }}

+ + +``` + +## Testing Conventions + +- Test classes must be `final class` +- Test method names use snake_case (e.g. `test_it_create_with_state`) +- PDF generation tests must `unlink()` files after assertion +- Output tests must use `ob_start()` / `ob_end_clean()` +- Reflection for private property access requires `setAccessible(true)` + +```php +final class WaybillTest extends TestCase +{ + public function test_path_exists(): void + { + $reflection = new ReflectionClass($waybill); + $path = $reflection->getProperty('path'); + $path->setAccessible(true); + $this->assertStringContainsString('dist', $path->getValue($waybill)); + } +} +``` + +## Common Pitfalls + +1. **NEVER use `array_values()` in `Factory::create()`** — This destroys associative array keys and breaks state merging. Use `array_replace()` instead. +2. **NEVER call `definition()` in `Waybill::write()`** — State won't be applied. Always use `create()` instead. +3. **NEVER instantiate `new \Mpdf\Mpdf()` directly in `Slicer`** — Always use `Support\Mpdf::instance()` to get Korean font config. +4. **ALWAYS set default value for `$path` in `Waybill`/`WaybillCollection`** — `private string $path = ''` is required to avoid uninitialized property warnings. + +## Composer Scripts + +| Script | Command | +| ------------------------ | ----------------------------- | +| `composer test` | `vendor/bin/phpunit` | +| `composer test-coverage` | Generate HTML coverage report | +| `composer lint` | Laravel Pint code style check | +| `composer apidoc` | Doctum API documentation | + +## CI/CD + +GitHub Actions workflows: + +- `.github/workflows/code-style.yml` — Laravel Pint check +- `.github/workflows/run-tests.yml` — PHPUnit test suite + +## License + +MIT diff --git a/README.md b/README.md index a81ef17..a78af32 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,21 @@ [![Total Downloads](https://img.shields.io/packagist/dt/cable8mm/waybill.svg)](https://packagist.org/packages/cable8mm/waybill) [![Packagist Stars](https://img.shields.io/packagist/stars/cable8mm/waybill)](https://github.com/cable8mm/waybill/stargazers) -A lightweight PHP library for generating PDF waybills with ease. This package allows developers to create and customize waybills in PDF format for courier and logistics services. It supports barcode generation, sender/receiver details, and customizable layouts. Perfect for automating shipping label creation in your e-commerce or logistics applications. +PHP로 PDF 운송장을 손쉽게 생성할 수 있는 라이브러리입니다. CJ대한통운을 기본으로 지원하며, 바코드 생성, 송신자/수신자 정보, 커스터마이징 가능한 레이아웃을 제공합니다. 쇼핑몰이나 물류 시스템에서 배송 레이블 생성을 자동화하는 데 적합합니다. -## Installation +## 설치 -You can install the package via composer: +Composer로 설치할 수 있습니다: ```bash composer require cable8mm/waybill ``` -## Usage +## 사용법 -Save a waybill in PDF format: +### 빠른 시작 + +기본 운송장을 PDF로 저장: ```php use Cable8mm\Waybill\Enums\ParcelService; @@ -30,16 +32,61 @@ Waybill::of(ParcelService::Cj) ->save('test.pdf'); ``` -Get a waybill array: +운송장 데이터를 배열로 가져오기 (API 응답 또는 CSV 내보내기용): ```php +use Cable8mm\Waybill\Enums\ParcelService; +use Cable8mm\Waybill\Waybill; + $waybill = Waybill::of(ParcelService::Cj) ->toArray(); ``` -Save multiple waybills for pdf format: +### 커스텀 데이터 사용하기 (`state()`) + +실제 주문 데이터로 특정 필드를 덮어씁니다: + +```php +use Cable8mm\Waybill\Enums\ParcelService; +use Cable8mm\Waybill\Waybill; + +Waybill::of(ParcelService::Cj) + ->state([ + 'seller' => ['name' => '내회사', 'phone' => '02-1234-5678'], + 'receiver' => ['name' => '홍길동', 'phone' => '010-1234-5678'], + 'tracking_number' => 'CJ-1234-5678-9012', + ]) + ->path(realpath(__DIR__.'/../dist')) + ->save('waybill.pdf'); +``` + +### 인스턴스 분리 생성 후 mpdf 주입 + +`Waybill` 인스턴스를 먼저 만들고, 나중에 `mpdf`를 주입합니다: ```php +use Cable8mm\Waybill\Enums\ParcelService; +use Cable8mm\Waybill\Support\Mpdf; +use Cable8mm\Waybill\Waybill; + +$mpdf = Mpdf::instance(); + +$waybill = Waybill::make(ParcelService::Cj) + ->mpdf($mpdf) + ->path(realpath(__DIR__.'/../dist')) + ->save('test.pdf'); +``` + +### 여러 개의 운송장 생성 + +하나의 PDF 파일에 여러 장의 운송장을 저장: + +```php +use Cable8mm\Waybill\Enums\ParcelService; +use Cable8mm\Waybill\Support\Mpdf; +use Cable8mm\Waybill\Waybill; +use Cable8mm\Waybill\WaybillCollection; + $mpdf = Mpdf::instance(); WaybillCollection::of(mpdf: $mpdf) @@ -48,8 +95,7 @@ WaybillCollection::of(mpdf: $mpdf) ->path(realpath(__DIR__.'/../dist')) ->save('collection.pdf'); -// or - +// 또는 한 번에 여러 개 추가: WaybillCollection::of(mpdf: $mpdf) ->add([ Waybill::of(ParcelService::Cj, mpdf: $mpdf), @@ -60,45 +106,70 @@ WaybillCollection::of(mpdf: $mpdf) ``` -Slice the page of the waybills: +### PDF에서 특정 운송장만 추출하기 + +여러 페이지로 된 PDF에서 원하는 페이지 하나만 잘라냅니다: ```php +use Cable8mm\Waybill\Enums\ParcelService; +use Cable8mm\Waybill\Slicer; + Slicer::of(ParcelService::Cj, 1) ->source('source.pdf') - ->save('one_page.pdf'); // or `->download('one_page.pdf')` + ->save('one_page.pdf'); // 또는 ->download('one_page.pdf') 로 다운로드 +``` + +### 설정 + +`config.php` 파일에서 mPDF 설정을 관리합니다. 한글 폰트(NanumBarunGothic), 용지 크기, 여백, 워터마크 등을 설정할 수 있습니다: + +```php +return [ + 'mode' => 'utf-8', + 'format' => 'A4', + 'custom_font_dir' => __DIR__.'/fonts', + 'custom_font_data' => [ + 'nbg' => [ + 'R' => 'NanumBarunGothic.ttf', + ], + ], + 'default_font' => 'nbg', + // ... +]; ``` -### How to customize +### 택배사 추가하기 (커스터마이징) -If you want to add another parcel service like UPS, you would need to make `Enum` and `Factory` classes, for example: +UPS 같은 새 택배사를 추가하려면 `Enum`과 `Factory` 클래스를 만들어야 합니다: -1. Create `UpsFactory.php` in the `src/Factories/` folder. -2. Add an `Ups` case to the `ParcelService` enum in `src/Enums/ParcelService.php`. -3. Implement `factoryClass()`, `stub()`, and `templateArea()` methods for the new case. +1. `src/Factories/` 폴더에 `UpsFactory.php` 생성 +2. `src/Enums/ParcelService.php` Enum에 `Ups` 케이스 추가 +3. `factoryClass()`, `stub()`, `templateArea()` 메서드 구현 +4. PDF 레이아웃을 위한 `stubs/Ups.stub` 파일 생성 -### Testing +### 테스트 ```bash composer test ``` -### Changelog +### 변경 내역 -Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. +자세한 내용은 [CHANGELOG](CHANGELOG.md)를 확인해주세요. -## Contributing +## 기여하기 -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. +기여 방법은 [CONTRIBUTING](CONTRIBUTING.md)을 참고해주세요. -### Security +### 보안 -If you discover any security related issues, please email instead of using the issue tracker. +보안 관련 이슈는 Issue Tracker 대신 으로 메일 부탁드립니다. -## Credits +## 크레딧 - [Samgu Lee](https://github.com/cable8mm) - [All Contributors](../../contributors) -## License +## 라이선스 -The MIT License (MIT). Please see [License File](LICENSE) for more information. +MIT 라이선스입니다. 자세한 내용은 [License File](LICENSE)을 확인해주세요.