Skip to content

Conversation

@nicolasiscoding
Copy link
Member

Summary

Implements a production-grade PHP SDK for TurboDocx that achieves TypeScript-equivalent strong typing using modern PHP 8.1+ features.

Key Features

  • 8 TurboSign methods for digital signatures (matching TypeScript SDK)
  • Strong typing with PHP 8.1 enums and readonly classes
  • Type-safe HTTP client with PHPDoc generics
  • Dual field positioning (coordinates + template anchors)
  • Comprehensive exception hierarchy (5 custom exceptions)
  • Magic byte file detection for PDF/DOCX/PPTX
  • Industry-standard dependencies (Guzzle, PHPStan level 8)

Architecture Highlights

  • Static method API matching TypeScript SDK pattern
  • Lazy initialization from environment variables
  • Smart response unwrapping (extracts { data: {...} })
  • Multipart form-data for file uploads
  • PSR-4 autoloading and PSR-12 coding standards

Type System

Enums (4):

  • SignatureFieldType - 11 field types
  • DocumentStatus - 6 document states
  • RecipientStatus - 3 recipient states
  • FieldPlacement - 5 template placement options

DTOs (10):

  • Core: Recipient, Field, TemplateConfig
  • Requests: SendSignatureRequest, CreateSignatureReviewLinkRequest
  • Responses: 8 response types for all operations

Files Created

  • 34 core implementation files (~1,670 lines)
  • 6 exception classes
  • 4 enum types
  • 10 DTO classes
  • 1 HTTP client with type-safe methods
  • 1 main TurboSign class with 8 static methods

What's Remaining

  • Unit tests with PHPUnit (Step 10)
  • Example files matching TypeScript patterns (Step 11)
  • README.md with complete documentation (Step 12)
  • Install dependencies and verify PHPStan passes (Step 13)

Test Plan

# Install dependencies
cd packages/php-sdk
composer install

# Run static analysis (should pass level 8)
composer phpstan

# Run tests (after creating tests)
composer test

# Check code style
composer cs-fix --dry-run

Dependencies

{
  "php": "^8.1",
  "guzzlehttp/guzzle": "^7.8",
  "psr/http-client": "^1.0",
  "psr/http-message": "^2.0"
}

Dev Dependencies: PHPUnit 10.5+, PHPStan 1.10+, PHP-CS-Fixer 3.48+

TypeScript → PHP Equivalents

TypeScript PHP 8.1+ Equivalent
type FieldType = 'signature' | 'date' enum SignatureFieldType: string { case SIGNATURE = 'signature'; }
interface Recipient { name: string } final readonly class Recipient { public function __construct(public string $name) {} }
request<T>(path): Promise<T> /** @template T */ public function request(string $path): mixed
optional?: string public ?string $optional = null
static configure(config) public static function configure(HttpClientConfig $config): void

Next Steps

  1. Add comprehensive unit tests
  2. Create 3 example files (simple, basic with template anchors, advanced)
  3. Write complete README.md matching TypeScript SDK structure
  4. Run composer install and verify all tests pass
  5. Ensure PHPStan level 8 passes with 0 errors

🤖 Generated with Claude Code

@nicolasiscoding nicolasiscoding changed the title feat: Add TurboDocx PHP SDK with TypeScript-equivalent strong typing feat: Add TurboDocx PHP SDK strong typing Jan 17, 2026
nicolasiscoding and others added 9 commits January 17, 2026 11:21
Remove email column from support table to focus on self-service channels
(Documentation, Discord, GitHub Issues). Updates width to 33% for better
layout with 3 columns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add new Explore the TurboDocx Ecosystem section featuring html-to-docx,
n8n-nodes-turbodocx, and TurboDocx Writer. Replaces callout with proper
table format for better discoverability of related tools.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Implements a production-grade PHP SDK for TurboDocx that mirrors the TypeScript SDK's API surface using modern PHP 8.1+ features.

Core Features:
- 8 TurboSign methods for digital signatures
- Strong typing with PHP 8.1 enums and readonly classes
- TypeScript-equivalent type safety via PHPDoc generics
- Coordinate-based and template anchor field positioning
- Comprehensive exception hierarchy (5 custom exceptions)
- Magic byte detection for file type identification
- Guzzle HTTP client with error mapping
- PSR-4 autoloading and PSR-12 coding standards

Architecture:
- Static method API matching TypeScript SDK pattern
- Lazy initialization from environment variables
- Smart response unwrapping (extracts { data: {...} })
- Multipart form-data for file uploads
- JSON body for URL/deliverable/template inputs

