Skip to content

Commit e3e98d9

Browse files
authoredFeb 3, 2025··
Add sniff to disallow repository usage in controller (#640)
1 parent 9b0e95a commit e3e98d9

File tree

7 files changed

+247
-0
lines changed

7 files changed

+247
-0
lines changed
 

‎.github/workflows/style.yml

+3
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,8 @@ jobs:
4343
- name: Create the phpcs.xml
4444
run: cp phpcs.xml.dist phpcs.xml
4545

46+
- name: Create the phpcs.xml
47+
run: phpcs --config-set installed_paths src/Standards
48+
4649
- name: Run PHP_CodeSniffer
4750
run: ~/.composer/vendor/bin/phpcs -n

‎phpcs.xml.dist

+2
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@
2525
<file>src/</file>
2626
<file>tests/</file>
2727

28+
<rule ref="RepositoryPattern.Controllers.DisallowRepositoryUsage"/>
29+
2830
</ruleset>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Standards\RepositoryPattern\Sniffs\Controllers;
6+
7+
use PHP_CodeSniffer\Files\File;
8+
use PHP_CodeSniffer\Sniffs\Sniff;
9+
10+
class DisallowRepositoryUsageSniff implements Sniff
11+
{
12+
public function register(): array
13+
{
14+
return [T_USE];
15+
}
16+
17+
public function process(File $phpcsFile, $stackPtr): void
18+
{
19+
$tokens = $phpcsFile->getTokens();
20+
$fileName = $phpcsFile->getFilename();
21+
22+
if (false === str_contains($fileName, 'src/Controller')) {
23+
return;
24+
}
25+
26+
$endPtr = $phpcsFile->findNext([T_SEMICOLON, T_AS], $stackPtr);
27+
$className = '';
28+
for ($i = $stackPtr + 1; $i < $endPtr; $i++) {
29+
$className .= $tokens[$i]['content'];
30+
}
31+
32+
if (true === str_contains($className, 'Repository')) {
33+
$error = 'Controllers must not use repositories directly; use services instead.';
34+
$phpcsFile->addError($error, $stackPtr, 'RepositoryUsageDetected');
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="RepositoryPattern">
3+
<description>A custom coding standard for repository pattern.</description>
4+
</ruleset>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Tests\Functional\Standards\RepositoryPattern\Sniffs\Controllers;
6+
7+
use PHP_CodeSniffer\Config;
8+
use PHP_CodeSniffer\Files\LocalFile;
9+
use PHP_CodeSniffer\Ruleset;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class DisallowRepositoryUsageSniffTest extends TestCase
13+
{
14+
private string $tempDir;
15+
private string $tempFilePath;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
$this->tempDir = sys_get_temp_dir().'/src/Controller';
22+
if (!is_dir($this->tempDir)) {
23+
mkdir($this->tempDir, 0o777, true);
24+
}
25+
26+
$this->tempFilePath = $this->tempDir.'/UserController.php';
27+
$this->createTemporaryFile($this->tempFilePath);
28+
}
29+
30+
protected function tearDown(): void
31+
{
32+
if (file_exists($this->tempFilePath)) {
33+
unlink($this->tempFilePath);
34+
}
35+
if (is_dir($this->tempDir)) {
36+
rmdir($this->tempDir);
37+
}
38+
if (is_dir(dirname($this->tempDir))) {
39+
rmdir(dirname($this->tempDir));
40+
}
41+
42+
parent::tearDown();
43+
}
44+
45+
public function testRepositoryUsageInControllerIsDetected(): void
46+
{
47+
$config = new Config();
48+
49+
$rulesetPath = dirname(__DIR__, 6).'/src/Standards/RepositoryPattern/ruleset.xml';
50+
$config->standards = [$rulesetPath];
51+
52+
$ruleset = new Ruleset($config);
53+
54+
$file = new LocalFile($this->tempFilePath, $ruleset, $config);
55+
$file->process();
56+
57+
$errors = $file->getErrors();
58+
$this->assertNotEmpty($errors, 'Expected an error to be detected.');
59+
60+
$foundError = $this->containsErrorMessage(
61+
$errors
62+
);
63+
64+
$this->assertTrue($foundError, 'Expected error message not found.');
65+
}
66+
67+
// phpcs:disable RepositoryPattern.Controllers.DisallowRepositoryUsage
68+
private function createTemporaryFile(string $path): void
69+
{
70+
$content = <<<'PHP'
71+
<?php
72+
73+
declare(strict_types=1);
74+
75+
namespace App\Controller;
76+
77+
use App\Repository\UserRepository;
78+
79+
class UserController
80+
{
81+
private $userRepository;
82+
83+
public function __construct(UserRepository $userRepository)
84+
{
85+
$this->userRepository = $userRepository;
86+
}
87+
88+
public function index()
89+
{
90+
$users = $this->userRepository->findAll();
91+
}
92+
}
93+
PHP;
94+
95+
$tempFile = fopen($path, 'w');
96+
if (false === $tempFile) {
97+
$this->fail('Failed to create temporary file.');
98+
}
99+
100+
fwrite($tempFile, $content);
101+
fclose($tempFile);
102+
}
103+
// phpcs:enable RepositoryPattern.Controllers.DisallowRepositoryUsage
104+
105+
private function containsErrorMessage(array $errors): bool
106+
{
107+
return array_any(
108+
$errors,
109+
fn ($lineErrors) => array_any(
110+
$lineErrors,
111+
fn ($errorMessages) => array_any(
112+
$errorMessages,
113+
fn ($errorMessage) => str_contains($errorMessage['message'], 'Controllers must not use repositories directly')
114+
)
115+
)
116+
);
117+
}
118+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Tests\Unit\Standards\RepositoryPattern\Sniffs\Controllers;
6+
7+
use App\Standards\RepositoryPattern\Sniffs\Controllers\DisallowRepositoryUsageSniff;
8+
use PHP_CodeSniffer\Files\File;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class DisallowRepositoryUsageSniffTest extends TestCase
12+
{
13+
public function testRegisterReturnsCorrectTokens(): void
14+
{
15+
$sniff = new DisallowRepositoryUsageSniff();
16+
$this->assertSame([T_USE], $sniff->register());
17+
}
18+
19+
public function testSniffDetectsRepositoryUsage(): void
20+
{
21+
$sniff = new DisallowRepositoryUsageSniff();
22+
$fileMock = $this->createMock(File::class);
23+
24+
$fileMock->method('getFilename')
25+
->willReturn('/src/Controller/Api/SomeController.php');
26+
27+
$tokens = [
28+
['content' => 'use'],
29+
['content' => ' App\\Repository\\SomeRepository'],
30+
['content' => ';'],
31+
];
32+
33+
$fileMock->method('getTokens')->willReturn($tokens);
34+
$fileMock->method('findNext')->willReturn(2);
35+
36+
$fileMock->expects($this->once())
37+
->method('addError')
38+
->with(
39+
'Controllers must not use repositories directly; use services instead.',
40+
$this->anything(),
41+
'RepositoryUsageDetected'
42+
);
43+
44+
$sniff->process($fileMock, 0);
45+
}
46+
47+
public function testSniffIgnoresNonControllerFiles(): void
48+
{
49+
$sniff = new DisallowRepositoryUsageSniff();
50+
$fileMock = $this->createMock(File::class);
51+
52+
$fileMock->method('getFilename')
53+
->willReturn('/src/Service/SomeService.php');
54+
55+
$fileMock->expects($this->never())->method('addError');
56+
57+
$sniff->process($fileMock, 0);
58+
}
59+
60+
public function testSniffIgnoresNonRepositoryUsage(): void
61+
{
62+
$sniff = new DisallowRepositoryUsageSniff();
63+
$fileMock = $this->createMock(File::class);
64+
65+
$fileMock->method('getFilename')
66+
->willReturn('/src/Controller/Api/SomeController.php');
67+
68+
$tokens = [
69+
['content' => 'use'],
70+
['content' => ' App\\Service\\SomeService'],
71+
['content' => ';'],
72+
];
73+
74+
$fileMock->method('getTokens')->willReturn($tokens);
75+
$fileMock->method('findNext')->willReturn(2);
76+
77+
$fileMock->expects($this->never())->method('addError');
78+
79+
$sniff->process($fileMock, 0);
80+
}
81+
}

‎tests/bootstrap.php

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
require dirname(__DIR__).'/vendor/autoload.php';
1111

12+
require dirname(__DIR__).'/vendor/squizlabs/php_codesniffer/tests/bootstrap.php';
13+
1214
if (method_exists(Dotenv::class, 'bootEnv')) {
1315
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
1416
}

0 commit comments

Comments
 (0)
Please sign in to comment.