Resolve Placeholders
This guide shows how to resolve placeholder markers into real class and method references.
What are Placeholders?
Placeholders are temporary markers used during development:
// Production code
#[TestedBy('@user-create')]
// Test code
->linksAndCovers('@user-create')They let you defer naming decisions until you're ready.
Basic Resolution
Step 1: Preview the resolution
./vendor/bin/testlink pair --dry-runOutput:
Pairing Placeholders
────────────────────
Running in dry-run mode. No files will be modified.
Scanning for placeholders...
Found Placeholders
✓ @user-create 1 production × 2 tests = 2 links
Production Files
src/Services/UserService.php
@user-create → UserServiceTest::test_creates_user
@user-create → UserServiceTest::test_validates_user
Test Files
tests/UserServiceTest.php
@user-create → UserService::create
Dry run complete. Would modify 2 file(s) with 3 change(s).Step 2: Apply the resolution
./vendor/bin/testlink pairStep 3: Verify
./vendor/bin/testlink validateResolving Specific Placeholders
Single placeholder
./vendor/bin/testlink pair --placeholder=@user-createMultiple placeholders
Run the command multiple times or resolve all at once:
# All placeholders
./vendor/bin/testlink pair
# Or specific ones
./vendor/bin/testlink pair --placeholder=@user-create
./vendor/bin/testlink pair --placeholder=@order-flowUnderstanding N:M Resolution
Placeholders create N:M relationships:
1 production method with @A + 3 tests with @A = 3 links
2 production methods with @B + 2 tests with @B = 4 linksExample
Before resolution:
// Production: 2 methods use @checkout
class OrderService
{
#[TestedBy('@checkout')]
public function validate(): bool { }
#[TestedBy('@checkout')]
public function process(): Order { }
}
// Tests: 3 tests use @checkout
test('validates order data')
->linksAndCovers('@checkout');
test('processes valid order')
->linksAndCovers('@checkout');
test('sends confirmation')
->linksAndCovers('@checkout');After resolution (2 × 3 = 6 links):
// Production
class OrderService
{
#[TestedBy('Tests\OrderServiceTest', 'validates order data')]
#[TestedBy('Tests\OrderServiceTest', 'processes valid order')]
#[TestedBy('Tests\OrderServiceTest', 'sends confirmation')]
public function validate(): bool { }
#[TestedBy('Tests\OrderServiceTest', 'validates order data')]
#[TestedBy('Tests\OrderServiceTest', 'processes valid order')]
#[TestedBy('Tests\OrderServiceTest', 'sends confirmation')]
public function process(): Order { }
}
// Tests
test('validates order data')
->linksAndCovers(OrderService::class.'::validate')
->linksAndCovers(OrderService::class.'::process');
test('processes valid order')
->linksAndCovers(OrderService::class.'::validate')
->linksAndCovers(OrderService::class.'::process');
test('sends confirmation')
->linksAndCovers(OrderService::class.'::validate')
->linksAndCovers(OrderService::class.'::process');Resolving by Path
Resolve placeholders in specific directories:
./vendor/bin/testlink pair --path=src/ServicesHandling Orphan Placeholders
What are orphan placeholders?
Placeholders that exist only on one side:
Warning: @user-delete has no matching tests
Warning: @orphan-test has no matching production codeHow to handle them
- Add matching code - Create the missing production/test
- Remove the placeholder - If it's no longer needed
- Use different placeholder - If you meant a different one
Finding orphans
./vendor/bin/testlink validateLook for:
✗ Found unresolved placeholder(s):
@user-delete
Production: App\UserService::delete
Tests: (none)Resolution in Different Frameworks
Pest resolution
Before:
test('creates user', function () {
// ...
})->linksAndCovers('@user-create');After:
test('creates user', function () {
// ...
})->linksAndCovers(UserService::class.'::create');PHPUnit resolution
Before:
#[LinksAndCovers('@user-create')]
public function test_creates_user(): voidAfter:
#[LinksAndCovers(UserService::class, 'create')]
public function test_creates_user(): voidUsing @@ Prefix for @see Tags
Instead of attributes, you can use the @@ prefix to generate @see tags in PHPDoc comments.
PHPUnit Only
The @@ prefix only works with PHPUnit. Pest tests do not support @see tags.
When to use @@
- You prefer documentation-style links
- Your team uses
@seetags for traceability - You want FQCN format in docblocks
Example
Before:
// Production
#[TestedBy('@@user-create')]
public function create(): User { }
// Test (PHPUnit)
#[LinksAndCovers('@@user-create')]
public function testCreatesUser(): void { }After:
// Production
/** @see \Tests\Unit\UserServiceTest::testCreatesUser */
public function create(): User { }
// Test
/** @see \App\Services\UserService::create */
public function testCreatesUser(): void { }Error with Pest
Using @@ with Pest tests results in an error:
Error: Placeholder @@user-create uses @@prefix (for @see tags) but Pest tests
do not support @see tags. Use @user-create instead.Switch to @user-create (single @) to use attributes instead.
Best Practices
1. Resolve before committing
# Check for unresolved placeholders
./vendor/bin/testlink validate
# If any found, resolve them
./vendor/bin/testlink pair2. Use in CI to block unresolved
- name: Check for placeholders
run: |
if ./vendor/bin/testlink validate 2>&1 | grep -q "unresolved placeholder"; then
echo "Found unresolved placeholders!"
exit 1
fi3. Preview complex resolutions
For placeholders with many matches:
./vendor/bin/testlink pair --dry-run --placeholder=@complex-featureReview the N:M expansion before applying.
4. Resolve incrementally
Don't let placeholders pile up:
# After each feature is complete
./vendor/bin/testlink pair