diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bc277b40..21cbc9d0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,27 +18,25 @@ jobs: matrix: php: - '8.3' + - '8.4' composer_options: [ "" ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 + - uses: ibexa/gh-workflows/actions/composer-install@main with: - php-version: ${{ matrix.php }} - coverage: none - extensions: pdo_sqlite, gd - tools: cs2pr - - - uses: "ramsey/composer-install@v1" - with: - dependency-versions: "highest" - composer-options: "${{ matrix.composer_options }}" + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - name: Setup problem matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Run PHPStan analysis + run: composer run-script phpstan + - name: Run test suite run: composer run-script --timeout=600 test @@ -50,22 +48,14 @@ jobs: php: - '8.3' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - - name: Setup PHP Action - uses: shivammathur/setup-php@v2 + - uses: ibexa/gh-workflows/actions/composer-install@main with: - php-version: ${{ matrix.php }} - coverage: none - extensions: 'pdo_sqlite, gd' - tools: cs2pr - - - uses: "ramsey/composer-install@v1" - with: - dependency-versions: "highest" + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} - name: Run code style check run: composer run-script check-cs -- --format=checkstyle | cs2pr - - - name: Run PHPStan analysis - run: composer run-script phpstan diff --git a/.github/workflows/rector.yaml b/.github/workflows/rector.yaml index 1d7c4413..23360ab8 100644 --- a/.github/workflows/rector.yaml +++ b/.github/workflows/rector.yaml @@ -1,13 +1,29 @@ name: Rector PHP on: - push: - branches: - - main - - '[0-9]+.[0-9]+' - pull_request: ~ + push: + branches: + - main + - '[0-9]+.[0-9]+' + pull_request: ~ jobs: - rector: - name: Run rector - uses: ibexa/gh-workflows/.github/workflows/rector.yml@main + rector: + name: Run rector + runs-on: "ubuntu-22.04" + strategy: + matrix: + php: + - '8.3' + steps: + - uses: actions/checkout@v6 + + - uses: ibexa/gh-workflows/actions/composer-install@main + with: + gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }} + gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }} + satis-network-key: ${{ secrets.SATIS_NETWORK_KEY }} + satis-network-token: ${{ secrets.SATIS_NETWORK_TOKEN }} + + - name: Run rector + run: vendor/bin/rector process --dry-run --ansi diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a28a8041..25194721 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -726,12 +726,6 @@ parameters: count: 1 path: src/lib/API/Context/ContentTypeContext.php - - - message: '#^Parameter \#1 \$array \(list\\) of array_values is already a list, call has no effect\.$#' - identifier: arrayValues.list - count: 1 - path: src/lib/API/Context/ContentTypeContext.php - - message: '#^Method Ibexa\\Behat\\API\\Context\\LanguageContext\:\:createLanguageIfNotExists\(\) has no return type specified\.$#' identifier: missingType.return @@ -1992,11 +1986,6 @@ parameters: count: 1 path: src/lib/Browser/FileUpload/FileUploadHelper.php - - - message: '#^Parameter \#1 \$string of function base64_encode expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/lib/Browser/FileUpload/FileUploadHelper.php - message: '#^Method Ibexa\\Behat\\Browser\\Filter\\BrowserLogFilter\:\:filter\(\) has parameter \$logEntries with no value type specified in iterable type array\.$#' @@ -2562,11 +2551,6 @@ parameters: count: 1 path: src/lib/Core/Log/LogFileReader.php - - - message: '#^Cannot call method execute\(\) on Facebook\\WebDriver\\Remote\\RemoteWebDriver\|null\.$#' - identifier: method.nonObject - count: 1 - path: src/lib/Core/Log/TestLogProvider.php - message: '#^Method Ibexa\\Behat\\Core\\Log\\TestLogProvider\:\:cacheLogs\(\) has parameter \$logs with no value type specified in iterable type array\.$#' diff --git a/phpstan.neon b/phpstan.neon index 5b018e1c..aed6d297 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - - phpstan-baseline.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-symfony/extension.neon + - phpstan-baseline.neon parameters: treatPhpDocTypesAsCertain: false diff --git a/src/lib/API/ContentData/FieldTypeData/ImageAssetDataProvider.php b/src/lib/API/ContentData/FieldTypeData/ImageAssetDataProvider.php index 68196eac..37ecb249 100644 --- a/src/lib/API/ContentData/FieldTypeData/ImageAssetDataProvider.php +++ b/src/lib/API/ContentData/FieldTypeData/ImageAssetDataProvider.php @@ -94,7 +94,7 @@ public function parseFromString(string $value) $locationURL = $this->argumentParser->parseUrl($value); $urlAlias = $this->urlAliasService->lookup($locationURL); - $location = $this->locationService->loadLocation($urlAlias->destination); + $location = $this->locationService->loadLocation((int) $urlAlias->destination); return new Value($location->getContentInfo()->id, $this->getFaker()->realText(100)); } diff --git a/src/lib/API/ContentData/FieldTypeData/ObjectRelationDataProvider.php b/src/lib/API/ContentData/FieldTypeData/ObjectRelationDataProvider.php index df036ab4..4d2011ab 100644 --- a/src/lib/API/ContentData/FieldTypeData/ObjectRelationDataProvider.php +++ b/src/lib/API/ContentData/FieldTypeData/ObjectRelationDataProvider.php @@ -61,7 +61,7 @@ protected function getContentID(string $locationPath) $locationURL = $this->argumentParser->parseUrl($locationPath); $urlAlias = $this->urlAliasService->lookup($locationURL); - $location = $this->locationService->loadLocation($urlAlias->destination); + $location = $this->locationService->loadLocation((int) $urlAlias->destination); return $location->getContentInfo()->id; } diff --git a/src/lib/API/Context/ContentTypeContext.php b/src/lib/API/Context/ContentTypeContext.php index 3f7a00ce..f1130580 100644 --- a/src/lib/API/Context/ContentTypeContext.php +++ b/src/lib/API/Context/ContentTypeContext.php @@ -114,7 +114,7 @@ private function parseSelectionSettings(string $settings): array $fields = explode(',', $settings); $isMultiple = $this->parseBool(explode(':', $fields[0])[1]); $options = explode(':', $fields[1])[1]; - $parsedOptions = array_values(explode('-', $options)); + $parsedOptions = explode('-', $options); $parsedSettings['isMultiple'] = $isMultiple; $parsedSettings['options'] = $parsedOptions; diff --git a/src/lib/API/Facade/ContentFacade.php b/src/lib/API/Facade/ContentFacade.php index 708df14d..d729bac8 100644 --- a/src/lib/API/Facade/ContentFacade.php +++ b/src/lib/API/Facade/ContentFacade.php @@ -62,7 +62,7 @@ public function createContentDraft($contentTypeIdentifier, $parentUrl, $language Assert::assertEquals(URLAlias::LOCATION, $parentUrlAlias->type); $parentLocationId = $parentUrlAlias->destination; - $locationCreateStruct = $this->locationService->newLocationCreateStruct($parentLocationId); + $locationCreateStruct = $this->locationService->newLocationCreateStruct((int) $parentLocationId); $this->contentDataProvider->setContentTypeIdentifier($contentTypeIdentifier); @@ -99,7 +99,7 @@ public function createDraftForExistingContent($locationURL, $language, $contentI $urlAlias = $this->urlAliasService->lookup($locationURL); Assert::assertEquals(URLAlias::LOCATION, $urlAlias->type); - $location = $this->locationService->loadLocation($urlAlias->destination); + $location = $this->locationService->loadLocation((int) $urlAlias->destination); $contentDraft = $this->contentService->createContentDraft($location->getContentInfo()); $contentUpdateStruct = $this->contentService->newContentUpdateStruct(); $contentUpdateStruct->initialLanguageCode = $language; @@ -121,7 +121,7 @@ public function getLocationByLocationURL($locationURL): Location Assert::assertEquals(URLAlias::LOCATION, $urlAlias->type); return $this->repository->sudo(function () use ($urlAlias) { - return $this->locationService->loadLocation($urlAlias->destination); + return $this->locationService->loadLocation((int) $urlAlias->destination); }); } diff --git a/src/lib/API/Facade/SearchFacade.php b/src/lib/API/Facade/SearchFacade.php index 5983c3c5..991597a7 100644 --- a/src/lib/API/Facade/SearchFacade.php +++ b/src/lib/API/Facade/SearchFacade.php @@ -52,7 +52,7 @@ public function getRandomChildFromPath(string $path): string $urlAlias = $this->urlAliasService->lookup($path); Assert::assertEquals(URLAlias::LOCATION, $urlAlias->type); - $location = $this->locationService->loadLocation($urlAlias->destination); + $location = $this->locationService->loadLocation((int) $urlAlias->destination); $query = new LocationQuery(); $query->performCount = false; diff --git a/src/lib/API/Facade/TrashFacade.php b/src/lib/API/Facade/TrashFacade.php index 5e245772..7c989508 100644 --- a/src/lib/API/Facade/TrashFacade.php +++ b/src/lib/API/Facade/TrashFacade.php @@ -37,7 +37,7 @@ public function trash(string $locationURL) $urlAlias = $this->urlAliasService->lookup($locationURL); Assert::assertEquals(URLAlias::LOCATION, $urlAlias->type); - $location = $this->locationService->loadLocation($urlAlias->destination); + $location = $this->locationService->loadLocation((int) $urlAlias->destination); $this->trashService->trash($location); } diff --git a/src/lib/Browser/Component/Component.php b/src/lib/Browser/Component/Component.php index 71d6609f..7e4f0794 100644 --- a/src/lib/Browser/Component/Component.php +++ b/src/lib/Browser/Component/Component.php @@ -21,7 +21,10 @@ use Ibexa\Behat\Browser\Locator\LocatorInterface; use Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException; use OAndreyev\Mink\Driver\WebDriver; -use RuntimeException; + +class DevToolsDriverUnavailableException extends \RuntimeException +{ +} abstract class Component implements ComponentInterface { @@ -79,7 +82,11 @@ protected function getDevToolsDriver(): ChromeDevToolsDriver $webDriver = $driver->getWebDriver(); if (null === $webDriver) { - throw new RuntimeException('Error happened when accessing the WebDriver'); + throw new DevToolsDriverUnavailableException('Error happened when accessing the WebDriver'); + } + + if (!($webDriver instanceof \Facebook\WebDriver\Remote\RemoteWebDriver)) { + throw new DevToolsDriverUnavailableException('Expected instance of Facebook\\WebDriver\\Remote\\RemoteWebDriver, got ' . (is_object($webDriver) ? get_class($webDriver) : gettype($webDriver))); } return new ChromeDevToolsDriver($webDriver); diff --git a/src/lib/Browser/FileUpload/FileUploadHelper.php b/src/lib/Browser/FileUpload/FileUploadHelper.php index de5ec188..80843592 100644 --- a/src/lib/Browser/FileUpload/FileUploadHelper.php +++ b/src/lib/Browser/FileUpload/FileUploadHelper.php @@ -12,6 +12,10 @@ use Behat\Mink\Session; use FriendsOfBehat\SymfonyExtension\Mink\MinkParameters; +class FileReadException extends \RuntimeException +{ +} + class FileUploadHelper { /** @var \Behat\Mink\Session */ @@ -32,12 +36,17 @@ public function getRemoteFileUploadPath($filename) $driver = $this->session->getDriver(); if ($driver instanceof Selenium2Driver) { - if (!preg_match('#[\w\\\/\.]*\.zip$#', $filename)) { + if (!preg_match('#[\w/.]*\.zip$#', $filename)) { throw new \InvalidArgumentException('Zip archive required to upload to remote browser machine.'); } + $fileContents = file_get_contents($localFile); + if ($fileContents === false) { + throw new FileReadException("Failed to read file: $localFile"); + } + return $driver->getWebDriverSession()->file([ - 'file' => base64_encode(file_get_contents($localFile)), + 'file' => base64_encode($fileContents), ]); } diff --git a/src/lib/Core/Debug/Matcher/ObjectFunctionCallChainMatcher.php b/src/lib/Core/Debug/Matcher/ObjectFunctionCallChainMatcher.php index 2c318a36..a411851e 100644 --- a/src/lib/Core/Debug/Matcher/ObjectFunctionCallChainMatcher.php +++ b/src/lib/Core/Debug/Matcher/ObjectFunctionCallChainMatcher.php @@ -13,6 +13,12 @@ class ObjectFunctionCallChainMatcher extends ObjectMethodsMatcher { + /** + * @param list $tokens + * @param array $info + * + * @return list + */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); @@ -72,14 +78,14 @@ public function getMatches(array $tokens, array $info = []): array // 4) Provide autocomplete for the last return type found in the chain return array_map(static function (string $function) { return $function . '()'; - }, \array_filter( + }, array_values(\array_filter( \get_class_methods($object), static function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var) && // also check that we do not suggest invoking a super method(__construct, __wakeup, …) !AbstractMatcher::startsWith('__', $var); } - )); + ))); } /** diff --git a/src/lib/Core/Debug/Matcher/ThisObjectMethodsMatcher.php b/src/lib/Core/Debug/Matcher/ThisObjectMethodsMatcher.php index 1b85b8c9..42f8be6f 100644 --- a/src/lib/Core/Debug/Matcher/ThisObjectMethodsMatcher.php +++ b/src/lib/Core/Debug/Matcher/ThisObjectMethodsMatcher.php @@ -16,6 +16,12 @@ class ThisObjectMethodsMatcher extends ObjectMethodsMatcher { + /** + * @param list $tokens + * @param array $info + * + * @return list + */ public function getMatches(array $tokens, array $info = []): array { $input = $this->getInput($tokens); @@ -46,11 +52,11 @@ public function getMatches(array $tokens, array $info = []): array return array_map(static function (string $function) { return $function . '()'; - }, \array_filter( + }, array_values(\array_filter( $methods, static function ($methodName) use ($input) { return AbstractMatcher::startsWith($input, $methodName); } - )); + ))); } } diff --git a/src/lib/Core/Debug/Shell/Shell.php b/src/lib/Core/Debug/Shell/Shell.php index c8e60602..5aa2bb28 100644 --- a/src/lib/Core/Debug/Shell/Shell.php +++ b/src/lib/Core/Debug/Shell/Shell.php @@ -18,7 +18,7 @@ class Shell extends BaseShell { /** - * @return array + * @return \Psy\TabCompletion\Matcher\AbstractMatcher[] */ protected function getDefaultMatchers(): array { diff --git a/src/lib/Core/Log/TestLogProvider.php b/src/lib/Core/Log/TestLogProvider.php index 4ea95f09..dfce3c1e 100644 --- a/src/lib/Core/Log/TestLogProvider.php +++ b/src/lib/Core/Log/TestLogProvider.php @@ -9,7 +9,6 @@ namespace Ibexa\Behat\Core\Log; use Behat\Mink\Session; -use Facebook\WebDriver\Remote\DriverCommand; use Ibexa\Behat\Browser\Filter\BrowserLogFilter; use OAndreyev\Mink\Driver\WebDriver; @@ -57,7 +56,14 @@ public function getBrowserLogs(): array private function getSeleniumLog(WebDriver $driver): array { - return $driver->getWebDriver()->execute(DriverCommand::GET_LOG, ['type' => 'browser']) ?? []; + $webDriver = $driver->getWebDriver(); + if (is_object($webDriver) && method_exists($webDriver, 'getLog')) { + return $webDriver->getLog('browser') ?? []; + } + + // Log a warning or handle gracefully if getLog is not available + // error_log('WebDriver does not support getLog method.'); + return []; } public function getApplicationLogs(): array