Skip to content

Commit

Permalink
Add sniff to disallow repository usage in controller (#640)
Browse files Browse the repository at this point in the history
  • Loading branch information
henriquelopeslima authored Feb 3, 2025
1 parent 9b0e95a commit e3e98d9
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@ jobs:
- name: Create the phpcs.xml
run: cp phpcs.xml.dist phpcs.xml

- name: Create the phpcs.xml
run: phpcs --config-set installed_paths src/Standards

- name: Run PHP_CodeSniffer
run: ~/.composer/vendor/bin/phpcs -n
2 changes: 2 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@
<file>src/</file>
<file>tests/</file>

<rule ref="RepositoryPattern.Controllers.DisallowRepositoryUsage"/>

</ruleset>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace App\Standards\RepositoryPattern\Sniffs\Controllers;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class DisallowRepositoryUsageSniff implements Sniff
{
public function register(): array
{
return [T_USE];
}

public function process(File $phpcsFile, $stackPtr): void
{
$tokens = $phpcsFile->getTokens();
$fileName = $phpcsFile->getFilename();

if (false === str_contains($fileName, 'src/Controller')) {
return;
}

$endPtr = $phpcsFile->findNext([T_SEMICOLON, T_AS], $stackPtr);
$className = '';
for ($i = $stackPtr + 1; $i < $endPtr; $i++) {
$className .= $tokens[$i]['content'];
}

if (true === str_contains($className, 'Repository')) {
$error = 'Controllers must not use repositories directly; use services instead.';
$phpcsFile->addError($error, $stackPtr, 'RepositoryUsageDetected');
}
}
}
4 changes: 4 additions & 0 deletions src/Standards/RepositoryPattern/ruleset.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<ruleset name="RepositoryPattern">
<description>A custom coding standard for repository pattern.</description>
</ruleset>
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace App\Tests\Functional\Standards\RepositoryPattern\Sniffs\Controllers;

use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Files\LocalFile;
use PHP_CodeSniffer\Ruleset;
use PHPUnit\Framework\TestCase;

class DisallowRepositoryUsageSniffTest extends TestCase
{
private string $tempDir;
private string $tempFilePath;

protected function setUp(): void
{
parent::setUp();

$this->tempDir = sys_get_temp_dir().'/src/Controller';
if (!is_dir($this->tempDir)) {
mkdir($this->tempDir, 0o777, true);
}

$this->tempFilePath = $this->tempDir.'/UserController.php';
$this->createTemporaryFile($this->tempFilePath);
}

protected function tearDown(): void
{
if (file_exists($this->tempFilePath)) {
unlink($this->tempFilePath);
}
if (is_dir($this->tempDir)) {
rmdir($this->tempDir);
}
if (is_dir(dirname($this->tempDir))) {
rmdir(dirname($this->tempDir));
}

parent::tearDown();
}

public function testRepositoryUsageInControllerIsDetected(): void
{
$config = new Config();

$rulesetPath = dirname(__DIR__, 6).'/src/Standards/RepositoryPattern/ruleset.xml';
$config->standards = [$rulesetPath];

$ruleset = new Ruleset($config);

$file = new LocalFile($this->tempFilePath, $ruleset, $config);
$file->process();

$errors = $file->getErrors();
$this->assertNotEmpty($errors, 'Expected an error to be detected.');

$foundError = $this->containsErrorMessage(
$errors
);

$this->assertTrue($foundError, 'Expected error message not found.');
}

// phpcs:disable RepositoryPattern.Controllers.DisallowRepositoryUsage
private function createTemporaryFile(string $path): void
{
$content = <<<'PHP'
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\UserRepository;
class UserController
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function index()
{
$users = $this->userRepository->findAll();
}
}
PHP;

$tempFile = fopen($path, 'w');
if (false === $tempFile) {
$this->fail('Failed to create temporary file.');
}

fwrite($tempFile, $content);
fclose($tempFile);
}
// phpcs:enable RepositoryPattern.Controllers.DisallowRepositoryUsage

private function containsErrorMessage(array $errors): bool
{
return array_any(
$errors,
fn ($lineErrors) => array_any(
$lineErrors,
fn ($errorMessages) => array_any(
$errorMessages,
fn ($errorMessage) => str_contains($errorMessage['message'], 'Controllers must not use repositories directly')
)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace App\Tests\Unit\Standards\RepositoryPattern\Sniffs\Controllers;

use App\Standards\RepositoryPattern\Sniffs\Controllers\DisallowRepositoryUsageSniff;
use PHP_CodeSniffer\Files\File;
use PHPUnit\Framework\TestCase;

class DisallowRepositoryUsageSniffTest extends TestCase
{
public function testRegisterReturnsCorrectTokens(): void
{
$sniff = new DisallowRepositoryUsageSniff();
$this->assertSame([T_USE], $sniff->register());
}

public function testSniffDetectsRepositoryUsage(): void
{
$sniff = new DisallowRepositoryUsageSniff();
$fileMock = $this->createMock(File::class);

$fileMock->method('getFilename')
->willReturn('/src/Controller/Api/SomeController.php');

$tokens = [
['content' => 'use'],
['content' => ' App\\Repository\\SomeRepository'],
['content' => ';'],
];

$fileMock->method('getTokens')->willReturn($tokens);
$fileMock->method('findNext')->willReturn(2);

$fileMock->expects($this->once())
->method('addError')
->with(
'Controllers must not use repositories directly; use services instead.',
$this->anything(),
'RepositoryUsageDetected'
);

$sniff->process($fileMock, 0);
}

public function testSniffIgnoresNonControllerFiles(): void
{
$sniff = new DisallowRepositoryUsageSniff();
$fileMock = $this->createMock(File::class);

$fileMock->method('getFilename')
->willReturn('/src/Service/SomeService.php');

$fileMock->expects($this->never())->method('addError');

$sniff->process($fileMock, 0);
}

public function testSniffIgnoresNonRepositoryUsage(): void
{
$sniff = new DisallowRepositoryUsageSniff();
$fileMock = $this->createMock(File::class);

$fileMock->method('getFilename')
->willReturn('/src/Controller/Api/SomeController.php');

$tokens = [
['content' => 'use'],
['content' => ' App\\Service\\SomeService'],
['content' => ';'],
];

$fileMock->method('getTokens')->willReturn($tokens);
$fileMock->method('findNext')->willReturn(2);

$fileMock->expects($this->never())->method('addError');

$sniff->process($fileMock, 0);
}
}
2 changes: 2 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

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

require dirname(__DIR__).'/vendor/squizlabs/php_codesniffer/tests/bootstrap.php';

if (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
}
Expand Down

0 comments on commit e3e98d9

Please sign in to comment.