diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5d31433307..e0c2ad4a0b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: php_version: '8.4' craft_version: '6' node_version: '22' - jobs: '["ecs", "prettier", "phpstan", "tests", "rector"]' + jobs: '["ecs", "prettier", "phpstan", "tests"]' working_directory: 'yii2-adapter' notify_slack: true slack_subteam: diff --git a/.github/workflows/laravel-ci.yml b/.github/workflows/laravel-ci.yml index 8c032095049..9c2b7d74e7d 100644 --- a/.github/workflows/laravel-ci.yml +++ b/.github/workflows/laravel-ci.yml @@ -34,6 +34,44 @@ jobs: - name: Run Pint run: pint --parallel --test --verbose + rector: + name: 'Code Quality / Rector' + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + coverage: none + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPOSER_AUTH_JSON: | + { + "http-basic": { + "repo.packagist.com": { + "username": "${{ secrets.packagist_username }}", + "password": "${{ secrets.packagist_token }}" + } + } + } + + - name: Set version + run: composer config version "6.x-dev" + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run Rector + run: vendor/bin/rector process --dry-run --ansi + phpstan: name: 'Code Quality / Phpstan' runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index e554f26ac79..c89253974e3 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,10 @@ "psr-4": { "CraftCms\\Cms\\Tests\\": "tests/", "CraftCms\\Cms\\Database\\": "database/", - "CraftCms\\Yii2Adapter\\Tests\\": "yii2-adapter/tests-laravel/" + "CraftCms\\Yii2Adapter\\Tests\\": "yii2-adapter/tests-laravel/", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" } }, "scripts": { @@ -118,7 +121,19 @@ "phpstan": "phpstan --memory-limit=1G", "rector": "rector", "tests": "./vendor/bin/pest --compact", - "tests-adapter": "./vendor/bin/pest --configuration ./yii2-adapter/phpunit.xml.dist --test-directory ./yii2-adapter/tests-laravel" + "tests-adapter": "./vendor/bin/pest --configuration ./yii2-adapter/phpunit.xml.dist --test-directory ./yii2-adapter/tests-laravel", + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve --ansi" + ] }, "config": { "sort-packages": true, diff --git a/src/Database/Migrations/Install.php b/src/Database/Migrations/Install.php index 0770a9fdca3..ac7a7e93d3e 100644 --- a/src/Database/Migrations/Install.php +++ b/src/Database/Migrations/Install.php @@ -488,6 +488,7 @@ public function createTables(): void $table->char('uid', 36)->default('0'); }); + Schema::dropIfExists(Table::MIGRATIONS); app(Migrator::class) ->getRepository() ->createRepository(); diff --git a/src/Http/Middleware/HandleInertiaRequests.php b/src/Http/Middleware/HandleInertiaRequests.php index cca6c5366b6..2728c5785e3 100644 --- a/src/Http/Middleware/HandleInertiaRequests.php +++ b/src/Http/Middleware/HandleInertiaRequests.php @@ -16,6 +16,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Inertia\Middleware; +use Override; class HandleInertiaRequests extends Middleware { @@ -33,7 +34,7 @@ class HandleInertiaRequests extends Middleware * * @see https://inertiajs.com/asset-versioning */ - #[\Override] + #[Override] public function version(Request $request): ?string { return parent::version($request); @@ -46,15 +47,20 @@ public function version(Request $request): ?string * * @return array */ - #[\Override] + #[Override] public function share(Request $request): array { - $currentSite = Sites::getCurrentSite(); $isInstalled = Cms::isInstalled(); + + if (! $isInstalled) { + return parent::share($request); + } + + $currentSite = Sites::getCurrentSite(); $updates = app(Updates::class); $nav = app(Navigation::class); - if ($isInstalled && ! $updates->isCraftUpdatePending()) { + if (! $updates->isCraftUpdatePending()) { $currentUser = Craft::$app->getUser()->getIdentity(); if (! $currentUser) { diff --git a/src/Translation/I18N.php b/src/Translation/I18N.php index ed5699d96d6..9a7ae35287e 100644 --- a/src/Translation/I18N.php +++ b/src/Translation/I18N.php @@ -295,7 +295,14 @@ public function translate(string|Stringable $message, array $parameters = [], ?s * Translate it using Laravel's translations. */ if ($translation === (string) $message) { - return __($message, $parameters, $locale); + $result = __($message, $parameters, $locale); + + // We're dealing with a message that's equal to a translation file (for example 'site') + if (is_array($result)) { + return $translation; + } + + return $result; } return $translation; diff --git a/testbench.yaml b/testbench.yaml index 6c968b2d7a0..2a25bb456a6 100644 --- a/testbench.yaml +++ b/testbench.yaml @@ -7,3 +7,27 @@ providers: - CraftCms\Cms\Providers\CraftServiceProvider - CraftCms\Yii2Adapter\Yii2ServiceProvider - Laravel\Wayfinder\WayfinderServiceProvider + +seeders: + - Workbench\Database\Seeders\DatabaseSeeder + +workbench: + welcome: true + start: /admin + install: false # We run the Craft install in DatabaseSeeder + user: 1 + guard: craft + sync: + - from: resources + to: public/vendor/craft + - from: storage + to: workbench/storage + reverse: true + build: + - migrate-fresh + discovers: + web: true + commands: true + config: true + factories: true + seeders: true diff --git a/workbench/.gitignore b/workbench/.gitignore new file mode 100644 index 00000000000..3badc3a0461 --- /dev/null +++ b/workbench/.gitignore @@ -0,0 +1,3 @@ +.env +.env.dusk +storage diff --git a/workbench/app/Models/.gitkeep b/workbench/app/Models/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php new file mode 100644 index 00000000000..85478feecb3 --- /dev/null +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -0,0 +1,27 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + ) + ->withMiddleware(function (Middleware $middleware): void {}) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); diff --git a/workbench/bootstrap/providers.php b/workbench/bootstrap/providers.php new file mode 100644 index 00000000000..3ac44ad1013 --- /dev/null +++ b/workbench/bootstrap/providers.php @@ -0,0 +1,5 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | 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 + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | 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 + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'), + + 'asset_url' => env('ASSET_URL'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the default one + | is not available. You may change the value to correspond to any of + | the languages which are currently supported by your application. + | + */ + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Faker Locale + |-------------------------------------------------------------------------- + | + | This locale will be used by the Faker PHP library when generating fake + | data for your database seeds. For example, this will be used to get + | localized telephone numbers, street address information and more. + | + */ + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on any + | requests to your application. You may add your own services to the + | arrays below to provide additional features to this application. + | + */ + + 'providers' => ServiceProvider::defaultProviders()->merge([ + // Package Service Providers... + ])->merge([ + // Application Service Providers... + // App\Providers\AppServiceProvider::class, + ])->merge([ + // Added Service Providers (Do not remove this line)... + ])->toArray(), + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. You may add any additional class aliases which should + | be loaded to the array. For speed, all aliases are lazy loaded. + | + */ + + 'aliases' => Facade::defaultAliases()->merge([ + // 'Example' => App\Facades\Example::class, + ])->toArray(), + +]; diff --git a/workbench/database/factories/.gitkeep b/workbench/database/factories/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/workbench/database/migrations/.gitkeep b/workbench/database/migrations/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/workbench/database/seeders/DatabaseSeeder.php b/workbench/database/seeders/DatabaseSeeder.php new file mode 100644 index 00000000000..23c42a0763d --- /dev/null +++ b/workbench/database/seeders/DatabaseSeeder.php @@ -0,0 +1,139 @@ +input = new ArrayInput([]); + $this->output = new OutputStyle($this->input, new ConsoleOutput); + $this->components = new Factory($this->output); + } + + /** + * Seed the application's database. + */ + public function run(): void + { + Context::forgetHidden('craft.info'); + Context::forgetHidden('craft.isInstalled'); + + File::cleanDirectory(config_path('craft/project')); + File::cleanDirectory(storage_path('runtime/compiled_classes')); + + Cache::lock(ProjectConfig::MUTEX_NAME)->forceRelease(); + + $site = new Site( + name: 'Craft test site', + handle: 'defaultSite', + language: 'en-US', + baseUrl: config('app.url'), + primary: true, + hasUrls: true, + ); + + new Install( + username: 'craftcms', + password: 'craftcms2018!!', + email: 'support@craftcms.com', + site: $site, + )->up(); + + $site = Sites::getCurrentSite(); + + $this->components->info('Creating default entry types & sections...'); + + $fieldLayout = null; + $this->components->task('Creating field layout', function () use (&$fieldLayout) { + $fieldLayout = FieldLayout::create([ + 'uid' => Str::uuid()->toString(), + 'type' => Entry::class, + 'config' => [ + 'tabs' => [ + [ + 'uid' => Str::uuid()->toString(), + 'name' => 'Content', + 'elements' => [ + [ + 'uid' => Str::uuid()->toString(), + 'type' => EntryTitleField::class, + 'required' => true, + ], + ], + ], + ], + ], + ]); + }); + + Fields::refreshFields(); + + $pageType = null; + $this->components->task('Page entry type', function () use ($fieldLayout, &$pageType) { + EntryTypes::saveEntryType($pageType = new EntryType( + fieldLayoutId: $fieldLayout->id, + name: 'Page', + handle: 'page', + )); + }); + + $this->createSection($site, 'Home', SectionType::Single, '__HOME__', [$pageType]); + $this->createSection($site, 'Pages', SectionType::Structure, '{parent.uri}/{slug}', [$pageType]); + $this->createSection($site, 'Posts', SectionType::Channel, 'blog/{slug}', [$pageType]); + } + + public function createSection(Site $site, string $title, SectionType $sectionType = SectionType::Channel, ?string $uriFormat = null, array $entryTypes = []): ?Section + { + $section = null; + + $this->components->task("{$title} section ({$sectionType->label()})", function () use ($uriFormat, $entryTypes, $sectionType, $title, $site, &$section) { + Sections::saveSection($section = new Section( + name: $title, + handle: Str::slug($title), + type: $sectionType, + siteSettings: [ + $site->id => new SectionSiteSettings( + siteId: $site->id, + hasUrls: ! is_null($uriFormat), + uriFormat: $uriFormat, + ), + ], + entryTypes: $entryTypes, + )); + }); + + return $section; + } +} diff --git a/workbench/resources/views/.gitkeep b/workbench/resources/views/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/workbench/routes/console.php b/workbench/routes/console.php new file mode 100644 index 00000000000..79297257142 --- /dev/null +++ b/workbench/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +// })->purpose('Display an inspiring quote'); diff --git a/workbench/routes/web.php b/workbench/routes/web.php new file mode 100644 index 00000000000..b3d9bbc7f37 --- /dev/null +++ b/workbench/routes/web.php @@ -0,0 +1 @@ +_illuminateRequest ??= app('request'); + $request = $this->_illuminateRequest ??= IlluminateRequest::capture(); $request->setLaravelSession(session()->driver()); diff --git a/yii2-adapter/tests/unit/helpers/UrlHelperTest.php b/yii2-adapter/tests/unit/helpers/UrlHelperTest.php index 676a7a184e1..ba5d22f4cd1 100644 --- a/yii2-adapter/tests/unit/helpers/UrlHelperTest.php +++ b/yii2-adapter/tests/unit/helpers/UrlHelperTest.php @@ -101,6 +101,8 @@ public function testIsFullUrl(bool $expected, string $url): void */ public function testCpUrlCreation(string $expected, string $path, array $params, string $scheme = 'https'): void { + Aliases::set('@web', 'http://localhost'); + $this->tester->mockCraftMethods('request', [ 'getIsSecureConnection' => false, 'getIsCpRequest' => true, diff --git a/yii2-adapter/tests/unit/web/ControllerTest.php b/yii2-adapter/tests/unit/web/ControllerTest.php index 83ad1eb2728..73a406bb998 100644 --- a/yii2-adapter/tests/unit/web/ControllerTest.php +++ b/yii2-adapter/tests/unit/web/ControllerTest.php @@ -16,6 +16,7 @@ use craft\web\TemplateResponseFormatter; use craft\web\View; use CraftCms\Cms\Cms; +use CraftCms\Cms\Support\Str; use Illuminate\Support\Facades\Crypt; use UnitTester; use yii\base\Action; @@ -113,7 +114,7 @@ public function testRedirectToPostedUrl(): void $default = $this->controller->redirectToPostedUrl(); // Test that with nothing passed in. It defaults to the base. See self::getBaseUrlForRedirect() for more info. - self::assertSame(TestSetup::SITE_URL, $default->headers->get('Location')); + self::assertSame(rtrim(TestSetup::SITE_URL, '/'), Str::before($default->headers->get('Location'), ':80')); // What happens when we pass in a param. Craft::$app->getRequest()->setBodyParams(['redirect' => $redirect]); @@ -168,7 +169,7 @@ public function testRedirect(): void self::assertSame(TestSetup::SITE_URL . 'do/stuff', $this->controller->redirect('do/stuff')->headers->get('Location')); // We dont use _getBaseUrlForRedirect because the :port80 wont work with urlWithScheme. - self::assertSame(TestSetup::SITE_URL, $this->controller->redirect(null)->headers->get('Location')); + self::assertSame(rtrim(TestSetup::SITE_URL, '/'), Str::before($this->controller->redirect(null)->headers->get('Location'), ':80')); // Absolute url self::assertSame(