diff --git a/.github/workflows/manifest.yml b/.github/workflows/manifest.yml index e03b53d42c..522a735073 100644 --- a/.github/workflows/manifest.yml +++ b/.github/workflows/manifest.yml @@ -15,9 +15,7 @@ jobs: name: Update manifest runs-on: ubuntu-latest env: - phpVersion: "8.4" - extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - key: winter-cms-cache-develop + GITHUB_TOKEN: ${{ secrets.COMPOSER_GITHUB_TOKEN }} steps: - name: Checkout changes uses: actions/checkout@v4 @@ -25,8 +23,8 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.phpVersion }} - extensions: ${{ env.extensions }} + php-version: 8.4 + extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - name: Install Composer dependencies run: composer install --no-interaction --no-progress --no-scripts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 537c81aeeb..ae6e66d89e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,59 +21,30 @@ jobs: runs-on: ${{ matrix.operatingSystem }} name: ${{ matrix.operatingSystem }} / JavaScript env: - nodeVersion: 16 - phpVersion: '8.2' - extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - key: winter-cms-cache-develop + GITHUB_TOKEN: ${{ secrets.COMPOSER_GITHUB_TOKEN }} steps: - name: Checkout changes - uses: actions/checkout@v3 - - - name: Setup extension cache - id: extcache - uses: shivammathur/cache-extensions@v1 - with: - php-version: ${{ env.phpVersion }} - extensions: ${{ env.extensions }} - key: ${{ env.key }} - - - name: Cache extensions - uses: actions/cache@v3 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ env.phpVersion }} - extensions: ${{ env.extensions }} + php-version: 8.2 + extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - name: Install Node uses: actions/setup-node@v3 with: - node-version: ${{ env.nodeVersion }} + node-version: 16 - name: Switch library dependency (develop) if: github.ref == 'refs/heads/develop' || github.base_ref == 'develop' - run: php ./.github/workflows/utilities/library-switcher "dev-develop as 1.2" + run: php ./.github/workflows/utilities/library-switcher "dev-wip/1.3 as 1.3" - name: Switch library dependency (1.2) if: github.head_ref == '1.2' || github.ref == 'refs/heads/1.2' || github.base_ref == '1.2' run: php ./.github/workflows/utilities/library-switcher "1.2.x-dev as 1.2" - - name: Setup dependency cache - id: composercache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install Composer dependencies run: composer install --no-interaction --no-progress --no-scripts @@ -95,41 +66,25 @@ jobs: max-parallel: 8 matrix: operatingSystem: [ubuntu-latest, windows-latest] - phpVersion: ['8.1', '8.2', '8.3', '8.4'] + phpVersion: ['8.2', '8.3', '8.4'] fail-fast: false runs-on: ${{ matrix.operatingSystem }} name: ${{ matrix.operatingSystem }} / PHP ${{ matrix.phpVersion }} env: - extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - key: winter-cms-cache-develop + GITHUB_TOKEN: ${{ secrets.COMPOSER_GITHUB_TOKEN }} steps: - name: Checkout changes - uses: actions/checkout@v3 - - - name: Setup extension cache - id: extcache - uses: shivammathur/cache-extensions@v1 - with: - php-version: ${{ matrix.phpVersion }} - extensions: ${{ env.extensions }} - key: ${{ env.key }} - - - name: Cache extensions - uses: actions/cache@v3 - with: - path: ${{ steps.extcache.outputs.dir }} - key: ${{ steps.extcache.outputs.key }} - restore-keys: ${{ steps.extcache.outputs.key }} + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.phpVersion }} - extensions: ${{ env.extensions }} + extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - name: Switch library dependency (develop) if: github.ref == 'refs/heads/develop' || github.base_ref == 'develop' - run: php ./.github/workflows/utilities/library-switcher "dev-develop as 1.2" + run: php ./.github/workflows/utilities/library-switcher "dev-wip/1.3 as 1.3" - name: Switch library dependency (1.0) if: github.head_ref == '1.0' || github.ref == 'refs/heads/1.0' || github.base_ref == '1.0' @@ -143,17 +98,6 @@ jobs: if: github.head_ref == '1.2' || github.ref == 'refs/heads/1.2' || github.base_ref == '1.2' run: php ./.github/workflows/utilities/library-switcher "1.2.x-dev as 1.2" - - name: Setup dependency cache - id: composercache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install Composer dependencies run: composer install --no-interaction --no-progress --no-scripts @@ -165,10 +109,6 @@ jobs: - name: Run post-update Composer scripts run: php artisan package:discover - - name: Setup problem matchers for PHPUnit - if: matrix.phpVersion == '8.1' - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Run Linting and Tests run: | composer lint diff --git a/.gitignore b/.gitignore index 878b446ce7..e03cf98b7b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ composer.lock selenium.php /bootstrap/compiled.php .phpunit.result.cache +.phpunit.cache # Hosting ignores php_errors.log @@ -26,6 +27,7 @@ nbproject .vscode !.devcontainer/.vscode _ide_helper.php +*.code-workspace # Other ignores .DS_Store diff --git a/composer.json b/composer.json index 7948a44910..49915a8696 100644 --- a/composer.json +++ b/composer.json @@ -29,22 +29,28 @@ "source": "https://github.com/wintercms/winter" }, "require": { - "php": ">=8.1", - "winter/storm": "dev-develop as 1.2", - "winter/wn-system-module": "dev-develop", - "winter/wn-backend-module": "dev-develop", - "winter/wn-cms-module": "dev-develop", - "laravel/framework": "^9.1", + "php": "^8.2", + "winter/storm": "dev-wip/1.3 as 1.3", + "winter/wn-system-module": "dev-wip/1.3", + "winter/wn-backend-module": "dev-wip/1.3", + "winter/wn-cms-module": "dev-wip/1.3", + "laravel/framework": "^12.0", "wikimedia/composer-merge-plugin": "~2.1.0" }, "require-dev": { - "phpunit/phpunit": "^9.5.8", + "phpunit/phpunit": "^11.0", "mockery/mockery": "^1.4.4", "fakerphp/faker": "^1.9.2", "squizlabs/php_codesniffer": "^3.2", "php-parallel-lint/php-parallel-lint": "^1.0", - "dms/phpunit-arraysubset-asserts": "^0.1.0|^0.2.1" + "dms/phpunit-arraysubset-asserts": "dev-add-phpunit-11-support" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/pieterocp/phpunit-arraysubset-asserts" + } + ], "scripts": { "post-create-project-cmd": [ "@php artisan winter:install", @@ -65,7 +71,6 @@ "phpcs --colors -nq --report=\"full\" --extensions=\"php\"" ] }, - "minimum-stability": "dev", "prefer-stable": true, "extra": { "merge-plugin": { diff --git a/config/app.php b/config/app.php index e4a78cb57e..e1ec05c6eb 100644 --- a/config/app.php +++ b/config/app.php @@ -2,6 +2,32 @@ return [ + /* + |-------------------------------------------------------------------------- + | Application Name + |-------------------------------------------------------------------------- + | + | This value is the name of your application. This value is used when the + | framework needs to place the application's name in a notification or + | any other location as required by the application or its packages. + | + */ + + 'name' => env('APP_NAME', 'Winter CMS'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + /* |-------------------------------------------------------------------------- | Application Debug Mode @@ -20,20 +46,7 @@ | */ - 'debug' => env('APP_DEBUG', true), - - /* - |-------------------------------------------------------------------------- - | Application Name - |-------------------------------------------------------------------------- - | - | This value is the name of your application. This value is used when the - | framework needs to place the application's name in a notification or - | any other location as required by the application or its packages. - | - */ - - 'name' => env('APP_NAME', 'Winter CMS'), + 'debug' => (bool) env('APP_DEBUG', true), /* |-------------------------------------------------------------------------- @@ -42,7 +55,7 @@ | | This URL is used by the console to properly generate URLs when using | the Artisan command line tool. You should set this to the root of - | your application so that it is used when running Artisan tasks. + | the application so that it's available within Artisan commands. | */ @@ -183,7 +196,6 @@ | will be used by the PHP date and date-time functions. We have gone | ahead and set this to a sensible default for you out of the box. | - | | -------- STOP! -------- | Before you change this value, consider carefully if that is actually | what you want to do. It is HIGHLY recommended that this is always set @@ -225,7 +237,7 @@ | */ - 'locale' => 'en', + 'locale' => env('APP_LOCALE', 'en'), /* |-------------------------------------------------------------------------- @@ -238,7 +250,7 @@ | */ - 'fallback_locale' => 'en', + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), /* |-------------------------------------------------------------------------- @@ -251,7 +263,7 @@ | */ - 'faker_locale' => 'en_US', + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), /* |-------------------------------------------------------------------------- @@ -264,9 +276,16 @@ | */ - 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC', + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + /* |-------------------------------------------------------------------------- | Autoloaded Service Providers diff --git a/config/hashing.php b/config/hashing.php index ad56664fb1..7bf45fba5c 100644 --- a/config/hashing.php +++ b/config/hashing.php @@ -48,4 +48,17 @@ 'threads' => 1, 'time' => 4, ], + + /* + |-------------------------------------------------------------------------- + | Password Rehashing + |-------------------------------------------------------------------------- + | + | Laravel 11 will automatically rehash your user's passwords during + | authentication if your hashing algorithm's "work factor" has been updated + | since the password was last hashed. + | + */ + + 'rehash_on_login' => false, ]; diff --git a/modules/backend/composer.json b/modules/backend/composer.json index bb1830994c..bb0d6050f7 100644 --- a/modules/backend/composer.json +++ b/modules/backend/composer.json @@ -23,9 +23,9 @@ } ], "require": { - "php": ">=8.1", + "php": "^8.2", "composer/installers": "~1.11.0", - "laravel/framework": "^9.1" + "laravel/framework": "^12.0" }, "replace": { "october/backend": "1.1.*" diff --git a/modules/backend/controllers/Index.php b/modules/backend/controllers/Index.php index 8b6abed758..aad9782de2 100644 --- a/modules/backend/controllers/Index.php +++ b/modules/backend/controllers/Index.php @@ -6,6 +6,8 @@ use Backend\Classes\Controller; use Backend\Widgets\ReportContainer; +use Winter\Storm\Support\Arr; + /** * Dashboard controller * @@ -72,10 +74,7 @@ protected function initReportContainer() protected function checkPermissionRedirect() { if (!$this->user->hasAccess('backend.access_dashboard')) { - $true = function () { - return true; - }; - if ($first = array_first(BackendMenu::listMainMenuItems(), $true)) { + if ($first = Arr::first(BackendMenu::listMainMenuItems())) { return Redirect::intended($first->url); } return Backend::redirect('backend/users/myaccount'); diff --git a/modules/backend/phpunit.xml b/modules/backend/phpunit.xml index 7c0631aa89..2023f43f78 100644 --- a/modules/backend/phpunit.xml +++ b/modules/backend/phpunit.xml @@ -1,17 +1,16 @@ - - - - ./tests - - + + + ./tests + + diff --git a/modules/backend/tests/classes/AuthManagerTest.php b/modules/backend/tests/classes/AuthManagerTest.php index cc112db10c..a4a528718b 100644 --- a/modules/backend/tests/classes/AuthManagerTest.php +++ b/modules/backend/tests/classes/AuthManagerTest.php @@ -2,9 +2,9 @@ namespace Backend\Tests\Classes; +use Backend\Classes\AuthManager; use System\Tests\Bootstrap\TestCase; use Winter\Storm\Exception\SystemException; -use Backend\Classes\AuthManager; class AuthManagerTest extends TestCase { @@ -44,6 +44,7 @@ protected function listNewPermissions() public function tearDown(): void { AuthManager::forgetInstance(); + parent::tearDown(); } public function testListPermissions() diff --git a/modules/backend/tests/traits/WidgetMakerTest.php b/modules/backend/tests/traits/WidgetMakerTest.php index 4d1caf2df9..be125946c3 100644 --- a/modules/backend/tests/traits/WidgetMakerTest.php +++ b/modules/backend/tests/traits/WidgetMakerTest.php @@ -9,6 +9,8 @@ class ExampleTraitClass { use \Backend\Traits\WidgetMaker; + protected Controller $controller; + public function __construct() { $this->controller = new Controller; @@ -35,8 +37,9 @@ public function setUp(): void { parent::setUp(); - $traitName = 'Backend\Traits\WidgetMaker'; - $this->traitObject = $this->getObjectForTrait($traitName); + $this->traitObject = new class { + use \Backend\Traits\WidgetMaker; + }; } public function testTraitObject() diff --git a/modules/cms/classes/ComponentManager.php b/modules/cms/classes/ComponentManager.php index 5388c53ac4..01858ce7c1 100644 --- a/modules/cms/classes/ComponentManager.php +++ b/modules/cms/classes/ComponentManager.php @@ -1,9 +1,11 @@ -=8.1", + "php": "^8.2", "composer/installers": "~1.11.0", - "laravel/framework": "^9.1" + "laravel/framework": "^12.0" }, "replace": { "october/cms": "1.1.*" diff --git a/modules/cms/phpunit.xml b/modules/cms/phpunit.xml index 4cb34b9f38..bb4330c7dc 100644 --- a/modules/cms/phpunit.xml +++ b/modules/cms/phpunit.xml @@ -1,17 +1,16 @@ - - - - ./tests - - + + + ./tests + + diff --git a/modules/cms/tests/classes/CmsObjectTest.php b/modules/cms/tests/classes/CmsObjectTest.php index 61bacb711a..02b83c1170 100644 --- a/modules/cms/tests/classes/CmsObjectTest.php +++ b/modules/cms/tests/classes/CmsObjectTest.php @@ -5,6 +5,7 @@ use System\Tests\Bootstrap\TestCase; use Cms\Classes\CmsObject; use Cms\Classes\Theme; +use PHPUnit\Framework\Attributes\Depends; class TestCmsObject extends CmsObject { @@ -236,9 +237,7 @@ public function testSave() $this->assertEquals($testContents, file_get_contents($destFilePath)); } - /** - * @depends testSave - */ + #[Depends('testSave')] public function testRename() { $theme = Theme::load('apitest'); @@ -265,9 +264,7 @@ public function testRename() $this->assertEquals($testContents, file_get_contents($destFilePath)); } - /** - * @depends testRename - */ + #[Depends('testRename')] public function testSaveSameName() { $theme = Theme::load('apitest'); @@ -288,9 +285,7 @@ public function testSaveSameName() $this->assertEquals($testContents, file_get_contents($filePath)); } - /** - * @depends testRename - */ + #[Depends('testRename')] public function testRenameToExistingFile() { $this->expectException(\Winter\Storm\Exception\ApplicationException::class); diff --git a/modules/cms/tests/classes/ControllerTest.php b/modules/cms/tests/classes/ControllerTest.php index 16d72cb9ce..6a098d6f52 100644 --- a/modules/cms/tests/classes/ControllerTest.php +++ b/modules/cms/tests/classes/ControllerTest.php @@ -2,12 +2,13 @@ namespace Cms\Tests\Classes; -use Cms; -use Request; -use System\Tests\Bootstrap\TestCase; -use Cms\Classes\Theme; use Cms\Classes\Controller; +use Cms\Classes\Theme; +use Cms\Facades\Cms; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Request; use System\Helpers\View; +use System\Tests\Bootstrap\TestCase; use Winter\Storm\Halcyon\Model; use Winter\Storm\Support\Facades\Config; @@ -79,7 +80,7 @@ public function testThemeCombineAssets(): void $cacheKey = 'combiner.' . str_before(basename($url), '-'); // Load the cached config - $combinerConfig = \Cache::get($cacheKey); + $combinerConfig = Cache::get($cacheKey); $this->assertIsString($combinerConfig); // Decode the config @@ -299,30 +300,32 @@ public function testChildThemePageLifeCycle() protected function configAjaxRequestMock($handler, $partials = false) { - $requestMock = $this - ->getMockBuilder('Illuminate\Http\Request') - ->disableOriginalConstructor() - ->setMethods(['ajax', 'method', 'header']) - ->getMock(); - - $map = [ - ['X_WINTER_REQUEST_HANDLER', null, $handler], - ['X_WINTER_REQUEST_PARTIALS', null, $partials], - ]; - - $requestMock->expects($this->any()) - ->method('ajax') - ->will($this->returnValue(true)); - - $requestMock->expects($this->any()) - ->method('method') - ->will($this->returnValue('POST')); - - $requestMock->expects($this->any()) - ->method('header') - ->will($this->returnValueMap($map)); - - return $requestMock; + return new class($handler, $partials) extends \Illuminate\Http\Request { + protected $handler; + protected $partials; + public function __construct($handler, $partials) + { + $this->handler = $handler; + $this->partials = $partials; + parent::__construct(); + } + public function ajax() + { + return true; + } + public function method() + { + return 'POST'; + } + public function header($key = null, $default = null) + { + return match ($key) { + 'X_WINTER_REQUEST_HANDLER' => $this->handler, + 'X_WINTER_REQUEST_PARTIALS' => $this->partials, + default => $default, + }; + } + }; } public function testAjaxHandlerNotFound() diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php index 1723f2f282..8d63cab773 100644 --- a/modules/system/classes/UpdateManager.php +++ b/modules/system/classes/UpdateManager.php @@ -135,6 +135,13 @@ public function bindContainerObjects() public function update() { try { + $connection = Schema::getConnection(); + if ($connection->getDriverName() === 'sqlite') { + if (version_compare($connection->getServerVersion(), '3.35', '<')) { + throw new Exception("SQLite version minimum requirement not met (>= 3.35)"); + } + }; + $firstUp = !Schema::hasTable($this->getMigrationTableName()); if ($firstUp) { $this->repository->createRepository(); diff --git a/modules/system/composer.json b/modules/system/composer.json index 6b5c4929d9..3d58afe4e6 100644 --- a/modules/system/composer.json +++ b/modules/system/composer.json @@ -23,9 +23,9 @@ } ], "require": { - "php": ">=8.1", + "php": "^8.2", "composer/installers": "~1.11.0", - "laravel/framework": "^9.1", + "laravel/framework": "^12.0", "composer/semver": "^3.2" }, "replace": { diff --git a/modules/system/console/WinterTest.php b/modules/system/console/WinterTest.php index 7d24d105c7..5a96ead94c 100644 --- a/modules/system/console/WinterTest.php +++ b/modules/system/console/WinterTest.php @@ -1,6 +1,7 @@ -option($type)) { foreach ($this->option($type) as $target) { + if (empty($target)) { + continue; + } $target = strtolower($target); if (!isset($configs[$type . 's'][$target])) { throw new ApplicationException(sprintf( @@ -123,8 +128,15 @@ public function handle() return $exitCode; } - // default to running all defined configs found - foreach (['modules', 'plugins'] as $type) { + // default to running all defined configs found, run all tests if no options provided + $types = [ + 'modules' => count($this->option('module')) + !$this->option('plugin'), + 'plugins' => count($this->option('plugin')) + !$this->option('module'), + ]; + foreach ($types as $type => $count) { + if (!$count) { + continue; + } foreach ($configs[$type] as $name => $config) { $this->info( $type === 'plugins' diff --git a/modules/system/console/asset/mix/MixCompile.php b/modules/system/console/asset/mix/MixCompile.php index 14559cb39d..c86d0faf73 100644 --- a/modules/system/console/asset/mix/MixCompile.php +++ b/modules/system/console/asset/mix/MixCompile.php @@ -19,7 +19,6 @@ class MixCompile extends AssetCompile protected $signature = 'mix:compile {webpackArgs?* : Arguments to pass through to the Webpack CLI} {--f|production : Runs compilation in "production" mode} - {--s|silent : Enables silent mode, no output will be shown.} {--d|disable-tty : Disable tty mode} {--e|stop-on-error : Exit once an error is encountered} {--m|manifest= : Defines package.json to use for compile} diff --git a/modules/system/console/asset/mix/MixCreate.php b/modules/system/console/asset/mix/MixCreate.php index 85e1259aee..437b98562f 100644 --- a/modules/system/console/asset/mix/MixCreate.php +++ b/modules/system/console/asset/mix/MixCreate.php @@ -15,7 +15,6 @@ class MixCreate extends AssetCreate protected $signature = 'mix:create {packageName : The package name to add configuration for} {--no-stubs : Disable stub file generation} - {--s|silent : Enables silent mode, no output will be shown.} {--f|force : Force file overwrites}'; /** diff --git a/modules/system/console/asset/mix/MixInstall.php b/modules/system/console/asset/mix/MixInstall.php index b08d8c4b96..272ad6852a 100644 --- a/modules/system/console/asset/mix/MixInstall.php +++ b/modules/system/console/asset/mix/MixInstall.php @@ -17,7 +17,6 @@ class MixInstall extends AssetInstall {--no-install : Tells Winter not to run npm install after config update.} {--npm= : Defines a custom path to the "npm" binary.} {--d|disable-tty : Disable tty mode.} - {--s|silent : Enables silent mode, no output will be shown.} {--p|package-json= : Defines a custom path to "package.json" file. Must be above the workspace path.}'; /** diff --git a/modules/system/console/asset/mix/MixWatch.php b/modules/system/console/asset/mix/MixWatch.php index 53aee49133..b30b84e7b5 100644 --- a/modules/system/console/asset/mix/MixWatch.php +++ b/modules/system/console/asset/mix/MixWatch.php @@ -17,7 +17,6 @@ class MixWatch extends MixCompile {webpackArgs?* : Arguments to pass through to the Webpack CLI} {--f|production : Runs compilation in "production" mode} {--m|manifest= : Defines package.json to use for compile} - {--s|silent : Enables silent mode, no output will be shown.} {--d|disable-tty : Disable tty mode} {--no-progress : Do not show mix progress}'; diff --git a/modules/system/console/asset/npm/NpmInstall.php b/modules/system/console/asset/npm/NpmInstall.php index da697e42ae..c2e89d917a 100644 --- a/modules/system/console/asset/npm/NpmInstall.php +++ b/modules/system/console/asset/npm/NpmInstall.php @@ -25,7 +25,6 @@ class NpmInstall extends NpmCommand {npmArgs?* : Arguments to pass through to the "npm" binary} {--npm= : Defines a custom path to the "npm" binary} {--d|dev : Install packages in devDependencies} - {--s|silent : Silent mode.} {--disable-tty : Disable tty mode}'; /** diff --git a/modules/system/console/asset/npm/NpmRun.php b/modules/system/console/asset/npm/NpmRun.php index cd45313acd..5bfea734d8 100644 --- a/modules/system/console/asset/npm/NpmRun.php +++ b/modules/system/console/asset/npm/NpmRun.php @@ -19,7 +19,6 @@ class NpmRun extends NpmCommand {script : The name of the script to run, as defined in the package.json "scripts" config.} {additionalArgs?* : Arguments to pass through to the script being run.} {--f|production : Runs the script in "production" mode.} - {--s|silent : Silent mode.} {--disable-tty : Disable tty mode}'; /** diff --git a/modules/system/console/asset/npm/NpmUpdate.php b/modules/system/console/asset/npm/NpmUpdate.php index f578c5c4c1..2668391f2b 100644 --- a/modules/system/console/asset/npm/NpmUpdate.php +++ b/modules/system/console/asset/npm/NpmUpdate.php @@ -25,7 +25,6 @@ class NpmUpdate extends NpmCommand {npmArgs?* : Arguments to pass through to the "npm" binary.} {--npm= : Defines a custom path to the "npm" binary.} {--a|save : Tell npm to update package.json.} - {--s|silent : Silent mode.} {--disable-tty : Disable tty mode}'; /** diff --git a/modules/system/console/asset/npm/NpmVersion.php b/modules/system/console/asset/npm/NpmVersion.php index 78e4a4a0b7..0fda33770c 100644 --- a/modules/system/console/asset/npm/NpmVersion.php +++ b/modules/system/console/asset/npm/NpmVersion.php @@ -19,7 +19,6 @@ class NpmVersion extends NpmCommand */ protected $signature = 'npm:version {--c|compatible : Report compatible version via exit code.} - {--s|silent : Silent mode.} {--disable-tty : Disable tty mode}'; /** diff --git a/modules/system/console/asset/vite/ViteCompile.php b/modules/system/console/asset/vite/ViteCompile.php index eb10fea008..5a40904182 100644 --- a/modules/system/console/asset/vite/ViteCompile.php +++ b/modules/system/console/asset/vite/ViteCompile.php @@ -18,7 +18,6 @@ class ViteCompile extends AssetCompile protected $signature = 'vite:compile {viteArgs?* : Arguments to pass through to the Vite CLI} {--f|production : Runs compilation in "production" mode} - {--s|silent : Enables silent mode, no output will be shown.} {--d|disable-tty : Disable tty mode} {--e|stop-on-error : Exit once an error is encountered} {--m|manifest= : Defines package.json to use for compile} diff --git a/modules/system/console/asset/vite/ViteCreate.php b/modules/system/console/asset/vite/ViteCreate.php index 9067fb82ea..a26829a4a6 100644 --- a/modules/system/console/asset/vite/ViteCreate.php +++ b/modules/system/console/asset/vite/ViteCreate.php @@ -17,7 +17,6 @@ class ViteCreate extends AssetCreate protected $signature = 'vite:create {packageName : The package name to add configuration for} {--no-stubs : Disable stub file generation} - {--s|silent : Enables silent mode, no output will be shown.} {--f|force : Force file overwrites}'; /** diff --git a/modules/system/console/asset/vite/ViteInstall.php b/modules/system/console/asset/vite/ViteInstall.php index 035e7328c8..f21c236715 100644 --- a/modules/system/console/asset/vite/ViteInstall.php +++ b/modules/system/console/asset/vite/ViteInstall.php @@ -19,7 +19,6 @@ class ViteInstall extends AssetInstall {--no-install : Tells Winter not to run npm install after config update.} {--npm= : Defines a custom path to the "npm" binary.} {--d|disable-tty : Disable tty mode.} - {--s|silent : Enables silent mode, no output will be shown.} {--p|package-json= : Defines a custom path to "package.json" file. Must be above the workspace path.}'; /** diff --git a/modules/system/console/asset/vite/ViteWatch.php b/modules/system/console/asset/vite/ViteWatch.php index 2825724793..2175f8a94a 100644 --- a/modules/system/console/asset/vite/ViteWatch.php +++ b/modules/system/console/asset/vite/ViteWatch.php @@ -19,7 +19,6 @@ class ViteWatch extends ViteCompile {viteArgs?* : Arguments to pass through to the Webpack CLI} {--f|production : Runs compilation in "production" mode} {--m|manifest= : Defines package.json to use for compile} - {--s|silent : Enables silent mode, no output will be shown.} {--d|disable-tty : Disable tty mode} {--no-progress : Do not show mix progress}'; diff --git a/modules/system/console/scaffold/test/phpunit.stub b/modules/system/console/scaffold/test/phpunit.stub index e7877ce389..e1ab1fedca 100644 --- a/modules/system/console/scaffold/test/phpunit.stub +++ b/modules/system/console/scaffold/test/phpunit.stub @@ -1,13 +1,7 @@ - diff --git a/modules/system/phpunit.xml b/modules/system/phpunit.xml index af5eee72a0..66ab407567 100644 --- a/modules/system/phpunit.xml +++ b/modules/system/phpunit.xml @@ -1,17 +1,16 @@ - - - - ./tests - - + + + ./tests + + diff --git a/modules/system/tests/ServiceProviderTest.php b/modules/system/tests/ServiceProviderTest.php index f583e962c9..4ce143ef31 100644 --- a/modules/system/tests/ServiceProviderTest.php +++ b/modules/system/tests/ServiceProviderTest.php @@ -2,9 +2,9 @@ namespace System\Tests; -use Db; -use Log; +use Illuminate\Support\Facades\Log; use System\Tests\Bootstrap\PluginTestCase; +use Winter\Storm\Support\Facades\DB; class ServiceProviderTest extends PluginTestCase { @@ -21,7 +21,7 @@ public function testRegisterLogging() 'key' => 'Dummy value', ]; Log::info($message, $details); - $latestLog = Db::table('system_event_logs')->latest()->first(); + $latestLog = DB::table('system_event_logs')->latest()->first(); $this->assertEquals($message, $latestLog->message); $this->assertEquals($details, json_decode($latestLog->details, true)); } diff --git a/modules/system/tests/bootstrap/PluginTestCase.php b/modules/system/tests/bootstrap/PluginTestCase.php index 5acaa26d5e..b78877be37 100644 --- a/modules/system/tests/bootstrap/PluginTestCase.php +++ b/modules/system/tests/bootstrap/PluginTestCase.php @@ -39,11 +39,7 @@ abstract class PluginTestCase extends TestCase */ public function createApplication() { - $app = require __DIR__ . '/../../../../bootstrap/app.php'; - $app->make('Illuminate\Contracts\Console\Kernel')->bootstrap(); - - $app['cache']->setDefaultDriver('array'); - $app->setLocale('en'); + $app = parent::createApplication(); $app->singleton('backend.auth', function ($app) { $app['auth.loaded'] = true; @@ -60,9 +56,6 @@ public function createApplication() $app['config']->set('database.default', 'testing'); } - // Set random encryption key - $app['config']->set('app.key', bin2hex(random_bytes(16))); - // Modify the plugin path away from the test context $app->setPluginsPath(realpath(base_path() . Config::get('cms.pluginsPath'))); diff --git a/modules/system/tests/bootstrap/TestCase.php b/modules/system/tests/bootstrap/TestCase.php index 3bdb052718..dd324fe061 100644 --- a/modules/system/tests/bootstrap/TestCase.php +++ b/modules/system/tests/bootstrap/TestCase.php @@ -2,8 +2,11 @@ namespace System\Tests\Bootstrap; -use ReflectionClass; +use Illuminate\Contracts\Console\Kernel; +use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Contracts\Foundation\Application; use PHPUnit\Framework\Assert; +use ReflectionClass; class TestCase extends \Illuminate\Foundation\Testing\TestCase { @@ -24,6 +27,29 @@ public function createApplication() // Set random encryption key $app['config']->set('app.key', bin2hex(random_bytes(16))); + // Override the Kernel call method to prevent symfony shell verbosity breaking scripts. + // @see: https://github.com/symfony/symfony/pull/53632 + // @see: https://github.com/symfony/symfony/pull/24425 + $app->bind(Kernel::class, function (Application $app) { + return new class($app, $app->make(Dispatcher::class)) extends \Winter\Storm\Foundation\Console\Kernel + { + public function call($command, array $parameters = [], $outputBuffer = null) + { + $result = parent::call($command, $parameters, $outputBuffer); + + $shellVerbosity = 0; + + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY=' . $shellVerbosity); + } + + $_ENV['SHELL_VERBOSITY'] = $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity; + + return $result; + } + }; + }); + return $app; } @@ -59,36 +85,26 @@ public static function setProtectedProperty($object, $name, $value) } /** - * Stub for `assertFileNotExists` to allow compatibility with both PHPUnit 8 and 9. - * - * @param string $filename - * @param string $message - * @return void + * Stub for `assertFileNotExists` */ public static function assertFileNotExists(string $filename, string $message = ''): void { - if (method_exists(Assert::class, 'assertFileDoesNotExist')) { - Assert::assertFileDoesNotExist($filename, $message); - return; - } - - Assert::assertFileNotExists($filename, $message); + Assert::assertFileDoesNotExist($filename, $message); } /** - * Stub for `assertRegExp` to allow compatibility with both PHPUnit 8 and 9. - * - * @param string $filename - * @param string $message - * @return void + * Stub for `assertRegExp` */ public static function assertRegExp(string $pattern, string $string, string $message = ''): void { - if (method_exists(Assert::class, 'assertMatchesRegularExpression')) { - Assert::assertMatchesRegularExpression($pattern, $string, $message); - return; - } + Assert::assertMatchesRegularExpression($pattern, $string, $message); + } - Assert::assertRegExp($pattern, $string, $message); + /** + * Stub for `assertObjectHasAttribute` + */ + public static function assertObjectHasAttribute(string $propertyName, $object, string $message = ''): void + { + Assert::assertObjectHasProperty($propertyName, $object, $message); } } diff --git a/modules/system/tests/classes/MediaLibraryTest.php b/modules/system/tests/classes/MediaLibraryTest.php index ac9e67f6ae..751f989f46 100644 --- a/modules/system/tests/classes/MediaLibraryTest.php +++ b/modules/system/tests/classes/MediaLibraryTest.php @@ -4,6 +4,7 @@ use System\Tests\Bootstrap\TestCase; use Illuminate\Filesystem\FilesystemAdapter; +use PHPUnit\Framework\Attributes\DataProvider; use System\Classes\MediaLibrary; class MediaLibraryTest extends TestCase @@ -20,7 +21,7 @@ public function tearDown(): void parent::tearDown(); } - public function invalidPathsProvider() + public static function invalidPathsProvider() { return [ ['./file'], @@ -36,7 +37,7 @@ public function invalidPathsProvider() ]; } - public function validPathsProvider() + public static function validPathsProvider() { return [ ['file'], @@ -61,18 +62,14 @@ public function validPathsProvider() ]; } - /** - * @dataProvider invalidPathsProvider - */ + #[DataProvider('invalidPathsProvider')] public function testInvalidPathsOnValidatePath($path) { $this->expectException('ApplicationException'); MediaLibrary::validatePath($path); } - /** - * @dataProvider validPathsProvider - */ + #[DataProvider('validPathsProvider')] public function testValidPathsOnValidatePath($path) { $result = MediaLibrary::validatePath($path); diff --git a/modules/system/tests/classes/SourceManifestTest.php b/modules/system/tests/classes/SourceManifestTest.php index 3b3ed20ea3..b5a692bdec 100644 --- a/modules/system/tests/classes/SourceManifestTest.php +++ b/modules/system/tests/classes/SourceManifestTest.php @@ -2,10 +2,10 @@ namespace System\Tests\Classes; +use System\Classes\FileManifest; +use System\Classes\SourceManifest; use System\Tests\Bootstrap\TestCase; use Winter\Storm\Argon\Argon; -use System\Classes\SourceManifest; -use System\Classes\FileManifest; class SourceManifestTest extends TestCase { @@ -53,6 +53,7 @@ public function tearDown(): void { Argon::setTestNow(); $this->deleteManifest(); + parent::tearDown(); } public function testCreateManifest() diff --git a/modules/system/tests/classes/VersionManagerTest.php b/modules/system/tests/classes/VersionManagerTest.php index b4d0463a4c..b760e3d617 100644 --- a/modules/system/tests/classes/VersionManagerTest.php +++ b/modules/system/tests/classes/VersionManagerTest.php @@ -2,6 +2,7 @@ namespace System\Tests\Classes; +use PHPUnit\Framework\Attributes\DataProvider; use System\Tests\Bootstrap\TestCase; use System\Classes\VersionManager; @@ -138,13 +139,7 @@ public function testGetNewFileVersions() $this->assertArrayHasKey('1.5.1', $result); } - /** - * @dataProvider versionInfoProvider - * - * @param $versionInfo - * @param $expectedComments - * @param $expectedScripts - */ + #[DataProvider('versionInfoProvider')] public function testExtractScriptsAndComments($versionInfo, $expectedComments, $expectedScripts) { $manager = VersionManager::instance(); @@ -157,7 +152,7 @@ public function testExtractScriptsAndComments($versionInfo, $expectedComments, $ $this->assertEquals($expectedScripts, $scripts); } - public function versionInfoProvider() + public static function versionInfoProvider() { return [ [ diff --git a/modules/system/tests/console/CreateCommandTest.php b/modules/system/tests/console/CreateCommandTest.php index 4bb186ba4a..b65947f17d 100644 --- a/modules/system/tests/console/CreateCommandTest.php +++ b/modules/system/tests/console/CreateCommandTest.php @@ -2,9 +2,8 @@ namespace System\Tests\Console; -use File; -use InvalidArgumentException; use System\Tests\Bootstrap\TestCase; +use Winter\Storm\Support\Facades\File; class CreateCommandTest extends TestCase { diff --git a/modules/system/tests/console/CreateMigrationTest.php b/modules/system/tests/console/CreateMigrationTest.php index 77e30eea71..bac810e607 100644 --- a/modules/system/tests/console/CreateMigrationTest.php +++ b/modules/system/tests/console/CreateMigrationTest.php @@ -2,13 +2,16 @@ namespace System\Tests\Console; -use File; -use Illuminate\Database\Schema\Blueprint; -use Schema; use System\Tests\Bootstrap\PluginTestCase; +use Winter\Storm\Support\Facades\File; +use Winter\Storm\Support\Facades\Schema; class CreateMigrationTest extends PluginTestCase { + protected ?string $versionFile = null; + protected ?string $versionFolder = null; + protected ?string $table = null; + public function setUp(): void { parent::setUp(); @@ -34,46 +37,50 @@ public function testCreateMigration() $columns = [ 'id' => ['type'=>'integer', 'index'=>'primary', 'required'=>true], - 'cb' => ['type'=>'boolean'], - 'switch' => ['type'=>'boolean'], + 'cb' => ['type'=>'tinyint'], + 'switch' => ['type'=>'tinyint'], 'int' => ['type'=>'integer'], 'uint' => ['type'=>'integer', 'required'=>true], - 'double' => ['type'=>'float'], + 'double' => ['type'=>'double'], 'range' => ['type'=>'integer', 'required'=>true], 'datetime' => ['type'=>'datetime'], 'date' => ['type'=>'date', 'required'=>true], 'time' => ['type'=>'time'], 'md' => ['type'=>'text'], 'textarea' => ['type'=>'text'], - 'text' => ['type'=>'string', 'required'=>true], + 'text' => ['type'=>'varchar', 'required'=>true], 'phone_id' => ['type'=>'integer', 'index'=>true, 'required'=>true], 'user_id' => ['type'=>'integer', 'index'=>true, 'required'=>true], 'data' => ['type'=>'text'], 'sort_order' => ['type'=>'integer', 'index'=>true], 'taggable_id' => ['type'=>'integer', 'index'=>'morphable_index'], - 'taggable_type' => ['type'=>'string', 'index'=>'morphable_index'], + 'taggable_type' => ['type'=>'varchar', 'index'=>'morphable_index'], 'created_at' => ['type'=>'datetime'], 'updated_at' => ['type'=>'datetime'], ]; - $table = Schema::getConnection()->getDoctrineSchemaManager()->listTableDetails($this->table); + $schemaBuilder = Schema::getConnection()->getSchemaBuilder(); + + $tableColumns = collect($schemaBuilder->getColumns($this->table)); + $tableIndexes = collect($schemaBuilder->getIndexes($this->table))->pluck('columns', 'name'); foreach ($columns as $name => $definition) { - $this->assertEquals(array_get($definition, 'type'), Schema::getColumnType($this->table, $name)); + $column = $tableColumns->where('name', $name)->first(); + + $this->assertEquals(array_get($definition, 'type'), $column['type_name']); + $this->assertEquals(array_get($definition, 'required', false), !$column['nullable']); // assert an index has been created for the primary, morph and foreign keys if ($indexName = array_get($definition, 'index')) { if ($indexName === true) { $indexName = sprintf("%s_%s_index", $this->table, $name); } - $this->assertTrue($table->hasIndex($indexName)); + $this->assertTrue($schemaBuilder->hasIndex($this->table, $indexName)); - if ($indexName === 'morphable_index') { - $index = $table->getIndex($indexName); - $this->assertTrue(in_array($name, $index->getColumns())); + if ($indexColumns = array_get($indexName, $tableIndexes)) { + $this->assertTrue(in_array($name, $indexColumns)); } } - $this->assertEquals(array_get($definition, 'required', false), $table->getColumn($name)->getNotnull()); } $migration->down(); diff --git a/modules/system/tests/console/WinterUtilTest.php b/modules/system/tests/console/WinterUtilTest.php index f604ba9439..f0ecb84d8b 100644 --- a/modules/system/tests/console/WinterUtilTest.php +++ b/modules/system/tests/console/WinterUtilTest.php @@ -94,5 +94,7 @@ protected function tearDown(): void } $this->artisan('winter:util compile lang')->execute(); + + parent::tearDown(); } } diff --git a/modules/system/tests/console/asset/mix/MixCreateTest.php b/modules/system/tests/console/asset/mix/MixCreateTest.php index f9fcb1f747..5247147269 100644 --- a/modules/system/tests/console/asset/mix/MixCreateTest.php +++ b/modules/system/tests/console/asset/mix/MixCreateTest.php @@ -230,5 +230,7 @@ public function tearDown(): void File::delete($file); } } + + parent::tearDown(); } } diff --git a/modules/system/tests/console/asset/mix/MixInstallTest.php b/modules/system/tests/console/asset/mix/MixInstallTest.php index 1494610628..9127977153 100644 --- a/modules/system/tests/console/asset/mix/MixInstallTest.php +++ b/modules/system/tests/console/asset/mix/MixInstallTest.php @@ -18,16 +18,16 @@ public function setUp(): void { parent::setUp(); - if (!File::exists(base_path('node_modules'))) { - $this->markTestSkipped('This test requires node_modules to be installed'); - } - // Define some helpful paths $this->fixturePath = base_path('modules/system/tests'); $this->jsonPath = $this->fixturePath . '/package.json'; $this->lockPath = $this->fixturePath . '/package-lock.json'; $this->backupPath = $this->fixturePath . '/package-testing.json'; + if (!File::exists(base_path('node_modules'))) { + $this->markTestSkipped('This test requires node_modules to be installed'); + } + // Add our testing theme because it won't be auto discovered PackageManager::instance()->registerPackage( 'theme-assettest', diff --git a/modules/system/tests/console/asset/npm/NpmInstallTest.php b/modules/system/tests/console/asset/npm/NpmInstallTest.php index 611ab20df7..8b28c57894 100644 --- a/modules/system/tests/console/asset/npm/NpmInstallTest.php +++ b/modules/system/tests/console/asset/npm/NpmInstallTest.php @@ -20,16 +20,16 @@ public function setUp(): void { parent::setUp(); - if (!File::exists(base_path('node_modules'))) { - $this->markTestSkipped('This test requires node_modules to be installed'); - } - // Define some helpful paths $this->themePath = base_path('modules/system/tests/fixtures/themes/npmtest'); $this->jsonPath = $this->themePath . '/package.json'; $this->lockPath = $this->themePath . '/package-lock.json'; $this->backupPath = $this->themePath . '/package.backup.json'; + if (!File::exists(base_path('node_modules'))) { + $this->markTestSkipped('This test requires node_modules to be installed'); + } + // Add our testing theme because it won't be auto discovered PackageManager::instance()->registerPackage( 'theme-npmtest', diff --git a/modules/system/tests/console/asset/npm/NpmUpdateTest.php b/modules/system/tests/console/asset/npm/NpmUpdateTest.php index 38b63d9827..7a509c2847 100644 --- a/modules/system/tests/console/asset/npm/NpmUpdateTest.php +++ b/modules/system/tests/console/asset/npm/NpmUpdateTest.php @@ -20,16 +20,16 @@ public function setUp(): void { parent::setUp(); - if (!File::exists(base_path('node_modules'))) { - $this->markTestSkipped('This test requires node_modules to be installed'); - } - // Define some helpful paths $this->themePath = base_path('modules/system/tests/fixtures/themes/npmtest'); $this->jsonPath = $this->themePath . '/package.json'; $this->lockPath = $this->themePath . '/package-lock.json'; $this->backupPath = $this->themePath . '/package.backup.json'; + if (!File::exists(base_path('node_modules'))) { + $this->markTestSkipped('This test requires node_modules to be installed'); + } + // Add our testing theme because it won't be auto discovered PackageManager::instance()->registerPackage( 'theme-npmtest', diff --git a/modules/system/tests/console/asset/vite/ViteCreateTest.php b/modules/system/tests/console/asset/vite/ViteCreateTest.php index cdcfc855ae..db09c2f5b8 100644 --- a/modules/system/tests/console/asset/vite/ViteCreateTest.php +++ b/modules/system/tests/console/asset/vite/ViteCreateTest.php @@ -230,5 +230,7 @@ public function tearDown(): void File::delete($file); } } + + parent::tearDown(); } } diff --git a/modules/system/tests/console/asset/vite/ViteInstallTest.php b/modules/system/tests/console/asset/vite/ViteInstallTest.php index 7d97ccf107..e2138116bc 100644 --- a/modules/system/tests/console/asset/vite/ViteInstallTest.php +++ b/modules/system/tests/console/asset/vite/ViteInstallTest.php @@ -12,21 +12,22 @@ class ViteInstallTest extends TestCase protected string $jsonPath; protected string $lockPath; protected string $backupPath; + protected string $fixturePath; public function setUp(): void { parent::setUp(); - if (!File::exists(base_path('node_modules'))) { - $this->markTestSkipped('This test requires node_modules to be installed'); - } - // Define some helpful paths $this->fixturePath = base_path('modules/system/tests'); $this->jsonPath = $this->fixturePath . '/package.json'; $this->lockPath = $this->fixturePath . '/package-lock.json'; $this->backupPath = $this->fixturePath . '/package-testing.json'; + if (!File::exists(base_path('node_modules'))) { + $this->markTestSkipped('This test requires node_modules to be installed'); + } + // Add our testing theme because it won't be auto discovered PackageManager::instance()->registerPackage( 'theme-assettest', diff --git a/modules/system/tests/fixtures/plugins/winter/tester/components/Comments.php b/modules/system/tests/fixtures/plugins/winter/tester/components/Comments.php index ab22ce6d41..f0b0e29d31 100644 --- a/modules/system/tests/fixtures/plugins/winter/tester/components/Comments.php +++ b/modules/system/tests/fixtures/plugins/winter/tester/components/Comments.php @@ -8,7 +8,7 @@ class Comments extends ComponentBase { private $users; - public function __construct(CodeBase $cmsObject = null, $properties = [], Users $users = null) + public function __construct(Users $users, ?CodeBase $cmsObject = null, $properties = []) { parent::__construct($cmsObject, $properties); $this->users = $users; diff --git a/phpunit.xml b/phpunit.xml index 72f7f52986..a53e3ba0c2 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,39 +1,34 @@ - - - ./modules/ - - - ./modules/backend/routes.php - ./modules/cms/routes.php - ./modules/system/routes.php - ./modules/backend/database - ./modules/cms/database - ./modules/system/database - - - - - ./modules/system - ./modules/cms - ./modules/backend - - - - - - - + + + ./modules/system + ./modules/cms + ./modules/backend + + + + + + + + + + + ./modules/ + + + ./modules/backend/routes.php + ./modules/cms/routes.php + ./modules/system/routes.php + ./modules/backend/database + ./modules/cms/database + ./modules/system/database + +