133 lines
4.4 KiB
PHP
133 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Architecture;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Enforces that every instance-based repository class has a co-located interface
|
|
* and correctly implements it.
|
|
*
|
|
* Excluded from this contract (static-method-only classes, no instance interface possible):
|
|
* - TenantCustomFieldDefinitionRepository
|
|
* - TenantCustomFieldOptionRepository
|
|
* - UserCustomFieldValueRepository
|
|
* - UserCustomFieldValueOptionRepository
|
|
* - PageRepository
|
|
* - PageContentRepository
|
|
*
|
|
* Also excluded (utility, not a domain repository):
|
|
* - RepoQuery
|
|
*/
|
|
class RepositoryInterfaceContractTest extends TestCase
|
|
{
|
|
use ProjectFileAssertionSupport;
|
|
|
|
/** @var list<string> */
|
|
private const EXCLUDED_CLASSES = [
|
|
'MintyPHP\\Repository\\Content\\PageContentRepository',
|
|
'MintyPHP\\Repository\\Content\\PageRepository',
|
|
'MintyPHP\\Repository\\CustomField\\TenantCustomFieldDefinitionRepository',
|
|
'MintyPHP\\Repository\\CustomField\\TenantCustomFieldOptionRepository',
|
|
'MintyPHP\\Repository\\CustomField\\UserCustomFieldValueRepository',
|
|
'MintyPHP\\Repository\\CustomField\\UserCustomFieldValueOptionRepository',
|
|
'MintyPHP\\Repository\\Support\\RepoQuery',
|
|
];
|
|
|
|
/**
|
|
* @return array<string, array{class: string, interfaceClass: string, interfaceFile: string}>
|
|
*/
|
|
private function collectRepositoryPairs(): array
|
|
{
|
|
$root = $this->projectRootPath();
|
|
$repoDir = $root . '/lib/Repository';
|
|
|
|
$pairs = [];
|
|
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($repoDir, \RecursiveDirectoryIterator::SKIP_DOTS)
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
/** @var \SplFileInfo $file */
|
|
if ($file->getExtension() !== 'php') {
|
|
continue;
|
|
}
|
|
|
|
$filename = $file->getFilename();
|
|
|
|
// Only concrete repository files (not interfaces, not utilities)
|
|
if (!str_ends_with($filename, 'Repository.php')) {
|
|
continue;
|
|
}
|
|
if (str_ends_with($filename, 'RepositoryInterface.php')) {
|
|
continue;
|
|
}
|
|
|
|
// Derive PSR-4 class name: lib/Repository/Foo/Bar.php → MintyPHP\Repository\Foo\Bar
|
|
$relativePath = ltrim(str_replace($root, '', $file->getPathname()), '/');
|
|
$className = 'MintyPHP\\' . str_replace(['/', '.php'], ['\\', ''], substr($relativePath, strlen('lib/')));
|
|
|
|
if (in_array($className, self::EXCLUDED_CLASSES, true)) {
|
|
continue;
|
|
}
|
|
|
|
$interfaceClass = $className . 'Interface';
|
|
$interfaceFile = substr($relativePath, 0, -strlen('.php')) . 'Interface.php';
|
|
|
|
$pairs[$className] = [
|
|
'class' => $className,
|
|
'interfaceClass' => $interfaceClass,
|
|
'interfaceFile' => $interfaceFile,
|
|
];
|
|
}
|
|
|
|
ksort($pairs);
|
|
return $pairs;
|
|
}
|
|
|
|
public function testEveryRepositoryHasACoLocatedInterface(): void
|
|
{
|
|
$pairs = $this->collectRepositoryPairs();
|
|
$this->assertNotEmpty($pairs, 'No repository classes found — check path resolution.');
|
|
|
|
$missing = [];
|
|
foreach ($pairs as $className => $pair) {
|
|
$root = $this->projectRootPath();
|
|
if (!file_exists($root . '/' . $pair['interfaceFile'])) {
|
|
$missing[] = $pair['interfaceFile'] . ' (for ' . $className . ')';
|
|
}
|
|
}
|
|
|
|
$this->assertSame(
|
|
[],
|
|
$missing,
|
|
"Missing interface files:\n" . implode("\n", $missing)
|
|
);
|
|
}
|
|
|
|
public function testEveryRepositoryImplementsItsInterface(): void
|
|
{
|
|
$pairs = $this->collectRepositoryPairs();
|
|
|
|
$violations = [];
|
|
foreach ($pairs as $className => $pair) {
|
|
if (!class_exists($className)) {
|
|
$violations[] = $className . ' — class not found (autoload issue?)';
|
|
continue;
|
|
}
|
|
|
|
$implemented = class_implements($className);
|
|
if ($implemented === false || !in_array($pair['interfaceClass'], $implemented, true)) {
|
|
$violations[] = $className . ' does not implement ' . $pair['interfaceClass'];
|
|
}
|
|
}
|
|
|
|
$this->assertSame(
|
|
[],
|
|
$violations,
|
|
"Repository interface contract violations:\n" . implode("\n", $violations)
|
|
);
|
|
}
|
|
}
|