diff --git a/.gitattributes b/.gitattributes index c2b4bb0759c9..b1d0822c10d5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,7 +22,7 @@ utils/ export-ignore .php-cs-fixer.no-header.php export-ignore .php-cs-fixer.tests.php export-ignore .php-cs-fixer.user-guide.php export-ignore -deptrac.yaml export-ignore +structarmed.php export-ignore phpmetrics.json export-ignore phpstan-baseline.php export-ignore phpstan-bootstrap.php export-ignore diff --git a/.github/workflows/test-deptrac.yml b/.github/workflows/test-deptrac.yml deleted file mode 100644 index ae8a12823a92..000000000000 --- a/.github/workflows/test-deptrac.yml +++ /dev/null @@ -1,81 +0,0 @@ -# When a PR is opened or a push is made, perform an -# architectural inspection on the code using Deptrac. -name: Deptrac - -on: - pull_request: - branches: - - 'develop' - - '4.*' - paths: - - 'app/**.php' - - 'system/**.php' - - 'composer.json' - - 'depfile.yaml' - - '.github/workflows/test-deptrac.yml' - push: - branches: - - 'develop' - - '4.*' - paths: - - 'app/**.php' - - 'system/**.php' - - 'composer.json' - - 'depfile.yaml' - - '.github/workflows/test-deptrac.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Architectural Inspection - runs-on: ubuntu-24.04 - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 - with: - php-version: '8.2' - tools: composer - extensions: intl, json, mbstring, gd, mysqlnd, xdebug, xml, sqlite3 - - - name: Validate composer.json - run: composer validate --strict - - - name: Get composer cache directory - id: composer-cache - run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache dependencies - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: ${{ steps.composer-cache.outputs.COMPOSER_CACHE_FILES_DIR }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Create Deptrac cache directory - run: mkdir -p build/ - - - name: Cache Deptrac results - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: build - key: ${{ runner.os }}-deptrac-${{ github.sha }} - restore-keys: ${{ runner.os }}-deptrac- - - - name: Install dependencies - run: composer update --ansi --no-interaction - - - name: Run architectural inspection - run: | - composer require --dev deptrac/deptrac - vendor/bin/deptrac analyze --cache-file=build/deptrac.cache - env: - GITHUB_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/admin/RELEASE.md b/admin/RELEASE.md index 4116520297e9..75b4e26b2b31 100644 --- a/admin/RELEASE.md +++ b/admin/RELEASE.md @@ -107,7 +107,7 @@ the existing content. ``` git diff --name-status upstream/master -- . ':!.github/' ':!admin/' ':!changelogs/' ':!contributing/' \ ':!system/' ':!tests/' ':!user_guide_src/' ':!utils/' \ - ':!*.json' ':!*.xml' ':!*.dist' ':!rector.php' ':!deptrac.yml' \ + ':!*.json' ':!*.xml' ':!*.dist' ':!rector.php' ':!structarmed.php' \ ':!phpstan*' ':!psalm*' ':!.php-cs-fixer.*' ':!LICENSE' ':!CHANGELOG.md' ``` * Note: `tests/` is not used for distribution repos. See `admin/starter/tests/`. diff --git a/composer.json b/composer.json index c9cb78318d4d..aed3cb6cb38c 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "psr/log": "^3.0" }, "require-dev": { - "boundwize/structarmed": "0.4.5", + "boundwize/structarmed": "0.5.4", "codeigniter/phpstan-codeigniter": "^1.5", "fakerphp/faker": "^1.24", "kint-php/kint": "^6.1", diff --git a/deptrac.yaml b/deptrac.yaml deleted file mode 100644 index 4f7d8367bae6..000000000000 --- a/deptrac.yaml +++ /dev/null @@ -1,285 +0,0 @@ -# Defines the layers for each framework -# component and their allowed interactions. -# The following components are exempt -# due to their global nature: -# - CLI & Commands -# - Config -# - Debug -# - Exception -# - Service -# - Validation\FormatRules -deptrac: - paths: - - ./app - - ./system - exclude_files: - - '#.*test.*#i' - layers: - - name: API - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\API\\.*$/' - - name: Cache - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Cache\\.*$/' - - name: Controller - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Controller$/' - - name: Cookie - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Cookie\\.*$/' - - name: Database - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Database\\.*$/' - - name: DataCaster - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\DataCaster\\.*$/' - - name: DataConverter - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\DataConverter\\.*$/' - - name: Email - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Email\\.*$/' - - name: Encryption - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Encryption\\.*$/' - - name: Entity - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Entity\\.*$/' - - name: Events - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Events\\.*$/' - - name: Files - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Files\\.*$/' - - name: Filters - collectors: - - type: bool - must: - - type: classNameRegex - value: '/^CodeIgniter\\Filters\\Filter.*$/' - - name: Format - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Format\\.*$/' - - name: Honeypot - collectors: - - type: classNameRegex - # includes the Filter - value: '/^CodeIgniter\\.*Honeypot.*$/' - - name: HTTP - collectors: - - type: bool - must: - - type: classNameRegex - value: '/^CodeIgniter\\HTTP\\.*$/' - must_not: - - type: classNameRegex - value: '(Exception|URI)' - - name: I18n - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\I18n\\.*$/' - - name: Images - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Images\\.*$/' - - name: Language - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Language\\.*$/' - - name: Log - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Log\\.*$/' - - name: Model - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\.*Model$/' - - name: Modules - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Modules\\.*$/' - - name: Pager - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Pager\\.*$/' - - name: Publisher - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Publisher\\.*$/' - - name: RESTful - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\RESTful\\.*$/' - - name: Router - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Router\\.*$/' - - name: Security - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Security\\.*$/' - - name: Session - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Session\\.*$/' - - name: Throttle - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Throttle\\.*$/' - - name: Typography - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\Typography\\.*$/' - - name: URI - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\HTTP\\URI$/' - - name: Validation - collectors: - - type: bool - must: - - type: classNameRegex - value: '/^CodeIgniter\\Validation\\.*$/' - must_not: - - type: classNameRegex - value: '/^CodeIgniter\\Validation\\FormatRules$/' - - name: View - collectors: - - type: classNameRegex - value: '/^CodeIgniter\\View\\.*$/' - ruleset: - API: - - Format - - HTTP - - Database - - Model - - Pager - - URI - Cache: - - I18n - Controller: - - HTTP - - Validation - Cookie: - - I18n - Database: - - Entity - - Events - - I18n - DataCaster: - - I18n - - URI - - Database - DataConverter: - - DataCaster - Email: - - I18n - - Events - Entity: - - DataCaster - - I18n - Files: - - I18n - Filters: - - HTTP - Honeypot: - - Filters - - HTTP - HTTP: - - Cookie - - Files - - I18n - - Security - - URI - Images: - - Files - - I18n - Model: - - Database - - DataCaster - - DataConverter - - Entity - - I18n - - Pager - - Validation - Pager: - - URI - - View - Publisher: - - Files - - URI - RESTful: - - +API - - +Controller - Router: - - HTTP - - I18n - Security: - - Cookie - - I18n - - Session - - HTTP - Session: - - Cookie - - HTTP - - Database - - I18n - Throttle: - - Cache - - I18n - Validation: - - HTTP - - Database - View: - - Cache - skip_violations: - # Individual class exemptions - CodeIgniter\Cache\ResponseCache: - - CodeIgniter\HTTP\CLIRequest - - CodeIgniter\HTTP\Header - - CodeIgniter\HTTP\IncomingRequest - - CodeIgniter\HTTP\ResponseInterface - CodeIgniter\DataCaster\DataCaster: - - CodeIgniter\Entity\Cast\CastInterface - - CodeIgniter\Entity\Exceptions\CastException - CodeIgniter\DataCaster\Exceptions\CastException: - - CodeIgniter\Entity\Exceptions\CastException - CodeIgniter\DataConverter\DataConverter: - - CodeIgniter\Entity\Entity - CodeIgniter\Entity\Cast\URICast: - - CodeIgniter\HTTP\URI - CodeIgniter\Log\Handlers\ChromeLoggerHandler: - - CodeIgniter\HTTP\ResponseInterface - CodeIgniter\Security\CheckPhpIni: - - CodeIgniter\View\Table - CodeIgniter\View\Table: - - CodeIgniter\Database\BaseResult - CodeIgniter\View\Plugins: - - CodeIgniter\HTTP\URI - - # BC changes that should be fixed - CodeIgniter\HTTP\ResponseTrait: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\ResponseInterface: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\Response: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\RedirectResponse: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\HTTP\DownloadResponse: - - CodeIgniter\Pager\PagerInterface - CodeIgniter\Validation\Validation: - - CodeIgniter\View\RendererInterface diff --git a/structarmed.php b/structarmed.php index 97ad5c018e60..69bfaf1d197f 100644 --- a/structarmed.php +++ b/structarmed.php @@ -11,6 +11,30 @@ * the LICENSE file that was distributed with this source code. */ +use CodeIgniter\Cache\ResponseCache; +use CodeIgniter\HTTP\CLIRequest; +use CodeIgniter\HTTP\Header; +use CodeIgniter\HTTP\IncomingRequest; +use CodeIgniter\HTTP\ResponseInterface; +use CodeIgniter\DataCaster\DataCaster; +use CodeIgniter\Entity\Cast\CastInterface; +use CodeIgniter\Entity\Exceptions\CastException; +use CodeIgniter\DataConverter\DataConverter; +use CodeIgniter\Entity\Entity; +use CodeIgniter\Entity\Cast\URICast; +use CodeIgniter\HTTP\URI; +use CodeIgniter\Log\Handlers\ChromeLoggerHandler; +use CodeIgniter\Security\CheckPhpIni; +use CodeIgniter\View\Table; +use CodeIgniter\Database\BaseResult; +use CodeIgniter\View\Plugins; +use CodeIgniter\HTTP\ResponseTrait; +use CodeIgniter\Pager\PagerInterface; +use CodeIgniter\HTTP\Response; +use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\DownloadResponse; +use CodeIgniter\Validation\Validation; +use CodeIgniter\View\RendererInterface; use Boundwize\StructArmed\Architecture; use Boundwize\StructArmed\Preset\Preset; use Boundwize\StructArmed\Preset\Presets\Psr4Preset; @@ -23,4 +47,106 @@ __DIR__ . '/system/ThirdParty', ]) ->cacheDirectory(is_dir('/tmp') ? '/tmp/structarmed' : null) - ->withPreset(Preset::PSR4()); + ->withPreset(Preset::PSR4()) + // Resolve CodeIgniter layers from class names because several layers share directories. + ->layerPattern('API', '/^CodeIgniter\\\\API\\\\.*$/') + ->layerPattern('Cache', '/^CodeIgniter\\\\Cache\\\\.*$/') + ->layerPattern('Controller', '/^CodeIgniter\\\\Controller$/') + ->layerPattern('Cookie', '/^CodeIgniter\\\\Cookie\\\\.*$/') + ->layerPattern('Database', '/^CodeIgniter\\\\Database\\\\.*$/') + ->layerPattern('DataCaster', '/^CodeIgniter\\\\DataCaster\\\\.*$/') + ->layerPattern('DataConverter', '/^CodeIgniter\\\\DataConverter\\\\.*$/') + ->layerPattern('Email', '/^CodeIgniter\\\\Email\\\\.*$/') + ->layerPattern('Encryption', '/^CodeIgniter\\\\Encryption\\\\.*$/') + ->layerPattern('Entity', '/^CodeIgniter\\\\Entity\\\\.*$/') + ->layerPattern('Events', '/^CodeIgniter\\\\Events\\\\.*$/') + ->layerPattern('Files', '/^CodeIgniter\\\\Files\\\\.*$/') + ->layerPattern('Filters', '/^CodeIgniter\\\\Filters\\\\Filter.*$/') + ->layerPattern('Format', '/^CodeIgniter\\\\Format\\\\.*$/') + ->layerPattern('Honeypot', '/^CodeIgniter\\\\.*Honeypot.*$/') + ->layerPattern('URI', '/^CodeIgniter\\\\HTTP\\\\URI$/') + ->layerPattern('HTTP', '/^CodeIgniter\\\\HTTP\\\\.*$/', '/(Exception|URI)/') + ->layerPattern('I18n', '/^CodeIgniter\\\\I18n\\\\.*$/') + ->layerPattern('Images', '/^CodeIgniter\\\\Images\\\\.*$/') + ->layerPattern('Language', '/^CodeIgniter\\\\Language\\\\.*$/') + ->layerPattern('Log', '/^CodeIgniter\\\\Log\\\\.*$/') + ->layerPattern('Model', '/^CodeIgniter\\\\.*Model$/') + ->layerPattern('Modules', '/^CodeIgniter\\\\Modules\\\\.*$/') + ->layerPattern('Pager', '/^CodeIgniter\\\\Pager\\\\.*$/') + ->layerPattern('Publisher', '/^CodeIgniter\\\\Publisher\\\\.*$/') + ->layerPattern('RESTful', '/^CodeIgniter\\\\RESTful\\\\.*$/') + ->layerPattern('Router', '/^CodeIgniter\\\\Router\\\\.*$/') + ->layerPattern('Security', '/^CodeIgniter\\\\Security\\\\.*$/') + ->layerPattern('Session', '/^CodeIgniter\\\\Session\\\\.*$/') + ->layerPattern('Throttle', '/^CodeIgniter\\\\Throttle\\\\.*$/') + ->layerPattern('Typography', '/^CodeIgniter\\\\Typography\\\\.*$/') + ->layerPattern('Validation', '/^CodeIgniter\\\\Validation\\\\.*$/', '/^CodeIgniter\\\\Validation\\\\FormatRules$/') + ->layerPattern('View', '/^CodeIgniter\\\\View\\\\.*$/') + ->ruleset([ + 'API' => ['Format', 'HTTP', 'Database', 'Model', 'Pager', 'URI'], + 'Cache' => ['I18n'], + 'Controller' => ['HTTP', 'Validation'], + 'Cookie' => ['I18n'], + 'Database' => ['Entity', 'Events', 'I18n'], + 'DataCaster' => ['I18n', 'URI', 'Database'], + 'DataConverter' => ['DataCaster'], + 'Email' => ['I18n', 'Events'], + 'Entity' => ['DataCaster', 'I18n'], + 'Files' => ['I18n'], + 'Filters' => ['HTTP'], + 'Honeypot' => ['Filters', 'HTTP'], + 'HTTP' => ['Cookie', 'Files', 'I18n', 'Security', 'URI'], + 'Images' => ['Files', 'I18n'], + 'Model' => ['Database', 'DataCaster', 'DataConverter', 'Entity', 'I18n', 'Pager', 'Validation'], + 'Pager' => ['URI', 'View'], + 'Publisher' => ['Files', 'URI'], + // +API = API + its allowed layers; +Controller = Controller + its allowed layers + 'RESTful' => ['API', 'Controller', 'Database', 'Format', 'HTTP', 'Model', 'Pager', 'URI', 'Validation'], + 'Router' => ['HTTP', 'I18n'], + 'Security' => ['Cookie', 'HTTP', 'I18n', 'Session'], + 'Session' => ['Cookie', 'Database', 'HTTP', 'I18n'], + 'Throttle' => ['Cache', 'I18n'], + 'Validation' => ['Database', 'HTTP'], + 'View' => ['Cache'], + ]) + ->skipPathsForRuleset(['*test*']) + // Skip violations for class-specific dependencies. + ->skipClassViolation(ResponseCache::class, [ + CLIRequest::class, + Header::class, + IncomingRequest::class, + ResponseInterface::class, + ]) + ->skipClassViolation(DataCaster::class, [ + CastInterface::class, + CastException::class, + ]) + ->skipClassViolation(\CodeIgniter\DataCaster\Exceptions\CastException::class, [ + CastException::class, + ]) + ->skipClassViolation(DataConverter::class, [ + Entity::class, + ]) + ->skipClassViolation(URICast::class, [ + URI::class, + ]) + ->skipClassViolation(ChromeLoggerHandler::class, [ + ResponseInterface::class, + ]) + ->skipClassViolation(CheckPhpIni::class, [ + Table::class, + ]) + ->skipClassViolation(Table::class, [ + BaseResult::class, + ]) + ->skipClassViolation(Plugins::class, [ + URI::class, + ]) + + // BC changes that should be fixed + ->skipClassViolation(ResponseTrait::class, [PagerInterface::class]) + ->skipClassViolation(ResponseInterface::class, [PagerInterface::class]) + ->skipClassViolation(Response::class, [PagerInterface::class]) + ->skipClassViolation(RedirectResponse::class, [PagerInterface::class]) + ->skipClassViolation(DownloadResponse::class, [PagerInterface::class]) + ->skipClassViolation(Validation::class, [RendererInterface::class]);