Two-Package Architecture
Why TestLink is split into two packages and how this ensures your production code works everywhere.
The Architecture
TestLink is distributed as two separate Composer packages:
┌─────────────────────────────────────┐
│ testflowlabs/test-attributes │ ← Production dependency
│ - #[TestedBy] │
│ - #[LinksAndCovers] │
│ - #[Links] │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ testflowlabs/testlink │ ← Dev dependency
│ - testlink report │
│ - testlink validate │
│ - testlink sync │
│ - testlink pair │
└─────────────────────────────────────┘Why Two Packages?
The Problem with One Package
Imagine TestLink was a single package installed as a dev dependency:
composer require --dev testflowlabs/testlinkNow you add #[TestedBy] to your production code:
// src/UserService.php
use TestFlowLabs\TestingAttributes\TestedBy;
class UserService
{
#[TestedBy(UserServiceTest::class, 'test_creates_user')]
public function create(array $data): User
{
// ...
}
}What happens in production?
composer install --no-dev # Production deploymentThe TestFlowLabs\TestingAttributes\TestedBy class doesn't exist!
PHP Fatal error: Class "TestFlowLabs\TestingAttributes\TestedBy" not foundYour production application crashes because PHP tries to load the attribute class when autoloading UserService.
The Solution: Separate Packages
By splitting into two packages:
test-attributescontains only the attribute classestestlinkcontains the CLI tools
You install them differently:
# Production dependency - always available
composer require testflowlabs/test-attributes
# Dev dependency - only during development
composer require --dev testflowlabs/testlinkNow in production:
composer install --no-dev- ✓
TestedByclass exists (fromtest-attributes) - ✓
UserServiceloads correctly - ✓ Application works
- ✗ CLI tools not installed (not needed in production)
What's in Each Package?
test-attributes (Production)
Minimal package containing only PHP 8 attributes:
namespace TestFlowLabs\TestingAttributes;
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class TestedBy
{
public function __construct(
public string $testClass,
public ?string $testMethod = null
) {}
}
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class LinksAndCovers
{
public function __construct(
public string $class,
public ?string $method = null
) {}
}
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Links
{
public function __construct(
public string $class,
public ?string $method = null
) {}
}This package:
- Has zero dependencies
- Contains only attribute definitions
- Is safe for production
- Has minimal footprint
testlink (Dev)
Full-featured CLI package:
src/
├── Console/ # CLI application
│ └── Command/ # report, validate, sync, pair
├── Scanner/ # Attribute scanners
├── Parser/ # Test file parsers
├── Validator/ # Link validators
├── Sync/ # Bidirectional sync
├── Placeholder/ # Placeholder pairing
└── Reporter/ # Output formattersThis package:
- Depends on
test-attributes - Contains CLI tools
- Only needed during development
- Has additional dependencies (Symfony Console, etc.)
Installation Pattern
Correct Installation
# Step 1: Install attributes for production code
composer require testflowlabs/test-attributes
# Step 2: Install CLI tools for development
composer require --dev testflowlabs/testlinkYour composer.json:
{
"require": {
"testflowlabs/test-attributes": "^1.0"
},
"require-dev": {
"testflowlabs/testlink": "^1.0"
}
}Common Mistake
# WRONG: Only installing as dev dependency
composer require --dev testflowlabs/testlinkThis installs testlink which depends on test-attributes, but both end up in require-dev. In production:
composer install --no-dev # Neither package installed!Result: Production crashes when loading classes with #[TestedBy].
Dependency Graph
Your Production Code
│
├── uses #[TestedBy]
│ │
│ └── requires testflowlabs/test-attributes (PRODUCTION)
│
Your Test Code
│
├── uses ->linksAndCovers()
│ │
│ └── loaded by testflowlabs/testlink (DEV)
│
└── uses #[LinksAndCovers]
│
└── requires testflowlabs/test-attributes (PRODUCTION)Why Attributes in Production?
You might ask: "Why are #[LinksAndCovers] and #[Links] in the production package? They're only used in tests!"
PHP Attribute Loading
PHP loads attribute classes when the class containing them is loaded:
// tests/UserServiceTest.php
use TestFlowLabs\TestingAttributes\LinksAndCovers;
class UserServiceTest extends TestCase
{
#[LinksAndCovers(UserService::class, 'create')]
public function test_creates_user(): void
{
// When PHP loads this class, it needs LinksAndCovers to exist
}
}If attributes were in testlink (dev-only), and you ran tests in a clean CI environment:
composer install # Installs all dependencies including dev
./vendor/bin/pest # Works fineBut with certain CI caching or partial installs, you might get:
Class "TestFlowLabs\TestingAttributes\LinksAndCovers" not foundConsistency is Safer
By putting all attributes in test-attributes:
- All attribute classes are always available together
- No confusion about which package provides which attribute
- Tests work reliably in any environment
Practical Implications
For Your composer.json
{
"require": {
"php": "^8.2",
"testflowlabs/test-attributes": "^1.0"
},
"require-dev": {
"pestphp/pest": "^2.0",
"testflowlabs/testlink": "^1.0"
}
}For CI/CD
# .github/workflows/ci.yml
jobs:
test:
steps:
- run: composer install # All dependencies
- run: ./vendor/bin/pest
- run: ./vendor/bin/testlink validate
deploy:
steps:
- run: composer install --no-dev # Only production
# testlink CLI not available, but that's fine
# test-attributes IS available for production codeFor Docker
# Development
FROM php:8.2
RUN composer install
# Production
FROM php:8.2
RUN composer install --no-dev --optimize-autoloader
# App works because test-attributes is a production dependencySummary
The two-package architecture ensures:
- Production stability - Attributes are always available
- Minimal footprint - Only attributes in production, not CLI tools
- Clear separation - Production vs. development concerns
- Reliable CI - Tests work regardless of install method
Remember: Always install test-attributes as a production dependency.
See Also
- Getting Started - Correct installation steps
- #[TestedBy] - Production code attribute
- #[LinksAndCovers] - Test code attribute