2045 lines
93 KiB
PHP
Executable File
2045 lines
93 KiB
PHP
Executable File
<?php
|
||
|
||
namespace App\Http\Controllers\Admin;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\File;
|
||
use Illuminate\Support\Facades\App;
|
||
use Illuminate\Support\Facades\Response;
|
||
use Illuminate\Support\Facades\View;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Illuminate\Support\Str;
|
||
// Use FQN in code to avoid aliasing issues with static analyzers
|
||
|
||
class CodeQualityController extends Controller
|
||
{
|
||
/**
|
||
* Regole di qualità del codice NetGescon
|
||
*/
|
||
private array $qualityRules = [
|
||
// Richiede il layout NetGescon nelle Blade
|
||
'layout_required' => [
|
||
'name' => 'Layout NetGescon Standard',
|
||
'description' => 'Le view Blade devono estendere admin.layouts.netgescon',
|
||
'pattern' => "/@extends\(['\"]admin\\.layouts\\.netgescon['\"]\)/m",
|
||
'suggestion' => "@extends('admin.layouts.netgescon')",
|
||
'severity' => 'error',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
// Escludi frammenti inclusi (partials/tabs) e componenti Blade
|
||
'excludeContains' => [
|
||
'resources/views/admin/gescon-import/partials',
|
||
'resources/views/admin/gescon-import/tabs',
|
||
'resources/views/components/menu',
|
||
'resources/views/components/',
|
||
'resources/views/admin/documenti/print-list.blade.php'
|
||
]
|
||
],
|
||
// Vietato l'uso del vecchio layout
|
||
'layout_forbidden_app' => [
|
||
'name' => 'Layout Obsoleto (layouts.app)',
|
||
'description' => 'Evita l\'uso del vecchio layout layouts.app',
|
||
'pattern' => "/@extends\(['\"]layouts\\.app['\"]\)/m",
|
||
'suggestion' => "Sostituisci con @extends('admin.layouts.netgescon')",
|
||
'severity' => 'error',
|
||
'mode' => 'prohibit',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'bootstrap_classes' => [
|
||
'name' => 'Classi Bootstrap/Tailwind Obsolete',
|
||
'description' => 'Rileva usage di classi Bootstrap o Tailwind non allineate allo stile NetGescon',
|
||
'pattern' => '/('
|
||
. '(?<!netgescon-)\bbtn(?:-outline)?-(?:primary|secondary|success|danger|warning|info)\b' // bootstrap buttons
|
||
. '|(?<!netgescon-)\bbtn\b' // generic btn
|
||
. '|\bcontainer(-fluid)?\b|\brow\b|\bcol(-[a-z0-9-]+)*\b' // grid
|
||
. '|\bd-(?:none|block|inline|inline-block|flex)\b|\bjustify-content-[a-z-]+\b|\balign-items-[a-z-]+\b' // layout
|
||
. '|\bform-control\b|\btable\b|\bbadge\b' // components
|
||
. '|\btext-(?:primary|secondary|success|danger|warning|info|muted)\b' // text colors
|
||
. '|\bbg-(?:primary|secondary|success|danger|warning|info|light|dark)\b' // bg colors
|
||
. '|\bfw-(?:bold|semibold|normal|light)\b' // font weight
|
||
. '|\bbg-blue-500\b|\bbg-gray-100\b|\btext-gray-900\b' // some Tailwind tones often misused
|
||
. ")/m",
|
||
'suggestion' => 'Usare classi NetGescon: netgescon-btn, netgescon-card, netgescon-text, ecc.',
|
||
'severity' => 'warning',
|
||
'mode' => 'prohibit',
|
||
'fileTypes' => ['blade'],
|
||
'excludeContains' => [
|
||
'resources/views/components/menu'
|
||
]
|
||
],
|
||
'netgescon_components' => [
|
||
'name' => 'Componenti NetGescon',
|
||
'description' => 'Rileva uso dei componenti NetGescon standard (informativo)',
|
||
'pattern' => '/(netgescon-btn|netgescon-card|netgescon-title|netgescon-text|netgescon-input)/m',
|
||
'suggestion' => 'Componenti NetGescon rilevati',
|
||
'severity' => 'info',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'controller_namespace' => [
|
||
'name' => 'Namespace Controller',
|
||
'description' => 'I controller devono avere un namespace valido',
|
||
'pattern' => '/namespace\\s+App\\\\Http\\\\Controllers(\\\\[A-Za-z]+)?;/',
|
||
'suggestion' => 'namespace App\\Http\\Controllers\\Admin; (per i controller admin)',
|
||
'severity' => 'error',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['php'],
|
||
'pathContains' => ['app/Http/Controllers']
|
||
],
|
||
'model_relationships' => [
|
||
'name' => 'Relazioni Eloquent',
|
||
'description' => 'Rileva la definizione delle relazioni Eloquent (informativo)',
|
||
'pattern' => '/(belongsTo|hasMany|hasOne|belongsToMany)\s*\(\s*[\'\"][A-Z]/m',
|
||
'suggestion' => 'Relazioni Eloquent definite correttamente',
|
||
'severity' => 'info',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['php']
|
||
],
|
||
'deprecated_helpers' => [
|
||
'name' => 'Helper Deprecati',
|
||
'description' => 'Rileva helper Laravel deprecati (array_get, array_set, str_is, starts_with, ends_with)',
|
||
'pattern' => '/\\b(array_get|array_set|str_is|starts_with|ends_with)\s*\(/m',
|
||
'suggestion' => 'Usare data_get(), Arr::set(), Str::is(), Str::startsWith(), Str::endsWith() o funzioni PHP 8',
|
||
'severity' => 'warning',
|
||
'mode' => 'prohibit',
|
||
'fileTypes' => ['php']
|
||
],
|
||
'security_issues' => [
|
||
'name' => 'Problemi di Sicurezza',
|
||
'description' => 'Rileva accesso diretto a superglobali o SQL non parametrizzato',
|
||
'pattern' => '/(\$_GET|\$_POST|\$_REQUEST|DB::raw\s*\(\s*[\'\"][^\'\"]*(SELECT|INSERT|UPDATE|DELETE))/im',
|
||
'suggestion' => 'Usare Request validation e Query Builder/parametri',
|
||
'severity' => 'critical',
|
||
'mode' => 'prohibit',
|
||
'fileTypes' => ['php']
|
||
],
|
||
'migration_standards' => [
|
||
'name' => 'Standard Migration',
|
||
'description' => 'Verifica uso delle API Laravel Migration (informativo)',
|
||
'pattern' => '/(Schema::create|Schema::table|Schema::dropIfExists)/m',
|
||
'suggestion' => 'Uso corretto delle Migration rilevato',
|
||
'severity' => 'info',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['php'],
|
||
'pathContains' => ['database/migrations']
|
||
],
|
||
// Debug Blade: bilanciamento @section/@endsection
|
||
'blade_sections_balance' => [
|
||
'name' => 'Bilanciamento Sezioni Blade',
|
||
'description' => 'Ogni @section deve avere un @endsection corrispondente',
|
||
'pattern' => '/@section\b|@endsection\b/m',
|
||
'suggestion' => 'Aggiungi o rimuovi le direttive per bilanciare le sezioni',
|
||
'severity' => 'error',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
// Debug Blade: div bilanciati
|
||
'unbalanced_divs' => [
|
||
'name' => 'Div non bilanciati',
|
||
'description' => 'Numero di <div> aperti non corrisponde ai </div> chiusi',
|
||
'pattern' => '/<div\b|<\/div>/im',
|
||
'balanceTag' => 'div',
|
||
'suggestion' => 'Verifica la struttura dei contenitori e chiudi tutti i div',
|
||
'severity' => 'error',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'unbalanced_navs' => [
|
||
'name' => 'Nav non bilanciati',
|
||
'description' => 'Numero di <nav> aperti non corrisponde ai </nav> chiusi',
|
||
'pattern' => '/<nav\b|<\/nav>/im',
|
||
'balanceTag' => 'nav',
|
||
'suggestion' => 'Verifica che <nav> sia chiuso correttamente',
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'unbalanced_uls' => [
|
||
'name' => 'Liste non bilanciate',
|
||
'description' => 'Numero di <ul> aperti non corrisponde ai </ul> chiusi',
|
||
'pattern' => '/<ul\b|<\/ul>/im',
|
||
'balanceTag' => 'ul',
|
||
'suggestion' => 'Verifica la chiusura delle liste <ul>',
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
// Altri contenitori comuni: section, header, footer, main, article
|
||
'unbalanced_sections' => [
|
||
'name' => 'Section non bilanciate',
|
||
'description' => 'Numero di <section> aperti non corrisponde ai </section> chiusi',
|
||
'pattern' => '/<section\b|<\/section>/im',
|
||
'balanceTag' => 'section',
|
||
'suggestion' => 'Verifica la chiusura dei tag <section>',
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'unbalanced_headers' => [
|
||
'name' => 'Header non bilanciati',
|
||
'description' => 'Numero di <header> aperti non corrisponde ai </header> chiusi',
|
||
'pattern' => '/<header\b|<\/header>/im',
|
||
'balanceTag' => 'header',
|
||
'suggestion' => 'Verifica la chiusura dei tag <header>',
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'unbalanced_footers' => [
|
||
'name' => 'Footer non bilanciati',
|
||
'description' => 'Numero di <footer> aperti non corrisponde ai </footer> chiusi',
|
||
'pattern' => '/<footer\b|<\/footer>/im',
|
||
'balanceTag' => 'footer',
|
||
'suggestion' => 'Verifica la chiusura dei tag <footer>',
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'unbalanced_main' => [
|
||
'name' => 'Main non bilanciati',
|
||
'description' => 'Numero di <main> aperti non corrisponde ai </main> chiusi',
|
||
'pattern' => '/<main\b|<\/main>/im',
|
||
'balanceTag' => 'main',
|
||
'suggestion' => 'Verifica la chiusura dei tag <main>',
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
'unbalanced_articles' => [
|
||
'name' => 'Article non bilanciati',
|
||
'description' => 'Numero di <article> aperti non corrisponde ai </article> chiusi',
|
||
'pattern' => '/<article\b|<\/article>/im',
|
||
'balanceTag' => 'article',
|
||
'suggestion' => 'Verifica la chiusura dei tag <article>',
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade']
|
||
],
|
||
// Debug specifico Gescon Import
|
||
// (Confluita in gescon_import_cards_required più in basso)
|
||
'gescon_import_menu_partial' => [
|
||
'name' => 'Sottomenu Gescon Import',
|
||
'description' => 'Includi il parziale del menu Gescon Import per una navigazione coerente',
|
||
'pattern' => '/gescon-import\.(partials|partial)\.(menu|nav)/m',
|
||
'suggestion' => "@include('admin.gescon-import.partials.menu')",
|
||
'severity' => 'warning',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/gescon-import/index.blade.php'],
|
||
'excludeContains' => [
|
||
'resources/views/admin/gescon-import/partials/menu.blade.php',
|
||
'resources/views/admin/gescon-import/partials'
|
||
],
|
||
],
|
||
'gescon_import_columns_detect' => [
|
||
'name' => 'Layout colonne (Gescon Import)',
|
||
'description' => 'Suggerisce uso di griglie/colonne per allineamento (grid-cols-*, col-span-*, flex)',
|
||
'pattern' => '/(grid-cols-|col-span-|md:grid-cols-|\bflex\b)/m',
|
||
'suggestion' => 'Usa grid/flex NetGescon: <div class="grid md:grid-cols-2 gap-6">…</div>',
|
||
'severity' => 'info',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/gescon-import']
|
||
],
|
||
'gescon_import_cards_required' => [
|
||
'name' => 'Card NetGescon coerenti',
|
||
'description' => 'I blocchi principali devono usare <div class="netgescon-card"> con bordo/ombra coerenti',
|
||
'pattern' => '/netgescon-card/m',
|
||
'suggestion' => 'Avvolgi i blocchi in <div class="netgescon-card"> … </div> e usa header/body/footer standard',
|
||
'severity' => 'warning',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/gescon-import'],
|
||
'excludeContains' => [
|
||
'resources/views/admin/gescon-import/partials',
|
||
'resources/views/admin/gescon-import/tabs'
|
||
]
|
||
],
|
||
// Controllo integrità sidebar NetGescon
|
||
'sidebar_toggle_integrity' => [
|
||
'name' => 'Integrità Toggle Sidebar',
|
||
'description' => 'Ogni toggleCategory(\'id\') deve avere #id-menu e #id-chevron nella sidebar',
|
||
'pattern' => '/toggleCategory\(\'([a-zA-Z0-9_-]+)\'/m',
|
||
'suggestion' => 'Aggiungi <ul id="id-menu"> e <i id="id-chevron"> per ogni categoria',
|
||
'severity' => 'error',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
|
||
],
|
||
// Sidebar: vieta onclick su <a> per il toggle; usare la freccia .ng-chevron-toggle
|
||
'sidebar_anchor_toggle_forbidden' => [
|
||
'name' => 'Toggle su <a> vietato',
|
||
'description' => 'Il toggle dell\'accordion non deve essere sull\'anchor; usare la freccia con classe ng-chevron-toggle',
|
||
'pattern' => '/<a[^>]*onclick\s*=\s*["\']toggleCategory\(/im',
|
||
'suggestion' => 'Rimuovi onclick dall\'anchor e aggiungi onclick alla freccia con classe ng-chevron-toggle',
|
||
'severity' => 'error',
|
||
'mode' => 'prohibit',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
|
||
],
|
||
// Sidebar: richiede presenza della classe ng-chevron-toggle (freccia cliccabile)
|
||
'sidebar_chevron_toggle_required' => [
|
||
'name' => 'Freccia Toggle Richiesta',
|
||
'description' => 'Le icone chevron devono avere la classe ng-chevron-toggle per il toggle indipendente',
|
||
'pattern' => '/ng-chevron-toggle/m',
|
||
'suggestion' => 'Aggiungi class="ng-chevron-toggle" all\'icona <i id="*-chevron">',
|
||
'severity' => 'warning',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
|
||
],
|
||
// Sidebar: la dashboard GESCON Import deve essere link di primo livello, non nel sottomenu
|
||
'sidebar_gescon_dashboard_top' => [
|
||
'name' => 'Dashboard Gescon Import al primo livello',
|
||
'description' => 'La voce GESCON IMPORT deve puntare direttamente alla dashboard (#dashboard)',
|
||
'pattern' => '/href=\\s*["\']\{\{\s*route\(\'admin\\.gescon-import\\.index\'\)\s*\}\}#dashboard["\']/m',
|
||
'suggestion' => "Imposta l'href principale a route('admin.gescon-import.index') . '#dashboard'",
|
||
'severity' => 'warning',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
|
||
],
|
||
'sidebar_gescon_dashboard_not_submenu' => [
|
||
'name' => 'Dashboard Gescon Import non nel sottomenu',
|
||
'description' => 'Rimuovere la voce "Dashboard Import" dal sottomenu Gescon',
|
||
'pattern' => '/Dashboard\s+Import/m',
|
||
'suggestion' => 'Elimina la voce secondaria e mantieni solo il link principale con #dashboard',
|
||
'severity' => 'info',
|
||
'mode' => 'prohibit',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/components/menu/sidebar-netgescon.blade.php']
|
||
],
|
||
// GESCON Import: garantire uso card NetGescon nella dashboard
|
||
'gescon_import_dashboard_cards' => [
|
||
'name' => 'Dashboard Gescon Import con Card NetGescon',
|
||
'description' => 'La dashboard di Gescon Import deve utilizzare i container netgescon-card',
|
||
'pattern' => '/netgescon-card/m',
|
||
'suggestion' => 'Avvolgi i blocchi in <div class="netgescon-card"> … </div>',
|
||
'severity' => 'warning',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/gescon-import/tabs/dashboard.blade.php']
|
||
],
|
||
// Rileva badge Bootstrap nelle viste Gescon Import
|
||
'bootstrap_badges' => [
|
||
'name' => 'Badge Bootstrap obsoleti',
|
||
'description' => 'Evita l\'uso di classi badge Bootstrap (badge bg-*) nelle viste Gescon Import',
|
||
'pattern' => '/\bbadge\s+bg-[a-z0-9-]+/im',
|
||
'suggestion' => 'Usa netgescon-badge e varianti (netgescon-badge-primary, -success, -warning, ...)',
|
||
'severity' => 'warning',
|
||
'mode' => 'prohibit',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/gescon-import']
|
||
],
|
||
// Verifica inclusion Material Design Icons nel layout admin
|
||
'mdi_stylesheet_required' => [
|
||
'name' => 'Material Design Icons non incluse',
|
||
'description' => 'Il layout admin deve includere il CSS delle Material Design Icons per le icone mdi-*',
|
||
'pattern' => '/materialdesignicons\.min\.css/m',
|
||
'suggestion' => '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">',
|
||
'severity' => 'error',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
|
||
],
|
||
// Verifica presenza Font Awesome nel layout admin
|
||
'fontawesome_stylesheet_required' => [
|
||
'name' => 'Font Awesome non incluso',
|
||
'description' => 'Il layout admin deve includere il CSS di Font Awesome per le icone fa-*/fas',
|
||
'pattern' => '/cdnjs\.cloudflare\.com\/ajax\/libs\/font-awesome/m',
|
||
'suggestion' => '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">',
|
||
'severity' => 'warning',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
|
||
],
|
||
// Verifica presenza CSS/JS NetGescon nel layout
|
||
'netgescon_css_required' => [
|
||
'name' => 'CSS NetGescon mancante',
|
||
'description' => 'Il layout admin deve includere css/netgescon-admin.css',
|
||
'pattern' => '/asset\(\'css\/netgescon-admin\.css\'\)/m',
|
||
'suggestion' => "<link rel=\"stylesheet\" href=\"{{ asset('css/netgescon-admin.css') }}\">",
|
||
'severity' => 'error',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
|
||
],
|
||
'netgescon_js_required' => [
|
||
'name' => 'JS NetGescon mancante',
|
||
'description' => 'Il layout admin deve includere js/netgescon-admin.js',
|
||
'pattern' => '/asset\(\'js\/netgescon-admin\.js\'\)/m',
|
||
'suggestion' => "<script src=\"{{ asset('js/netgescon-admin.js') }}\"></script>",
|
||
'severity' => 'error',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
|
||
],
|
||
// Verifica presenza Vite assets nel layout
|
||
'vite_assets_required' => [
|
||
'name' => 'Asset Vite mancanti',
|
||
'description' => 'Il layout admin deve caricare gli asset con @vite([...])',
|
||
'pattern' => '/@vite\s*\(/m',
|
||
'suggestion' => "@vite(['resources/css/app.css', 'resources/js/app.js'])",
|
||
'severity' => 'warning',
|
||
'mode' => 'require',
|
||
'fileTypes' => ['blade'],
|
||
'pathContains' => ['resources/views/admin/layouts/netgescon.blade.php']
|
||
],
|
||
// Uso mdi-* senza layout o link diretto
|
||
'mdi_usage_requires_layout_or_link' => [
|
||
'name' => 'Uso icone MDI senza stile',
|
||
'description' => 'Rilevato uso di classi mdi-* senza estendere il layout NetGescon o linkare il CSS MDI',
|
||
'pattern' => '/\bmdi(-|\s)/m',
|
||
'suggestion' => "Estendi @extends('admin.layouts.netgescon') o includi il CSS MDI nella pagina",
|
||
'severity' => 'error',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade'],
|
||
'excludeContains' => [
|
||
'resources/views/components/',
|
||
'partials/',
|
||
'resources/views/admin/gescon-import/tabs'
|
||
]
|
||
],
|
||
// Uso fa-/fas senza layout o link diretto
|
||
'fa_usage_requires_layout_or_link' => [
|
||
'name' => 'Uso icone Font Awesome senza stile',
|
||
'description' => 'Rilevato uso di classi fa*/fas senza estendere il layout NetGescon o linkare il CSS Font Awesome',
|
||
'pattern' => '/\bfa[srldb]?-?[a-z0-9-]*/im',
|
||
'suggestion' => "Estendi @extends('admin.layouts.netgescon') o includi il CSS di Font Awesome",
|
||
'severity' => 'warning',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade'],
|
||
'excludeContains' => [
|
||
'resources/views/components/',
|
||
'partials/',
|
||
'resources/views/admin/gescon-import/tabs'
|
||
]
|
||
],
|
||
// Uso classi NetGescon senza CSS
|
||
'netgescon_btn_usage_requires_css' => [
|
||
'name' => 'Uso netgescon-btn senza CSS',
|
||
'description' => 'Rilevato uso di classi NetGescon (netgescon-*) senza estendere il layout o includere il CSS',
|
||
'pattern' => '/netgescon-\w+/m',
|
||
'suggestion' => "Estendi @extends('admin.layouts.netgescon') o includi css/netgescon-admin.css",
|
||
'severity' => 'error',
|
||
'mode' => 'detect',
|
||
'fileTypes' => ['blade'],
|
||
'excludeContains' => [
|
||
'resources/views/components/',
|
||
'partials/',
|
||
'resources/views/admin/gescon-import/tabs'
|
||
]
|
||
],
|
||
];
|
||
|
||
// Mapping utility per filtro modulo/menù
|
||
private array $modulePaths = [
|
||
'gescon-import' => [
|
||
'resources/views/admin/gescon-import',
|
||
'app/Http/Controllers/Admin/GesconImportController.php',
|
||
],
|
||
'documenti' => [
|
||
'resources/views/admin/documenti',
|
||
'app/Http/Controllers/Admin/DocumentiController.php',
|
||
],
|
||
'stabili' => [
|
||
'resources/views/admin/stabili',
|
||
'app/Http/Controllers/Admin/StabileController.php',
|
||
],
|
||
'contabilita-gescon' => [
|
||
'resources/views/admin/contabilita-gescon',
|
||
'app/Http/Controllers/Admin/ContabilitaGesconController.php',
|
||
],
|
||
'supporto' => [
|
||
'resources/views/components/menu',
|
||
'app/Http/Controllers/Admin/CodeQualityController.php',
|
||
'routes/web.php',
|
||
],
|
||
];
|
||
|
||
private array $menuPaths = [
|
||
'documenti' => ['resources/views/admin/documenti'],
|
||
'stabili' => ['resources/views/admin/stabili'],
|
||
'condomini' => ['resources/views/admin/condomini'],
|
||
'amministrazione' => ['resources/views/admin/amministrazione'],
|
||
'contabilita' => ['resources/views/admin/contabilita', 'resources/views/admin/contabilita-gescon'],
|
||
'gestione-economica' => ['resources/views/admin/gestione-economica'],
|
||
'moduli' => ['resources/views/admin/moduli'],
|
||
'gescon-import' => ['resources/views/admin/gescon-import'],
|
||
'supporto' => ['resources/views/components/menu', 'resources/views/admin/code-quality'],
|
||
];
|
||
|
||
/**
|
||
* Dashboard controllo qualità
|
||
*/
|
||
public function index(\Illuminate\Http\Request $request)
|
||
{
|
||
$scanPath = $request->get('path', userSetting('code_quality.path', 'app,resources/views'));
|
||
$severityFilter = $request->get('severity', userSetting('code_quality.severity', 'all'));
|
||
// Normalizza severity per evitare valori non validi
|
||
$allowedSeverities = ['all', 'critical', 'error', 'warning', 'info'];
|
||
if (!in_array($severityFilter, $allowedSeverities, true)) {
|
||
$severityFilter = 'all';
|
||
}
|
||
$moduleFilter = $request->get('module', userSetting('code_quality.module', 'all'));
|
||
$menuFilter = $request->get('menu', userSetting('code_quality.menu', 'all'));
|
||
$includeDataChecks = filter_var($request->get('include_data_checks', userSetting('code_quality.include_data_checks', true)), FILTER_VALIDATE_BOOLEAN);
|
||
|
||
$results = $this->scanCodeQuality($scanPath, $severityFilter, $moduleFilter, $menuFilter, $includeDataChecks);
|
||
$totalScanned = $this->countFilesInPaths($scanPath, $moduleFilter, $menuFilter);
|
||
|
||
return View::make('admin.code-quality.index', [
|
||
'results' => $results,
|
||
'scanPath' => $scanPath,
|
||
'severityFilter' => $severityFilter,
|
||
'totalScanned' => $totalScanned,
|
||
'moduleFilter' => $moduleFilter,
|
||
'menuFilter' => $menuFilter,
|
||
'includeDataChecks' => $includeDataChecks,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Scansiona qualità del codice
|
||
*/
|
||
public function scan(\Illuminate\Http\Request $request)
|
||
{
|
||
$scanPath = $request->get('path', userSetting('code_quality.path', 'app,resources/views'));
|
||
$severityFilter = $request->get('severity', userSetting('code_quality.severity', 'all'));
|
||
$allowedSeverities = ['all', 'critical', 'error', 'warning', 'info'];
|
||
if (!in_array($severityFilter, $allowedSeverities, true)) {
|
||
$severityFilter = 'all';
|
||
}
|
||
$moduleFilter = $request->get('module', userSetting('code_quality.module', 'all'));
|
||
$menuFilter = $request->get('menu', userSetting('code_quality.menu', 'all'));
|
||
$includeDataChecks = filter_var($request->get('include_data_checks', userSetting('code_quality.include_data_checks', true)), FILTER_VALIDATE_BOOLEAN);
|
||
|
||
// Salva preferenze per utente
|
||
setUserSetting('code_quality.path', $scanPath);
|
||
setUserSetting('code_quality.severity', $severityFilter);
|
||
setUserSetting('code_quality.module', $moduleFilter);
|
||
setUserSetting('code_quality.menu', $menuFilter);
|
||
setUserSetting('code_quality.include_data_checks', $includeDataChecks);
|
||
|
||
$results = $this->scanCodeQuality($scanPath, $severityFilter, $moduleFilter, $menuFilter, $includeDataChecks);
|
||
$totalScanned = $this->countFilesInPaths($scanPath, $moduleFilter, $menuFilter);
|
||
|
||
return Response::json([
|
||
'success' => true,
|
||
'results' => $results,
|
||
'summary' => $this->generateSummary($results, $totalScanned)
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Esporta i risultati in JSON o CSV (rigenera la scansione con i parametri correnti)
|
||
*/
|
||
public function export(\Illuminate\Http\Request $request)
|
||
{
|
||
$scanPath = $request->get('path', userSetting('code_quality.path', 'app,resources/views'));
|
||
$severityFilter = $request->get('severity', userSetting('code_quality.severity', 'all'));
|
||
$moduleFilter = $request->get('module', userSetting('code_quality.module', 'all'));
|
||
$menuFilter = $request->get('menu', userSetting('code_quality.menu', 'all'));
|
||
$includeDataChecks = filter_var($request->get('include_data_checks', userSetting('code_quality.include_data_checks', true)), FILTER_VALIDATE_BOOLEAN);
|
||
$format = strtolower($request->get('format', 'csv'));
|
||
|
||
$results = $this->scanCodeQuality($scanPath, $severityFilter, $moduleFilter, $menuFilter, $includeDataChecks);
|
||
$totalScanned = $this->countFilesInPaths($scanPath, $moduleFilter, $menuFilter);
|
||
$summary = $this->generateSummary($results, $totalScanned);
|
||
|
||
if ($format === 'json') {
|
||
$payload = [
|
||
'params' => compact('scanPath', 'severityFilter', 'moduleFilter', 'menuFilter'),
|
||
'summary' => $summary,
|
||
'results' => $results,
|
||
];
|
||
$json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||
return Response::make($json, 200, [
|
||
'Content-Type' => 'application/json; charset=UTF-8',
|
||
'Content-Disposition' => 'attachment; filename="code-quality-report.json"',
|
||
]);
|
||
}
|
||
|
||
// CSV flat: file, module, menu, rule, severity, line, suggestion, match
|
||
$rows = [
|
||
['file', 'module', 'menu', 'rule', 'name', 'severity', 'line', 'description', 'suggestion', 'match']
|
||
];
|
||
foreach ($results as $fileResult) {
|
||
foreach ($fileResult['issues'] as $issue) {
|
||
$rows[] = [
|
||
$fileResult['file'] ?? '',
|
||
$fileResult['module'] ?? '',
|
||
$fileResult['menu'] ?? '',
|
||
$issue['rule'] ?? '',
|
||
$issue['name'] ?? '',
|
||
$issue['severity'] ?? '',
|
||
$issue['line'] ?? '',
|
||
$issue['description'] ?? '',
|
||
$issue['suggestion'] ?? '',
|
||
$issue['match'] ?? '',
|
||
];
|
||
}
|
||
}
|
||
|
||
$fh = fopen('php://temp', 'w+');
|
||
foreach ($rows as $r) {
|
||
// CSV in UTF-8 con separatore ';' per excel-friendly
|
||
fputcsv($fh, $r, ';');
|
||
}
|
||
rewind($fh);
|
||
$csv = stream_get_contents($fh);
|
||
fclose($fh);
|
||
|
||
return Response::make($csv, 200, [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Content-Disposition' => 'attachment; filename="code-quality-report.csv"',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Esegue la scansione effettiva
|
||
*/
|
||
private function scanCodeQuality(string $scanPaths, string $severityFilter, string $moduleFilter = 'all', string $menuFilter = 'all', bool $includeDataChecks = true): array
|
||
{
|
||
$results = [];
|
||
$paths = explode(',', $scanPaths);
|
||
|
||
foreach ($paths as $path) {
|
||
$path = trim($path);
|
||
$fullPath = App::basePath($path);
|
||
|
||
if (!is_dir($fullPath) && !is_file($fullPath)) {
|
||
continue;
|
||
}
|
||
|
||
$files = $this->getFilesToScan($fullPath);
|
||
|
||
foreach ($files as $file) {
|
||
// Lettura sicura del contenuto con gestione eccezioni
|
||
try {
|
||
$content = @file_get_contents($file);
|
||
} catch (\Throwable $e) {
|
||
$relativePath = str_replace(App::basePath('') . '/', '', $file);
|
||
$results[] = [
|
||
'file' => $relativePath,
|
||
'module' => $this->classifyModule($relativePath),
|
||
'menu' => $this->classifyMenu($relativePath),
|
||
'issues' => [[
|
||
'rule' => 'file_unreadable',
|
||
'name' => 'File non leggibile',
|
||
'description' => 'Impossibile leggere il file: ' . $e->getMessage(),
|
||
'severity' => 'critical',
|
||
'line' => 1,
|
||
'match' => '—',
|
||
'suggestion' => 'Verifica i permessi o escludi il percorso dalla scansione',
|
||
'context' => []
|
||
]]
|
||
];
|
||
continue;
|
||
}
|
||
$relativePath = str_replace(App::basePath('') . '/', '', $file);
|
||
|
||
// Applica filtri modulo/menù
|
||
if (!$this->pathMatchesFilters($relativePath, $moduleFilter, $menuFilter)) {
|
||
continue;
|
||
}
|
||
|
||
// Analisi con isolamento degli errori per file
|
||
try {
|
||
$fileResults = $this->analyzeFile($file, $content, $relativePath);
|
||
} catch (\Throwable $e) {
|
||
$fileResults = [[
|
||
'rule' => 'scan_exception',
|
||
'name' => 'Errore di scansione file',
|
||
'description' => 'Eccezione durante la scansione: ' . $e->getMessage(),
|
||
'severity' => 'critical',
|
||
'line' => 1,
|
||
'match' => '—',
|
||
'suggestion' => 'Riduci il file o escludilo dalla scansione',
|
||
'context' => []
|
||
]];
|
||
}
|
||
|
||
if (!empty($fileResults)) {
|
||
$results[] = [
|
||
'file' => $relativePath,
|
||
'module' => $this->classifyModule($relativePath),
|
||
'menu' => $this->classifyMenu($relativePath),
|
||
'issues' => $fileResults
|
||
];
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($includeDataChecks) {
|
||
$results = array_merge($results, $this->runArchiveConsistencyChecks());
|
||
}
|
||
|
||
// Filtro per severity
|
||
if ($severityFilter !== 'all') {
|
||
$results = $this->filterBySeverity($results, $severityFilter);
|
||
}
|
||
|
||
return $results;
|
||
}
|
||
|
||
private function runArchiveConsistencyChecks(): array
|
||
{
|
||
$results = [];
|
||
|
||
$millesimiIssues = $this->checkMillesimiConsistency();
|
||
if (!empty($millesimiIssues)) {
|
||
$results[] = [
|
||
'file' => 'quality://archives/millesimi',
|
||
'module' => 'contabilita-gescon',
|
||
'menu' => 'condomini',
|
||
'issues' => $millesimiIssues,
|
||
];
|
||
}
|
||
|
||
$legacyIssues = $this->checkLegacyIndividualChargesConsistency();
|
||
if (!empty($legacyIssues)) {
|
||
$results[] = [
|
||
'file' => 'quality://archives/legacy-individuali',
|
||
'module' => 'gescon-import',
|
||
'menu' => 'contabilita',
|
||
'issues' => $legacyIssues,
|
||
];
|
||
}
|
||
|
||
$legacyIncassiIssues = $this->checkLegacyIncassiConsistency();
|
||
if (!empty($legacyIncassiIssues)) {
|
||
$results[] = [
|
||
'file' => 'quality://archives/legacy-incassi',
|
||
'module' => 'gescon-import',
|
||
'menu' => 'contabilita',
|
||
'issues' => $legacyIncassiIssues,
|
||
];
|
||
}
|
||
|
||
$procedureIssues = $this->checkProcedureArchiveConsistency();
|
||
if (!empty($procedureIssues)) {
|
||
$results[] = [
|
||
'file' => 'quality://procedures/archive-mapping',
|
||
'module' => 'supporto',
|
||
'menu' => 'supporto',
|
||
'issues' => $procedureIssues,
|
||
];
|
||
}
|
||
|
||
$anagraficaIssues = $this->checkStabileRegistryConsistency();
|
||
if (!empty($anagraficaIssues)) {
|
||
$results[] = [
|
||
'file' => 'quality://archives/stabile-anagrafica',
|
||
'module' => 'stabili',
|
||
'menu' => 'condomini',
|
||
'issues' => $anagraficaIssues,
|
||
];
|
||
}
|
||
|
||
return $results;
|
||
}
|
||
|
||
private function checkStabileRegistryConsistency(): array
|
||
{
|
||
$issues = [];
|
||
|
||
if (!Schema::hasTable('stabili')) {
|
||
return [
|
||
$this->makeQualityIssue(
|
||
'anagrafica_missing_stabili',
|
||
'Archivio stabili mancante',
|
||
'La tabella stabili non è disponibile: impossibile verificare allineamento anagrafico.',
|
||
'critical',
|
||
'Ripristina la tabella stabili e riesegui il controllo qualità.'
|
||
),
|
||
];
|
||
}
|
||
|
||
if (Schema::hasTable('unita_immobiliari') && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
|
||
$orphanUnita = DB::table('unita_immobiliari as u')
|
||
->leftJoin('stabili as s', 's.id', '=', 'u.stabile_id')
|
||
->whereNull('s.id')
|
||
->count();
|
||
if ($orphanUnita > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_unita_orfane_stabile',
|
||
'Unità immobiliari senza stabile valido',
|
||
'Rilevate ' . $orphanUnita . ' unità immobiliari con stabile_id non valido.',
|
||
'error',
|
||
'Allinea unita_immobiliari.stabile_id con stabili.id.'
|
||
);
|
||
}
|
||
}
|
||
|
||
if (Schema::hasTable('tabelle_millesimali') && Schema::hasColumn('tabelle_millesimali', 'stabile_id')) {
|
||
$orphanTabelle = DB::table('tabelle_millesimali as t')
|
||
->leftJoin('stabili as s', 's.id', '=', 't.stabile_id')
|
||
->whereNull('s.id')
|
||
->count();
|
||
if ($orphanTabelle > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_tabelle_orfane_stabile',
|
||
'Tabelle millesimali senza stabile valido',
|
||
'Rilevate ' . $orphanTabelle . ' tabelle millesimali con stabile_id non valido.',
|
||
'error',
|
||
'Allinea tabelle_millesimali.stabile_id con stabili.id.'
|
||
);
|
||
}
|
||
}
|
||
|
||
if (
|
||
Schema::hasTable('dettaglio_millesimi')
|
||
&& Schema::hasTable('tabelle_millesimali')
|
||
&& Schema::hasTable('unita_immobiliari')
|
||
&& Schema::hasColumn('tabelle_millesimali', 'stabile_id')
|
||
&& Schema::hasColumn('unita_immobiliari', 'stabile_id')
|
||
) {
|
||
$mismatchMillesimi = DB::table('dettaglio_millesimi as d')
|
||
->join('tabelle_millesimali as t', 't.id', '=', 'd.tabella_millesimale_id')
|
||
->join('unita_immobiliari as u', 'u.id', '=', 'd.unita_immobiliare_id')
|
||
->whereRaw('COALESCE(t.stabile_id, 0) <> COALESCE(u.stabile_id, 0)')
|
||
->count();
|
||
|
||
if ($mismatchMillesimi > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_millesimi_stabile_mismatch',
|
||
'Dettagli millesimali su unità di stabile diverso',
|
||
'Rilevate ' . $mismatchMillesimi . ' righe dettaglio_millesimi con stabile tabella diverso dallo stabile unità.',
|
||
'critical',
|
||
'Usa una sola anagrafica stabile: tabella e unità devono appartenere allo stesso stabile.'
|
||
);
|
||
}
|
||
}
|
||
|
||
if (
|
||
Schema::hasTable('rubrica_ruoli')
|
||
&& Schema::hasTable('unita_immobiliari')
|
||
&& Schema::hasColumn('rubrica_ruoli', 'stabile_id')
|
||
&& Schema::hasColumn('rubrica_ruoli', 'unita_immobiliare_id')
|
||
&& Schema::hasColumn('unita_immobiliari', 'stabile_id')
|
||
) {
|
||
$mismatchRuoli = DB::table('rubrica_ruoli as rr')
|
||
->join('unita_immobiliari as u', 'u.id', '=', 'rr.unita_immobiliare_id')
|
||
->whereNotNull('rr.stabile_id')
|
||
->whereRaw('COALESCE(rr.stabile_id, 0) <> COALESCE(u.stabile_id, 0)')
|
||
->count();
|
||
|
||
if ($mismatchRuoli > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_rubrica_stabile_mismatch',
|
||
'Ruoli rubrica disallineati con stabile dell’unità',
|
||
'Rilevate ' . $mismatchRuoli . ' righe rubrica_ruoli con stabile_id diverso da quello di unita_immobiliare_id.',
|
||
'error',
|
||
'Allinea rubrica_ruoli.stabile_id al valore stabile dell’unità collegata.'
|
||
);
|
||
}
|
||
}
|
||
|
||
if (
|
||
Schema::hasTable('persone_unita_relazioni')
|
||
&& Schema::hasTable('persone')
|
||
&& Schema::hasTable('unita_immobiliari')
|
||
) {
|
||
$orphanRelPersona = DB::table('persone_unita_relazioni as pur')
|
||
->leftJoin('persone as p', 'p.id', '=', 'pur.persona_id')
|
||
->whereNull('p.id')
|
||
->count();
|
||
if ($orphanRelPersona > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_relazioni_persona_orfane',
|
||
'Relazioni persona-unità senza persona valida',
|
||
'Rilevate ' . $orphanRelPersona . ' righe in persone_unita_relazioni senza persona associata.',
|
||
'error',
|
||
'Bonifica le relazioni orfane su persone_unita_relazioni.persona_id.'
|
||
);
|
||
}
|
||
|
||
$orphanRelUnita = DB::table('persone_unita_relazioni as pur')
|
||
->leftJoin('unita_immobiliari as u', 'u.id', '=', 'pur.unita_id')
|
||
->whereNull('u.id')
|
||
->count();
|
||
if ($orphanRelUnita > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_relazioni_unita_orfane',
|
||
'Relazioni persona-unità senza unità valida',
|
||
'Rilevate ' . $orphanRelUnita . ' righe in persone_unita_relazioni senza unità associata.',
|
||
'error',
|
||
'Bonifica le relazioni orfane su persone_unita_relazioni.unita_id.'
|
||
);
|
||
}
|
||
}
|
||
|
||
if (Schema::hasTable('incassi')) {
|
||
if (Schema::hasColumn('incassi', 'gestione_id') && Schema::hasTable('gestioni_contabili') && Schema::hasColumn('gestioni_contabili', 'id')) {
|
||
$orphanIncassiGestione = DB::table('incassi as i')
|
||
->leftJoin('gestioni_contabili as g', 'g.id', '=', 'i.gestione_id')
|
||
->whereNotNull('i.gestione_id')
|
||
->whereNull('g.id')
|
||
->count();
|
||
if ($orphanIncassiGestione > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_incassi_gestione_orfana',
|
||
'Incassi con gestione non valida',
|
||
'Rilevati ' . $orphanIncassiGestione . ' incassi con gestione_id non presente in gestioni_contabili.',
|
||
'error',
|
||
'Allinea incassi.gestione_id alle gestioni contabili esistenti.'
|
||
);
|
||
}
|
||
}
|
||
|
||
if (Schema::hasColumn('incassi', 'importo_pagato_euro') && Schema::hasColumn('incassi', 'importo_euro')) {
|
||
$scaledRows = DB::table('incassi')
|
||
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
|
||
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > (ABS(COALESCE(importo_euro, 0)) * 1000)')
|
||
->count();
|
||
|
||
if ($scaledRows > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_incassi_scaled_amounts',
|
||
'Incassi con importo_pagato_euro in scala legacy diversa',
|
||
'Rilevati ' . $scaledRows . ' incassi dove importo_pagato_euro è in scala diversa da importo_euro (es. x10000).',
|
||
'warning',
|
||
'In visualizzazione e import usa importo_euro/importo come fonte primaria, importo_pagato_euro solo come fallback normalizzato.'
|
||
);
|
||
}
|
||
|
||
$signMismatch = DB::table('incassi')
|
||
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
|
||
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > 0')
|
||
->whereRaw('SIGN(COALESCE(importo_euro, 0)) <> SIGN(COALESCE(importo_pagato_euro, 0))')
|
||
->count();
|
||
|
||
if ($signMismatch > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_incassi_sign_mismatch',
|
||
'Incassi con segno incoerente tra campi importo',
|
||
'Rilevati ' . $signMismatch . ' incassi con segno differente tra importo_euro e importo_pagato_euro.',
|
||
'error',
|
||
'Verifica import legacy e imposta regola unica di segno in fase di caricamento.'
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($issues)) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'anagrafica_registry_consistent',
|
||
'Anagrafica stabile coerente',
|
||
'Nessuna anomalia rilevata tra stabile, unità, millesimi, ruoli, relazioni e incassi.',
|
||
'info',
|
||
'Mantieni una sola anagrafica stabile come fonte unica per tutte le procedure.'
|
||
);
|
||
}
|
||
|
||
return $issues;
|
||
}
|
||
|
||
private function checkProcedureArchiveConsistency(): array
|
||
{
|
||
$issues = [];
|
||
|
||
$appPath = App::basePath('app');
|
||
if (!is_dir($appPath)) {
|
||
return $issues;
|
||
}
|
||
|
||
$millesimiTables = [
|
||
'dettaglio_millesimi',
|
||
'dettagli_tabelle_millesimali',
|
||
'dettagli_millesimali',
|
||
];
|
||
|
||
$tableRefs = [
|
||
'dettaglio_millesimi' => [],
|
||
'dettagli_tabelle_millesimali' => [],
|
||
'dettagli_millesimali' => [],
|
||
];
|
||
$dettPersFiles = [];
|
||
$legacyWeakMapping = [];
|
||
$filamentStabileSensitiveNoContext = [];
|
||
|
||
try {
|
||
$files = File::allFiles($appPath);
|
||
} catch (\Throwable $e) {
|
||
return [
|
||
$this->makeQualityIssue(
|
||
'procedure_scan_failed',
|
||
'Scansione procedure non disponibile',
|
||
'Impossibile leggere i file applicativi: ' . $e->getMessage(),
|
||
'warning',
|
||
'Verifica permessi filesystem e riesegui il controllo qualità.'
|
||
),
|
||
];
|
||
}
|
||
|
||
foreach ($files as $file) {
|
||
$path = $file->getPathname();
|
||
if (!str_ends_with($path, '.php')) {
|
||
continue;
|
||
}
|
||
|
||
$relative = str_replace(App::basePath('') . DIRECTORY_SEPARATOR, '', $path);
|
||
$content = @file_get_contents($path);
|
||
if (!is_string($content) || $content === '') {
|
||
continue;
|
||
}
|
||
|
||
$lc = Str::lower($content);
|
||
|
||
foreach ($millesimiTables as $tableName) {
|
||
if (Str::contains($lc, Str::lower($tableName))) {
|
||
$tableRefs[$tableName][] = $relative;
|
||
}
|
||
}
|
||
|
||
if (Str::contains($lc, 'dett_pers')) {
|
||
$dettPersFiles[] = $relative;
|
||
$hasNspe = Str::contains($lc, 'n_spe');
|
||
$hasLegacyComposite = Str::contains($lc, 'unico') || Str::contains($lc, 'n_stra') || Str::contains($lc, 'tipo_gestione');
|
||
if ($hasNspe && !$hasLegacyComposite) {
|
||
$legacyWeakMapping[] = $relative;
|
||
}
|
||
}
|
||
|
||
if (Str::startsWith($relative, 'app/Filament/Pages/')) {
|
||
$mentionsSensitiveArchive =
|
||
Str::contains($lc, 'tabellamillesimale')
|
||
|| Str::contains($lc, 'dettagliomillesimi')
|
||
|| Str::contains($lc, 'unitaimmobiliare')
|
||
|| Str::contains($lc, 'personaunitarelazione')
|
||
|| Str::contains($lc, 'incasso')
|
||
|| Str::contains($lc, "'stabile_id'")
|
||
|| Str::contains($lc, 'stabile_id');
|
||
|
||
$hasStabileContext = Str::contains($lc, 'stabilecontext::resolveactivestabileid')
|
||
|| Str::contains($lc, 'stabilecontext::resolveactive')
|
||
|| Str::contains($lc, 'app\\support\\stabilecontext');
|
||
|
||
if ($mentionsSensitiveArchive && !$hasStabileContext) {
|
||
$filamentStabileSensitiveNoContext[] = $relative;
|
||
}
|
||
}
|
||
}
|
||
|
||
$usedTables = collect($tableRefs)
|
||
->filter(fn ($filesForTable) => !empty($filesForTable))
|
||
->map(fn ($filesForTable) => array_values(array_unique($filesForTable)))
|
||
->all();
|
||
|
||
if (count($usedTables) > 1) {
|
||
$parts = [];
|
||
foreach ($usedTables as $table => $filesForTable) {
|
||
$parts[] = $table . ' (' . count($filesForTable) . ' file)';
|
||
}
|
||
|
||
$issues[] = $this->makeQualityIssue(
|
||
'procedure_millesimi_multi_archive_usage',
|
||
'Procedure millesimi su archivi multipli',
|
||
'Le procedure applicative usano più tabelle dettaglio millesimi: ' . implode(', ', $parts) . '.',
|
||
'warning',
|
||
'Definisci archivio canonico e allinea tutte le procedure a quella sorgente.'
|
||
);
|
||
|
||
foreach ($usedTables as $table => $filesForTable) {
|
||
$sample = implode(', ', array_slice($filesForTable, 0, 3));
|
||
$issues[] = $this->makeQualityIssue(
|
||
'procedure_millesimi_table_usage_' . $table,
|
||
'Utilizzo tabella ' . $table,
|
||
'Riferimenti in ' . count($filesForTable) . ' file. Esempi: ' . $sample,
|
||
'info',
|
||
'Conferma che questi file debbano puntare alla stessa sorgente dati.'
|
||
);
|
||
}
|
||
}
|
||
|
||
if (!empty($dettPersFiles) && !empty($legacyWeakMapping)) {
|
||
$sample = implode(', ', array_slice(array_values(array_unique($legacyWeakMapping)), 0, 5));
|
||
$issues[] = $this->makeQualityIssue(
|
||
'procedure_legacy_dett_pers_weak_join',
|
||
'Correlazione dett_pers non completa in alcune procedure',
|
||
'Alcuni file sembrano correlare dett_pers solo su n_spe senza chiave composita legacy (unico/n_stra/tipo_gestione). Esempi: ' . $sample,
|
||
'warning',
|
||
'Usa correlazione completa per evitare mismatch tra gestioni/anni.'
|
||
);
|
||
}
|
||
|
||
if (!empty($dettPersFiles)) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'procedure_legacy_dett_pers_presence',
|
||
'Procedure con addebiti individuali legacy',
|
||
'Rilevati ' . count(array_unique($dettPersFiles)) . ' file che referenziano dett_pers.',
|
||
'info',
|
||
'Valuta un service condiviso per centralizzare regole di correlazione e validazione.'
|
||
);
|
||
}
|
||
|
||
if (!empty($filamentStabileSensitiveNoContext)) {
|
||
$sample = implode(', ', array_slice(array_values(array_unique($filamentStabileSensitiveNoContext)), 0, 5));
|
||
$issues[] = $this->makeQualityIssue(
|
||
'procedure_filament_stabile_context_missing',
|
||
'Pagine Filament sensibili senza contesto stabile esplicito',
|
||
'Trovate pagine Filament che toccano archivi anagrafici/millesimi/incassi ma senza uso esplicito di StabileContext. Esempi: ' . $sample,
|
||
'warning',
|
||
'Uniforma le pagine usando StabileContext per garantire una sola anagrafica stabile coerente.'
|
||
);
|
||
}
|
||
|
||
return $issues;
|
||
}
|
||
|
||
private function checkMillesimiConsistency(): array
|
||
{
|
||
$issues = [];
|
||
|
||
if (!Schema::hasTable('tabelle_millesimali')) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'archive_missing_tabelle_millesimali',
|
||
'Tabella tabelle_millesimali mancante',
|
||
'La tabella principale delle tabelle millesimali non esiste nel DB attivo.',
|
||
'critical',
|
||
'Crea/verifica migrazioni tabelle millesimali e riesegui la scansione.'
|
||
);
|
||
return $issues;
|
||
}
|
||
|
||
$sources = [];
|
||
if (Schema::hasTable('dettaglio_millesimi') && Schema::hasColumn('dettaglio_millesimi', 'tabella_millesimale_id')) {
|
||
$sources['dettaglio_millesimi'] = [
|
||
'table' => 'dettaglio_millesimi',
|
||
'fk' => 'tabella_millesimale_id',
|
||
];
|
||
}
|
||
if (Schema::hasTable('dettagli_tabelle_millesimali') && Schema::hasColumn('dettagli_tabelle_millesimali', 'tabella_millesimale_id')) {
|
||
$sources['dettagli_tabelle_millesimali'] = [
|
||
'table' => 'dettagli_tabelle_millesimali',
|
||
'fk' => 'tabella_millesimale_id',
|
||
];
|
||
}
|
||
if (Schema::hasTable('dettagli_millesimali') && Schema::hasColumn('dettagli_millesimali', 'tabella_id')) {
|
||
$sources['dettagli_millesimali'] = [
|
||
'table' => 'dettagli_millesimali',
|
||
'fk' => 'tabella_id',
|
||
];
|
||
}
|
||
|
||
if (empty($sources)) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'archive_millesimi_no_detail_sources',
|
||
'Sorgenti dettaglio millesimi non trovate',
|
||
'Nessuna tabella dettaglio millesimi disponibile (dettaglio_millesimi / dettagli_tabelle_millesimali / dettagli_millesimali).',
|
||
'critical',
|
||
'Uniforma schema e mapping modelli verso una sorgente dettaglio consistente.'
|
||
);
|
||
return $issues;
|
||
}
|
||
|
||
$totalsBySource = [];
|
||
foreach ($sources as $sourceKey => $cfg) {
|
||
$totalsBySource[$sourceKey] = DB::table($cfg['table'])
|
||
->selectRaw($cfg['fk'] . ' as tabella_id, COUNT(*) as righe, COALESCE(SUM(COALESCE(millesimi,0)),0) as totale')
|
||
->groupBy($cfg['fk'])
|
||
->get()
|
||
->keyBy('tabella_id')
|
||
->all();
|
||
|
||
$orphanCount = DB::table($cfg['table'] . ' as d')
|
||
->leftJoin('tabelle_millesimali as t', 't.id', '=', 'd.' . $cfg['fk'])
|
||
->whereNull('t.id')
|
||
->count();
|
||
if ($orphanCount > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'archive_orphan_' . $sourceKey,
|
||
'Dettagli millesimali orfani in ' . $cfg['table'],
|
||
'Rilevati ' . $orphanCount . ' record senza tabella padre in ' . $cfg['table'] . '.',
|
||
'error',
|
||
'Bonifica record orfani o ripristina tabelle padre mancanti.'
|
||
);
|
||
}
|
||
}
|
||
|
||
$tables = DB::table('tabelle_millesimali')
|
||
->select(['id', 'totale_millesimi', 'codice_tabella', 'nome_tabella'])
|
||
->get();
|
||
|
||
foreach ($tables as $tab) {
|
||
$activeSources = [];
|
||
foreach ($totalsBySource as $sourceKey => $rowsByTabella) {
|
||
$row = $rowsByTabella[$tab->id] ?? null;
|
||
if ($row && ((int) ($row->righe ?? 0)) > 0) {
|
||
$activeSources[$sourceKey] = [
|
||
'righe' => (int) ($row->righe ?? 0),
|
||
'totale' => (float) ($row->totale ?? 0),
|
||
];
|
||
}
|
||
}
|
||
|
||
$label = trim((string) (($tab->codice_tabella ?? '') . ' ' . ($tab->nome_tabella ?? '')));
|
||
$label = $label !== '' ? $label : ('id=' . (string) $tab->id);
|
||
|
||
if (count($activeSources) > 1) {
|
||
$sourceText = collect($activeSources)->map(fn ($v, $k) => $k . ': ' . number_format((float) $v['totale'], 3, '.', ''))
|
||
->implode(' | ');
|
||
$issues[] = $this->makeQualityIssue(
|
||
'archive_millesimi_multi_source',
|
||
'Tabella millesimale presente in più archivi dettaglio',
|
||
'La tabella ' . $label . ' (id ' . $tab->id . ') ha dettagli in più sorgenti: ' . $sourceText . '.',
|
||
'warning',
|
||
'Definisci una sorgente canonica per evitare mismatch tra procedure.'
|
||
);
|
||
}
|
||
|
||
if (empty($activeSources)) {
|
||
continue;
|
||
}
|
||
|
||
$priority = ['dettaglio_millesimi', 'dettagli_tabelle_millesimali', 'dettagli_millesimali'];
|
||
$primaryKey = null;
|
||
foreach ($priority as $pk) {
|
||
if (isset($activeSources[$pk])) {
|
||
$primaryKey = $pk;
|
||
break;
|
||
}
|
||
}
|
||
if ($primaryKey === null) {
|
||
$primaryKey = array_key_first($activeSources);
|
||
}
|
||
|
||
$primaryTotal = (float) ($activeSources[$primaryKey]['totale'] ?? 0);
|
||
$tabTotal = (float) ($tab->totale_millesimi ?? 0);
|
||
if (abs($primaryTotal - $tabTotal) > 0.05) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'archive_millesimi_total_mismatch',
|
||
'Totale tabella diverso dal dettaglio',
|
||
'Tabella ' . $label . ' (id ' . $tab->id . '): totale tabella ' . number_format($tabTotal, 3, '.', '') . ' vs dettaglio (' . $primaryKey . ') ' . number_format($primaryTotal, 3, '.', '') . '.',
|
||
'error',
|
||
'Allinea il totale con il prospetto dettaglio o rigenera i dettagli.'
|
||
);
|
||
}
|
||
|
||
if (abs($primaryTotal - 1000.0) > 0.1) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'archive_millesimi_not_1000',
|
||
'Tabella millesimale non bilanciata a 1000',
|
||
'Tabella ' . $label . ' (id ' . $tab->id . ') totale dettaglio ' . number_format($primaryTotal, 3, '.', '') . ' (atteso 1000).',
|
||
'warning',
|
||
'Ribilancia la tabella o verifica eccezioni business previste.'
|
||
);
|
||
}
|
||
}
|
||
|
||
return $issues;
|
||
}
|
||
|
||
private function checkLegacyIndividualChargesConsistency(): array
|
||
{
|
||
$issues = [];
|
||
|
||
if (!Schema::connection('gescon_import')->hasTable('dett_pers')) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_missing_dett_pers',
|
||
'Tabella legacy dett_pers mancante',
|
||
'Nella connessione gescon_import non esiste la tabella dett_pers.',
|
||
'critical',
|
||
'Verifica connessione gescon_import o import dello schema legacy.'
|
||
);
|
||
return $issues;
|
||
}
|
||
|
||
$requiredColumns = ['unico', 'n_stra', 'tipo_gestione', 'n_spe', 'id_cond', 'cond_inq', 'importo_euro'];
|
||
$missingColumns = [];
|
||
foreach ($requiredColumns as $column) {
|
||
if (!Schema::connection('gescon_import')->hasColumn('dett_pers', $column)) {
|
||
$missingColumns[] = $column;
|
||
}
|
||
}
|
||
|
||
if (!empty($missingColumns)) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_dett_pers_missing_columns',
|
||
'Colonne chiave mancanti in dett_pers',
|
||
'Mancano colonne in dett_pers: ' . implode(', ', $missingColumns) . '.',
|
||
'critical',
|
||
'Aggiorna lo schema legacy o adatta le procedure di mapping.'
|
||
);
|
||
return $issues;
|
||
}
|
||
|
||
$ambiguousGroups = DB::connection('gescon_import')
|
||
->table('dett_pers')
|
||
->selectRaw('unico, tipo_gestione, n_stra, COUNT(DISTINCT n_spe) as nspe_count, COUNT(*) as righe')
|
||
->groupBy(['unico', 'tipo_gestione', 'n_stra'])
|
||
->havingRaw('COUNT(DISTINCT n_spe) > 1')
|
||
->limit(5)
|
||
->get();
|
||
if ($ambiguousGroups->isNotEmpty()) {
|
||
$examples = $ambiguousGroups->map(fn ($r) => 'unico ' . $r->unico . ' / ' . $r->tipo_gestione . ' / n_stra ' . $r->n_stra . ' => n_spe ' . $r->nspe_count)->implode(' ; ');
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_dett_pers_ambiguous_mapping',
|
||
'Mapping dett_pers ambiguo per spese individuali',
|
||
'Trovati gruppi con più n_spe per stessa chiave unico/tipo_gestione/n_stra. Esempi: ' . $examples,
|
||
'warning',
|
||
'Usa la chiave completa e gestisci eventuali versioni duplicate in import.'
|
||
);
|
||
}
|
||
|
||
if (Schema::connection('gescon_import')->hasTable('operazioni')) {
|
||
$missingByNSpe = DB::connection('gescon_import')
|
||
->table('operazioni as o')
|
||
->leftJoin('dett_pers as dp', 'dp.n_spe', '=', 'o.n_spe')
|
||
->where('o.cod_spe', 'I05')
|
||
->whereNull('dp.n_spe')
|
||
->count();
|
||
|
||
if ($missingByNSpe > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_i05_missing_by_nspe',
|
||
'Spese I05 senza dettaglio dett_pers su n_spe',
|
||
'Rilevate ' . $missingByNSpe . ' operazioni I05 senza righe dett_pers legate per n_spe.',
|
||
'warning',
|
||
'Usa anche mapping unico/tipo_gestione/n_stra nelle procedure di riparto.'
|
||
);
|
||
}
|
||
}
|
||
|
||
$emptyAmounts = DB::connection('gescon_import')
|
||
->table('dett_pers')
|
||
->where(function ($q) {
|
||
$q->whereNull('importo_euro')->orWhere('importo_euro', 0);
|
||
})
|
||
->count();
|
||
if ($emptyAmounts > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_dett_pers_empty_amounts',
|
||
'Righe dett_pers con importo nullo/zero',
|
||
'Rilevate ' . $emptyAmounts . ' righe dett_pers con importo_euro nullo o 0.',
|
||
'info',
|
||
'Valuta fallback su importo legacy alternativo o esclusione dai riparti.'
|
||
);
|
||
}
|
||
|
||
return $issues;
|
||
}
|
||
|
||
private function checkLegacyIncassiConsistency(): array
|
||
{
|
||
$issues = [];
|
||
|
||
if (!Schema::connection('gescon_import')->hasTable('incassi')) {
|
||
return [
|
||
$this->makeQualityIssue(
|
||
'legacy_missing_incassi',
|
||
'Tabella legacy incassi mancante',
|
||
'Nella connessione gescon_import non esiste la tabella incassi.',
|
||
'critical',
|
||
'Verifica connessione/schema legacy per gli incassi.'
|
||
),
|
||
];
|
||
}
|
||
|
||
$required = ['dt_empag', 'importo_euro', 'importo_pagato_euro', 'cod_cond'];
|
||
$missing = [];
|
||
foreach ($required as $column) {
|
||
if (!Schema::connection('gescon_import')->hasColumn('incassi', $column)) {
|
||
$missing[] = $column;
|
||
}
|
||
}
|
||
|
||
if (!empty($missing)) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_incassi_missing_columns',
|
||
'Colonne chiave mancanti in incassi legacy',
|
||
'Mancano colonne in incassi: ' . implode(', ', $missing) . '.',
|
||
'critical',
|
||
'Aggiorna lo schema legacy o adatta le procedure di mapping incassi.'
|
||
);
|
||
return $issues;
|
||
}
|
||
|
||
$scaledRows = DB::connection('gescon_import')
|
||
->table('incassi')
|
||
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
|
||
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > (ABS(COALESCE(importo_euro, 0)) * 1000)')
|
||
->count();
|
||
|
||
if ($scaledRows > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_incassi_scaled_amounts',
|
||
'Incassi legacy con importo_pagato_euro in scala differente',
|
||
'Rilevati ' . $scaledRows . ' record incassi con importo_pagato_euro in scala maggiore rispetto a importo_euro (tipicamente x10000).',
|
||
'warning',
|
||
'In UI e importazioni usa importo_euro/importo come fonte principale e normalizza importo_pagato_euro come fallback.'
|
||
);
|
||
}
|
||
|
||
$signMismatch = DB::connection('gescon_import')
|
||
->table('incassi')
|
||
->whereRaw('ABS(COALESCE(importo_euro, 0)) > 0')
|
||
->whereRaw('ABS(COALESCE(importo_pagato_euro, 0)) > 0')
|
||
->whereRaw('SIGN(COALESCE(importo_euro, 0)) <> SIGN(COALESCE(importo_pagato_euro, 0))')
|
||
->count();
|
||
|
||
if ($signMismatch > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_incassi_sign_mismatch',
|
||
'Incassi legacy con segno incoerente tra campi importo',
|
||
'Rilevati ' . $signMismatch . ' record con segno differente tra importo_euro e importo_pagato_euro.',
|
||
'error',
|
||
'Verifica trasformazioni di segno e assicurati di visualizzare un solo campo normalizzato.'
|
||
);
|
||
}
|
||
|
||
$missingDate = DB::connection('gescon_import')
|
||
->table('incassi')
|
||
->whereNull('dt_empag')
|
||
->count();
|
||
|
||
if ($missingDate > 0) {
|
||
$issues[] = $this->makeQualityIssue(
|
||
'legacy_incassi_missing_dates',
|
||
'Incassi legacy senza data pagamento',
|
||
'Rilevati ' . $missingDate . ' record senza dt_empag.',
|
||
'warning',
|
||
'Usa data_incasso come fallback o completa i dati in import.'
|
||
);
|
||
}
|
||
|
||
return $issues;
|
||
}
|
||
|
||
private function makeQualityIssue(string $rule, string $name, string $description, string $severity, string $suggestion, int $line = 1, string $match = '—'): array
|
||
{
|
||
return [
|
||
'rule' => $rule,
|
||
'name' => $name,
|
||
'description' => $description,
|
||
'severity' => $severity,
|
||
'line' => $line,
|
||
'match' => $match,
|
||
'suggestion' => $suggestion,
|
||
'context' => [],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Ottiene lista file da scansionare
|
||
*/
|
||
private function getFilesToScan(string $path): array
|
||
{
|
||
$files = [];
|
||
|
||
if (File::isFile($path)) {
|
||
return [$path];
|
||
}
|
||
|
||
$iterator = new \RecursiveIteratorIterator(
|
||
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
|
||
);
|
||
|
||
foreach ($iterator as $file) {
|
||
if ($file->isFile()) {
|
||
$extension = $file->getExtension();
|
||
if (in_array($extension, ['php', 'blade.php', 'js', 'vue', 'ts'])) {
|
||
// Escludi alcune cartelle rumorose
|
||
$pathname = $file->getPathname();
|
||
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) continue;
|
||
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'node_modules' . DIRECTORY_SEPARATOR)) continue;
|
||
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR)) continue;
|
||
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'SAFE_BACKUPS' . DIRECTORY_SEPARATOR)) continue;
|
||
if (str_contains($pathname, DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'phpmyadmin' . DIRECTORY_SEPARATOR)) continue;
|
||
// Salta file troppo grandi (> 1 MB) per evitare consumo eccessivo di memoria/tempo
|
||
try {
|
||
if (@filesize($pathname) !== false && @filesize($pathname) > 1024 * 1024) continue;
|
||
} catch (\Throwable $e) {
|
||
}
|
||
$files[] = $file->getPathname();
|
||
}
|
||
}
|
||
}
|
||
|
||
return $files;
|
||
}
|
||
|
||
/**
|
||
* Analizza singolo file
|
||
*/
|
||
private function analyzeFile(string $filePath, string $content, string $relativePath): array
|
||
{
|
||
$issues = [];
|
||
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
|
||
$isBladeFile = Str::endsWith($filePath, '.blade.php');
|
||
|
||
foreach ($this->qualityRules as $ruleKey => $rule) {
|
||
// Applica regole specifiche per tipo file e percorso
|
||
if (!$this->shouldApplyRule($ruleKey, $rule, $extension, $isBladeFile, $relativePath)) {
|
||
continue;
|
||
}
|
||
|
||
// Gestione regole diagnostiche speciali
|
||
if ($ruleKey === 'blade_sections_balance') {
|
||
// Conta tutte le @section
|
||
$sections = preg_match_all('/@section\b/m', $content);
|
||
// Sezioni inline del tipo: @section('title', '...') che NON richiedono @endsection
|
||
$inlineSections = preg_match_all('/@section\s*\(\s*[^),]+,\s*[^)]+\)/m', $content);
|
||
$requiredEnd = max(0, $sections - $inlineSections);
|
||
$endsections = preg_match_all('/@endsection\b/m', $content);
|
||
if ($requiredEnd !== $endsections) {
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'] . " (section: $sections (inline: $inlineSections), endsection richiesti: $requiredEnd, trovati: $endsections)",
|
||
'severity' => $rule['severity'],
|
||
'line' => 1,
|
||
'match' => '—',
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, 1)
|
||
];
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Gestione regola per file troppo lunghi
|
||
if ($ruleKey === 'monster_class') {
|
||
$lineCount = substr_count($content, "\n") + 1;
|
||
if ($lineCount > ($rule['lineThreshold'] ?? 500)) {
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'] . " (Trovate: $lineCount righe)",
|
||
'severity' => $rule['severity'],
|
||
'line' => 1,
|
||
'match' => "File con $lineCount righe",
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, 1)
|
||
];
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Integrità toggle della sidebar: verifica presenza di #<id>-menu e #<id>-chevron
|
||
if ($ruleKey === 'sidebar_toggle_integrity') {
|
||
// Applica solo al file sidebar-netgescon
|
||
if (!Str::contains($relativePath, 'resources/views/components/menu/sidebar-netgescon.blade.php')) {
|
||
continue;
|
||
}
|
||
// Trova tutte le categorie usate nel toggleCategory('id', ...)
|
||
preg_match_all($rule['pattern'], $content, $m, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||
if (!empty($m)) {
|
||
foreach ($m as $match) {
|
||
$cat = is_array($match[1]) ? $match[1][0] : $match[1];
|
||
$hasMenu = Str::contains($content, 'id="' . $cat . '-menu"');
|
||
$hasChevron = Str::contains($content, 'id="' . $cat . '-chevron"');
|
||
if (!$hasMenu || !$hasChevron) {
|
||
$offset = is_array($match[0]) ? ($match[0][1] ?? 0) : 0;
|
||
$lineNumber = substr_count(substr($content, 0, $offset), "\n") + 1;
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'] . " — categoria: $cat (menu: " . ($hasMenu ? 'OK' : 'MANCANTE') . ", chevron: " . ($hasChevron ? 'OK' : 'MANCANTE') . ")",
|
||
'severity' => $rule['severity'],
|
||
'line' => $lineNumber,
|
||
'match' => is_array($match[0]) ? ($match[0][0] ?? "toggleCategory('$cat')") : (string)$match[0],
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, max(1, $lineNumber))
|
||
];
|
||
}
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (isset($rule['balanceTag'])) {
|
||
$tag = $rule['balanceTag'];
|
||
$opens = preg_match_all('/<' . $tag . '\\b/im', $content);
|
||
$closes = preg_match_all('/<\\/' . $tag . '>/im', $content);
|
||
if ($opens !== $closes) {
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'] . " (aperti: $opens, chiusi: $closes)",
|
||
'severity' => $rule['severity'],
|
||
'line' => 1,
|
||
'match' => '—',
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, 1)
|
||
];
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Regole di co-occorrenza: uso di icone/classi senza layout o link CSS/JS
|
||
if (in_array($ruleKey, ['mdi_usage_requires_layout_or_link', 'fa_usage_requires_layout_or_link', 'netgescon_btn_usage_requires_css'], true)) {
|
||
preg_match_all($rule['pattern'], $content, $matches, PREG_OFFSET_CAPTURE);
|
||
if (!empty($matches[0])) {
|
||
$extendsNetgescon = (bool)preg_match("/@extends\(['\"]admin\\.layouts\\.netgescon['\"]\)/m", $content);
|
||
$hasMdiLink = (bool)preg_match('/materialdesignicons\\.min\\.css/m', $content);
|
||
$hasFaLink = (bool)preg_match('/cdnjs\\.cloudflare\\.com\\/ajax\\/libs\\/font-awesome/m', $content);
|
||
$hasNgCss = (bool)preg_match("/asset\('css\\/netgescon-admin\\.css'\)/m", $content);
|
||
|
||
foreach ($matches[0] as $match) {
|
||
$lineNumber = substr_count(substr($content, 0, $match[1]), "\n") + 1;
|
||
$missing = [];
|
||
if ($ruleKey === 'mdi_usage_requires_layout_or_link') {
|
||
if (!$extendsNetgescon && !$hasMdiLink) {
|
||
$missing[] = 'Layout NetGescon o CSS MDI';
|
||
}
|
||
} elseif ($ruleKey === 'fa_usage_requires_layout_or_link') {
|
||
if (!$extendsNetgescon && !$hasFaLink) {
|
||
$missing[] = 'Layout NetGescon o CSS Font Awesome';
|
||
}
|
||
} else { // netgescon_btn_usage_requires_css
|
||
if (!$extendsNetgescon && !$hasNgCss) {
|
||
$missing[] = 'Layout NetGescon o CSS NetGescon';
|
||
}
|
||
}
|
||
|
||
if (!empty($missing)) {
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'] . ' — mancante: ' . implode(', ', $missing),
|
||
'severity' => $rule['severity'],
|
||
'line' => $lineNumber,
|
||
'match' => trim($match[0]),
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, $lineNumber)
|
||
];
|
||
}
|
||
}
|
||
}
|
||
continue;
|
||
}
|
||
|
||
preg_match_all($rule['pattern'], $content, $matches, PREG_OFFSET_CAPTURE);
|
||
|
||
$mode = $rule['mode'] ?? 'detect';
|
||
|
||
// Aggregazione per regole informative rumorose: una sola issue per file con conteggi
|
||
if (
|
||
$mode === 'detect' && ($rule['severity'] ?? 'info') === 'info'
|
||
&& in_array($ruleKey, ['netgescon_components', 'gescon_import_columns_detect'], true)
|
||
) {
|
||
$count = isset($matches[0]) ? count($matches[0]) : 0;
|
||
if ($count > 0) {
|
||
// Prepara un riassunto specifico per regola
|
||
$summaryMatch = '';
|
||
$lineNumber = 1;
|
||
if ($ruleKey === 'netgescon_components') {
|
||
$tokens = [
|
||
'netgescon-btn',
|
||
'netgescon-card',
|
||
'netgescon-title',
|
||
'netgescon-text',
|
||
'netgescon-input'
|
||
];
|
||
$parts = [];
|
||
foreach ($tokens as $t) {
|
||
$c = preg_match_all('/' . preg_quote($t, '/') . '/m', $content);
|
||
if ($c > 0) $parts[] = $t . ':' . $c;
|
||
}
|
||
$summaryMatch = 'componenti: ' . ($parts ? implode(', ', $parts) : ('totali:' . $count));
|
||
} else {
|
||
// gescon_import_columns_detect
|
||
$tokens = [
|
||
'grid-cols-' => 'grid-cols',
|
||
'md:grid-cols-' => 'md:grid-cols',
|
||
'col-span-' => 'col-span',
|
||
'\\bflex\\b' => 'flex'
|
||
];
|
||
$parts = [];
|
||
foreach ($tokens as $pattern => $label) {
|
||
$c = preg_match_all('/' . $pattern . '/m', $content);
|
||
if ($c > 0) $parts[] = $label . ':' . $c;
|
||
}
|
||
$summaryMatch = 'layout: ' . ($parts ? implode(', ', $parts) : ('totali:' . $count));
|
||
}
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'],
|
||
'severity' => $rule['severity'],
|
||
'line' => $lineNumber,
|
||
'match' => $summaryMatch,
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, $lineNumber)
|
||
];
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if ($mode === 'require') {
|
||
// Se non viene trovato il pattern, segna un problema
|
||
if (empty($matches[0])) {
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'],
|
||
'severity' => $rule['severity'],
|
||
'line' => 1,
|
||
'match' => '—',
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, 1)
|
||
];
|
||
}
|
||
} else {
|
||
// Per 'prohibit' e 'detect' creiamo una issue per ogni match
|
||
if (!empty($matches[0])) {
|
||
foreach ($matches[0] as $match) {
|
||
$lineNumber = substr_count(substr($content, 0, $match[1]), "\n") + 1;
|
||
$issues[] = [
|
||
'rule' => $ruleKey,
|
||
'name' => $rule['name'],
|
||
'description' => $rule['description'],
|
||
'severity' => $rule['severity'],
|
||
'line' => $lineNumber,
|
||
'match' => trim($match[0]),
|
||
'suggestion' => $rule['suggestion'] ?? '',
|
||
'context' => $this->getLineContext($content, $lineNumber)
|
||
];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return $issues;
|
||
}
|
||
|
||
/**
|
||
* Verifica se applicare la regola al file
|
||
*/
|
||
private function shouldApplyRule(string $ruleKey, array $rule, string $extension, bool $isBladeFile, string $relativePath): bool
|
||
{
|
||
// Filtri per tipo file
|
||
$fileTypes = $rule['fileTypes'] ?? ['php', 'blade'];
|
||
$isPhp = $extension === 'php' && !$isBladeFile;
|
||
$fileTypeOk = (
|
||
($isBladeFile && in_array('blade', $fileTypes)) ||
|
||
($isPhp && in_array('php', $fileTypes))
|
||
);
|
||
|
||
if (!$fileTypeOk) {
|
||
return false;
|
||
}
|
||
|
||
// Filtri per percorso
|
||
if (!empty($rule['pathContains']) && is_array($rule['pathContains'])) {
|
||
$ok = false;
|
||
foreach ($rule['pathContains'] as $needle) {
|
||
if (Str::contains($relativePath, $needle)) {
|
||
$ok = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$ok) return false;
|
||
}
|
||
|
||
// Esclusioni per percorso
|
||
if (!empty($rule['excludeContains']) && is_array($rule['excludeContains'])) {
|
||
foreach ($rule['excludeContains'] as $needle) {
|
||
if (Str::contains($relativePath, $needle)) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Ottiene contesto della riga
|
||
*/
|
||
private function getLineContext(string $content, int $lineNumber): array
|
||
{
|
||
$lines = explode("\n", $content);
|
||
$start = max(0, $lineNumber - 3);
|
||
$end = min(count($lines) - 1, $lineNumber + 2);
|
||
|
||
$context = [];
|
||
for ($i = $start; $i <= $end; $i++) {
|
||
$context[] = [
|
||
'number' => $i + 1,
|
||
'content' => $lines[$i] ?? '',
|
||
'highlight' => ($i + 1) === $lineNumber
|
||
];
|
||
}
|
||
|
||
return $context;
|
||
}
|
||
|
||
/**
|
||
* Filtra per severity
|
||
*/
|
||
private function filterBySeverity(array $results, string $severity): array
|
||
{
|
||
foreach ($results as &$fileResult) {
|
||
$fileResult['issues'] = array_filter($fileResult['issues'], function ($issue) use ($severity) {
|
||
return $issue['severity'] === $severity;
|
||
});
|
||
}
|
||
|
||
return array_filter($results, function ($fileResult) {
|
||
return !empty($fileResult['issues']);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Genera summary dei risultati
|
||
*/
|
||
private function generateSummary(array $results, ?int $totalScannedFiles = null): array
|
||
{
|
||
$summary = [
|
||
// Numero di file scansionati (se fornito), altrimenti file con problemi
|
||
'total_files' => $totalScannedFiles ?? count($results),
|
||
'total_scanned_files' => $totalScannedFiles ?? count($results),
|
||
'total_issues' => 0,
|
||
'by_severity' => [
|
||
'critical' => 0,
|
||
'error' => 0,
|
||
'warning' => 0,
|
||
'info' => 0
|
||
],
|
||
'by_rule' => [],
|
||
'by_module' => [],
|
||
'by_menu' => []
|
||
];
|
||
|
||
foreach ($results as $fileResult) {
|
||
foreach ($fileResult['issues'] as $issue) {
|
||
$summary['total_issues']++;
|
||
$summary['by_severity'][$issue['severity']]++;
|
||
|
||
$ruleKey = $issue['rule'];
|
||
if (!isset($summary['by_rule'][$ruleKey])) {
|
||
$summary['by_rule'][$ruleKey] = [
|
||
'name' => $issue['name'],
|
||
'count' => 0
|
||
];
|
||
}
|
||
$summary['by_rule'][$ruleKey]['count']++;
|
||
|
||
$module = $fileResult['module'] ?? 'unknown';
|
||
$menu = $fileResult['menu'] ?? 'unknown';
|
||
if (!isset($summary['by_module'][$module])) $summary['by_module'][$module] = 0;
|
||
if (!isset($summary['by_menu'][$menu])) $summary['by_menu'][$menu] = 0;
|
||
$summary['by_module'][$module]++;
|
||
$summary['by_menu'][$menu]++;
|
||
}
|
||
}
|
||
|
||
return $summary;
|
||
}
|
||
|
||
/**
|
||
* Applica fix automatici
|
||
*/
|
||
public function autofix(Request $request)
|
||
{
|
||
$filePath = $request->get('file');
|
||
$ruleKey = $request->get('rule');
|
||
$preview = filter_var($request->get('preview', false), FILTER_VALIDATE_BOOLEAN);
|
||
|
||
if (!$filePath || !$ruleKey) {
|
||
return \Illuminate\Support\Facades\Response::json(['success' => false, 'message' => 'Parametri mancanti']);
|
||
}
|
||
|
||
$fullPath = \Illuminate\Support\Facades\App::basePath($filePath);
|
||
if (!File::exists($fullPath)) {
|
||
return \Illuminate\Support\Facades\Response::json(['success' => false, 'message' => 'File non trovato']);
|
||
}
|
||
|
||
$content = File::get($fullPath);
|
||
$fixedContent = $this->applyAutoFix($content, $ruleKey);
|
||
|
||
if ($fixedContent !== $content) {
|
||
// Se è richiesto solo l'anteprima o il file non è scrivibile, restituisci il contenuto proposto
|
||
if ($preview || !is_writable($fullPath)) {
|
||
return \Illuminate\Support\Facades\Response::json([
|
||
'success' => false,
|
||
'message' => !is_writable($fullPath)
|
||
? 'File non scrivibile: esegui la modifica manualmente o aggiorna i permessi'
|
||
: 'Anteprima fix (nessuna scrittura eseguita)',
|
||
'canWrite' => is_writable($fullPath),
|
||
'proposed' => $fixedContent,
|
||
'permissionHints' => !is_writable($fullPath) ? [
|
||
'Rendi scrivibile il file per l\'utente del web server (www-data) o per il tuo utente.',
|
||
'Valuta di cambiare proprietario/gruppo del progetto o applicare ACL (setfacl) per la modifica.',
|
||
'In alternativa, copia il contenuto proposto manualmente nel file e salva.'
|
||
] : [],
|
||
]);
|
||
}
|
||
|
||
// Scrittura consentita
|
||
File::put($fullPath, $fixedContent);
|
||
return \Illuminate\Support\Facades\Response::json(['success' => true, 'message' => 'Fix applicato con successo']);
|
||
}
|
||
|
||
return \Illuminate\Support\Facades\Response::json(['success' => false, 'message' => 'Nessun fix automatico disponibile']);
|
||
}
|
||
|
||
/**
|
||
* Applica fix automatico specifico
|
||
*/
|
||
private function applyAutoFix(string $content, string $ruleKey): string
|
||
{
|
||
switch ($ruleKey) {
|
||
case 'layout_required':
|
||
// Prependi l'extends NetGescon se manca e se sembra una view completa (non partial)
|
||
if (!preg_match("/@extends\(['\"]admin\\.layouts\\.netgescon['\"]\)/m", $content)) {
|
||
// Inserisci prima della prima direttiva @section o all'inizio del file
|
||
$lines = preg_split('/\r?\n/', $content);
|
||
$insertion = "@extends('admin.layouts.netgescon')\n";
|
||
$inserted = false;
|
||
foreach ($lines as $idx => $line) {
|
||
if (preg_match('/^\s*@section\b/', $line)) {
|
||
array_splice($lines, $idx, 0, [$insertion]);
|
||
$inserted = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$inserted) array_unshift($lines, rtrim($insertion));
|
||
$content = implode("\n", $lines);
|
||
}
|
||
break;
|
||
case 'layout_forbidden_app':
|
||
$content = preg_replace(
|
||
"/@extends\(['\"]layouts\.app['\"]\)/",
|
||
"@extends('admin.layouts.netgescon')",
|
||
$content
|
||
);
|
||
break;
|
||
|
||
case 'bootstrap_classes':
|
||
// Bottoni
|
||
$content = str_replace(
|
||
['btn-primary', 'btn-secondary', 'btn-success', 'btn-danger', 'btn-warning'],
|
||
['netgescon-btn netgescon-btn-primary', 'netgescon-btn netgescon-btn-secondary', 'netgescon-btn netgescon-btn-success', 'netgescon-btn netgescon-btn-danger', 'netgescon-btn netgescon-btn-warning'],
|
||
$content
|
||
);
|
||
$content = str_replace(
|
||
['btn-outline-primary', 'btn-outline-secondary', 'btn-outline-success', 'btn-outline-danger', 'btn-outline-warning'],
|
||
['netgescon-btn netgescon-btn-outline-primary', 'netgescon-btn netgescon-btn-outline-secondary', 'netgescon-btn netgescon-btn-outline-success', 'netgescon-btn netgescon-btn-outline-danger', 'netgescon-btn netgescon-btn-outline-warning'],
|
||
$content
|
||
);
|
||
// Layout di base
|
||
$content = str_replace(['container-fluid'], ['px-6'], $content);
|
||
$content = preg_replace('/\brow\b/', 'grid md:grid-cols-12 gap-4', $content);
|
||
// Colonne Bootstrap comuni in griglia 12 -> col-span
|
||
$content = preg_replace('/\bcol-12\b/', 'col-span-12', $content);
|
||
$content = preg_replace('/\bcol-6\b/', 'col-span-12 md:col-span-6', $content);
|
||
$content = preg_replace('/\bcol-4\b/', 'col-span-12 md:col-span-4', $content);
|
||
$content = preg_replace('/\bcol-3\b/', 'col-span-12 md:col-span-3', $content);
|
||
// Tabelle e form
|
||
$content = preg_replace('/\btable\b/', 'table', $content);
|
||
$content = preg_replace('/\bform-control\b/', 'netgescon-input', $content);
|
||
// Utility display/flex
|
||
$content = preg_replace('/\bd-flex\b/', 'flex', $content);
|
||
$content = preg_replace('/\binline-d-flex\b/', 'inline-flex', $content);
|
||
// Justify/align mapping
|
||
$content = preg_replace('/\bjustify-content-between\b/', 'justify-between', $content);
|
||
$content = preg_replace('/\bjustify-content-end\b/', 'justify-end', $content);
|
||
$content = preg_replace('/\bjustify-content-start\b/', 'justify-start', $content);
|
||
$content = preg_replace('/\balign-items-center\b/', 'items-center', $content);
|
||
$content = preg_replace('/\balign-items-start\b/', 'items-start', $content);
|
||
$content = preg_replace('/\balign-items-end\b/', 'items-end', $content);
|
||
// Tipografia e colori comuni
|
||
$content = preg_replace('/\bfw-bold\b/', 'font-bold', $content);
|
||
$content = preg_replace('/\btext-muted\b/', 'text-gray-500', $content);
|
||
$content = preg_replace('/\btext-dark\b/', 'text-gray-900', $content);
|
||
$content = preg_replace('/\bbg-light\b/', 'bg-gray-100', $content);
|
||
// Badge Bootstrap -> NetGescon
|
||
$content = preg_replace('/\bbadge\s+bg-primary\b/', 'netgescon-badge netgescon-badge-primary', $content);
|
||
$content = preg_replace('/\bbadge\s+bg-secondary\b/', 'netgescon-badge netgescon-badge-secondary', $content);
|
||
$content = preg_replace('/\bbadge\s+bg-success\b/', 'netgescon-badge netgescon-badge-success', $content);
|
||
$content = preg_replace('/\bbadge\s+bg-danger\b/', 'netgescon-badge netgescon-badge-danger', $content);
|
||
$content = preg_replace('/\bbadge\s+bg-warning\b/', 'netgescon-badge netgescon-badge-warning', $content);
|
||
$content = preg_replace('/\bbadge\s+bg-info\b/', 'netgescon-badge netgescon-badge-info', $content);
|
||
break;
|
||
|
||
case 'deprecated_helpers':
|
||
$replacements = [
|
||
'array_get(' => 'data_get(',
|
||
'array_set(' => 'data_set(',
|
||
'str_is(' => 'Str::is(',
|
||
'starts_with(' => 'Str::startsWith(',
|
||
'ends_with(' => 'Str::endsWith(',
|
||
];
|
||
foreach ($replacements as $old => $new) {
|
||
$content = str_replace($old, $new, $content);
|
||
}
|
||
break;
|
||
}
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* Conta i file scansionabili nei percorsi indicati
|
||
*/
|
||
private function countFilesInPaths(string $scanPaths, string $moduleFilter = 'all', string $menuFilter = 'all'): int
|
||
{
|
||
$count = 0;
|
||
$paths = explode(',', $scanPaths);
|
||
foreach ($paths as $path) {
|
||
$path = trim($path);
|
||
$fullPath = \Illuminate\Support\Facades\App::basePath($path);
|
||
if (!File::exists($fullPath)) continue;
|
||
$files = $this->getFilesToScan($fullPath);
|
||
if ($moduleFilter !== 'all' || $menuFilter !== 'all') {
|
||
$files = array_filter($files, function ($f) use ($moduleFilter, $menuFilter) {
|
||
$rel = str_replace(\Illuminate\Support\Facades\App::basePath('') . '/', '', $f);
|
||
return $this->pathMatchesFilters($rel, $moduleFilter, $menuFilter);
|
||
});
|
||
}
|
||
$count += count($files);
|
||
}
|
||
return $count;
|
||
}
|
||
|
||
private function pathMatchesFilters(string $relativePath, string $moduleFilter, string $menuFilter): bool
|
||
{
|
||
$moduleOk = true;
|
||
$menuOk = true;
|
||
if ($moduleFilter !== 'all' && isset($this->modulePaths[$moduleFilter])) {
|
||
$moduleOk = false;
|
||
foreach ($this->modulePaths[$moduleFilter] as $p) {
|
||
if (Str::contains($relativePath, $p)) {
|
||
$moduleOk = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if ($menuFilter !== 'all' && isset($this->menuPaths[$menuFilter])) {
|
||
$menuOk = false;
|
||
foreach ($this->menuPaths[$menuFilter] as $p) {
|
||
if (Str::contains($relativePath, $p)) {
|
||
$menuOk = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return $moduleOk && $menuOk;
|
||
}
|
||
|
||
private function classifyModule(string $relativePath): string
|
||
{
|
||
foreach ($this->modulePaths as $key => $paths) {
|
||
foreach ($paths as $p) {
|
||
if (Str::contains($relativePath, $p)) return $key;
|
||
}
|
||
}
|
||
return 'core';
|
||
}
|
||
|
||
private function classifyMenu(string $relativePath): string
|
||
{
|
||
foreach ($this->menuPaths as $key => $paths) {
|
||
foreach ($paths as $p) {
|
||
if (Str::contains($relativePath, $p)) return $key;
|
||
}
|
||
}
|
||
return 'generic';
|
||
}
|
||
}
|