diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d1729..dc9ba96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,40 @@ CHANGELOG ========= +3.6 +--- + + * Make `HttpClientTestCase` and `TranslatorTest` compatible with PHPUnit 10+ + * Add `NamespacedPoolInterface` to support namespace-based invalidation + +3.5 +--- + + * Add `ServiceCollectionInterface` + * Deprecate `ServiceSubscriberTrait`, use `ServiceMethodsSubscriberTrait` instead + +3.4 +--- + + * Allow custom working directory in `TestHttpServer` + +3.3 +--- + + * Add option `crypto_method` to `HttpClientInterface` to define the minimum TLS version to accept + +3.2 +--- + + * Allow `ServiceSubscriberInterface::getSubscribedServices()` to return `SubscribedService[]` + +3.0 +--- + + * Bump to PHP 8 minimum + * Add native return types + * Remove deprecated features + 2.5 --- diff --git a/Cache/CacheInterface.php b/Cache/CacheInterface.php index 70cb0d5..3e4aaf6 100644 --- a/Cache/CacheInterface.php +++ b/Cache/CacheInterface.php @@ -29,20 +29,22 @@ interface CacheInterface * requested key, that could be used e.g. for expiration control. It could also * be an ItemInterface instance when its additional features are needed. * - * @param string $key The key of the item to retrieve from the cache - * @param callable|CallbackInterface $callback Should return the computed value for the given key/item - * @param float|null $beta A float that, as it grows, controls the likeliness of triggering - * early expiration. 0 disables it, INF forces immediate expiration. - * The default (or providing null) is implementation dependent but should - * typically be 1.0, which should provide optimal stampede protection. - * See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration - * @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()} + * @template T * - * @return mixed + * @param string $key The key of the item to retrieve from the cache + * @param (callable(CacheItemInterface,bool):T)|(callable(ItemInterface,bool):T)|CallbackInterface $callback + * @param float|null $beta A float that, as it grows, controls the likeliness of triggering + * early expiration. 0 disables it, INF forces immediate expiration. + * The default (or providing null) is implementation dependent but should + * typically be 1.0, which should provide optimal stampede protection. + * See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration + * @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()} + * + * @return T * * @throws InvalidArgumentException When $key is not valid or when $beta is negative */ - public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null); + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed; /** * Removes an item from the pool. diff --git a/Cache/CacheTrait.php b/Cache/CacheTrait.php index b9feafb..4c5449b 100644 --- a/Cache/CacheTrait.php +++ b/Cache/CacheTrait.php @@ -25,28 +25,20 @@ class_exists(InvalidArgumentException::class); */ trait CacheTrait { - /** - * {@inheritdoc} - * - * @return mixed - */ - public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed { return $this->doGet($this, $key, $callback, $beta, $metadata); } - /** - * {@inheritdoc} - */ public function delete(string $key): bool { return $this->deleteItem($key); } - private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null, ?LoggerInterface $logger = null) + private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null, ?LoggerInterface $logger = null): mixed { - if (0 > $beta = $beta ?? 1.0) { - throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)) extends \InvalidArgumentException implements InvalidArgumentException { }; + if (0 > $beta ??= 1.0) { + throw new class(\sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)) extends \InvalidArgumentException implements InvalidArgumentException {}; } $item = $pool->getItem($key); @@ -60,9 +52,9 @@ private function doGet(CacheItemPoolInterface $pool, string $key, callable $call if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX)) { // force applying defaultLifetime to expiry $item->expiresAt(null); - $logger && $logger->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [ + $logger?->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [ 'key' => $key, - 'delta' => sprintf('%.1f', $expiry - $now), + 'delta' => \sprintf('%.1f', $expiry - $now), ]); } } diff --git a/Cache/CallbackInterface.php b/Cache/CallbackInterface.php index 7dae2aa..15941e9 100644 --- a/Cache/CallbackInterface.php +++ b/Cache/CallbackInterface.php @@ -17,6 +17,8 @@ * Computes and returns the cached value of an item. * * @author Nicolas Grekas + * + * @template T */ interface CallbackInterface { @@ -24,7 +26,7 @@ interface CallbackInterface * @param CacheItemInterface|ItemInterface $item The item to compute the value for * @param bool &$save Should be set to false when the value should not be saved in the pool * - * @return mixed The computed value for the passed item + * @return T The computed value for the passed item */ - public function __invoke(CacheItemInterface $item, bool &$save); + public function __invoke(CacheItemInterface $item, bool &$save): mixed; } diff --git a/Cache/ItemInterface.php b/Cache/ItemInterface.php index 10c0488..8c4c512 100644 --- a/Cache/ItemInterface.php +++ b/Cache/ItemInterface.php @@ -54,7 +54,7 @@ interface ItemInterface extends CacheItemInterface * @throws InvalidArgumentException When $tag is not valid * @throws CacheException When the item comes from a pool that is not tag-aware */ - public function tag($tags): self; + public function tag(string|iterable $tags): static; /** * Returns a list of metadata info that were saved alongside with the cached value. diff --git a/Cache/NamespacedPoolInterface.php b/Cache/NamespacedPoolInterface.php new file mode 100644 index 0000000..cd67bc0 --- /dev/null +++ b/Cache/NamespacedPoolInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\InvalidArgumentException; + +/** + * Enables namespace-based invalidation by prefixing keys with backend-native namespace separators. + * + * Note that calling `withSubNamespace()` MUST NOT mutate the pool, but return a new instance instead. + * + * When tags are used, they MUST ignore sub-namespaces. + * + * @author Nicolas Grekas + */ +interface NamespacedPoolInterface +{ + /** + * @throws InvalidArgumentException If the namespace contains characters found in ItemInterface's RESERVED_CHARACTERS + */ + public function withSubNamespace(string $namespace): static; +} diff --git a/Cache/README.md b/Cache/README.md index 7085a69..ffe0833 100644 --- a/Cache/README.md +++ b/Cache/README.md @@ -3,7 +3,7 @@ Symfony Cache Contracts A set of abstractions extracted out of the Symfony components. -Can be used to build on semantics that the Symfony components proved useful - and +Can be used to build on semantics that the Symfony components proved useful and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/Cache/TagAwareCacheInterface.php b/Cache/TagAwareCacheInterface.php index 7c4cf11..8e0b6be 100644 --- a/Cache/TagAwareCacheInterface.php +++ b/Cache/TagAwareCacheInterface.php @@ -34,5 +34,5 @@ interface TagAwareCacheInterface extends CacheInterface * * @throws InvalidArgumentException When $tags is not valid */ - public function invalidateTags(array $tags); + public function invalidateTags(array $tags): bool; } diff --git a/Cache/composer.json b/Cache/composer.json index 9f45e17..b713c29 100644 --- a/Cache/composer.json +++ b/Cache/composer.json @@ -16,11 +16,8 @@ } ], "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0|^3.0" - }, - "suggest": { - "symfony/cache-implementation": "" + "php": ">=8.1", + "psr/cache": "^3.0" }, "autoload": { "psr-4": { "Symfony\\Contracts\\Cache\\": "" } @@ -28,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/Deprecation/README.md b/Deprecation/README.md index 4957933..9814864 100644 --- a/Deprecation/README.md +++ b/Deprecation/README.md @@ -22,5 +22,5 @@ trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use This will generate the following message: `Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.` -While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty +While not recommended, the deprecation notices can be completely ignored by declaring an empty `function trigger_deprecation() {}` in your application. diff --git a/Deprecation/composer.json b/Deprecation/composer.json index cc7cc12..5533b5c 100644 --- a/Deprecation/composer.json +++ b/Deprecation/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.1" + "php": ">=8.1" }, "autoload": { "files": [ @@ -25,7 +25,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/Deprecation/function.php b/Deprecation/function.php index d437150..2d56512 100644 --- a/Deprecation/function.php +++ b/Deprecation/function.php @@ -20,7 +20,7 @@ * * @author Nicolas Grekas */ - function trigger_deprecation(string $package, string $version, string $message, ...$args): void + function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void { @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); } diff --git a/EventDispatcher/Event.php b/EventDispatcher/Event.php index 46dcb2b..2e7f998 100644 --- a/EventDispatcher/Event.php +++ b/EventDispatcher/Event.php @@ -30,11 +30,8 @@ */ class Event implements StoppableEventInterface { - private $propagationStopped = false; + private bool $propagationStopped = false; - /** - * {@inheritdoc} - */ public function isPropagationStopped(): bool { return $this->propagationStopped; diff --git a/EventDispatcher/EventDispatcherInterface.php b/EventDispatcher/EventDispatcherInterface.php index 81f4e89..2d7840d 100644 --- a/EventDispatcher/EventDispatcherInterface.php +++ b/EventDispatcher/EventDispatcherInterface.php @@ -21,11 +21,13 @@ interface EventDispatcherInterface extends PsrEventDispatcherInterface /** * Dispatches an event to all registered listeners. * - * @param object $event The event to pass to the event handlers/listeners + * @template T of object + * + * @param T $event The event to pass to the event handlers/listeners * @param string|null $eventName The name of the event to dispatch. If not supplied, * the class of $event should be used instead. * - * @return object The passed $event MUST be returned + * @return T The passed $event MUST be returned */ public function dispatch(object $event, ?string $eventName = null): object; } diff --git a/EventDispatcher/README.md b/EventDispatcher/README.md index b1ab4c0..332b961 100644 --- a/EventDispatcher/README.md +++ b/EventDispatcher/README.md @@ -3,7 +3,7 @@ Symfony EventDispatcher Contracts A set of abstractions extracted out of the Symfony components. -Can be used to build on semantics that the Symfony components proved useful - and +Can be used to build on semantics that the Symfony components proved useful and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/EventDispatcher/composer.json b/EventDispatcher/composer.json index 660df81..d156b44 100644 --- a/EventDispatcher/composer.json +++ b/EventDispatcher/composer.json @@ -16,19 +16,16 @@ } ], "require": { - "php": ">=7.2.5", + "php": ">=8.1", "psr/event-dispatcher": "^1" }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, "autoload": { "psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" } }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/HttpClient/HttpClientInterface.php b/HttpClient/HttpClientInterface.php index dac97ba..a7c8737 100644 --- a/HttpClient/HttpClientInterface.php +++ b/HttpClient/HttpClientInterface.php @@ -19,8 +19,6 @@ * * @see HttpClientTestCase for a reference test suite * - * @method static withOptions(array $options) Returns a new instance of the client with new default options - * * @author Nicolas Grekas */ interface HttpClientInterface @@ -68,6 +66,7 @@ interface HttpClientInterface 'ciphers' => null, 'peer_fingerprint' => null, 'capture_peer_cert_chain' => false, + 'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, // STREAM_CRYPTO_METHOD_TLSv*_CLIENT - minimum TLS version 'extra' => [], // array - additional options that can be ignored if unsupported, unlike regular options ]; @@ -91,5 +90,10 @@ public function request(string $method, string $url, array $options = []): Respo * @param ResponseInterface|iterable $responses One or more responses created by the current HTTP client * @param float|null $timeout The idle timeout before yielding timeout chunks */ - public function stream($responses, ?float $timeout = null): ResponseStreamInterface; + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface; + + /** + * Returns a new instance of the client with new default options. + */ + public function withOptions(array $options): static; } diff --git a/HttpClient/README.md b/HttpClient/README.md index 03b3a69..24d72f5 100644 --- a/HttpClient/README.md +++ b/HttpClient/README.md @@ -3,7 +3,7 @@ Symfony HttpClient Contracts A set of abstractions extracted out of the Symfony components. -Can be used to build on semantics that the Symfony components proved useful - and +Can be used to build on semantics that the Symfony components proved useful and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/HttpClient/ResponseInterface.php b/HttpClient/ResponseInterface.php index 7c84a98..44611cd 100644 --- a/HttpClient/ResponseInterface.php +++ b/HttpClient/ResponseInterface.php @@ -13,7 +13,6 @@ use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; -use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; @@ -37,7 +36,7 @@ public function getStatusCode(): int; * * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes * - * @return string[][] The headers of the response keyed by header names in lowercase + * @return array> The headers of the response keyed by header names in lowercase * * @throws TransportExceptionInterface When a network error occurs * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached @@ -105,5 +104,5 @@ public function cancel(): void; * @return mixed An array of all available info, or one of them when $type is * provided, or null when an unsupported type is requested */ - public function getInfo(?string $type = null); + public function getInfo(?string $type = null): mixed; } diff --git a/HttpClient/Test/Fixtures/web/index.php b/HttpClient/Test/Fixtures/web/index.php index db4d551..399f8bd 100644 --- a/HttpClient/Test/Fixtures/web/index.php +++ b/HttpClient/Test/Fixtures/web/index.php @@ -30,7 +30,7 @@ } foreach ($_SERVER as $k => $v) { - if (0 === strpos($k, 'HTTP_')) { + if (str_starts_with($k, 'HTTP_')) { $vars[$k] = $v; } } @@ -42,6 +42,7 @@ exit; case '/head': + header('X-Request-Vars: '.json_encode($vars, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); header('Content-Length: '.strlen($json), true); break; @@ -198,6 +199,16 @@ ]); exit; + + case '/custom': + if (isset($_GET['status'])) { + http_response_code((int) $_GET['status']); + } + if (isset($_GET['headers']) && is_array($_GET['headers'])) { + foreach ($_GET['headers'] as $header) { + header($header); + } + } } header('Content-Type: application/json', true); diff --git a/HttpClient/Test/HttpClientTestCase.php b/HttpClient/Test/HttpClientTestCase.php index 08825f7..9a528f6 100644 --- a/HttpClient/Test/HttpClientTestCase.php +++ b/HttpClient/Test/HttpClientTestCase.php @@ -11,6 +11,7 @@ namespace Symfony\Contracts\HttpClient\Test; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; @@ -25,7 +26,7 @@ abstract class HttpClientTestCase extends TestCase { public static function setUpBeforeClass(): void { - if (!function_exists('ob_gzhandler')) { + if (!\function_exists('ob_gzhandler')) { static::markTestSkipped('The "ob_gzhandler" function is not available.'); } @@ -145,7 +146,7 @@ public function testConditionalBuffering() $this->assertSame($firstContent, $secondContent); - $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () { return false; }]); + $response = $client->request('GET', 'http://localhost:8057', ['buffer' => fn () => false]); $response->getContent(); $this->expectException(TransportExceptionInterface::class); @@ -236,13 +237,13 @@ public function testClientError() try { $response->getHeaders(); $this->fail(ClientExceptionInterface::class.' expected'); - } catch (ClientExceptionInterface $e) { + } catch (ClientExceptionInterface) { } try { $response->getContent(); $this->fail(ClientExceptionInterface::class.' expected'); - } catch (ClientExceptionInterface $e) { + } catch (ClientExceptionInterface) { } $this->assertSame(404, $response->getStatusCode()); @@ -256,7 +257,7 @@ public function testClientError() $this->assertTrue($chunk->isFirst()); } $this->fail(ClientExceptionInterface::class.' expected'); - } catch (ClientExceptionInterface $e) { + } catch (ClientExceptionInterface) { } } @@ -276,14 +277,14 @@ public function testDnsError() try { $response->getStatusCode(); $this->fail(TransportExceptionInterface::class.' expected'); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { $this->addToAssertionCount(1); } try { $response->getStatusCode(); $this->fail(TransportExceptionInterface::class.' still expected'); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { $this->addToAssertionCount(1); } @@ -293,7 +294,7 @@ public function testDnsError() foreach ($client->stream($response) as $r => $chunk) { } $this->fail(TransportExceptionInterface::class.' expected'); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { $this->addToAssertionCount(1); } @@ -447,7 +448,7 @@ public function testMaxRedirects() try { $response->getHeaders(); $this->fail(RedirectionExceptionInterface::class.' expected'); - } catch (RedirectionExceptionInterface $e) { + } catch (RedirectionExceptionInterface) { } $this->assertSame(302, $response->getStatusCode()); @@ -881,7 +882,7 @@ public function testTimeoutOnInitialize() try { $response->getContent(); $this->fail(TransportExceptionInterface::class.' expected'); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { } } $responses = []; @@ -914,7 +915,7 @@ public function testTimeoutOnDestruct() try { unset($response); $this->fail(TransportExceptionInterface::class.' expected'); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { } } @@ -1025,6 +1026,7 @@ public function testNoProxy() /** * @requires extension zlib */ + #[RequiresPhpExtension('zlib')] public function testAutoEncodingRequest() { $client = $this->getHttpClient(__FUNCTION__); @@ -1098,6 +1100,7 @@ public function testInformationalResponseStream() /** * @requires extension zlib */ + #[RequiresPhpExtension('zlib')] public function testUserlandEncodingRequest() { $client = $this->getHttpClient(__FUNCTION__); @@ -1120,6 +1123,7 @@ public function testUserlandEncodingRequest() /** * @requires extension zlib */ + #[RequiresPhpExtension('zlib')] public function testGzipBroken() { $client = $this->getHttpClient(__FUNCTION__); @@ -1140,7 +1144,7 @@ public function testMaxDuration() try { $response->getContent(); - } catch (TransportExceptionInterface $e) { + } catch (TransportExceptionInterface) { $this->addToAssertionCount(1); } @@ -1152,14 +1156,10 @@ public function testMaxDuration() public function testWithOptions() { $client = $this->getHttpClient(__FUNCTION__); - if (!method_exists($client, 'withOptions')) { - $this->markTestSkipped(sprintf('Not implementing "%s::withOptions()" is deprecated.', get_debug_type($client))); - } - $client2 = $client->withOptions(['base_uri' => 'http://localhost:8057/']); $this->assertNotSame($client, $client2); - $this->assertSame(\get_class($client), \get_class($client2)); + $this->assertSame($client::class, $client2::class); $response = $client2->request('GET', '/'); $this->assertSame(200, $response->getStatusCode()); diff --git a/HttpClient/Test/TestHttpServer.php b/HttpClient/Test/TestHttpServer.php index 0bea6de..ec47050 100644 --- a/HttpClient/Test/TestHttpServer.php +++ b/HttpClient/Test/TestHttpServer.php @@ -16,13 +16,15 @@ class TestHttpServer { - private static $process = []; + private static array $process = []; /** - * @return Process + * @param string|null $workingDirectory */ - public static function start(int $port = 8057) + public static function start(int $port = 8057/* , ?string $workingDirectory = null */): Process { + $workingDirectory = \func_get_args()[1] ?? __DIR__.'/Fixtures/web'; + if (0 > $port) { $port = -$port; $ip = '[::1]'; @@ -40,7 +42,7 @@ public static function start(int $port = 8057) $finder = new PhpExecutableFinder(); $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', $ip.':'.$port])); - $process->setWorkingDirectory(__DIR__.'/Fixtures/web'); + $process->setWorkingDirectory($workingDirectory); $process->start(); self::$process[$port] = $process; diff --git a/HttpClient/composer.json b/HttpClient/composer.json index b76cab8..a67a753 100644 --- a/HttpClient/composer.json +++ b/HttpClient/composer.json @@ -16,18 +16,18 @@ } ], "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/http-client-implementation": "" + "php": ">=8.1" }, "autoload": { - "psr-4": { "Symfony\\Contracts\\HttpClient\\": "" } + "psr-4": { "Symfony\\Contracts\\HttpClient\\": "" }, + "exclude-from-classmap": [ + "/Test/" + ] }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/Service/Attribute/SubscribedService.php b/Service/Attribute/SubscribedService.php index 10d1bc3..f850b84 100644 --- a/Service/Attribute/SubscribedService.php +++ b/Service/Attribute/SubscribedService.php @@ -11,10 +11,15 @@ namespace Symfony\Contracts\Service\Attribute; -use Symfony\Contracts\Service\ServiceSubscriberTrait; +use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; +use Symfony\Contracts\Service\ServiceSubscriberInterface; /** - * Use with {@see ServiceSubscriberTrait} to mark a method's return type + * For use as the return value for {@see ServiceSubscriberInterface}. + * + * @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi')) + * + * Use with {@see ServiceMethodsSubscriberTrait} to mark a method's return type * as a subscribed service. * * @author Kevin Bond @@ -22,12 +27,21 @@ #[\Attribute(\Attribute::TARGET_METHOD)] final class SubscribedService { + /** @var object[] */ + public array $attributes; + /** - * @param string|null $key The key to use for the service - * If null, use "ClassName::methodName" + * @param string|null $key The key to use for the service + * @param class-string|null $type The service class + * @param bool $nullable Whether the service is optional + * @param object|object[] $attributes One or more dependency injection attributes to use */ public function __construct( - public ?string $key = null + public ?string $key = null, + public ?string $type = null, + public bool $nullable = false, + array|object $attributes = [], ) { + $this->attributes = \is_array($attributes) ? $attributes : [$attributes]; } } diff --git a/Service/README.md b/Service/README.md index 41e054a..42841a5 100644 --- a/Service/README.md +++ b/Service/README.md @@ -3,7 +3,7 @@ Symfony Service Contracts A set of abstractions extracted out of the Symfony components. -Can be used to build on semantics that the Symfony components proved useful - and +Can be used to build on semantics that the Symfony components proved useful and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/Service/ResetInterface.php b/Service/ResetInterface.php index 1af1075..a4f389b 100644 --- a/Service/ResetInterface.php +++ b/Service/ResetInterface.php @@ -26,5 +26,8 @@ */ interface ResetInterface { + /** + * @return void + */ public function reset(); } diff --git a/Service/ServiceCollectionInterface.php b/Service/ServiceCollectionInterface.php new file mode 100644 index 0000000..2333139 --- /dev/null +++ b/Service/ServiceCollectionInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * A ServiceProviderInterface that is also countable and iterable. + * + * @author Kevin Bond + * + * @template-covariant T of mixed + * + * @extends ServiceProviderInterface + * @extends \IteratorAggregate + */ +interface ServiceCollectionInterface extends ServiceProviderInterface, \Countable, \IteratorAggregate +{ +} diff --git a/Service/ServiceLocatorTrait.php b/Service/ServiceLocatorTrait.php index 74dfa43..bbe4548 100644 --- a/Service/ServiceLocatorTrait.php +++ b/Service/ServiceLocatorTrait.php @@ -26,34 +26,23 @@ class_exists(NotFoundExceptionInterface::class); */ trait ServiceLocatorTrait { - private $factories; - private $loading = []; - private $providedTypes; + private array $loading = []; + private array $providedTypes; /** - * @param callable[] $factories + * @param array $factories */ - public function __construct(array $factories) - { - $this->factories = $factories; + public function __construct( + private array $factories, + ) { } - /** - * {@inheritdoc} - * - * @return bool - */ - public function has(string $id) + public function has(string $id): bool { return isset($this->factories[$id]); } - /** - * {@inheritdoc} - * - * @return mixed - */ - public function get(string $id) + public function get(string $id): mixed { if (!isset($this->factories[$id])) { throw $this->createNotFoundException($id); @@ -75,12 +64,9 @@ public function get(string $id) } } - /** - * {@inheritdoc} - */ public function getProvidedServices(): array { - if (null === $this->providedTypes) { + if (!isset($this->providedTypes)) { $this->providedTypes = []; foreach ($this->factories as $name => $factory) { @@ -104,16 +90,16 @@ private function createNotFoundException(string $id): NotFoundExceptionInterface } else { $last = array_pop($alternatives); if ($alternatives) { - $message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); + $message = \sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); } else { - $message = sprintf('only knows about the "%s" service.', $last); + $message = \sprintf('only knows about the "%s" service.', $last); } } if ($this->loading) { - $message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); + $message = \sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); } else { - $message = sprintf('Service "%s" not found: the current service locator %s', $id, $message); + $message = \sprintf('Service "%s" not found: the current service locator %s', $id, $message); } return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { @@ -122,7 +108,7 @@ private function createNotFoundException(string $id): NotFoundExceptionInterface private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface { - return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { + return new class(\sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { }; } } diff --git a/Service/ServiceMethodsSubscriberTrait.php b/Service/ServiceMethodsSubscriberTrait.php new file mode 100644 index 0000000..844be89 --- /dev/null +++ b/Service/ServiceMethodsSubscriberTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; +use Symfony\Contracts\Service\Attribute\SubscribedService; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @author Kevin Bond + */ +trait ServiceMethodsSubscriberTrait +{ + protected ContainerInterface $container; + + public static function getSubscribedServices(): array + { + $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } + + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + } + + if (!$returnType = $method->getReturnType()) { + throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + } + + /* @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= self::class.'::'.$method->name; + $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); + + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; + } + } + + return $services; + } + + #[Required] + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + $ret = null; + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { + $ret = parent::setContainer($container); + } + + $this->container = $container; + + return $ret; + } +} diff --git a/Service/ServiceProviderInterface.php b/Service/ServiceProviderInterface.php index c60ad0b..2e71f00 100644 --- a/Service/ServiceProviderInterface.php +++ b/Service/ServiceProviderInterface.php @@ -18,9 +18,18 @@ * * @author Nicolas Grekas * @author Mateusz Sip + * + * @template-covariant T of mixed */ interface ServiceProviderInterface extends ContainerInterface { + /** + * @return T + */ + public function get(string $id): mixed; + + public function has(string $id): bool; + /** * Returns an associative array of service types keyed by the identifiers provided by the current container. * @@ -30,7 +39,7 @@ interface ServiceProviderInterface extends ContainerInterface * * ['foo' => '?'] means the container provides service name "foo" of unspecified type * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null * - * @return string[] The provided service types, keyed by service names + * @return array The provided service types, keyed by service names */ public function getProvidedServices(): array; } diff --git a/Service/ServiceSubscriberInterface.php b/Service/ServiceSubscriberInterface.php index 098ab90..3da1916 100644 --- a/Service/ServiceSubscriberInterface.php +++ b/Service/ServiceSubscriberInterface.php @@ -11,6 +11,8 @@ namespace Symfony\Contracts\Service; +use Symfony\Contracts\Service\Attribute\SubscribedService; + /** * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. * @@ -29,7 +31,8 @@ interface ServiceSubscriberInterface { /** - * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * Returns an array of service types (or {@see SubscribedService} objects) required + * by such instances, optionally keyed by the service names used internally. * * For mandatory dependencies: * @@ -47,7 +50,13 @@ interface ServiceSubscriberInterface * * ['?Psr\Log\LoggerInterface'] is a shortcut for * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] * - * @return string[] The required service types, optionally keyed by service names + * additionally, an array of {@see SubscribedService}'s can be returned: + * + * * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)] + * * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)] + * * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))] + * + * @return string[]|SubscribedService[] The required service types, optionally keyed by service names */ - public static function getSubscribedServices(); + public static function getSubscribedServices(): array; } diff --git a/Service/ServiceSubscriberTrait.php b/Service/ServiceSubscriberTrait.php index 6c560a4..58ea7c5 100644 --- a/Service/ServiceSubscriberTrait.php +++ b/Service/ServiceSubscriberTrait.php @@ -12,91 +12,65 @@ namespace Symfony\Contracts\Service; use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\SubscribedService; +trigger_deprecation('symfony/contracts', 'v3.5', '"%s" is deprecated, use "ServiceMethodsSubscriberTrait" instead.', ServiceSubscriberTrait::class); + /** - * Implementation of ServiceSubscriberInterface that determines subscribed services from - * method return types. Service ids are available as "ClassName::methodName". + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @property ContainerInterface $container * * @author Kevin Bond + * + * @deprecated since symfony/contracts v3.5, use ServiceMethodsSubscriberTrait instead */ trait ServiceSubscriberTrait { - /** @var ContainerInterface */ - protected $container; - - /** - * {@inheritdoc} - */ public static function getSubscribedServices(): array { $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; - $attributeOptIn = false; - - if (\PHP_VERSION_ID >= 80000) { - foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { - if (self::class !== $method->getDeclaringClass()->name) { - continue; - } - - if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { - continue; - } - - if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { - throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); - } - if (!$returnType = $method->getReturnType()) { - throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); - } - - $serviceId = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; - - if ($returnType->allowsNull()) { - $serviceId = '?'.$serviceId; - } - - $services[$attribute->newInstance()->key ?? self::class.'::'.$method->name] = $serviceId; - $attributeOptIn = true; + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if (self::class !== $method->getDeclaringClass()->name) { + continue; } - } - if (!$attributeOptIn) { - foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { - if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { - continue; - } - - if (self::class !== $method->getDeclaringClass()->name) { - continue; - } + if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } - if (!($returnType = $method->getReturnType()) instanceof \ReflectionNamedType) { - continue; - } + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + throw new \LogicException(\sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + } - if ($returnType->isBuiltin()) { - continue; - } + if (!$returnType = $method->getReturnType()) { + throw new \LogicException(\sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + } - if (\PHP_VERSION_ID >= 80000) { - trigger_deprecation('symfony/service-contracts', '2.5', 'Using "%s" in "%s" without using the "%s" attribute on any method is deprecated.', ServiceSubscriberTrait::class, self::class, SubscribedService::class); - } + /** @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= self::class.'::'.$method->name; + $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + $attribute->nullable = $attribute->nullable ?: $returnType->allowsNull(); - $services[self::class.'::'.$method->name] = '?'.($returnType instanceof \ReflectionNamedType ? $returnType->getName() : $returnType); + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; } } return $services; } - /** - * @required - * - * @return ContainerInterface|null - */ - public function setContainer(ContainerInterface $container) + #[Required] + public function setContainer(ContainerInterface $container): ?ContainerInterface { $ret = null; if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { diff --git a/Service/Test/ServiceLocatorTestCase.php b/Service/Test/ServiceLocatorTestCase.php index 8696db7..fdd5b27 100644 --- a/Service/Test/ServiceLocatorTestCase.php +++ b/Service/Test/ServiceLocatorTestCase.php @@ -12,15 +12,17 @@ namespace Symfony\Contracts\Service\Test; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Symfony\Contracts\Service\ServiceLocatorTrait; abstract class ServiceLocatorTestCase extends TestCase { /** - * @return ContainerInterface + * @param array $factories */ - protected function getServiceLocator(array $factories) + protected function getServiceLocator(array $factories): ContainerInterface { return new class($factories) implements ContainerInterface { use ServiceLocatorTrait; @@ -30,9 +32,9 @@ protected function getServiceLocator(array $factories) public function testHas() { $locator = $this->getServiceLocator([ - 'foo' => function () { return 'bar'; }, - 'bar' => function () { return 'baz'; }, - function () { return 'dummy'; }, + 'foo' => fn () => 'bar', + 'bar' => fn () => 'baz', + fn () => 'dummy', ]); $this->assertTrue($locator->has('foo')); @@ -43,8 +45,8 @@ function () { return 'dummy'; }, public function testGet() { $locator = $this->getServiceLocator([ - 'foo' => function () { return 'bar'; }, - 'bar' => function () { return 'baz'; }, + 'foo' => fn () => 'bar', + 'bar' => fn () => 'baz', ]); $this->assertSame('bar', $locator->get('foo')); @@ -69,27 +71,27 @@ public function testGetDoesNotMemoize() public function testThrowsOnUndefinedInternalService() { - if (!$this->getExpectedException()) { - $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); - $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); - } $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, ]); + $this->expectException(NotFoundExceptionInterface::class); + $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); + $locator->get('foo'); } public function testThrowsOnCircularReference() { - $this->expectException(\Psr\Container\ContainerExceptionInterface::class); - $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, 'bar' => function () use (&$locator) { return $locator->get('baz'); }, 'baz' => function () use (&$locator) { return $locator->get('bar'); }, ]); + $this->expectException(ContainerExceptionInterface::class); + $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); + $locator->get('foo'); } } diff --git a/Service/composer.json b/Service/composer.json index f058637..bc2e99a 100644 --- a/Service/composer.json +++ b/Service/composer.json @@ -16,23 +16,23 @@ } ], "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" }, - "suggest": { - "symfony/service-implementation": "" - }, "autoload": { - "psr-4": { "Symfony\\Contracts\\Service\\": "" } + "psr-4": { "Symfony\\Contracts\\Service\\": "" }, + "exclude-from-classmap": [ + "/Test/" + ] }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/Tests/Cache/CacheTraitTest.php b/Tests/Cache/CacheTraitTest.php index baf6ef4..dcc0124 100644 --- a/Tests/Cache/CacheTraitTest.php +++ b/Tests/Cache/CacheTraitTest.php @@ -43,9 +43,7 @@ public function testSave() $cache->expects($this->once()) ->method('save'); - $callback = function (CacheItemInterface $item) { - return 'computed data'; - }; + $callback = fn (CacheItemInterface $item) => 'computed data'; $cache->get('key', $callback); } @@ -71,7 +69,7 @@ public function testNoCallbackCallOnHit() ->method('save'); $callback = function (CacheItemInterface $item) { - $this->assertTrue(false, 'This code should never be reached'); + $this->fail('This code should never be reached'); }; $cache->get('key', $callback); @@ -101,9 +99,7 @@ public function testRecomputeOnBetaInf() $cache->expects($this->once()) ->method('save'); - $callback = function (CacheItemInterface $item) { - return 'computed data'; - }; + $callback = fn (CacheItemInterface $item) => 'computed data'; $cache->get('key', $callback, \INF); } @@ -114,9 +110,7 @@ public function testExceptionOnNegativeBeta() ->onlyMethods(['getItem', 'save']) ->getMock(); - $callback = function (CacheItemInterface $item) { - return 'computed data'; - }; + $callback = fn (CacheItemInterface $item) => 'computed data'; $this->expectException(\InvalidArgumentException::class); $cache->get('key', $callback, -2); diff --git a/Tests/Fixtures/TestServiceSubscriberUnion.php b/Tests/Fixtures/TestServiceSubscriberUnion.php deleted file mode 100644 index 6bd8bbd..0000000 --- a/Tests/Fixtures/TestServiceSubscriberUnion.php +++ /dev/null @@ -1,25 +0,0 @@ -container->get(__METHOD__); - } - - private function method2(): Service1|Service2 - { - return $this->container->get(__METHOD__); - } - - private function method3(): Service1|Service2|null - { - return $this->container->get(__METHOD__); - } -} diff --git a/Tests/Service/LegacyTestService.php b/Tests/Service/LegacyTestService.php new file mode 100644 index 0000000..471e186 --- /dev/null +++ b/Tests/Service/LegacyTestService.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Tests\Service; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; +use Symfony\Contracts\Service\Attribute\SubscribedService; +use Symfony\Contracts\Service\ServiceSubscriberInterface; +use Symfony\Contracts\Service\ServiceSubscriberTrait; + +class LegacyParentTestService +{ + public function aParentService(): Service1 + { + } + + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + return $container; + } +} + +class LegacyTestService extends LegacyParentTestService implements ServiceSubscriberInterface +{ + use ServiceSubscriberTrait; + + protected $container; + + #[SubscribedService] + public function aService(): Service2 + { + return $this->container->get(__METHOD__); + } + + #[SubscribedService(nullable: true)] + public function nullableInAttribute(): Service2 + { + if (!$this->container->has(__METHOD__)) { + throw new \LogicException(); + } + + return $this->container->get(__METHOD__); + } + + #[SubscribedService] + public function nullableReturnType(): ?Service2 + { + return $this->container->get(__METHOD__); + } + + #[SubscribedService(attributes: new Required())] + public function withAttribute(): ?Service2 + { + return $this->container->get(__METHOD__); + } +} + +class LegacyChildTestService extends LegacyTestService +{ + #[SubscribedService] + public function aChildService(): LegacyService3 + { + return $this->container->get(__METHOD__); + } +} + +class LegacyParentWithMagicCall +{ + public function __call($method, $args) + { + throw new \BadMethodCallException('Should not be called.'); + } + + public static function __callStatic($method, $args) + { + throw new \BadMethodCallException('Should not be called.'); + } +} + +class LegacyService3 +{ +} + +class LegacyParentTestService2 +{ + /** @var ContainerInterface */ + protected $container; + + public function setContainer(ContainerInterface $container) + { + $previous = $this->container ?? null; + $this->container = $container; + + return $previous; + } +} diff --git a/Tests/Service/ServiceMethodsSubscriberTraitTest.php b/Tests/Service/ServiceMethodsSubscriberTraitTest.php new file mode 100644 index 0000000..4d67a84 --- /dev/null +++ b/Tests/Service/ServiceMethodsSubscriberTraitTest.php @@ -0,0 +1,181 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Tests\Service; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; +use Symfony\Contracts\Service\Attribute\SubscribedService; +use Symfony\Contracts\Service\ServiceLocatorTrait; +use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; +use Symfony\Contracts\Service\ServiceSubscriberInterface; + +class ServiceMethodsSubscriberTraitTest extends TestCase +{ + public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices() + { + $expected = [ + TestService::class.'::aService' => Service2::class, + TestService::class.'::nullableInAttribute' => '?'.Service2::class, + TestService::class.'::nullableReturnType' => '?'.Service2::class, + new SubscribedService(TestService::class.'::withAttribute', Service2::class, true, new Required()), + ]; + + $this->assertEquals($expected, ChildTestService::getSubscribedServices()); + } + + public function testSetContainerIsCalledOnParent() + { + $container = new class([]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + + $this->assertSame($container, (new TestService())->setContainer($container)); + } + + public function testParentNotCalledIfHasMagicCall() + { + $container = new class([]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + $service = new class extends ParentWithMagicCall { + use ServiceMethodsSubscriberTrait; + }; + + $this->assertNull($service->setContainer($container)); + $this->assertSame([], $service::getSubscribedServices()); + } + + public function testParentNotCalledIfNoParent() + { + $container = new class([]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + $service = new class { + use ServiceMethodsSubscriberTrait; + }; + + $this->assertNull($service->setContainer($container)); + $this->assertSame([], $service::getSubscribedServices()); + } + + public function testSetContainerCalledFirstOnParent() + { + $container1 = new class([]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + $container2 = clone $container1; + + $testService = new TestService2(); + $this->assertNull($testService->setContainer($container1)); + $this->assertSame($container1, $testService->setContainer($container2)); + } +} + +class ParentTestService +{ + public function aParentService(): Service1 + { + } + + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + return $container; + } +} + +class TestService extends ParentTestService implements ServiceSubscriberInterface +{ + use ServiceMethodsSubscriberTrait; + + protected ContainerInterface $container; + + #[SubscribedService] + public function aService(): Service2 + { + return $this->container->get(__METHOD__); + } + + #[SubscribedService(nullable: true)] + public function nullableInAttribute(): Service2 + { + if (!$this->container->has(__METHOD__)) { + throw new \LogicException(); + } + + return $this->container->get(__METHOD__); + } + + #[SubscribedService] + public function nullableReturnType(): ?Service2 + { + return $this->container->get(__METHOD__); + } + + #[SubscribedService(attributes: new Required())] + public function withAttribute(): ?Service2 + { + return $this->container->get(__METHOD__); + } +} + +class ChildTestService extends TestService +{ + #[SubscribedService] + public function aChildService(): Service3 + { + return $this->container->get(__METHOD__); + } +} + +class ParentWithMagicCall +{ + public function __call($method, $args) + { + throw new \BadMethodCallException('Should not be called.'); + } + + public static function __callStatic($method, $args) + { + throw new \BadMethodCallException('Should not be called.'); + } +} + +class Service1 +{ +} + +class Service2 +{ +} + +class Service3 +{ +} + +class ParentTestService2 +{ + protected ContainerInterface $container; + + public function setContainer(ContainerInterface $container) + { + $previous = $this->container ?? null; + $this->container = $container; + + return $previous; + } +} + +class TestService2 extends ParentTestService2 implements ServiceSubscriberInterface +{ + use ServiceMethodsSubscriberTrait; +} diff --git a/Tests/Service/ServiceSubscriberTraitTest.php b/Tests/Service/ServiceSubscriberTraitTest.php index 1d86e72..bf0db2c 100644 --- a/Tests/Service/ServiceSubscriberTraitTest.php +++ b/Tests/Service/ServiceSubscriberTraitTest.php @@ -13,37 +13,32 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; -use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Component1\Dir1\Service1; -use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\OtherDir\Component1\Dir2\Service2; +use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\SubscribedService; use Symfony\Contracts\Service\ServiceLocatorTrait; use Symfony\Contracts\Service\ServiceSubscriberInterface; use Symfony\Contracts\Service\ServiceSubscriberTrait; -use Symfony\Contracts\Tests\Fixtures\TestServiceSubscriberUnion; +/** + * @group legacy + */ class ServiceSubscriberTraitTest extends TestCase { - /** - * @group legacy - */ - public function testLegacyMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices() + public static function setUpBeforeClass(): void { - $expected = [LegacyTestService::class.'::aService' => '?'.Service2::class]; - - $this->assertEquals($expected, LegacyChildTestService::getSubscribedServices()); + class_exists(LegacyTestService::class); } - /** - * @requires PHP 8 - */ public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices() { $expected = [ - TestService::class.'::aService' => Service2::class, - TestService::class.'::nullableService' => '?'.Service2::class, + LegacyTestService::class.'::aService' => Service2::class, + LegacyTestService::class.'::nullableInAttribute' => '?'.Service2::class, + LegacyTestService::class.'::nullableReturnType' => '?'.Service2::class, + new SubscribedService(LegacyTestService::class.'::withAttribute', Service2::class, true, new Required()), ]; - $this->assertEquals($expected, ChildTestService::getSubscribedServices()); + $this->assertEquals($expected, LegacyChildTestService::getSubscribedServices()); } public function testSetContainerIsCalledOnParent() @@ -52,7 +47,7 @@ public function testSetContainerIsCalledOnParent() use ServiceLocatorTrait; }; - $this->assertSame($container, (new TestService())->setContainer($container)); + $this->assertSame($container, (new LegacyTestService())->setContainer($container)); } public function testParentNotCalledIfHasMagicCall() @@ -60,8 +55,10 @@ public function testParentNotCalledIfHasMagicCall() $container = new class([]) implements ContainerInterface { use ServiceLocatorTrait; }; - $service = new class() extends ParentWithMagicCall { + $service = new class extends LegacyParentWithMagicCall { use ServiceSubscriberTrait; + + private $container; }; $this->assertNull($service->setContainer($container)); @@ -73,8 +70,10 @@ public function testParentNotCalledIfNoParent() $container = new class([]) implements ContainerInterface { use ServiceLocatorTrait; }; - $service = new class() { + $service = new class { use ServiceSubscriberTrait; + + private $container; }; $this->assertNull($service->setContainer($container)); @@ -88,107 +87,10 @@ public function testSetContainerCalledFirstOnParent() }; $container2 = clone $container1; - $testService = new TestService2(); + $testService = new class extends LegacyParentTestService2 implements ServiceSubscriberInterface { + use ServiceSubscriberTrait; + }; $this->assertNull($testService->setContainer($container1)); $this->assertSame($container1, $testService->setContainer($container2)); } - - /** - * @requires PHP 8 - * - * @group legacy - */ - public function testMethodsWithUnionReturnTypesAreIgnored() - { - $expected = [TestServiceSubscriberUnion::class.'::method1' => '?Symfony\Contracts\Tests\Fixtures\Service1']; - - $this->assertEquals($expected, TestServiceSubscriberUnion::getSubscribedServices()); - } -} - -class ParentTestService -{ - public function aParentService(): Service1 - { - } - - public function setContainer(ContainerInterface $container) - { - return $container; - } -} - -class LegacyTestService extends ParentTestService implements ServiceSubscriberInterface -{ - use ServiceSubscriberTrait; - - public function aService(): Service2 - { - } -} - -class LegacyChildTestService extends LegacyTestService -{ - public function aChildService(): Service3 - { - } -} - -class TestService extends ParentTestService implements ServiceSubscriberInterface -{ - use ServiceSubscriberTrait; - - #[SubscribedService] - public function aService(): Service2 - { - } - - #[SubscribedService] - public function nullableService(): ?Service2 - { - } -} - -class ChildTestService extends TestService -{ - #[SubscribedService] - public function aChildService(): Service3 - { - } -} - -class ParentWithMagicCall -{ - public function __call($method, $args) - { - throw new \BadMethodCallException('Should not be called.'); - } - - public static function __callStatic($method, $args) - { - throw new \BadMethodCallException('Should not be called.'); - } -} - -class Service3 -{ -} - -class ParentTestService2 -{ - /** @var ContainerInterface */ - protected $container; - - public function setContainer(ContainerInterface $container) - { - $previous = $this->container; - $this->container = $container; - - return $previous; - } -} - -class TestService2 extends ParentTestService2 implements ServiceSubscriberInterface -{ - use ServiceSubscriberTrait; } diff --git a/Translation/LocaleAwareInterface.php b/Translation/LocaleAwareInterface.php index 693f92b..db40ba1 100644 --- a/Translation/LocaleAwareInterface.php +++ b/Translation/LocaleAwareInterface.php @@ -16,7 +16,7 @@ interface LocaleAwareInterface /** * Sets the current locale. * - * @param string $locale The locale + * @return void * * @throws \InvalidArgumentException If the locale contains invalid characters */ @@ -24,8 +24,6 @@ public function setLocale(string $locale); /** * Returns the current locale. - * - * @return string */ - public function getLocale(); + public function getLocale(): string; } diff --git a/Translation/README.md b/Translation/README.md index 42e5c51..b211d58 100644 --- a/Translation/README.md +++ b/Translation/README.md @@ -3,7 +3,7 @@ Symfony Translation Contracts A set of abstractions extracted out of the Symfony components. -Can be used to build on semantics that the Symfony components proved useful - and +Can be used to build on semantics that the Symfony components proved useful and that already have battle tested implementations. See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/Translation/Test/TranslatorTest.php b/Translation/Test/TranslatorTest.php index dff86dd..5342f5b 100644 --- a/Translation/Test/TranslatorTest.php +++ b/Translation/Test/TranslatorTest.php @@ -11,7 +11,10 @@ namespace Symfony\Contracts\Translation\Test; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; +use Symfony\Component\Translation\TranslatableMessage; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; @@ -30,7 +33,7 @@ */ class TranslatorTest extends TestCase { - private $defaultLocale; + private string $defaultLocale; protected function setUp(): void { @@ -43,12 +46,9 @@ protected function tearDown(): void \Locale::setDefault($this->defaultLocale); } - /** - * @return TranslatorInterface - */ - public function getTranslator() + public function getTranslator(): TranslatorInterface { - return new class() implements TranslatorInterface { + return new class implements TranslatorInterface { use TranslatorTrait; }; } @@ -56,6 +56,7 @@ public function getTranslator() /** * @dataProvider getTransTests */ + #[DataProvider('getTransTests')] public function testTrans($expected, $id, $parameters) { $translator = $this->getTranslator(); @@ -66,6 +67,7 @@ public function testTrans($expected, $id, $parameters) /** * @dataProvider getTransChoiceTests */ + #[DataProvider('getTransChoiceTests')] public function testTransChoiceWithExplicitLocale($expected, $id, $number) { $translator = $this->getTranslator(); @@ -78,6 +80,8 @@ public function testTransChoiceWithExplicitLocale($expected, $id, $number) * * @dataProvider getTransChoiceTests */ + #[DataProvider('getTransChoiceTests')] + #[RequiresPhpExtension('intl')] public function testTransChoiceWithDefaultLocale($expected, $id, $number) { $translator = $this->getTranslator(); @@ -88,6 +92,7 @@ public function testTransChoiceWithDefaultLocale($expected, $id, $number) /** * @dataProvider getTransChoiceTests */ + #[DataProvider('getTransChoiceTests')] public function testTransChoiceWithEnUsPosix($expected, $id, $number) { $translator = $this->getTranslator(); @@ -106,6 +111,7 @@ public function testGetSetLocale() /** * @requires extension intl */ + #[RequiresPhpExtension('intl')] public function testGetLocaleReturnsDefaultLocaleIfNotSet() { $translator = $this->getTranslator(); @@ -119,10 +125,12 @@ public function testGetLocaleReturnsDefaultLocaleIfNotSet() public static function getTransTests() { - return [ - ['Symfony is great!', 'Symfony is great!', []], - ['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']], - ]; + yield ['Symfony is great!', 'Symfony is great!', []]; + yield ['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']]; + + if (class_exists(TranslatableMessage::class)) { + yield ['He said "Symfony is awesome!".', 'He said "%what%".', ['%what%' => new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'])]]; + } } public static function getTransChoiceTests() @@ -142,6 +150,7 @@ public static function getTransChoiceTests() /** * @dataProvider getInterval */ + #[DataProvider('getInterval')] public function testInterval($expected, $number, $interval) { $translator = $this->getTranslator(); @@ -167,6 +176,7 @@ public static function getInterval() /** * @dataProvider getChooseTests */ + #[DataProvider('getChooseTests')] public function testChoose($expected, $id, $number, $locale = null) { $translator = $this->getTranslator(); @@ -184,11 +194,13 @@ public function testReturnMessageIfExactlyOneStandardRuleIsGiven() /** * @dataProvider getNonMatchingMessages */ + #[DataProvider('getNonMatchingMessages')] public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) { - $this->expectException(\InvalidArgumentException::class); $translator = $this->getTranslator(); + $this->expectException(\InvalidArgumentException::class); + $translator->trans($id, ['%count%' => $number]); } @@ -298,6 +310,7 @@ public static function getChooseTests() /** * @dataProvider failingLangcodes */ + #[DataProvider('failingLangcodes')] public function testFailedLangcodes($nplural, $langCodes) { $matrix = $this->generateTestData($langCodes); @@ -307,6 +320,7 @@ public function testFailedLangcodes($nplural, $langCodes) /** * @dataProvider successLangcodes */ + #[DataProvider('successLangcodes')] public function testLangcodes($nplural, $langCodes) { $matrix = $this->generateTestData($langCodes); @@ -317,10 +331,8 @@ public function testLangcodes($nplural, $langCodes) * This array should contain all currently known langcodes. * * As it is impossible to have this ever complete we should try as hard as possible to have it almost complete. - * - * @return array */ - public static function successLangcodes() + public static function successLangcodes(): array { return [ ['1', ['ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky']], @@ -339,7 +351,7 @@ public static function successLangcodes() * * @return array with nplural together with langcodes */ - public static function failingLangcodes() + public static function failingLangcodes(): array { return [ ['1', ['fa']], @@ -353,25 +365,24 @@ public static function failingLangcodes() /** * We validate only on the plural coverage. Thus the real rules is not tested. * - * @param string $nplural Plural expected - * @param array $matrix Containing langcodes and their plural index values - * @param bool $expectSuccess + * @param string $nplural Plural expected + * @param array $matrix Containing langcodes and their plural index values */ - protected function validateMatrix($nplural, $matrix, $expectSuccess = true) + protected function validateMatrix(string $nplural, array $matrix, bool $expectSuccess = true) { foreach ($matrix as $langCode => $data) { $indexes = array_flip($data); if ($expectSuccess) { $this->assertCount($nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms."); } else { - $this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); + $this->assertNotCount($nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms."); } } } protected function generateTestData($langCodes) { - $translator = new class() { + $translator = new class { use TranslatorTrait { getPluralizationRule as public; } diff --git a/Translation/TranslatorInterface.php b/Translation/TranslatorInterface.php index 85ca166..7fa6987 100644 --- a/Translation/TranslatorInterface.php +++ b/Translation/TranslatorInterface.php @@ -13,8 +13,6 @@ /** * @author Fabien Potencier - * - * @method string getLocale() Returns the default locale */ interface TranslatorInterface { @@ -59,9 +57,12 @@ interface TranslatorInterface * @param string|null $domain The domain for the message or null to use the default * @param string|null $locale The locale or null to use the default * - * @return string - * * @throws \InvalidArgumentException If the locale contains invalid characters */ - public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null); + public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string; + + /** + * Returns the default locale. + */ + public function getLocale(): string; } diff --git a/Translation/TranslatorTrait.php b/Translation/TranslatorTrait.php index ac01d73..afedd99 100644 --- a/Translation/TranslatorTrait.php +++ b/Translation/TranslatorTrait.php @@ -20,35 +20,33 @@ */ trait TranslatorTrait { - private $locale; + private ?string $locale = null; /** - * {@inheritdoc} + * @return void */ public function setLocale(string $locale) { $this->locale = $locale; } - /** - * {@inheritdoc} - * - * @return string - */ - public function getLocale() + public function getLocale(): string { return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'); } - /** - * {@inheritdoc} - */ public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { if (null === $id || '' === $id) { return ''; } + foreach ($parameters as $k => $v) { + if ($v instanceof TranslatableInterface) { + $parameters[$k] = $v->trans($this, $locale); + } + } + if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) { return strtr($id, $parameters); } @@ -119,7 +117,7 @@ public function trans(?string $id, array $parameters = [], ?string $domain = nul return strtr($standardRules[0], $parameters); } - $message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); + $message = \sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); if (class_exists(InvalidArgumentException::class)) { throw new InvalidArgumentException($message); @@ -142,121 +140,92 @@ private function getPluralizationRule(float $number, string $locale): int { $number = abs($number); - switch ('pt_BR' !== $locale && 'en_US_POSIX' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) { - case 'af': - case 'bn': - case 'bg': - case 'ca': - case 'da': - case 'de': - case 'el': - case 'en': - case 'en_US_POSIX': - case 'eo': - case 'es': - case 'et': - case 'eu': - case 'fa': - case 'fi': - case 'fo': - case 'fur': - case 'fy': - case 'gl': - case 'gu': - case 'ha': - case 'he': - case 'hu': - case 'is': - case 'it': - case 'ku': - case 'lb': - case 'ml': - case 'mn': - case 'mr': - case 'nah': - case 'nb': - case 'ne': - case 'nl': - case 'nn': - case 'no': - case 'oc': - case 'om': - case 'or': - case 'pa': - case 'pap': - case 'ps': - case 'pt': - case 'so': - case 'sq': - case 'sv': - case 'sw': - case 'ta': - case 'te': - case 'tk': - case 'ur': - case 'zu': - return (1 == $number) ? 0 : 1; - - case 'am': - case 'bh': - case 'fil': - case 'fr': - case 'gun': - case 'hi': - case 'hy': - case 'ln': - case 'mg': - case 'nso': - case 'pt_BR': - case 'ti': - case 'wa': - return ($number < 2) ? 0 : 1; - - case 'be': - case 'bs': - case 'hr': - case 'ru': - case 'sh': - case 'sr': - case 'uk': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'cs': - case 'sk': - return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2); - - case 'ga': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2); - - case 'lt': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'sl': - return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)); - - case 'mk': - return (1 == $number % 10) ? 0 : 1; - - case 'mt': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)); - - case 'lv': - return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2); - - case 'pl': - return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2); - - case 'cy': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)); - - case 'ro': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2); - - case 'ar': - return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))); - - default: - return 0; - } + return match ('pt_BR' !== $locale && 'en_US_POSIX' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) { + 'af', + 'bn', + 'bg', + 'ca', + 'da', + 'de', + 'el', + 'en', + 'en_US_POSIX', + 'eo', + 'es', + 'et', + 'eu', + 'fa', + 'fi', + 'fo', + 'fur', + 'fy', + 'gl', + 'gu', + 'ha', + 'he', + 'hu', + 'is', + 'it', + 'ku', + 'lb', + 'ml', + 'mn', + 'mr', + 'nah', + 'nb', + 'ne', + 'nl', + 'nn', + 'no', + 'oc', + 'om', + 'or', + 'pa', + 'pap', + 'ps', + 'pt', + 'so', + 'sq', + 'sv', + 'sw', + 'ta', + 'te', + 'tk', + 'ur', + 'zu' => (1 == $number) ? 0 : 1, + 'am', + 'bh', + 'fil', + 'fr', + 'gun', + 'hi', + 'hy', + 'ln', + 'mg', + 'nso', + 'pt_BR', + 'ti', + 'wa' => ($number < 2) ? 0 : 1, + 'be', + 'bs', + 'hr', + 'ru', + 'sh', + 'sr', + 'uk' => ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2), + 'cs', + 'sk' => (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2), + 'ga' => (1 == $number) ? 0 : ((2 == $number) ? 1 : 2), + 'lt' => ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2), + 'sl' => (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)), + 'mk' => (1 == $number % 10) ? 0 : 1, + 'mt' => (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)), + 'lv' => (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2), + 'pl' => (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2), + 'cy' => (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)), + 'ro' => (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2), + 'ar' => (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))), + default => 0, + }; } } diff --git a/Translation/composer.json b/Translation/composer.json index 65fe243..b7220b8 100644 --- a/Translation/composer.json +++ b/Translation/composer.json @@ -16,18 +16,18 @@ } ], "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/translation-implementation": "" + "php": ">=8.1" }, "autoload": { - "psr-4": { "Symfony\\Contracts\\Translation\\": "" } + "psr-4": { "Symfony\\Contracts\\Translation\\": "" }, + "exclude-from-classmap": [ + "/Test/" + ] }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" }, "thanks": { "name": "symfony/contracts", diff --git a/composer.json b/composer.json index f70b8b7..be90b35 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "symfony/contracts", "type": "library", "description": "A set of abstractions extracted out of the Symfony components", - "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards", "dev"], "homepage": "https://symfony.com", "license": "MIT", "authors": [ @@ -16,9 +16,9 @@ } ], "require": { - "php": ">=7.2.5", - "psr/cache": "^1.0|^2.0|^3.0", - "psr/container": "^1.1", + "php": ">=8.1", + "psr/cache": "^3.0", + "psr/container": "^1.1|^2.0", "psr/event-dispatcher": "^1.0" }, "require-dev": { @@ -35,13 +35,6 @@ "symfony/service-contracts": "self.version", "symfony/translation-contracts": "self.version" }, - "suggest": { - "symfony/cache-implementation": "", - "symfony/event-dispatcher-implementation": "", - "symfony/http-client-implementation": "", - "symfony/service-implementation": "", - "symfony/translation-implementation": "" - }, "autoload": { "psr-4": { "Symfony\\Contracts\\": "" }, "files": [ "Deprecation/function.php" ], @@ -52,7 +45,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-main": "2.5-dev" + "dev-main": "3.6-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fd93d02..947db86 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - - + + ./ - - ./Tests - ./Service/Test/ - ./Translation/Test/ - ./vendor - - - + + + ./Tests + ./Service/Test/ + ./Translation/Test/ + ./vendor + +