Use short class names for framework models to enable overrides
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
189
app/RSpade/CodeQuality/Rules/PHP/ModelFqcnReference_CodeQualityRule.php
Executable file
189
app/RSpade/CodeQuality/Rules/PHP/ModelFqcnReference_CodeQualityRule.php
Executable file
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\RSpade\CodeQuality\Rules\PHP;
|
||||||
|
|
||||||
|
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ModelFqcnReferenceRule - Prohibits inline FQCN references to framework models
|
||||||
|
*
|
||||||
|
* This rule ensures that code uses portable model class references instead of
|
||||||
|
* hardcoded fully-qualified class names. This allows developers to override
|
||||||
|
* framework models by placing custom versions in /rsx/models/.
|
||||||
|
*
|
||||||
|
* Prohibited patterns:
|
||||||
|
* - \App\RSpade\Core\Models\User_Model::...
|
||||||
|
* - \App\RSpade\Core\Models\Site_Model::find(...)
|
||||||
|
* - 'App\RSpade\Core\Models\Login_User_Model' (string references)
|
||||||
|
*
|
||||||
|
* Allowed patterns:
|
||||||
|
* - use App\RSpade\Core\Models\User_Model; (use statements are fine)
|
||||||
|
* - \User_Model::... (short class name)
|
||||||
|
* - User_Model::... (via use statement)
|
||||||
|
* - namespace App\RSpade\Core\Models; (namespace declarations)
|
||||||
|
*
|
||||||
|
* Exception:
|
||||||
|
* - Files within /system/app/RSpade/Core/Models/ (the models themselves)
|
||||||
|
*/
|
||||||
|
class ModelFqcnReference_CodeQualityRule extends CodeQualityRule_Abstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the unique rule identifier
|
||||||
|
*/
|
||||||
|
public function get_id(): string
|
||||||
|
{
|
||||||
|
return 'PHP-FQCN-MODEL-01';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable rule name
|
||||||
|
*/
|
||||||
|
public function get_name(): string
|
||||||
|
{
|
||||||
|
return 'Model FQCN Reference Check';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rule description
|
||||||
|
*/
|
||||||
|
public function get_description(): string
|
||||||
|
{
|
||||||
|
return 'Prohibits inline FQCN references to App\RSpade\Core\Models classes. Use short class names (\\User_Model) to allow model overrides.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file patterns this rule applies to
|
||||||
|
*/
|
||||||
|
public function get_file_patterns(): array
|
||||||
|
{
|
||||||
|
return ['*.php'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this rule is called during manifest scan
|
||||||
|
*/
|
||||||
|
public function is_called_during_manifest_scan(): bool
|
||||||
|
{
|
||||||
|
return false; // Only run during rsx:check
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default severity for this rule
|
||||||
|
*/
|
||||||
|
public function get_default_severity(): string
|
||||||
|
{
|
||||||
|
return 'high';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check a file for violations
|
||||||
|
*/
|
||||||
|
public function check(string $file_path, string $contents, array $metadata = []): void
|
||||||
|
{
|
||||||
|
// Skip files within the Models directory itself (they define the namespace)
|
||||||
|
if (str_contains($file_path, '/system/app/RSpade/Core/Models/')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip archived files
|
||||||
|
if (str_contains($file_path, '/archive/') || str_contains($file_path, '/archived/')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file-level exception is present
|
||||||
|
if (str_contains($contents, '@' . $this->get_id() . '-EXCEPTION')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = explode("\n", $contents);
|
||||||
|
|
||||||
|
foreach ($lines as $line_number => $line) {
|
||||||
|
$actual_line_number = $line_number + 1;
|
||||||
|
|
||||||
|
// Skip if line has exception comment
|
||||||
|
if (str_contains($line, '@' . $this->get_id() . '-EXCEPTION')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip use statements - those are fine
|
||||||
|
if (preg_match('/^\s*use\s+/', $line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip namespace declarations
|
||||||
|
if (preg_match('/^\s*namespace\s+/', $line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip comments
|
||||||
|
$trimmed = trim($line);
|
||||||
|
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*') || str_starts_with($trimmed, '/*')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for inline FQCN references to App\RSpade\Core\Models\
|
||||||
|
// Pattern 1: \App\RSpade\Core\Models\Something (backslash-prefixed)
|
||||||
|
// Pattern 2: 'App\RSpade\Core\Models\Something' or "..." (string references)
|
||||||
|
|
||||||
|
// Check for backslash-prefixed FQCN in code
|
||||||
|
if (preg_match('/\\\\App\\\\RSpade\\\\Core\\\\Models\\\\(\w+)/', $line, $matches)) {
|
||||||
|
$model_name = $matches[1];
|
||||||
|
$this->add_violation(
|
||||||
|
$file_path,
|
||||||
|
$actual_line_number,
|
||||||
|
"Inline FQCN reference to framework model '\\App\\RSpade\\Core\\Models\\{$model_name}' prevents model overrides",
|
||||||
|
trim($line),
|
||||||
|
$this->build_suggestion($model_name),
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for string references (for dynamic class usage)
|
||||||
|
if (preg_match('/[\'"]App\\\\RSpade\\\\Core\\\\Models\\\\(\w+)[\'"]/', $line, $matches)) {
|
||||||
|
$model_name = $matches[1];
|
||||||
|
$this->add_violation(
|
||||||
|
$file_path,
|
||||||
|
$actual_line_number,
|
||||||
|
"String FQCN reference to framework model 'App\\RSpade\\Core\\Models\\{$model_name}' prevents model overrides",
|
||||||
|
trim($line),
|
||||||
|
$this->build_suggestion($model_name, true),
|
||||||
|
'high'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build suggestion for fixing the violation
|
||||||
|
*/
|
||||||
|
private function build_suggestion(string $model_name, bool $is_string = false): string
|
||||||
|
{
|
||||||
|
$suggestions = [];
|
||||||
|
$suggestions[] = "Framework models should be referenced by short class name to allow developer overrides.";
|
||||||
|
$suggestions[] = "";
|
||||||
|
|
||||||
|
if ($is_string) {
|
||||||
|
$suggestions[] = "Change:";
|
||||||
|
$suggestions[] = " \$class = 'App\\RSpade\\Core\\Models\\{$model_name}';";
|
||||||
|
$suggestions[] = "";
|
||||||
|
$suggestions[] = "To:";
|
||||||
|
$suggestions[] = " \$class = '{$model_name}';";
|
||||||
|
} else {
|
||||||
|
$suggestions[] = "Change:";
|
||||||
|
$suggestions[] = " \\App\\RSpade\\Core\\Models\\{$model_name}::method()";
|
||||||
|
$suggestions[] = "";
|
||||||
|
$suggestions[] = "To:";
|
||||||
|
$suggestions[] = " \\{$model_name}::method()";
|
||||||
|
}
|
||||||
|
|
||||||
|
$suggestions[] = "";
|
||||||
|
$suggestions[] = "This allows developers to override {$model_name} by creating:";
|
||||||
|
$suggestions[] = " /rsx/models/" . strtolower(str_replace('_Model', '', $model_name)) . "_model.php";
|
||||||
|
$suggestions[] = "";
|
||||||
|
$suggestions[] = "Note: 'use App\\RSpade\\Core\\Models\\{$model_name};' statements are allowed";
|
||||||
|
$suggestions[] = "because PHP resolves them at parse time before the override system.";
|
||||||
|
|
||||||
|
return implode("\n", $suggestions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ class Rsx_Formdata_Generator_Controller extends Rsx_Controller_Abstract
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Require developer role
|
// Require developer role
|
||||||
if (!Permission::has_role(\App\RSpade\Core\Models\User_Model::ROLE_DEVELOPER)) {
|
if (!Permission::has_role(\User_Model::ROLE_DEVELOPER)) {
|
||||||
return response_unauthorized('Developer access required');
|
return response_unauthorized('Developer access required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user