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 @@ - 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 }} 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 #} + +
{{ 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 f617d6e..a78af32 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,21 @@ [](https://packagist.org/packages/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 for 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() + ->toArray(); +``` + +### 커스텀 데이터 사용하기 (`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'); ``` -Save multiple waybills for pdf format: +### 여러 개의 운송장 생성 + +하나의 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,56 +95,81 @@ WaybillCollection::of(mpdf: $mpdf) ->path(realpath(__DIR__.'/../dist')) ->save('collection.pdf'); -// or - +// 또는 한 번에 여러 개 추가: 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'); ``` -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 `Enums` and `Factory` class, for example: +UPS 같은 새 택배사를 추가하려면 `Enum`과 `Factory` 클래스를 만들어야 합니다: -1. Make `UpsFactory.php` into `src/Factories' folder. -2. Make `Enum` element into `src/Enums` folder. +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