mkgrow / content-control
PHPWord extension for Content Controls (Structured Document Tags) conforming to ISO/IEC 29500-1:2016 §17.5.2. Enables document-level content protection and metadata tagging in Word .docx files.
Requires
- php: >=8.2
- ext-dom: *
- ext-mbstring: *
- ext-zip: *
- phpoffice/phpword: ^1.4
- ramsey/uuid: ^4.7
Requires (Dev)
- pestphp/pest: ^3.0
- phpstan/phpstan: 2.0
- phpstan/phpstan-strict-rules: 2.0
README
Portuguese version: readme.pt-BR.md
ContentControl is a PHP library that extends PHPOffice/PHPWord to add Word Content Controls (Structured Document Tags/SDTs) to .docx files. It enables document-level content protection and metadata tagging conforming to ISO/IEC 29500-1:2016 §17.5.2.
Table of Contents
- Installation
- Quick Start
- Features
- Documentation
- Testing
- Changelog and Contributing
- Security
- Credits
- License
Installation
Install via Composer:
composer require mkgrow/content-control
Requirements:
- PHP >= 8.2
- ext-dom
- ext-mbstring
- ext-zip
- phpoffice/phpword ^1.4
- ramsey/uuid ^4.7
Quick Start
Creating a New Document with Content Controls
<?php require 'vendor/autoload.php'; use MkGrow\ContentControl\ContentControl; // Create a new document $cc = new ContentControl(); $section = $cc->addSection(); // Add text element $text = $section->addText('This field is protected'); // Wrap with Content Control $cc->addContentControl($text, [ 'alias' => 'Protected Field', 'tag' => 'field_1', 'type' => ContentControl::TYPE_RICH_TEXT, 'lockType' => ContentControl::LOCK_SDT_LOCKED ]); // Save the document $cc->save('protected_document.docx');
Modifying Existing Documents
<?php use MkGrow\ContentControl\ContentProcessor; // Open existing template $processor = new ContentProcessor('template.docx'); // Replace SDT content by tag $processor->replaceContent('field_1', 'Updated value'); // Update text while preserving formatting $processor->setValue('field_2', 'New text'); // Save changes $processor->save('output.docx');
Building Tables with Content Controls (v0.6.0+)
<?php use MkGrow\ContentControl\ContentControl; use MkGrow\ContentControl\Bridge\TableBuilder; // Create document and table via PHPWord API $cc = new ContentControl(); $section = $cc->addSection(); $table = $section->addTable(['borderSize' => 6, 'borderColor' => '1F4788']); // Build table using native PHPWord API $row = $table->addRow(); $row->addCell(3000)->addText('Name', ['bold' => true]); $row->addCell(3000)->addText('Value', ['bold' => true]); $row2 = $table->addRow(); $row2->addCell(3000)->addText('Item 1'); $priceCell = $row2->addCell(3000); $priceText = $priceCell->addText('$100'); // Wrap elements with Content Controls via TableBuilder $builder = new TableBuilder($table); $builder->addContentControl($priceText, [ 'tag' => 'price_1', 'alias' => 'Price', 'inlineLevel' => true, // Required for cell-level elements 'runLevel' => true, // Wraps <w:r> instead of <w:p> 'lockType' => ContentControl::LOCK_SDT_LOCKED, ]); $cc->save('table_document.docx');
For more examples, see the samples/ directory.
Features
Core Capabilities:
- Content Control Support: Add Word Content Controls (SDTs) to any PHPWord element
- ISO/IEC 29500-1:2016 Compliance: Full adherence to OOXML standard §17.5.2
- Document Protection: Lock SDTs, content, or both to prevent unauthorized modifications
- Template Processing: Modify existing DOCX files with XPath-based SDT location
- Run-Level SDT Wrapping: Wrap individual
<w:r>text runs with Content Controls (v0.6.0+) - TableBuilder v2: Direct PHPWord Table API with
addContentControl()delegation (v0.6.0+) - SDT Unwrap Finalization: Remove SDT wrappers while preserving visible content via
removeAllControlContents()(v0.7.1+) - GROUP SDT Replacement: Replace placeholder SDTs with complex multi-element structures
- Header/Footer Support: Add Content Controls to headers and footers (v0.2.0+)
- UUID v5 Hashing: Zero-collision element identification for template injection (v0.4.2+)
PHP Compatibility:
- PHP 8.2+ with full typed properties support
- PSR-4 autoloading (
MkGrow\ContentControlnamespace) - All classes are
final(composition over inheritance) - Immutable value objects using readonly properties
Quality Standards:
- PHPStan Level 9 static analysis with strict rules
- 82%+ code coverage with 559+ tests (Pest framework)
- Zero-collision UUID v5 hashing for element identification
- Single Responsibility Principle across all components
Supported Content Control Types:
TYPE_RICH_TEXT- Full formatting support (default)TYPE_PLAIN_TEXT- Simple text without formattingTYPE_PICTURE- Image controlsTYPE_GROUP- Container for multiple elements
Lock Types:
LOCK_NONE- No protection (default)LOCK_SDT_LOCKED- SDT cannot be deleted, content editableLOCK_CONTENT_LOCKED- SDT deletable, content read-onlyLOCK_UNLOCKED- Explicitly no lock
Supported PHPWord Elements:
- Text - Simple text nodes
- TextRun - Formatted text with multiple runs
- Table - Complete table structures
- Cell - Individual table cells (requires
inlineLevel: true) - Title - Heading elements (depth 0-9)
- Image - VML inline/floating images
Documentation
Architecture Overview
ContentControl follows a composition-based architecture with no inheritance hierarchies. All 8 core classes are final, promoting extension via composition rather than inheritance.
Design Philosophy:
- Single Responsibility: Each class has one clear purpose
- Immutability: Value objects use readonly properties for predictability
- No Duplication: v3.0+ uses DOM manipulation (not string replacement) to prevent duplicate SDTs
- Depth-First Processing: Elements sorted by depth (Cell before Table) for correct nested structures
Core Patterns:
- Proxy/Facade Pattern:
ContentControlacts as proxy for PHPWord with SDT functionality - Bridge Pattern:
TableBuilderbridges PHPWord table creation with SDT template injection - Registry Pattern:
SDTRegistrymaintains element-to-config mapping with collision-free ID generation - Service Layer:
SDTInjectoroperates as stateless service for DOM manipulation
Version Evolution:
- v1.x/v2.x: String-based XML manipulation (deprecated)
- v3.0: DOM manipulation with XPath - current standard
- v0.4.0: Inline-level SDT support with
inlineLevelflag - v0.4.2: Fluent API, UUID v5 hashing, GROUP SDT replacement
- v0.5.0: TableBuilder::setStyles() method (must be called before first addRow)
- v0.5.2: Fix content hash strategy for inline-level Text/TextRun hash collisions
- v0.6.0: Run-level SDT wrapping (CT_SdtContentRun), TableBuilder v2 with direct PHPWord Table API
- v0.7.0:
ContentProcessor::replaceContent()supportsTableBuilder, fluentend()removal completed - v0.7.1:
removeAllControlContents()now unwraps SDTs preserving visible content
┌─────────────────────────────────────────────────────────────┐
│ ContentControl │
│ (Facade/Proxy for PHPWord with SDT support) │
│ │
│ - Creates documents via PHPWord delegation │
│ - Registers SDTs in SDTRegistry │
│ - Saves with SDTInjector DOM manipulation │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SDTRegistry │
│ (ID generation and element-config mapping) │
│ │
│ - Generates unique 8-digit IDs with collision detection │
│ - Maps elements to SDT configurations │
│ - Validates duplicate elements/IDs │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SDTInjector │
│ (DOM manipulation service layer) │
│ │
│ - Opens DOCX as ZIP, loads XML as DOMDocument │
│ - Locates elements via ElementLocator (XPath) │
│ - Wraps elements with <w:sdt> in DOM tree │
│ - Processes document.xml, headers, footers │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ElementLocator │
│ (XPath-based element location) │
│ │
│ - Finds elements by content hash (UUID v5) │
│ - Fallback to type + registration order │
│ - Supports inline-level (cell) and block-level elements │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ ContentProcessor │
│ (Template modification via XPath) │
│ │
│ - Opens existing DOCX files │
│ - Modifies SDTs: replaceContent, setValue, appendContent │
│ - GROUP SDT replacement: replaceGroupContent │
│ - Saves with in-place updates │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ TableBuilder │
│ (Bridge for tables with SDTs) │
│ │
│ - v0.7.x: Direct PHPWord Table API + addContentControl() │
│ - Constructor accepts Table|ContentControl|null │
│ - Run-level SDT wrapping (runLevel: true) │
│ - Template injection: injectInto(ContentProcessor, tag) │
│ - Legacy fluent API deprecated (removal in v0.8.0) │
└─────────────────────────────────────────────────────────────┘
Core Components
1. ContentControl
Purpose: Facade/proxy for PHPWord that adds SDT registration and injection capabilities.
Location: src/ContentControl.php
Key Methods:
__construct(?PhpWord $phpWord = null)- Create new document or wrap existing PHPWord instanceaddSection()- Delegate to PHPWord for section creationaddContentControl(object $element, array $options = []): object- Register element for SDT wrappingsave(string $filename, string $format = 'Word2007'): void- Save document with injected SDTsgetPhpWord(): PhpWord- Access underlying PHPWord instancegetSDTRegistry(): SDTRegistry- Access registry for advanced use
Workflow:
- Create document via PHPWord delegation
- Register elements with
addContentControl() - Save - triggers
SDTInjectorto wrap elements in DOM - Modified XML written to DOCX ZIP
Example:
$cc = new ContentControl(); $section = $cc->addSection(); $text = $section->addText('Protected content'); $cc->addContentControl($text, [ 'id' => '12345678', // Optional, auto-generated if omitted 'alias' => 'Display Name', 'tag' => 'metadata-id', 'type' => ContentControl::TYPE_RICH_TEXT, 'lockType' => ContentControl::LOCK_SDT_LOCKED, 'inlineLevel' => false // true for cell-level elements ]); $cc->save('output.docx');
For detailed documentation, see docs/contentcontrol.md.
2. ContentProcessor
Purpose: Open and modify existing DOCX files via XPath-based SDT manipulation.
Location: src/ContentProcessor.php
Key Methods:
__construct(string $documentPath)- Open existing DOCX for modificationfindSdt(string $tag): ?array- Locate SDT by tag (returns DOM info)replaceContent(string $tag, string|AbstractElement $value): bool- Replace entire SDT contentsetValue(string $tag, string $value): bool- Replace text preserving formattingappendContent(string $tag, AbstractElement $element): bool- Add content to existing SDTreplaceGroupContent(string $tag, ContentControl $structure): bool- Replace GROUP SDT with complex structureremoveContent(string $tag): bool- Clear SDT contentremoveAllControlContents(bool $block = false): int- Unwrap all SDTs preserving content, optionally lock documentsave(string $outputPath = ''): void- Save changes (in-place if no path provided)
Workflow:
// 1. Open template $processor = new ContentProcessor('template.docx'); // 2. Modify SDTs $processor->replaceContent('field_1', 'New value'); $processor->setValue('field_2', 'Text with preserved formatting'); // 3. Replace GROUP SDT with complex structure $complexStructure = new ContentControl(); $section = $complexStructure->addSection(); $table = $section->addTable(); // ... build complex content $processor->replaceGroupContent('group_placeholder', $complexStructure); // 4. Save $processor->save('output.docx');
Important: ContentProcessor is single-use. After save(), the ZIP is closed and no further modifications are possible. Create a new instance for additional changes.
For detailed documentation, see docs/contentprocessor.md.
3. TableBuilder
Purpose: Bridge pattern for creating PHPWord tables with Content Controls and injecting into templates.
Location: src/Bridge/TableBuilder.php
Two Distinct Workflows:
Workflow 1: Direct Creation with v2 API (v0.6.0+) (recommended)
// Create table via PHPWord directly $cc = new ContentControl(); $section = $cc->addSection(); $table = $section->addTable(['borderSize' => 6, 'borderColor' => '1F4788']); $row = $table->addRow(); $cell = $row->addCell(3000); $text = $cell->addText('Protected Value'); // Pass Table to TableBuilder, register SDTs $builder = new TableBuilder($table); $builder->addContentControl($text, [ 'tag' => 'value-1', 'runLevel' => true, // Wrap <w:r> instead of <w:p> 'inlineLevel' => true, // Element is inside a cell ]); $cc->save('output.docx');
Workflow 2: Template Injection (build table, inject into template placeholder)
// Build table in new ContentControl $cc = new ContentControl(); $builder = new TableBuilder($cc); // ... build table via fluent or direct API // Inject into template $processor = new ContentProcessor('template.docx'); $builder->injectInto($processor, 'placeholder-tag'); $processor->save('output.docx');
Critical Rules:
- NEVER call
injectInto()on ContentControl - only accepts ContentProcessor - ALWAYS call
setStyles()BEFORE firstaddRow()- throws exception if table exists - Cell-level SDTs REQUIRE
inlineLevel: truein configuration - Run-level SDTs use
runLevel: trueto wrap<w:r>instead of<w:p>(v0.6.0+)
v2 API (v0.6.0+):
__construct(Table|ContentControl|null $source)- Accept Table, ContentControl, or nulladdContentControl(AbstractElement $element, array $config = []): self- Register SDT wrapping for any elementinjectInto(ContentProcessor $processor, string $targetTag): void- Inject into templatesetStyles(array $style): self- Configure table styles (must be before addRow)getContentControl(): ContentControl- Access underlying ContentControl instance
Legacy Fluent API (deprecated since v0.6.0, removal in v0.8.0):
addRow(?int $height = null, array $style = []): RowBuilderRowBuilder::addCell(int $width, array $style = []): CellBuilderCellBuilder::addText(string $text, array $style = []): selfCellBuilder::withContentControl(array $config): selfCellBuilder::end(): RowBuilder/RowBuilder::end(): TableBuilder
See Migration Guide for fluent-to-direct API migration examples.
For detailed documentation, see docs/TableBuilder.md.
Configuration
SDT Configuration Options
All Content Controls support the following configuration options:
$config = [ 'id' => '12345678', // 8-digit ID (auto-generated if omitted) 'alias' => 'Display Name', // Name shown in Word UI 'tag' => 'metadata_id', // Programmatic identifier 'type' => ContentControl::TYPE_RICH_TEXT, // SDT type 'lockType' => ContentControl::LOCK_SDT_LOCKED, // Protection level 'inlineLevel' => false, // true for cell-level elements (REQUIRED) 'runLevel' => false // true to wrap <w:r> instead of <w:p> (v0.6.0+) ];
ID Constraints:
- Exactly 8 characters
- Numeric only (0-9)
- Range: 10000000 to 99999999
- Auto-generated with collision detection if omitted
Type Constants:
ContentControl::TYPE_RICH_TEXT- Full formatting (default)ContentControl::TYPE_PLAIN_TEXT- Simple textContentControl::TYPE_PICTURE- Image controlContentControl::TYPE_GROUP- Container for multiple elements
Lock Type Constants:
ContentControl::LOCK_NONE- No protection (default)ContentControl::LOCK_SDT_LOCKED- SDT cannot be deleted, content editableContentControl::LOCK_CONTENT_LOCKED- SDT deletable, content lockedContentControl::LOCK_UNLOCKED- Explicitly no lock
Inline Level Flag:
- MUST be
truefor elements inside table cells - Default:
false(block-level elements) - Affects XPath search priority (cells before rootElement)
Run Level Flag (v0.6.0+):
- When
true, wraps individual<w:r>(text runs) instead of<w:p>(paragraphs) - Default:
false(paragraph-level wrapping) - Conforms to CT_SdtContentRun (ECMA-376 Part 4 S17.5.2.30)
- Can be combined with
inlineLevel: truefor cell-scoped run-level wrapping
Table Styles Configuration
$tableStyles = [ 'borderSize' => 6, // Border width in eighths of a point 'borderColor' => '1F4788', // Hex color without # 'cellMargin' => 80, // Default cell margin in twips 'alignment' => 'center', // left, center, right 'width' => 100, // Table width 'unit' => 'pct', // pct (percentage) or dxa (twips) 'layout' => 'autofit' // fixed or autofit ]; $builder->setStyles($tableStyles);
Important: setStyles() must be called BEFORE first addRow() call. Throws ContentControlException if table already exists.
Error Handling
Exception Hierarchy
All library-specific exceptions extend MkGrow\ContentControl\Exception\ContentControlException:
use MkGrow\ContentControl\Exception\ContentControlException; use MkGrow\ContentControl\Exception\DocumentNotFoundException; use MkGrow\ContentControl\Exception\ZipArchiveException; use MkGrow\ContentControl\Exception\TemporaryFileException;
Exception Types:
| Exception | Thrown When | Example Scenario |
|---|---|---|
ContentControlException |
General library errors | Invalid configuration, duplicate elements |
DocumentNotFoundException |
word/document.xml missing from DOCX |
Corrupted ZIP archive |
ZipArchiveException |
ZIP manipulation failures | File permissions, disk space |
TemporaryFileException |
Temp file cleanup fails after 3 retries | Windows file locks |
InvalidArgumentException |
Invalid parameters | Duplicate IDs, invalid SDT types |
RuntimeException |
DOM/XML processing errors | Malformed XML, serialization failures |
Best Practices:
use MkGrow\ContentControl\ContentControl; use MkGrow\ContentControl\Exception\ContentControlException; try { $cc = new ContentControl(); $section = $cc->addSection(); $text = $section->addText('Content'); $cc->addContentControl($text, [ 'id' => '12345678', 'tag' => 'field_1' ]); $cc->save('output.docx'); } catch (ContentControlException $e) { // Library-specific errors error_log("ContentControl error: " . $e->getMessage()); } catch (\InvalidArgumentException $e) { // Invalid parameters error_log("Invalid argument: " . $e->getMessage()); } catch (\RuntimeException $e) { // General runtime errors error_log("Runtime error: " . $e->getMessage()); }
Common Error Scenarios:
-
Duplicate Element Registration:
// Throws InvalidArgumentException $cc->addContentControl($text, ['tag' => 'field_1']); $cc->addContentControl($text, ['tag' => 'field_2']); // Same element!
-
Duplicate ID:
$cc->addContentControl($text1, ['id' => '12345678']); $cc->addContentControl($text2, ['id' => '12345678']); // ID collision!
-
Invalid ID Format:
// Throws InvalidArgumentException $cc->addContentControl($text, ['id' => '123']); // Must be 8 digits
-
Missing inlineLevel for Cell SDTs:
// SDT wrapping may fail or wrap incorrect element $cellText = $cell->addText('Value'); $cc->addContentControl($cellText, ['tag' => 'cell_1']); // Missing inlineLevel! // CORRECT: $cc->addContentControl($cellText, [ 'tag' => 'cell_1', 'inlineLevel' => true // REQUIRED ]);
-
setStyles After addRow:
$builder->addRow(); // Table created $builder->setStyles([...]); // Throws ContentControlException! // CORRECT: $builder->setStyles([...])->addRow();
-
injectInto on ContentControl:
$builder = new TableBuilder($cc); $row = $builder->addRow(); $row->addCell(); $builder->injectInto($cc, 'tag'); // Type error! Expects ContentProcessor // CORRECT for template injection: $processor = new ContentProcessor('template.docx'); $builder->injectInto($processor, 'tag');
Logging and Debugging
DOCX Inspection
DOCX files are ZIP archives containing XML. To inspect generated Content Controls:
Windows PowerShell:
# Extract DOCX Expand-Archive generated.docx -DestinationPath temp -Force # View SDTs Get-Content temp/word/document.xml | Select-String '<w:sdt' # Pretty print XML [xml]$xml = Get-Content temp/word/document.xml $xml.Save("temp/formatted.xml") code temp/formatted.xml
Linux/macOS Bash:
# Extract DOCX unzip -q generated.docx -d temp/ # View SDTs cat temp/word/document.xml | grep '<w:sdt' # Pretty print XML xmllint --format temp/word/document.xml > temp/formatted.xml cat temp/formatted.xml
Expected SDT Structure
Valid Content Control XML (ISO/IEC 29500-1:2016 §17.5.2):
Block-level SDT (wraps <w:p>, <w:tbl>, <w:tc>):
<w:sdt> <w:sdtPr> <w:id w:val="12345678"/> <w:alias w:val="Display Name"/> <w:tag w:val="metadata-tag"/> <w:lock w:val="sdtLocked"/> <w:richText/> <!-- or w:text, w:picture, w:group --> </w:sdtPr> <w:sdtContent> <!-- Original element (w:p, w:tbl, w:tc, etc.) --> <w:p> <w:r> <w:t>Protected content</w:t> </w:r> </w:p> </w:sdtContent> </w:sdt>
Run-level SDT (v0.6.0+) (wraps <w:r> inside <w:p>, CT_SdtContentRun):
<w:p> <w:sdt> <w:sdtPr> <w:id w:val="12345678"/> <w:alias w:val="First Name"/> <w:tag w:val="first-name"/> <w:richText/> </w:sdtPr> <w:sdtContent> <w:r> <w:rPr><w:b/></w:rPr> <w:t>John</w:t> </w:r> </w:sdtContent> </w:sdt> </w:p>
Common Debugging Scenarios
-
Duplicate SDTs:
- Symptom: Multiple
<w:sdt>wrappers around same element - Cause: Using v2.x string replacement (deprecated)
- Check: Verify
SDTInjector::$processedElementsregistry marks element before wrapping - Solution: Upgrade to v3.0+ with DOM manipulation
- Symptom: Multiple
-
Missing SDTs:
- Symptom: Element not wrapped with SDT in output
- Cause:
ElementLocatorXPath query doesn't match element - Debug: Check
findElementInDOM()return value, verify element type and order - Solution: Ensure element type is supported, check registration order
-
Malformed XML:
- Symptom: Word cannot open file, reports corruption
- Cause: Invalid XML structure or namespace issues
- Debug: Use
libxml_get_errors()afterDOMDocument::loadXML() - Solution: Validate XML with
xmllint --noout temp/word/document.xml
-
Namespace Pollution:
- Symptom: Redundant
xmlns:wdeclarations in SDT elements - Cause: Manual XML string creation instead of DOM methods
- Solution: Use
createElementNS()with namespace URI, relies on root inheritance
- Symptom: Redundant
PHPStan Analysis
Run PHPStan Level 9 analysis to catch type errors:
composer analyse
Output includes line-specific errors with context. Fix issues before committing.
Element Cache Statistics
For advanced debugging, inspect element identification cache:
use MkGrow\ContentControl\ElementIdentifier; // Get cache stats $stats = ElementIdentifier::getCacheStats(); echo "Cached markers: " . $stats['markers'] . "\n"; echo "Cached hashes: " . $stats['hashes'] . "\n"; // Clear cache if needed (testing only) ElementIdentifier::clearCache();
Testing
Running Tests
The project uses Pest for testing with 559+ tests and 82%+ code coverage.
All Tests:
composer test
Unit Tests Only:
composer test:unit
Feature Tests Only:
composer test:feature
Coverage Report (Enforces 80% Minimum):
composer test:coverage
HTML Coverage Report:
composer test:coverage-html
# Open coverage/html/index.html in browser
Test Structure
Unit Tests (tests/Unit/):
- Test individual classes in isolation
- Mock dependencies for controllable environment
- Fast execution, no file I/O
Feature Tests (tests/Feature/):
- Integration tests with real DOCX generation
- Verify PHPWord integration
- Validate XML structure in generated files
Key Test Categories:
AdvancedHeaderFooterTest.php- SDT injection in headers/footersFluentTableBuilderTest.php- Fluent API validationGroupSdtReplacementTest.php- Complex structure replacementInlineLevelSDTTest.php- Cell-level SDT wrappingNoDuplicationTest.php- v3.0 DOM manipulation verificationImageHashCollisionTest.php- UUID v5 collision resistanceNestedSDTDetectionTest.php- Multi-level SDT preservationRunLevelSDTTest.php- Run-level<w:r>SDT wrapping (v0.6.0)TableBuilderV2Test.php- Direct Table API + addContentControl (v0.6.0)DeprecationTest.php- Deprecation notice validation (v0.6.0)ContentProcessorProtectionTest.php- SDT unwrap behavior and edge cases (v0.7.1)ContentProcessorAdvancedTest.php- Public API unwrap behavior validation (v0.7.1)
Custom Pest Expectations
The test suite includes custom expectations defined in tests/Pest.php:
expect($xml)->toBeValidXml(); // Validates XML well-formedness expect($xml)->toHaveXmlElement('w:sdt'); // Checks element via XPath expect($xml)->toHaveXmlAttribute('w:id', '12345678'); // Verifies attribute
Static Analysis
PHPStan Level 9:
composer analyse
Combined CI Check (Analysis + Coverage):
composer ci
Full Check (Analysis + All Tests):
composer check
Manual Testing in Word
After generating DOCX files, verify in Microsoft Word:
- Open Developer Tab: File → Options → Customize Ribbon → Enable Developer
- View Content Controls: Developer → Design Mode
- Check Properties: Click SDT → Properties button
- Test Protection: Try editing/deleting locked SDTs
- Verify Display Names: Hover over SDT to see alias
For comprehensive Word testing checklist, see docs/MANUAL_TESTING_GUIDE.md.
Changelog and Contributing
Changelog: See CHANGELOG.md for version history and release notes.
Detailed v0.7.1 release notes: docs/0.x/CHANGELOG-v0.7.1.md
Contributing: See CONTRIBUTING.md for development guidelines, coding standards, and pull request process.
Code of Conduct: Be professional, respectful, and constructive. Focus on technical merit and project improvement.
Security
Reporting Vulnerabilities:
If you discover a security vulnerability in ContentControl, please DO NOT open a public issue or pull request.
Instead, report it privately via email to: mateusbandeiraweb@gmail.com
Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if available)
We will respond within 48 hours and work with you to address the issue responsibly.
Security Best Practices:
- Never pass untrusted user input directly to
ContentProcessor::replaceContent()without validation - Validate file paths before processing to prevent directory traversal attacks
- Use
LIBXML_NONETflag (enabled by default) to prevent XXE (XML External Entity) attacks - Sanitize user-provided SDT tags and aliases
- Verify DOCX file integrity before processing
Credits
Main Contributors:
- Mateus Bandeira - Creator and Lead Developer
Third-Party Assets:
- PHPOffice/PHPWord - Core Word document manipulation
- ramsey/uuid - UUID v5 generation for collision-free hashing
- Pest - Testing framework
- PHPStan - Static analysis tool
Acknowledgments:
- ISO/IEC JTC 1/SC 34 for the Office Open XML standard
- PHPOffice community for excellent documentation and support
License
ContentControl is licensed under the MIT License. See LICENSE for details.
Documentation: docs/README.md
Samples: samples/README.md
GitHub: mateusbandeira182/ContentControl
Issues: GitHub Issues