Type System:
- 4 enums: SignatureFieldType, DocumentStatus, RecipientStatus, FieldPlacement
- 10 DTOs: Recipient, Field, TemplateConfig + Request/Response types
- Readonly classes for immutability
- Named parameters for builder-like API

Dependencies:
- PHP 8.1+ (enums, typed properties, readonly)
- Guzzle 7.8+ for HTTP client
- PSR standards compliance
- PHPStan level 8 for static analysis

Files Created: 34 core implementation files
Next Steps: Tests, examples, documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds complete documentation and examples for the PHP SDK:

Examples (3 files):
- turbosign-send-simple.php: Template anchors with direct sending
- turbosign-basic.php: Review link workflow with template anchors
- turbosign-advanced.php: Coordinate-based positioning, advanced field types, status polling

README Features:
- Installation and quick start guide
- Complete API reference for all 8 methods
- Field types and positioning strategies (coordinates + template anchors)
- File input methods (upload, URL, deliverable ID)
- Error handling with typed exceptions
- Environment variable configuration
- TypeScript → PHP equivalents table
- Testing and development guide

Documentation Structure:
- Mirrors TypeScript SDK README format
- PHP 8.1+ code examples with named parameters
- PHPStan level 8 static analysis guidance
- PSR-12 coding standards
- Composer installation and testing commands

Total: 4 files (3 examples + README)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Created 5 unit test files (31 tests, 82 assertions)
  - ExceptionTest: Tests all custom exception types
  - FieldTest: Tests field creation with coordinates and templates
  - FileTypeDetectorTest: Tests magic byte detection for PDF/DOCX/PPTX
  - HttpClientConfigTest: Tests configuration validation
  - RecipientTest: Tests recipient validation and serialization

- Fixed PHP 8.1 compatibility issues:
  - Changed `readonly` class syntax to individual readonly properties
  - Renamed Exception::$code to $errorCode to avoid property conflict

- Fixed PHPStan level 8 errors:
  - Added RequestException type checking for hasResponse()
  - Removed unused $orgId property
  - Fixed template type annotations
  - Added use statements for Recipient and Field in request classes

- Code quality improvements:
  - Ran PHP-CS-Fixer (PSR-12 formatting)
  - Updated PHPStan configuration
  - All tests passing
  - Zero PHPStan errors

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Updated root README to include PHP SDK in available SDKs table
- Added PHP to installation quick start section
- Added PHP 8.1+ to requirements table
- Updated PHP SDK README:
  - Fixed errorCode property reference in error handling section
  - Added quality metrics section highlighting test results
  - Added comprehensive testing documentation
- Updated CHANGELOG.md:
  - Released v0.1.0 with complete feature set
  - Documented all features, type system, and testing metrics
  - Added TypeScript → PHP equivalents mapping

Quality Metrics:
- 31 unit tests with 82 assertions (100% passing)
- PHPStan level 8 static analysis (0 errors)
- PSR-12 code formatting compliance
- PHP 8.1+ compatibility

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Added test-php job to ci.yml workflow:
  - PHP 8.1 setup with required extensions
  - Composer validation
  - Unit tests (31 tests, 82 assertions)
  - PHPStan level 8 static analysis
  - PSR-12 code style checks

- Created publish-php.yml workflow:
  - Triggers on release tags matching php-sdk-v*
  - Validates package before publishing
  - Runs full test suite and quality checks
  - Packagist auto-detects new tags for publishing

Workflow Features:
- Uses shivammathur/setup-php@v2 for PHP environment
- Validates composer.json structure
- Ensures all tests pass before publishing
- Zero-configuration Packagist publishing via Git tags

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
nicolasiscoding and others added 2 commits January 17, 2026 11:25
- Changed from coordinate-based positioning to template anchors
- Added all advanced field types: signature, date, full_name, company, title, text
- Demonstrates readonly fields with default values
- Shows required checkbox with default checked
- Includes multiline text field
- Matches TypeScript SDK gold standard exactly

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed three response types that had incorrect structures:

1. VoidDocumentResponse:
   - Changed from {documentId, status, voidedAt}
   - To {success, message} (matches TS/Go)
   - Updated void() method to manually construct response (backend returns empty data)

2. ResendEmailResponse:
   - Changed from {documentId, message, resentAt}
   - To {success, recipientCount} (matches TS/Go)

3. AuditTrailResponse:
   - Changed from {documentId, entries}
   - To {document: AuditTrailDocument, auditTrail} (matches TS/Go)
   - Added new AuditTrailDocument class with {id, name} properties

