Skip to content

Duplicate DTO Detection

The Duplicate DTO Checker automatically scans test files for duplicate DTO class names before running tests, preventing cryptic PHP errors with clear, actionable error messages.

When multiple test files define DTOs with the same class name, PHP throws a fatal error:

Fatal error: Cannot declare class UserDto, because the name is already in use

This error:

  • Stops all tests from running
  • Doesn’t show which files have the conflict
  • Wastes time debugging

The Duplicate DTO Checker solves this by detecting conflicts before tests run and showing exactly which files need to be fixed.

The checker runs automatically when you execute tests:

  1. Scans all PHP files in the tests/ directory
  2. Finds all classes extending SimpleDto or LiteDto
  3. Checks for duplicate class names (considering namespaces)
  4. Reports any duplicates with file locations
  5. Exits with an error if duplicates are found

When duplicates are detected, you’ll see:

⚠️ DUPLICATE DTO CLASSES FOUND!
The following DTO classes are defined in multiple test files:
📦 UserDto:
- tests/Unit/SimpleDto/SimpleDtoTest.php
- tests/Integration/Symfony/DtoValueResolverTest.php
- tests/Integration/Laravel/DtoValueResolverTest.php
📦 AddressDto:
- tests/Unit/SimpleDto/DotNotationAccessTest.php
- tests/Unit/LiteDto/DotNotationAccessTest.php
⚠️ This can cause test failures with unclear error messages.
Please rename the DTOs to make them unique (e.g., SimpleDtoUserDto, SymfonyUserDto, etc.)
To disable this check, set the environment variable: SKIP_DUPLICATE_DTO_CHECK=1

Temporarily disable the check during development:

Terminal window
SKIP_DUPLICATE_DTO_CHECK=1 vendor/bin/pest

Or in your shell:

Terminal window
export SKIP_DUPLICATE_DTO_CHECK=1
vendor/bin/pest

The checker automatically excludes:

  • Fixtures directories: DTOs in tests/*/Fixtures/ can have duplicate names
  • Helper files: DtoTestHelper.php and DuplicateDtoChecker.php are excluded
  • Test files: DuplicateDtoCheckerTest.php is excluded

The checker considers namespaces when detecting duplicates:

tests/Unit/SimpleDto/SimpleDtoTest.php
namespace Tests\Unit\SimpleDto;
class UserDto extends SimpleDto { ... }
// tests/Integration/Symfony/DtoValueResolverTest.php
namespace Tests\Integration\Symfony;
class UserDto extends SimpleDto { ... }

These are different classes because they have different namespaces.

tests/Unit/SimpleDto/SimpleDtoTest.php
namespace Tests\Unit;
class UserDto extends SimpleDto { ... }
// tests/Unit/LiteDto/LiteDtoTest.php
namespace Tests\Unit;
class UserDto extends SimpleDto { ... }

These are duplicate classes because they have the same namespace.

tests/Unit/SimpleDto/SimpleDtoTest.php
class UserDto extends SimpleDto { ... }
// tests/Integration/Symfony/DtoValueResolverTest.php
class UserDto extends SimpleDto { ... }

These are duplicate classes because neither has a namespace.

Add context-specific prefixes to make names unique:

tests/Unit/SimpleDto/SimpleDtoTest.php
class SimpleDtoUserDto extends SimpleDto { ... }
// tests/Integration/Symfony/DtoValueResolverTest.php
class SymfonyUserDto extends SimpleDto { ... }
// tests/Integration/Laravel/DtoValueResolverTest.php
class LaravelUserDto extends SimpleDto { ... }

Put DTOs in different namespaces:

tests/Unit/SimpleDto/SimpleDtoTest.php
namespace Tests\Unit\SimpleDto;
class UserDto extends SimpleDto { ... }
// tests/Integration/Symfony/DtoValueResolverTest.php
namespace Tests\Integration\Symfony;
class UserDto extends SimpleDto { ... }

If a DTO is used across multiple tests, move it to a Fixtures directory:

tests/Unit/Fixtures/UserDto.php
namespace Tests\Unit\Fixtures;
class UserDto extends SimpleDto { ... }
// tests/Unit/SimpleDto/SimpleDtoTest.php
use Tests\Unit\Fixtures\UserDto;
test('creates user dto', function() {
$dto = new UserDto(...);
});
// tests/Unit/LiteDto/LiteDtoTest.php
use Tests\Unit\Fixtures\UserDto;
test('converts to lite dto', function() {
$dto = new UserDto(...);
});

Make DTO names specific to their test context:

// ✅ Good - Clear context
class SimpleDtoValidationUserDto extends SimpleDto { ... }
class SymfonyControllerUserDto extends SimpleDto { ... }
class LaravelRequestUserDto extends SimpleDto { ... }
// ❌ Bad - Generic names
class UserDto extends SimpleDto { ... }
class TestDto extends SimpleDto { ... }

Group related DTOs in namespaces:

tests/Unit/SimpleDto/Validation/ValidationTest.php
namespace Tests\Unit\SimpleDto\Validation;
class UserDto extends SimpleDto { ... }
// tests/Unit/SimpleDto/Casting/CastingTest.php
namespace Tests\Unit\SimpleDto\Casting;
class UserDto extends SimpleDto { ... }

Avoid duplication by using Fixtures:

tests/Fixtures/Dtos/UserDto.php
namespace Tests\Fixtures\Dtos;
class UserDto extends SimpleDto { ... }
// Use in multiple tests
use Tests\Fixtures\Dtos\UserDto;

You can also run the checker manually in your code:

use Tests\Unit\Helpers\DuplicateDtoChecker;
// Check and throw exception on duplicates
DuplicateDtoChecker::check(__DIR__ . '/tests');
// Check and only print warning (don't throw)
$duplicates = DuplicateDtoChecker::check(__DIR__ . '/tests', false);
if (!empty($duplicates)) {
foreach ($duplicates as $className => $files) {
echo "Duplicate: $className\n";
foreach ($files as $file) {
echo " - $file\n";
}
}
}

The checker uses:

  • RecursiveDirectoryIterator: Scans all PHP files recursively
  • Regular expressions: Finds class definitions and namespaces
  • Static analysis: No classes are loaded or instantiated

This makes it fast and safe to run before tests.

The checker is optimized for speed:

  • Scans ~100 test files in less than 50ms
  • No impact on test execution time
  • Runs once at startup, not per test