diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df04d93d..8e920a80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,3 +85,18 @@ jobs: - name: Run PHPStan run: vendor/bin/phpstan analyse -c phpstan.neon --error-format=checkstyle | cs2pr + + - name: Setup rector cache + uses: actions/cache@v5 + with: + path: ${{ runner.temp }}/rector + key: ${{ runner.os }}-rector-${{ hashFiles('rector.php', 'composer.lock') }} + restore-keys: ${{ runner.os }}-rector- + + - name: Create rector cache dir + run: mkdir -p ${{ runner.temp }}/rector + + - name: Run rector + env: + RECTOR_CACHE_DIR: ${{ runner.temp }}/rector + run: composer rector-setup && composer rector-check diff --git a/composer.json b/composer.json index 8b10a1ac..95f67662 100644 --- a/composer.json +++ b/composer.json @@ -64,6 +64,9 @@ "annotate": "bin/cake annotate all", "illuminate": "bin/cake illuminate code", "phpstan": "phpstan analyse", - "phpstan-baseline": "phpstan --generate-baseline" + "phpstan-baseline": "phpstan --generate-baseline", + "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.3.1\" && mv composer.backup composer.json", + "rector-check": "vendor/bin/rector process --dry-run", + "rector-fix": "vendor/bin/rector process" } } diff --git a/composer.lock b/composer.lock index 251d42d3..81652b56 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b3630e9577c98cfef62a1adceda3fcb2", + "content-hash": "4b66bd69fb9bc41e261f452a283b1fa7", "packages": [ { "name": "admad/cakephp-social-auth", @@ -4464,6 +4464,66 @@ ], "time": "2025-08-19T18:57:03+00:00" }, + { + "name": "rector/rector", + "version": "2.3.9", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "917842143fd9f5331a2adefc214b8d7143bd32c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/917842143fd9f5331a2adefc214b8d7143bd32c4", + "reference": "917842143fd9f5331a2adefc214b8d7143bd32c4", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "phpstan/phpstan": "^2.1.40" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "homepage": "https://getrector.com/", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/2.3.9" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2026-03-16T09:43:55+00:00" + }, { "name": "sebastian/cli-parser", "version": "4.2.0", diff --git a/config/app.php b/config/app.php index 766524a9..d56227fa 100644 --- a/config/app.php +++ b/config/app.php @@ -119,7 +119,7 @@ 'default' => [ 'className' => FileEngine::class, 'path' => CACHE, - 'url' => env('CACHE_DEFAULT_URL', null), + 'url' => env('CACHE_DEFAULT_URL'), ], /* @@ -134,7 +134,7 @@ 'path' => CACHE . 'persistent' . DS, 'serialize' => true, 'duration' => '+1 years', - 'url' => env('CACHE_CAKECORE_URL', null), + 'url' => env('CACHE_CAKECORE_URL'), ], /* @@ -149,7 +149,7 @@ 'path' => CACHE . 'models' . DS, 'serialize' => true, 'duration' => '+1 years', - 'url' => env('CACHE_CAKEMODEL_URL', null), + 'url' => env('CACHE_CAKEMODEL_URL'), ], ], @@ -250,7 +250,7 @@ //'password' => null, 'client' => null, 'tls' => false, - 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), + 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL'), ], ], @@ -364,7 +364,7 @@ 'className' => FileLog::class, 'path' => LOGS, 'file' => 'debug', - 'url' => env('LOG_DEBUG_URL', null), + 'url' => env('LOG_DEBUG_URL'), 'scopes' => null, 'levels' => ['notice', 'info', 'debug'], ], @@ -372,7 +372,7 @@ 'className' => FileLog::class, 'path' => LOGS, 'file' => 'error', - 'url' => env('LOG_ERROR_URL', null), + 'url' => env('LOG_ERROR_URL'), 'scopes' => null, 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], ], @@ -381,7 +381,7 @@ 'className' => FileLog::class, 'path' => LOGS, 'file' => 'queries', - 'url' => env('LOG_QUERIES_URL', null), + 'url' => env('LOG_QUERIES_URL'), 'scopes' => ['cake.database.queries'], ], ], @@ -459,7 +459,7 @@ */ 'DebugKit' => [ 'forceEnable' => filter_var(env('DEBUG_KIT_FORCE_ENABLE', false), FILTER_VALIDATE_BOOLEAN), - 'safeTld' => env('DEBUG_KIT_SAFE_TLD', null), + 'safeTld' => env('DEBUG_KIT_SAFE_TLD'), 'ignoreAuthorization' => env('DEBUG_KIT_IGNORE_AUTHORIZATION', false), ], diff --git a/config/app_local.example.php b/config/app_local.example.php index b4fd7ddc..bcc09034 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -60,7 +60,7 @@ /* * You can use a DSN string to set the entire configuration */ - 'url' => env('DATABASE_URL', null), + 'url' => env('DATABASE_URL'), ], /* @@ -91,7 +91,7 @@ 'username' => null, 'password' => null, 'client' => null, - 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null), + 'url' => env('EMAIL_TRANSPORT_DEFAULT_URL'), ], ], ]; diff --git a/config/bootstrap.php b/config/bootstrap.php index 103c9ec5..d3198001 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -194,12 +194,12 @@ * If you don't use these checks you can safely remove this code * and the mobiledetect package from composer.json. */ -ServerRequest::addDetector('mobile', function ($request) { +ServerRequest::addDetector('mobile', function ($request): bool { $detector = new MobileDetect(); return $detector->isMobile(); }); -ServerRequest::addDetector('tablet', function ($request) { +ServerRequest::addDetector('tablet', function ($request): bool { $detector = new MobileDetect(); return $detector->isTablet(); diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..d922060a --- /dev/null +++ b/rector.php @@ -0,0 +1,70 @@ +withPaths([ + __DIR__ . '/config', + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + + ->withCache( + cacheClass: FileCacheStorage::class, + cacheDirectory: $cacheDir, + ) + + ->withPhpSets() + ->withAttributesSets() + + ->withSets([ + SetList::CODE_QUALITY, + SetList::CODING_STYLE, + SetList::DEAD_CODE, + SetList::EARLY_RETURN, + SetList::INSTANCEOF, + SetList::TYPE_DECLARATION, + ]) + + ->withSkip([ + ClassPropertyAssignToConstructorPromotionRector::class, + CatchExceptionNameMatchingTypeRector::class, + ClosureToArrowFunctionRector::class, + RemoveUselessReturnTagRector::class, + ReturnTypeFromStrictFluentReturnRector::class, + NewlineAfterStatementRector::class, + StringClassNameToClassConstantRector::class, + ReturnTypeFromStrictTypedCallRector::class, + ParamTypeByMethodCallTypeRector::class, + CompactToVariablesRector::class, + SplitDoubleAssignRector::class, + ChangeOrIfContinueToMultiContinueRector::class, + ExplicitBoolCompareRector::class, + NewlineBeforeNewAssignSetRector::class, + DisallowedEmptyRuleFixerRector::class, + RemoveUselessParamTagRector::class, + ]); diff --git a/src/Application.php b/src/Application.php index 52cdbb5b..159af708 100644 --- a/src/Application.php +++ b/src/Application.php @@ -152,7 +152,6 @@ public function services(ContainerInterface $container): void } /** - * @param \Psr\Http\Message\ServerRequestInterface $request * @return \Authentication\AuthenticationServiceInterface */ public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface diff --git a/src/Command/CleanCommand.php b/src/Command/CleanCommand.php index b8af682d..f8088fe9 100644 --- a/src/Command/CleanCommand.php +++ b/src/Command/CleanCommand.php @@ -14,8 +14,6 @@ class CleanCommand extends Command { /** * The name of this command. - * - * @var string */ protected string $name = 'clean'; @@ -44,9 +42,9 @@ public static function getDescription(): string * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io - * @return int|null|void The exit code or null for success + * @return void The exit code or null for success */ - public function execute(Arguments $args, ConsoleIo $io) + public function execute(Arguments $args, ConsoleIo $io): void { $confirmation = $io->ask( 'Are you sure you want to clean all packages data and related tags? This action cannot be undone. (yes/no)', diff --git a/src/Command/DevserverCommand.php b/src/Command/DevserverCommand.php index 31a6b2e7..9d1c32a6 100644 --- a/src/Command/DevserverCommand.php +++ b/src/Command/DevserverCommand.php @@ -17,8 +17,6 @@ class DevserverCommand extends Command { /** * The name of this command. - * - * @var string */ protected string $name = 'devserver'; @@ -49,7 +47,7 @@ public static function getDescription(): string * @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined * @return \Cake\Console\ConsoleOptionParser The built parser. */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { return parent::buildOptionParser($parser) ->setDescription(static::getDescription()); @@ -102,10 +100,10 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if (!$process->isRunning()) { $poll = false; $exitCode = $process->getExitCode(); - $io->error("$name has died with code $exitCode."); + $io->error(sprintf('%s has died with code %s.', $name, $exitCode)); $errorOutput = trim($process->getErrorOutput()); if ($errorOutput !== '') { - $io->error("$name | $errorOutput"); + $io->error(sprintf('%s | %s', $name, $errorOutput)); } break; } @@ -115,7 +113,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if (!empty($output)) { foreach (explode("\n", trim($output)) as $line) { if ($line !== '') { - $io->info("$name | $line"); + $io->info(sprintf('%s | %s', $name, $line)); } } } @@ -124,7 +122,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if (!empty($error)) { foreach (explode("\n", trim($error)) as $line) { if ($line !== '') { - $io->comment("$name | $line"); + $io->comment(sprintf('%s | %s', $name, $line)); } } } @@ -135,7 +133,6 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $io->verbose('Start shutdown'); foreach ($servers as $server) { - /** @var \Symfony\Component\Process\Process $process */ $process = $server['process']; if ($process->isRunning()) { $process->stop(1); // graceful timeout of 1 second diff --git a/src/Command/SyncPackagesCommand.php b/src/Command/SyncPackagesCommand.php index 31d002d6..22926764 100644 --- a/src/Command/SyncPackagesCommand.php +++ b/src/Command/SyncPackagesCommand.php @@ -47,12 +47,10 @@ class SyncPackagesCommand extends Command '5' => [0, 1, 2, 3], ]; - private Client $client; + private readonly Client $client; /** * The name of this command. - * - * @var string */ protected string $name = 'sync_packages'; @@ -91,9 +89,9 @@ public function __construct( * * @param \Cake\Console\Arguments $args The command arguments. * @param \Cake\Console\ConsoleIo $io The console io - * @return int|null|void The exit code or null for success + * @return void The exit code or null for success */ - public function execute(Arguments $args, ConsoleIo $io) + public function execute(Arguments $args, ConsoleIo $io): void { $packagesTable = $this->fetchTable('Packages'); $touchedIds = []; @@ -141,7 +139,6 @@ public function execute(Arguments $args, ConsoleIo $io) } /** - * @param string $packageName * @return array{package: string, description: string, repo_url: string, downloads: int, stars: int, tag_list: array, latest_stable_version: ?string, latest_stable_release_date: ?\Cake\I18n\Date, is_abandoned: bool} */ private function getDataForPackage(string $packageName): array @@ -189,9 +186,9 @@ private function getDataForPackage(string $packageName): array $stableVersions = array_filter( $versions, - fn(Version $version) => preg_match('/^v?\d+\.\d+(\.\d+)?$/', $version->getVersion()), + fn(Version $version): int|false => preg_match('/^v?\d+\.\d+(\.\d+)?$/', $version->getVersion()), ); - usort($stableVersions, function ($a, $b) { + usort($stableVersions, function ($a, $b): int { return version_compare($a->getVersion(), $b->getVersion()); }); /** @var \Packagist\Api\Result\Package\Version|false $latestStable */ @@ -211,12 +208,11 @@ private function getDataForPackage(string $packageName): array } /** - * @param \Packagist\Api\Result\Package\Version|null $version * @return \Cake\I18n\Date|null */ private function extractReleaseDate(?Version $version): ?Date { - if (!$version || $version->getTime() === '') { + if (!$version instanceof Version || $version->getTime() === '') { return null; } @@ -268,8 +264,6 @@ private function appendVersionTags( } /** - * @param string $leftConstraint - * @param string $rightConstraint * @return bool */ private function constraintsIntersect(string $leftConstraint, string $rightConstraint): bool diff --git a/src/Console/Installer.php b/src/Console/Installer.php index 6ef3c2dd..8f4b236a 100644 --- a/src/Console/Installer.php +++ b/src/Console/Installer.php @@ -121,7 +121,7 @@ public static function setFolderPermissions(string $dir, IOInterface $io): void // ask if the permissions should be changed if ($io->isInteractive()) { $validator = function (string $arg): string { - if (in_array($arg, ['Y', 'y', 'N', 'n'])) { + if (in_array($arg, ['Y', 'y', 'N', 'n'], true)) { return $arg; } throw new Exception('This is not a valid answer. Please choose Y or n.'); @@ -142,7 +142,7 @@ public static function setFolderPermissions(string $dir, IOInterface $io): void $changePerms = function (string $path) use ($io): void { $currentPerms = fileperms($path) & 0777; $worldWritable = $currentPerms | 0007; - if ($worldWritable == $currentPerms) { + if ($worldWritable === $currentPerms) { return; } @@ -207,7 +207,7 @@ public static function setSecuritySaltInFile(string $dir, IOInterface $io, strin $content = str_replace('__SALT__', $newKey, $content, $count); - if ($count == 0) { + if ($count === 0) { $io->write('No Security.salt placeholder to replace.'); return; @@ -243,7 +243,7 @@ public static function setAppNameInFile(string $dir, IOInterface $io, string $ap $content = str_replace('__APP_NAME__', $appName, $content, $count); - if ($count == 0) { + if ($count === 0) { $io->write('No __APP_NAME__ placeholder to replace.'); return; diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php index f4416c8c..995313aa 100644 --- a/src/Controller/ErrorController.php +++ b/src/Controller/ErrorController.php @@ -53,7 +53,7 @@ public function beforeFilter(EventInterface $event) * @param \Cake\Event\EventInterface<\Cake\Controller\Controller> $event Event. * @return void */ - public function beforeRender(EventInterface $event) + public function beforeRender(EventInterface $event): void { parent::beforeRender($event); diff --git a/src/Controller/PackagesController.php b/src/Controller/PackagesController.php index ec2acfc0..88f88426 100644 --- a/src/Controller/PackagesController.php +++ b/src/Controller/PackagesController.php @@ -27,9 +27,9 @@ public function initialize(): void /** * Index method * - * @return \Cake\Http\Response|null|void Renders view + * @return \Cake\Http\Response Renders view */ - public function index() + public function index(): Response { // Add default sort if no sort is provided $queryParams = $this->request->getQueryParams(); @@ -97,6 +97,8 @@ public function index() $phpTags = $this->sortVersionTags($phpTags, 'PHP'); $this->set(compact('featuredPackages', 'packages', 'cakephpTags', 'phpTags')); + + return $this->render(); } /** @@ -148,7 +150,6 @@ public function autocomplete(): Response } /** - * @param mixed $value * @return bool */ protected function hasActiveFilterValue(mixed $value): bool @@ -175,8 +176,6 @@ protected function hasActiveFilterValue(mixed $value): bool } /** - * @param array $tags - * @param string $prefix * @return array */ protected function sortVersionTags(array $tags, string $prefix): array diff --git a/src/Controller/PagesController.php b/src/Controller/PagesController.php index 0834c3b6..7d370f7c 100644 --- a/src/Controller/PagesController.php +++ b/src/Controller/PagesController.php @@ -79,7 +79,7 @@ public function display(string ...$path): ?Response if (Configure::read('debug')) { throw $exception; } - throw new NotFoundException(); + throw new NotFoundException($exception->getMessage(), $exception->getCode(), $exception); } } } diff --git a/src/Event/AfterGithubIdentify.php b/src/Event/AfterGithubIdentify.php index a261818d..4038f587 100644 --- a/src/Event/AfterGithubIdentify.php +++ b/src/Event/AfterGithubIdentify.php @@ -43,7 +43,7 @@ public function afterIdentify(EventInterface $event, User $user): void $http = new Client([ 'headers' => [ - 'Authorization' => "Bearer {$token}", + 'Authorization' => 'Bearer ' . $token, 'Accept' => 'application/vnd.github+json', 'User-Agent' => 'plugins.cakephp.org', ], diff --git a/src/Model/Entity/Package.php b/src/Model/Entity/Package.php index d52098ac..5357ec85 100644 --- a/src/Model/Entity/Package.php +++ b/src/Model/Entity/Package.php @@ -4,6 +4,7 @@ namespace App\Model\Entity; use Cake\ORM\Entity; +use Tags\Model\Entity\Tag; /** * Package Entity @@ -78,19 +79,17 @@ protected function _getPhpTagGroups(): array } /** - * @param string $prefix * @return array<\Tags\Model\Entity\Tag> */ protected function extractVersionTags(string $prefix): array { - return array_filter($this->tags, static function ($tag) use ($prefix) { + return array_filter($this->tags, static function (Tag $tag) use ($prefix): bool { return str_starts_with($tag->label, $prefix . ':'); }); } /** * @param array<\Tags\Model\Entity\Tag> $tags - * @param string $prefix * @return array> */ protected function groupVersionTags(array $tags, string $prefix): array @@ -125,17 +124,30 @@ protected function groupVersionTags(array $tags, string $prefix): array } public const FIELD_ID = 'id'; + public const FIELD_PACKAGE = 'package'; + public const FIELD_DESCRIPTION = 'description'; + public const FIELD_REPO_URL = 'repo_url'; + public const FIELD_DOWNLOADS = 'downloads'; + public const FIELD_STARS = 'stars'; + public const FIELD_LATEST_STABLE_VERSION = 'latest_stable_version'; + public const FIELD_LATEST_STABLE_RELEASE_DATE = 'latest_stable_release_date'; + public const FIELD_CAKE_PHP_TAGS = 'cake_php_tags'; + public const FIELD_CAKE_PHP_TAG_GROUPS = 'cake_php_tag_groups'; + public const FIELD_PHP_TAGS = 'php_tags'; + public const FIELD_PHP_TAG_GROUPS = 'php_tag_groups'; + public const FIELD_TAGGED = 'tagged'; + public const FIELD_TAGS = 'tags'; } diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index 27135cd7..44dd9c32 100644 --- a/src/Model/Entity/User.php +++ b/src/Model/Entity/User.php @@ -36,12 +36,20 @@ class User extends Entity ]; public const FIELD_ID = 'id'; + public const FIELD_FIRST_NAME = 'first_name'; + public const FIELD_LAST_NAME = 'last_name'; + public const FIELD_EMAIL = 'email'; + public const FIELD_USERNAME = 'username'; + public const FIELD_CREATED = 'created'; + public const FIELD_MODIFIED = 'modified'; + public const FIELD_SOCIAL_PROFILE = 'social_profile'; + public const FIELD_IS_CAKEPHP_DEV = 'is_cakephp_dev'; } diff --git a/src/Model/Filter/PackagesCollection.php b/src/Model/Filter/PackagesCollection.php index 7113e8a3..d5e324fc 100644 --- a/src/Model/Filter/PackagesCollection.php +++ b/src/Model/Filter/PackagesCollection.php @@ -28,7 +28,6 @@ public function initialize(): void } /** - * @param string $filterName * @return void */ protected function addTaggedSlugFilter(string $filterName): void diff --git a/src/View/AjaxView.php b/src/View/AjaxView.php index c37f3184..e5cfbdbb 100644 --- a/src/View/AjaxView.php +++ b/src/View/AjaxView.php @@ -27,8 +27,6 @@ class AjaxView extends AppView * The name of the layout file to render the view inside of. The name * specified is the filename of the layout in /templates/Layout without * the .php extension. - * - * @var string */ protected string $layout = 'ajax'; diff --git a/tests/TestCase/ApplicationTest.php b/tests/TestCase/ApplicationTest.php index 6a65c52b..3ec6acf7 100644 --- a/tests/TestCase/ApplicationTest.php +++ b/tests/TestCase/ApplicationTest.php @@ -37,7 +37,7 @@ class ApplicationTest extends TestCase * * @return void */ - public function testBootstrap() + public function testBootstrap(): void { Configure::write('debug', false); $app = new Application(dirname(__DIR__, 2) . '/config'); @@ -54,7 +54,7 @@ public function testBootstrap() * * @return void */ - public function testBootstrapInDebug() + public function testBootstrapInDebug(): void { Configure::write('debug', true); $app = new Application(dirname(__DIR__, 2) . '/config'); @@ -69,7 +69,7 @@ public function testBootstrapInDebug() * * @return void */ - public function testMiddleware() + public function testMiddleware(): void { $app = new Application(dirname(__DIR__, 2) . '/config'); $middleware = new MiddlewareQueue(); diff --git a/tests/TestCase/Command/SyncPackagesCommandTest.php b/tests/TestCase/Command/SyncPackagesCommandTest.php index 6efa07d4..8202509c 100644 --- a/tests/TestCase/Command/SyncPackagesCommandTest.php +++ b/tests/TestCase/Command/SyncPackagesCommandTest.php @@ -26,7 +26,6 @@ public function testHasExplicitCakePhpDependency(): void { $command = new SyncPackagesCommand(); $method = new ReflectionMethod($command, 'hasExplicitCakePhpDependency'); - $method->setAccessible(true); $this->assertTrue($method->invoke($command, ['PHP: 8.2', 'CakePHP: 5.0'])); $this->assertFalse($method->invoke($command, ['PHP: 8.2'])); @@ -39,7 +38,6 @@ public function testExtractReleaseDate(): void { $command = new SyncPackagesCommand(); $method = new ReflectionMethod($command, 'extractReleaseDate'); - $method->setAccessible(true); $version = new Version(); $version->fromArray([