These changes ensure complete parity with the TypeScript and Go SDK implementations.
@nicolasiscoding nicolasiscoding marked this pull request as ready for review January 17, 2026 16:51
@nicolasiscoding
Copy link
Member Author

Code review

Found 2 issues:

  1. Publish workflow uses github.ref_name instead of github.event.release.tag_name (this exact bug was already fixed in commit 8731242 for JS SDK: "The publish workflows were using github.ref_name which evaluates to 'main' for release events, causing them to be skipped.")

validate:
runs-on: ubuntu-latest
if: startsWith(github.ref_name, 'php-sdk-v')
defaults:

  1. Workflow installs dependencies with --no-dev flag but then attempts to run tests, PHPStan, and code style checks which are dev dependencies (phpunit, phpstan, php-cs-fixer are in require-dev)

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-dev
- name: Run tests
run: composer test
- name: Run PHPStan
run: composer phpstan
- name: Check code style
run: composer cs-fix -- --dry-run --diff

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@nicolasiscoding
Copy link
Member Author

Code review

Found 3 issues:

  1. Copy-paste bug: senderName field is assigned $request->senderEmail instead of $request->senderName (comment on line 109 says "Use request senderEmail/senderName if provided")

// Use request senderEmail/senderName if provided, otherwise fall back to configured values
$formData['senderEmail'] = $request->senderEmail ?? $senderConfig['sender_email'];
if ($request->senderName !== null || $senderConfig['sender_name'] !== null) {
$formData['senderName'] = $request->senderEmail ?? $senderConfig['sender_name'];
}

  1. Publish workflow uses github.ref_name instead of github.event.release.tag_name (this exact bug was already fixed in commit 8731242 for JS SDK: "The publish workflows were using github.ref_name which evaluates to 'main' for release events, causing them to be skipped.")

validate:
runs-on: ubuntu-latest
if: startsWith(github.ref_name, 'php-sdk-v')
defaults:

  1. Workflow installs dependencies with --no-dev flag but then attempts to run tests, PHPStan, and code style checks which are dev dependencies (phpunit, phpstan, php-cs-fixer are in require-dev)

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-dev
- name: Run tests
run: composer test
- name: Run PHPStan
run: composer phpstan
- name: Check code style
run: composer cs-fix -- --dry-run --diff

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@nicolasiscoding
Copy link
Member Author

Additional note: Code hygiene

The .php-cs-fixer.cache file is a build artifact and should not be committed to the repository. Consider adding it to .gitignore:

# packages/php-sdk/.gitignore
.php-cs-fixer.cache

This follows the same pattern already established for .phpunit.result.cache which is already properly ignored.

