diff --git a/.github/workflows/harness.yml b/.github/workflows/harness.yml index bcacc6be..64f0c300 100644 --- a/.github/workflows/harness.yml +++ b/.github/workflows/harness.yml @@ -14,6 +14,6 @@ permissions: {} jobs: harness: - uses: Pollora/harness/.github/workflows/php-package.yml@v0.1.15 + uses: Pollora/harness/.github/workflows/php-package.yml@v0.1.23 permissions: contents: read diff --git a/.harness.json b/.harness.json index 30298095..c67d0871 100644 --- a/.harness.json +++ b/.harness.json @@ -3,7 +3,18 @@ "mode": "enforce", "ratchet": { "phpstan_level": 5, - "phpstan_baseline": 58, - "coverage": 53 - } + "phpstan_baseline": 56, + "coverage": 53, + "exempted_rules": 2 + }, + "allow_rules": [ + { + "rule": "ARCH-01", + "reason": "Modules\\Domain and Plugin\\Domain extend Illuminate\\Support\\Collection (ModuleCollection, PluginCollection); both are published classes, so removing the dependency changes the public contract. To lift at the next major." + }, + { + "rule": "ARCH-03", + "reason": "The application layer of Modules, Plugin and Theme still builds Infrastructure services directly (registrars, managers, discovery orchestrator), and the module named Application is read as a layer. Route, Schedule and VersionCheck are fixed; the rest needs ports, one module at a time." + } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0684aa4a..f90c71f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `pollora:make:post-type` (and its `pollora:make-posttype` alias) can create the theme views of the new post type: `--single-view` writes `single-{slug}.blade.php`, `--archive-view` writes `archive-{slug}.blade.php`, `--with-views` writes both. They go to `resources/views` of the theme given with `--theme`, or of the active theme, and are named after the slug discovery registers the post type under (cut to 20 characters), so the template hierarchy renders them with no route. An existing view is left as it is, with a warning +### Changed +- The architecture rules of the Pollora harness (`tests/Arch.php`) now run in CI: `pollora/harness` is required at `^0.1.23` and the workflow calls it at `v0.1.23`. Two rules stay exempted in `.harness.json` with a reason (ARCH-01 and ARCH-03, the dependencies the Modules, Plugin and Theme application layers still have on infrastructure); the number of exempted rules is capped by the ratchet +- `PluginManager` and `ThemeManager` read the console detection service from the container they receive instead of the `resolve()` helper, and the plugin and theme modules read `VAPOR_MAINTENANCE_MODE` from the environment variables instead of `env()` outside a configuration file. Nothing changes for a site +- `WordPressTypeResolverInterface` moved to `Pollora\Route\Domain\Contracts`; the former name, `Pollora\Route\Infrastructure\Services\Contracts\WordPressTypeResolverInterface`, is an alias and keeps working +- `PackageVersionChecker` depends on the new `Pollora\VersionCheck\Domain\Contracts\VersionSourceResolver`, which `VersionSources` implements and the container binds to it + ## [v13.40.0](https://github.com/Pollora/framework/compare/v13.39.3...v13.40.0) - 2026-11-04 ### Changed diff --git a/composer.json b/composer.json index ff3f58f6..5c6a0d0e 100644 --- a/composer.json +++ b/composer.json @@ -125,7 +125,7 @@ "wp-cli/wp-cli": "^2.12", "larastan/larastan": "^3.13", "orchestra/testbench": "^11.3.0", - "pollora/harness": "^0.1.15" + "pollora/harness": "^0.1.23" }, "autoload-dev": { "psr-4": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3e78a904..b64fda12 100755 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -66,12 +66,6 @@ parameters: count: 1 path: src/Plugin/Infrastructure/Models/LaravelPluginModule.php - - - message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' - identifier: larastan.noEnvCallsOutsideOfConfig - count: 1 - path: src/Plugin/Infrastructure/Models/LaravelPluginModule.php - - message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' identifier: larastan.noEnvCallsOutsideOfConfig @@ -156,12 +150,6 @@ parameters: count: 1 path: src/Theme/Infrastructure/Models/LaravelThemeModule.php - - - message: '#^Called ''env'' outside of the config directory which returns null when the config is cached, use ''config''\.$#' - identifier: larastan.noEnvCallsOutsideOfConfig - count: 1 - path: src/Theme/Infrastructure/Models/LaravelThemeModule.php - - message: '#^Call to an undefined method Illuminate\\Contracts\\View\\View\:\:makeLoader\(\)\.$#' identifier: method.notFound diff --git a/src/Modules/Infrastructure/Providers/ModuleServiceProvider.php b/src/Modules/Infrastructure/Providers/ModuleServiceProvider.php index bd4f4464..83c6f79d 100644 --- a/src/Modules/Infrastructure/Providers/ModuleServiceProvider.php +++ b/src/Modules/Infrastructure/Providers/ModuleServiceProvider.php @@ -32,6 +32,7 @@ use Pollora\Modules\UI\Console\ModuleOutdatedCommand; use Pollora\Modules\UI\Http\ModulesAdminPage; use Pollora\Modules\UI\Http\ModuleVersionsHealthCheck; +use Pollora\VersionCheck\Domain\Contracts\VersionSourceResolver; use Pollora\VersionCheck\Infrastructure\Sources\HttpGet; use Pollora\VersionCheck\Infrastructure\Sources\VersionSources; @@ -68,6 +69,8 @@ public function register(): void base_path('composer.json'), $app->make('config')->get('modules.versions.github_token'), )); + + $this->app->bind(VersionSourceResolver::class, VersionSources::class); } public function boot(): void diff --git a/src/Plugin/Application/Services/PluginManager.php b/src/Plugin/Application/Services/PluginManager.php index 392988f8..5192a2d7 100644 --- a/src/Plugin/Application/Services/PluginManager.php +++ b/src/Plugin/Application/Services/PluginManager.php @@ -63,7 +63,7 @@ public function __construct( protected ?ModuleRepositoryInterface $repository = null, ?ConsoleDetectionService $consoleDetectionService = null ) { - $this->consoleDetectionService = $consoleDetectionService ?? resolve(ConsoleDetectionService::class); + $this->consoleDetectionService = $consoleDetectionService ?? $app->get(ConsoleDetectionService::class); } /** diff --git a/src/Plugin/Infrastructure/Models/LaravelPluginModule.php b/src/Plugin/Infrastructure/Models/LaravelPluginModule.php index 3bddfaed..7a3babbd 100644 --- a/src/Plugin/Infrastructure/Models/LaravelPluginModule.php +++ b/src/Plugin/Infrastructure/Models/LaravelPluginModule.php @@ -7,6 +7,7 @@ use Illuminate\Container\Container; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\Application; +use Illuminate\Support\Env; use Illuminate\Support\Str; use Pollora\Hook\Domain\Contract\Action; use Pollora\Plugin\Domain\Models\PluginModule; @@ -49,7 +50,7 @@ public function __construct( public function getCachedServicesPath(): string { // Check if we are running on a Laravel Vapor managed instance - if (! is_null(config('vapor.maintenance_mode', env('VAPOR_MAINTENANCE_MODE')))) { + if (! is_null(config('vapor.maintenance_mode', Env::get('VAPOR_MAINTENANCE_MODE')))) { return Str::replaceLast('config.php', $this->getSnakeName().'_plugin.php', $this->app->getCachedConfigPath()); } diff --git a/src/Route/Application/UseCases/BindWordPressParametersUseCase.php b/src/Route/Application/UseCases/BindWordPressParametersUseCase.php index 7df02d58..ed67a400 100644 --- a/src/Route/Application/UseCases/BindWordPressParametersUseCase.php +++ b/src/Route/Application/UseCases/BindWordPressParametersUseCase.php @@ -5,7 +5,7 @@ namespace Pollora\Route\Application\UseCases; use Illuminate\Routing\Route; -use Pollora\Route\Infrastructure\Services\Contracts\WordPressTypeResolverInterface; +use Pollora\Route\Domain\Contracts\WordPressTypeResolverInterface; use Psr\Log\LoggerInterface; /** diff --git a/src/Route/Application/UseCases/RegisterWordPressTypesUseCase.php b/src/Route/Application/UseCases/RegisterWordPressTypesUseCase.php index 3f817cd2..d15d9840 100644 --- a/src/Route/Application/UseCases/RegisterWordPressTypesUseCase.php +++ b/src/Route/Application/UseCases/RegisterWordPressTypesUseCase.php @@ -5,7 +5,7 @@ namespace Pollora\Route\Application\UseCases; use Illuminate\Contracts\Container\Container; -use Pollora\Route\Infrastructure\Services\Contracts\WordPressTypeResolverInterface; +use Pollora\Route\Domain\Contracts\WordPressTypeResolverInterface; use Psr\Log\LoggerInterface; /** diff --git a/src/Route/Domain/Contracts/WordPressTypeResolverInterface.php b/src/Route/Domain/Contracts/WordPressTypeResolverInterface.php new file mode 100644 index 00000000..82662371 --- /dev/null +++ b/src/Route/Domain/Contracts/WordPressTypeResolverInterface.php @@ -0,0 +1,41 @@ +lateAfter(); - return count(array_filter(Schedule::engine()->index(), static fn (Event $event): bool => $event->driver === LaravelQueueDriver::NAME && $event->timestamp < $limit)); + return count(array_filter(Schedule::engine()->index(), static fn (Event $event): bool => $event->driver === self::QUEUE_DRIVER && $event->timestamp < $limit)); } /** diff --git a/src/Theme/Application/Services/ThemeManager.php b/src/Theme/Application/Services/ThemeManager.php index 0907ac08..54219786 100755 --- a/src/Theme/Application/Services/ThemeManager.php +++ b/src/Theme/Application/Services/ThemeManager.php @@ -88,7 +88,7 @@ public function __construct( protected ?ThemeRegistrarInterface $registrar = null, ?ConsoleDetectionService $consoleDetectionService = null ) { - $this->consoleDetectionService = $consoleDetectionService ?? resolve(ConsoleDetectionService::class); + $this->consoleDetectionService = $consoleDetectionService ?? $app->make(ConsoleDetectionService::class); } /** diff --git a/src/Theme/Infrastructure/Models/LaravelThemeModule.php b/src/Theme/Infrastructure/Models/LaravelThemeModule.php index 97ce737a..a300cfdc 100644 --- a/src/Theme/Infrastructure/Models/LaravelThemeModule.php +++ b/src/Theme/Infrastructure/Models/LaravelThemeModule.php @@ -7,6 +7,7 @@ use Illuminate\Container\Container; use Illuminate\Foundation\AliasLoader; use Illuminate\Foundation\Application; +use Illuminate\Support\Env; use Illuminate\Support\Str; use Pollora\Hook\Domain\Contract\Action; use Pollora\Theme\Domain\Models\ThemeModule; @@ -36,7 +37,7 @@ public function __construct( public function getCachedServicesPath(): string { // Check if we are running on a Laravel Vapor managed instance - if (! is_null(config('vapor.maintenance_mode', env('VAPOR_MAINTENANCE_MODE')))) { + if (! is_null(config('vapor.maintenance_mode', Env::get('VAPOR_MAINTENANCE_MODE')))) { return Str::replaceLast('config.php', $this->getSnakeName().'_theme.php', $this->app->getCachedConfigPath()); } diff --git a/src/VersionCheck/Application/Services/PackageVersionChecker.php b/src/VersionCheck/Application/Services/PackageVersionChecker.php index 43d8b9cc..430d37ff 100644 --- a/src/VersionCheck/Application/Services/PackageVersionChecker.php +++ b/src/VersionCheck/Application/Services/PackageVersionChecker.php @@ -5,8 +5,8 @@ namespace Pollora\VersionCheck\Application\Services; use Composer\InstalledVersions; +use Pollora\VersionCheck\Domain\Contracts\VersionSourceResolver; use Pollora\VersionCheck\Domain\Services\StableVersions; -use Pollora\VersionCheck\Infrastructure\Sources\VersionSources; /** * Whether an installed Composer package is up to date. @@ -22,7 +22,7 @@ class PackageVersionChecker public const int NEGATIVE_TTL = 3600; - public function __construct(private readonly VersionSources $sources) {} + public function __construct(private readonly VersionSourceResolver $sources) {} public function current(string $package): ?string { diff --git a/src/VersionCheck/Domain/Contracts/VersionSourceResolver.php b/src/VersionCheck/Domain/Contracts/VersionSourceResolver.php new file mode 100644 index 00000000..388c85df --- /dev/null +++ b/src/VersionCheck/Domain/Contracts/VersionSourceResolver.php @@ -0,0 +1,13 @@ +>|null diff --git a/tests/Feature/ModuleActivation/ModuleVersionsTest.php b/tests/Feature/ModuleActivation/ModuleVersionsTest.php index 43374d09..4492c32a 100644 --- a/tests/Feature/ModuleActivation/ModuleVersionsTest.php +++ b/tests/Feature/ModuleActivation/ModuleVersionsTest.php @@ -10,6 +10,7 @@ use Pollora\Modules\UI\Console\ModuleOutdatedCommand; use Pollora\Modules\UI\Http\ModuleVersionsHealthCheck; use Pollora\VersionCheck\Application\Services\PackageVersionChecker; +use Pollora\VersionCheck\Domain\Contracts\VersionSourceResolver; use Pollora\VersionCheck\Domain\Services\StableVersions; use Pollora\VersionCheck\Infrastructure\Sources\ComposerRepositorySource; use Pollora\VersionCheck\Infrastructure\Sources\FirstAnsweringSource; @@ -215,7 +216,8 @@ public function getPath(): string }, array_keys($this->paths)); } }); - $this->app->instance(VersionSources::class, new VersionSources(new HttpGet, $this->directory.'/composer.json')); + $this->app->instance(VersionSources::class, $sources = new VersionSources(new HttpGet, $this->directory.'/composer.json')); + $this->app->instance(VersionSourceResolver::class, $sources); fakeHttp(['https://repo.packagist.org/p2/nwidart/laravel-modules.json' => ['packages' => ['nwidart/laravel-modules' => [['version' => '99.0.0']]]]]); }); diff --git a/tests/Feature/ModuleActivation/ModulesAdminPageTest.php b/tests/Feature/ModuleActivation/ModulesAdminPageTest.php index e4376de1..e07b3b8f 100644 --- a/tests/Feature/ModuleActivation/ModulesAdminPageTest.php +++ b/tests/Feature/ModuleActivation/ModulesAdminPageTest.php @@ -10,6 +10,7 @@ use Pollora\Modules\Infrastructure\Activation\ConnectorActivator; use Pollora\Modules\Infrastructure\Activation\JsonStateConnector; use Pollora\Modules\UI\Http\ModulesAdminPage; +use Pollora\VersionCheck\Domain\Contracts\VersionSourceResolver; use Pollora\VersionCheck\Infrastructure\Sources\HttpGet; use Pollora\VersionCheck\Infrastructure\Sources\VersionSources; @@ -100,7 +101,8 @@ public function find(string $name): ?object } }); - $this->app->instance(VersionSources::class, new VersionSources(new HttpGet, $this->directory.'/composer.json')); + $this->app->instance(VersionSources::class, $sources = new VersionSources(new HttpGet, $this->directory.'/composer.json')); + $this->app->instance(VersionSourceResolver::class, $sources); $this->page = $this->app->make(ModulesAdminPage::class); }); diff --git a/tests/Unit/Architecture/ArchitectureRefactorTest.php b/tests/Unit/Architecture/ArchitectureRefactorTest.php new file mode 100644 index 00000000..aae86716 --- /dev/null +++ b/tests/Unit/Architecture/ArchitectureRefactorTest.php @@ -0,0 +1,122 @@ +toBeTrue() + ->and((new ReflectionClass($former))->getName())->toBe(WordPressTypeResolverInterface::class); + }); + + it('is still implemented by the resolver under the former name', function (): void { + $former = 'Pollora\\Route\\Infrastructure\\Services\\Contracts\\WordPressTypeResolverInterface'; + + expect(is_subclass_of(WordPressTypeResolver::class, $former))->toBeTrue(); + }); +}); + +describe('ScheduleStatus', function (): void { + it('counts late events under the name the queue driver files them under', function (): void { + $driverName = (new ReflectionClassConstant(ScheduleStatus::class, 'QUEUE_DRIVER'))->getValue(); + + expect($driverName)->toBe(LaravelQueueDriver::NAME); + }); +}); + +describe('managers take the console detection service from the container they receive', function (): void { + it('for plugins', function (): void { + $container = new Container; + $service = Mockery::mock(ConsoleDetectionService::class); + $container->instance(ConsoleDetectionService::class, $service); + + $manager = new PluginManager($container, null); + + expect((new ReflectionProperty($manager, 'consoleDetectionService'))->getValue($manager))->toBe($service); + }); + + it('for themes', function (): void { + $container = new Container; + $service = Mockery::mock(ConsoleDetectionService::class); + $container->instance(ConsoleDetectionService::class, $service); + + $manager = new ThemeManager($container, Mockery::mock(ViewFinderInterface::class), null); + + expect((new ReflectionProperty($manager, 'consoleDetectionService'))->getValue($manager))->toBe($service); + }); +}); + +describe('Vapor maintenance mode', function (): void { + it('reads VAPOR_MAINTENANCE_MODE from the environment', function (): void { + putenv('VAPOR_MAINTENANCE_MODE=on'); + + try { + expect(Env::get('VAPOR_MAINTENANCE_MODE'))->toBe(getenv('VAPOR_MAINTENANCE_MODE')) + ->and(Env::get('VAPOR_MAINTENANCE_MODE'))->toBe('on'); + } finally { + putenv('VAPOR_MAINTENANCE_MODE'); + } + }); + + /** + * A container that answers the two cache paths the way Laravel's application does. + */ + function vaporApplication(): Container + { + $app = new class extends Container + { + public function getCachedConfigPath(): string + { + return '/cache/config/config.php'; + } + + public function getCachedServicesPath(): string + { + return '/cache/services/services.php'; + } + }; + $app->instance('config', new Repository); + Container::setInstance($app); + + return $app; + } + + it('caches the plugin and theme services next to the configuration on Vapor', function (): void { + putenv('VAPOR_MAINTENANCE_MODE=on'); + $app = vaporApplication(); + + try { + expect((new LaravelPluginModule('blog', '/tmp/plugin', $app))->getCachedServicesPath())->toBe('/cache/config/blog_plugin.php') + ->and((new LaravelThemeModule('journal', '/tmp/theme', $app))->getCachedServicesPath())->toBe('/cache/config/journal_theme.php'); + } finally { + putenv('VAPOR_MAINTENANCE_MODE'); + } + }); + + it('caches them next to the services outside Vapor', function (): void { + putenv('VAPOR_MAINTENANCE_MODE'); + $app = vaporApplication(); + + expect((new LaravelPluginModule('blog', '/tmp/plugin', $app))->getCachedServicesPath())->toBe('/cache/services/blog_plugin.php') + ->and((new LaravelThemeModule('journal', '/tmp/theme', $app))->getCachedServicesPath())->toBe('/cache/services/journal_theme.php'); + }); +}); diff --git a/tests/Unit/Route/Application/UseCases/BindWordPressParametersUseCaseTest.php b/tests/Unit/Route/Application/UseCases/BindWordPressParametersUseCaseTest.php index 6dfed40c..213a8cc8 100644 --- a/tests/Unit/Route/Application/UseCases/BindWordPressParametersUseCaseTest.php +++ b/tests/Unit/Route/Application/UseCases/BindWordPressParametersUseCaseTest.php @@ -5,8 +5,8 @@ use Illuminate\Http\Request; use Mockery as m; use Pollora\Route\Application\UseCases\BindWordPressParametersUseCase; +use Pollora\Route\Domain\Contracts\WordPressTypeResolverInterface; use Pollora\Route\Infrastructure\Models\Route; -use Pollora\Route\Infrastructure\Services\Contracts\WordPressTypeResolverInterface; use Psr\Log\LoggerInterface; beforeEach(function (): void { diff --git a/tests/Unit/Route/Application/UseCases/RegisterWordPressTypesUseCaseTest.php b/tests/Unit/Route/Application/UseCases/RegisterWordPressTypesUseCaseTest.php index 26fc4997..504e15e0 100644 --- a/tests/Unit/Route/Application/UseCases/RegisterWordPressTypesUseCaseTest.php +++ b/tests/Unit/Route/Application/UseCases/RegisterWordPressTypesUseCaseTest.php @@ -5,7 +5,7 @@ use Illuminate\Container\Container; use Mockery as m; use Pollora\Route\Application\UseCases\RegisterWordPressTypesUseCase; -use Pollora\Route\Infrastructure\Services\Contracts\WordPressTypeResolverInterface; +use Pollora\Route\Domain\Contracts\WordPressTypeResolverInterface; use Psr\Log\LoggerInterface; beforeEach(function (): void { diff --git a/tests/Unit/Route/Infrastructure/Services/WordPressTypeResolverTest.php b/tests/Unit/Route/Infrastructure/Services/WordPressTypeResolverTest.php index 28ffac3a..ed8dbe05 100644 --- a/tests/Unit/Route/Infrastructure/Services/WordPressTypeResolverTest.php +++ b/tests/Unit/Route/Infrastructure/Services/WordPressTypeResolverTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Pollora\Route\Infrastructure\Services\Contracts\WordPressTypeResolverInterface; +use Pollora\Route\Domain\Contracts\WordPressTypeResolverInterface; use Pollora\Route\Infrastructure\Services\Resolvers\WordPressTypeResolver; describe('WordPressTypeResolver', function (): void {