Skip to content

Commit 28aba82

Browse files
committed
[container] Centralize trait-only service resolution
1 parent a40c792 commit 28aba82

16 files changed

Lines changed: 585 additions & 347 deletions

src/Console/Command/TestsCommand.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use FastForward\DevTools\Console\Input\HasCacheOption;
2525
use FastForward\DevTools\Console\Input\HasJsonOption;
2626
use FastForward\DevTools\Composer\Json\ComposerJsonInterface;
27-
use FastForward\DevTools\Environment\RuntimeEnvironmentInterface;
2827
use FastForward\DevTools\Filesystem\FilesystemInterface;
2928
use FastForward\DevTools\Path\DevToolsPathResolver;
3029
use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator;
@@ -34,7 +33,6 @@
3433
use FastForward\DevTools\Path\ManagedWorkspace;
3534
use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface;
3635
use InvalidArgumentException;
37-
use Psr\Log\LoggerInterface;
3836
use Psr\Log\LogLevel;
3937
use RuntimeException;
4038
use Symfony\Component\Config\FileLocatorInterface;
@@ -82,8 +80,6 @@ final class TestsCommand extends Command
8280
* @param ProcessBuilderInterface $processBuilder the builder used to assemble the PHPUnit process
8381
* @param ProcessQueueInterface $processQueue the queue used to execute PHPUnit
8482
* @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver
85-
* @param RuntimeEnvironmentInterface $runtimeEnvironment the runtime environment capability resolver
86-
* @param LoggerInterface $logger the output-aware logger
8783
*/
8884
public function __construct(
8985
private readonly CoverageSummaryLoaderInterface $coverageSummaryLoader,
@@ -94,8 +90,6 @@ public function __construct(
9490
private readonly ProcessBuilderInterface $processBuilder,
9591
private readonly ProcessQueueInterface $processQueue,
9692
private readonly ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver,
97-
private readonly RuntimeEnvironmentInterface $runtimeEnvironment,
98-
private readonly LoggerInterface $logger,
9993
) {
10094
parent::__construct();
10195
}

src/Console/Command/Traits/HasCommandLogger.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@
1919

2020
namespace FastForward\DevTools\Console\Command\Traits;
2121

22+
use FastForward\DevTools\Container\ContainerFactory;
2223
use LogicException;
2324
use Psr\Log\LoggerInterface;
2425

2526
/**
2627
* Resolves the logger expected by command result helper traits.
2728
*
28-
* The consuming command is expected to expose an initialized `$logger`
29-
* property so reusable traits can log without coupling themselves to a
30-
* specific constructor signature.
29+
* The consuming command MAY expose an initialized `$logger` property. When it
30+
* does not, the trait SHALL resolve the shared logger from the DevTools
31+
* container so reusable traits can stay decoupled from constructor wiring.
3132
*/
3233
trait HasCommandLogger
3334
{
@@ -38,22 +39,28 @@ trait HasCommandLogger
3839
*/
3940
public function getLogger(): LoggerInterface
4041
{
41-
if (! property_exists($this, 'logger') || null === $this->logger) {
42+
if (property_exists($this, 'logger') && $this->logger instanceof LoggerInterface) {
43+
return $this->logger;
44+
}
45+
46+
if (property_exists($this, 'logger') && null !== $this->logger) {
4247
throw new LogicException(\sprintf(
43-
'Commands using %s MUST expose an initialized $logger property with an instance of %s.',
48+
'Commands using %s MUST expose a %s instance on the $logger property.',
4449
LogsCommandResults::class,
4550
LoggerInterface::class,
4651
));
4752
}
4853

49-
if (! $this->logger instanceof LoggerInterface) {
54+
$logger = ContainerFactory::get(LoggerInterface::class);
55+
56+
if (! $logger instanceof LoggerInterface) {
5057
throw new LogicException(\sprintf(
51-
'Commands using %s MUST expose a %s instance on the $logger property.',
58+
'Commands using %s MUST resolve a %s instance from the shared container.',
5259
LogsCommandResults::class,
5360
LoggerInterface::class,
5461
));
5562
}
5663

57-
return $this->logger;
64+
return $logger;
5865
}
5966
}

src/Console/DevTools.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,14 @@
2020
namespace FastForward\DevTools\Console;
2121

2222
use FastForward\DevTools\Console\Command\SelfUpdateCommand;
23+
use FastForward\DevTools\Container\ContainerFactory;
2324
use FastForward\DevTools\Environment\EnvironmentInterface;
2425
use FastForward\DevTools\Environment\RuntimeEnvironmentInterface;
2526
use FastForward\DevTools\Path\ManagedWorkspace;
2627
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
2728
use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface;
2829
use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface;
2930
use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface;
30-
use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider;
31-
use DI\Container;
3231
use Override;
3332
use Psr\Container\ContainerInterface;
3433
use Symfony\Component\Console\Application;
@@ -65,11 +64,6 @@ final class DevTools extends Application
6564
*/
6665
private const array RAW_OUTPUT_COMMANDS = ['changelog:next-version', 'changelog:show'];
6766

68-
/**
69-
* @var ContainerInterface holds the static container instance for global access within the DevTools context
70-
*/
71-
private static ?ContainerInterface $container = null;
72-
7367
/**
7468
* Initializes the DevTools global context and dependency graph.
7569
*
@@ -177,20 +171,15 @@ public function doRun(InputInterface $input, OutputInterface $output): int
177171
*/
178172
public static function create(): self
179173
{
180-
return self::getContainer()->get(self::class);
174+
return ContainerFactory::get(self::class);
181175
}
182176

183177
/**
184178
* Retrieves the shared DevTools service container.
185179
*/
186180
public static function getContainer(): ContainerInterface
187181
{
188-
if (! self::$container instanceof ContainerInterface) {
189-
$serviceProvider = new DevToolsServiceProvider();
190-
self::$container = new Container($serviceProvider->getFactories());
191-
}
192-
193-
return self::$container;
182+
return ContainerFactory::create();
194183
}
195184

196185
/**

src/Console/Input/HasJsonOption.php

Lines changed: 6 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
namespace FastForward\DevTools\Console\Input;
2121

22-
use Ergebnis\AgentDetector\Detector;
22+
use FastForward\DevTools\Container\ContainerFactory;
2323
use FastForward\DevTools\Environment\RuntimeEnvironmentInterface;
2424
use Symfony\Component\Console\Input\InputInterface;
2525
use Symfony\Component\Console\Input\InputOption;
@@ -80,22 +80,21 @@ protected function isPrettyJsonOutput(InputInterface $input): bool
8080
*
8181
* Commands MAY opt into runtime-environment-aware behavior by exposing a
8282
* `$runtimeEnvironment` property. Commands that do not expose it SHALL fall
83-
* back to lightweight agent detection based on process environment
84-
* variables, except while the PHPUnit test runtime is active.
83+
* back to the shared runtime-environment service from the DevTools container.
8584
*/
8685
private function isImplicitJsonOutputEnabled(): bool
8786
{
8887
$runtimeEnvironment = $this->resolveRuntimeEnvironment();
8988

90-
if ($runtimeEnvironment instanceof RuntimeEnvironmentInterface) {
91-
return $runtimeEnvironment->isAgentPresent() && ! $runtimeEnvironment->isComposerTestRun();
89+
if (! $runtimeEnvironment instanceof RuntimeEnvironmentInterface) {
90+
$runtimeEnvironment = ContainerFactory::get(RuntimeEnvironmentInterface::class);
9291
}
9392

94-
if ($this->isPhpUnitRuntime() || $this->isComposerTestRunEnvironmentEnabled()) {
93+
if (! $runtimeEnvironment instanceof RuntimeEnvironmentInterface) {
9594
return false;
9695
}
9796

98-
return (new Detector())->isAgentPresent($this->resolveEnvironmentVariables());
97+
return $runtimeEnvironment->isAgentPresent() && ! $runtimeEnvironment->isComposerTestRun();
9998
}
10099

101100
/**
@@ -113,48 +112,4 @@ private function resolveRuntimeEnvironment(): ?RuntimeEnvironmentInterface
113112

114113
return $this->runtimeEnvironment;
115114
}
116-
117-
/**
118-
* Returns whether the current process is executing inside PHPUnit.
119-
*/
120-
private function isPhpUnitRuntime(): bool
121-
{
122-
return \defined('PHPUNIT_COMPOSER_INSTALL');
123-
}
124-
125-
/**
126-
* Returns whether the Composer test runtime flag is enabled.
127-
*/
128-
private function isComposerTestRunEnvironmentEnabled(): bool
129-
{
130-
$value = $_SERVER['COMPOSER_TESTS_ARE_RUNNING'] ?? getenv('COMPOSER_TESTS_ARE_RUNNING');
131-
132-
if (false === $value || null === $value) {
133-
return false;
134-
}
135-
136-
return \in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true);
137-
}
138-
139-
/**
140-
* Returns environment variables suitable for lightweight agent detection.
141-
*
142-
* @return array<string, string>
143-
*/
144-
private function resolveEnvironmentVariables(): array
145-
{
146-
$environmentVariables = [];
147-
148-
foreach ([$_SERVER, $_ENV] as $environment) {
149-
foreach ($environment as $name => $value) {
150-
if (! \is_string($name) || ! \is_string($value)) {
151-
continue;
152-
}
153-
154-
$environmentVariables[$name] ??= $value;
155-
}
156-
}
157-
158-
return $environmentVariables;
159-
}
160115
}

src/Container/ContainerFactory.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Fast Forward Development Tools for PHP projects.
7+
*
8+
* This file is part of fast-forward/dev-tools project.
9+
*
10+
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
11+
* @license https://opensource.org/licenses/MIT MIT License
12+
*
13+
* @see https://github.com/php-fast-forward/
14+
* @see https://github.com/php-fast-forward/dev-tools
15+
* @see https://github.com/php-fast-forward/dev-tools/issues
16+
* @see https://php-fast-forward.github.io/dev-tools/
17+
* @see https://datatracker.ietf.org/doc/html/rfc2119
18+
*/
19+
20+
namespace FastForward\DevTools\Container;
21+
22+
use DI\Container;
23+
use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider;
24+
use Psr\Container\ContainerInterface;
25+
26+
/**
27+
* Builds and caches the shared DevTools dependency injection container.
28+
*
29+
* The factory centralizes container bootstrapping so command traits and other
30+
* internal helpers can resolve services without duplicating bootstrap logic or
31+
* depending on the console application entrypoint.
32+
*/
33+
final class ContainerFactory
34+
{
35+
private static ?Container $container = null;
36+
37+
/**
38+
* Creates or returns the shared DevTools container instance.
39+
*
40+
* @return ContainerInterface the shared container instance
41+
*/
42+
public static function create(): ContainerInterface
43+
{
44+
if (! self::$container instanceof Container) {
45+
$serviceProvider = new DevToolsServiceProvider();
46+
self::$container = new Container($serviceProvider->getFactories());
47+
}
48+
49+
return self::$container;
50+
}
51+
52+
/**
53+
* Resolves a service from the shared DevTools container.
54+
*
55+
* @template T
56+
*
57+
* @param string|class-string<T> $id the service identifier
58+
*
59+
* @return mixed|T the resolved service
60+
*/
61+
public static function get(string $id): mixed
62+
{
63+
return self::create()->get($id);
64+
}
65+
66+
/**
67+
* Returns whether the shared DevTools container can resolve a service.
68+
*
69+
* @param string $id the service identifier
70+
*/
71+
public static function has(string $id): bool
72+
{
73+
return self::create()->has($id);
74+
}
75+
76+
/**
77+
* Overrides a shared service entry for the current process.
78+
*
79+
* @internal this method exists so tests can replace container entries with doubles
80+
*
81+
* @param string $id the service identifier
82+
* @param mixed $value the replacement service entry
83+
*/
84+
public static function set(string $id, mixed $value): void
85+
{
86+
self::create();
87+
self::$container?->set($id, $value);
88+
}
89+
90+
/**
91+
* Resets the cached shared container instance.
92+
*
93+
* @internal this method exists so tests can isolate container state between test cases
94+
*/
95+
public static function reset(): void
96+
{
97+
self::$container = null;
98+
}
99+
}

0 commit comments

Comments
 (0)