{"php":"8.1.2-1ubuntu2.23","version":"3.92.5:v3.92.5#260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58","indent":" ","lineEnding":"\n","rules":{"nullable_type_declaration":true,"operator_linebreak":true,"ordered_types":{"null_adjustment":"always_last","sort_algorithm":"none"},"single_class_element_per_statement":true,"types_spaces":true,"array_indentation":true,"array_syntax":true,"cast_spaces":true,"concat_space":{"spacing":"one"},"function_declaration":{"closure_fn_spacing":"none"},"method_argument_space":{"after_heredoc":true},"new_with_parentheses":{"anonymous_class":false},"single_line_empty_body":true,"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const","const_import","do","else","elseif","enum","final","finally","for","foreach","function","function_import","if","insteadof","interface","match","named_argument","namespace","new","private","protected","public","readonly","static","switch","trait","try","type_colon","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"trailing_comma_in_multiline":{"after_heredoc":true},"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"octal_notation":true,"clean_namespace":true,"no_unset_cast":true,"assign_null_coalescing_to_coalesce_equal":true,"normalize_index_brace":true,"heredoc_indentation":true,"no_whitespace_before_comma_in_array":{"after_heredoc":true},"list_syntax":true,"ternary_to_null_coalescing":true},"ruleCustomisationPolicyVersion":"null-policy","hashes":{"src\/Types\/Responses\/DocumentStatusResponse.php":"d8ee94a24adf08de706151c736740433","src\/Types\/Responses\/RecipientResponse.php":"2be9ebbf78b2580738f377d309d972a6","src\/Types\/Responses\/ResendEmailResponse.php":"617ad3e6dc5e285a7f7eba39c28e27b5","src\/Types\/Responses\/CreateSignatureReviewLinkResponse.php":"b48437e5cfae0c0f6da69447d0b77aeb","src\/Types\/Responses\/AuditTrailEntry.php":"f9166373f561ea10c2ada0df0a6e84e6","src\/Types\/Responses\/AuditTrailResponse.php":"78b63e118c3e16ab02fd003dda9144bb","src\/Types\/TemplateConfig.php":"0ec89a359cd96d3288655445c9ca6fd2","src\/Types\/DocumentStatus.php":"2fed08781601bef50a3b954d25358efb","src\/Config\/HttpClientConfig.php":"f7e6eddcfb1683878787a69b3169f9bf","src\/Utils\/FileTypeDetector.php":"4b2b5cefa95a66a9e60f0f5c58957843","src\/HttpClient.php":"3df249c3d227fec46c9166fda2136705","src\/Types\/Requests\/SendSignatureRequest.php":"946444c2427c6c007697ba38a4034f9d","src\/Types\/Requests\/CreateSignatureReviewLinkRequest.php":"fb2e01ff32ee4c213dbfde56ca2a89cc","src\/Types\/RecipientStatus.php":"521a3cda203b15d55b88adcdf8496314","src\/Types\/SignatureFieldType.php":"c6aadd75d6dd70d5dee541cb50ebe6bd","src\/Types\/FieldPlacement.php":"4ccf5542d301622ca1334023da950661","src\/Types\/Field.php":"7450b2382b749aea1bac593a49eb4a8c","src\/Types\/Recipient.php":"1b189d736882f86104c099e638902172","src\/Types\/Responses\/VoidDocumentResponse.php":"ddd3ae666bac4f2afe2ca7c920a82862","src\/Types\/Responses\/SendSignatureResponse.php":"726040aa8bea1a69087462dc39679182","tests\/Unit\/ExceptionTest.php":"26916fb98cc92c005122328002b64802","tests\/Unit\/HttpClientConfigTest.php":"38e46d187d3aefbe9c3ad5e67e4ae601","tests\/Unit\/FileTypeDetectorTest.php":"115b73196c15d4588850d48080af7439","tests\/Unit\/FieldTest.php":"3d191da93bb6deff4513deca4b04b8f7","tests\/Unit\/RecipientTest.php":"9d7aa09ed80be80c42a5ebaa69c9db31","tests\/bootstrap.php":"b5314409ebe2120103df9fc68c80f85b","src\/Exceptions\/TurboDocxException.php":"043c9ea9bd2920b639e1fb3362291bda","src\/Exceptions\/AuthenticationException.php":"db149cd1e6286ebc63067aab9771c548","src\/Exceptions\/RateLimitException.php":"3e5a51939816aae31e001abf40bc1330","src\/Exceptions\/NotFoundException.php":"527ee7f69115ee3ffd341f707fbb602e","src\/Exceptions\/ValidationException.php":"76767df0b4b19f7752bcbd0b6a3e2a8b","src\/Exceptions\/NetworkException.php":"ca849a99b44415bb9e01003134231216","src\/TurboSign.php":"9d9fb9d675d8891a78f47bda9eec9f0a","examples\/turbosign-advanced.php":"7223d2697d0b84a62347741332ff7c92","examples\/turbosign-basic.php":"d012644eab6d5448f4e5a7f3ee2003b6","examples\/turbosign-send-simple.php":"514967e37aa7e3dc76827081a58196cb"}}

nicolasiscoding and others added 2 commits January 17, 2026 12:27
- Fix copy-paste bug where senderName was assigned senderEmail value in createSignatureReviewLink()
- Add unit tests to verify senderName field handling
- Fix publish workflow to use github.event.release.tag_name instead of github.ref_name
- Remove --no-dev flag from composer install to include dev dependencies for tests
- Add .php-cs-fixer.cache to .gitignore and remove from repository

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add proper PHPDoc annotations for array parameters and return type
to satisfy PHPStan level 8 requirements.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@nicolasiscoding
Copy link
Member Author

📚 Documentation Ready

Comprehensive PHP SDK documentation has been created and is ready for review!

Documentation PR: TurboDocx/Docs#44

What's Included

The documentation PR adds:

  • 791 lines of Stripe/PostHog-like elegant documentation
  • Modern PHP 8.1+ patterns (enums, named parameters, readonly classes)
  • All 7 API methods fully documented
  • 11 field types with complete examples
  • Comprehensive error handling
  • Integration with existing SDK docs (added PHP to all examples)

Preview

The docs showcase the PHP SDK's features:

  • HttpClientConfig::fromEnvironment() convenience method
  • Strong typing with enums and readonly classes
  • PSR-12 compliant examples
  • Progressive complexity from simple to advanced

Once both PRs are merged, developers will have production-ready SDK + documentation! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants