feature #33497 [Contracts] Add parameter type declarations to contracts (derrabus)

This PR was merged into the 5.0-dev branch.

Discussion
----------

[Contracts] Add parameter type declarations to contracts

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #32179
| License       | MIT
| Doc PR        | N/A

This PR proposes to create a php 7.2 version of the contracts that maintains BC with Symfony 4. The PR suggests to bump the contracts version to ~~1.2~~ 2.0 on the master branch. We would still be able to maintain the contracts 1.1 branch on Symfony's 4.4 branch, should we need to patch the current contracts in the future.

This move would allow us to add parameter type declarations to existing contracts interfaces and make use of them in Symfony 5. Especially the Translation and EventDispatcher components benefit a lot from this bump, imho.

Contracts that will be added on the road to Symfony 6 wouldn't be restricted to the capabilities of php 7.1, which would be another benefit in my opinion.

~~<sup>1</sup> Test currently fail because the translator is called with `null` as translation key. That possibility should be deprecated imho.~~

Commits
-------

4de3773979 Add parameter type declarations to contracts.
This commit is contained in:
Fabien Potencier 2019-11-09 12:23:17 +01:00
commit 9aa7492fdc
29 changed files with 90 additions and 184 deletions

View File

@ -23,9 +23,10 @@
"twig/twig": "^2.10|^3.0",
"psr/cache": "~1.0",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
"psr/link": "^1.0",
"psr/log": "~1.0",
"symfony/contracts": "^1.1.7|^2",
"symfony/contracts": "^2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-icu": "~1.0",

View File

@ -108,7 +108,7 @@ class DataCollectorTranslatorPassTest extends TestCase
class TranslatorWithTranslatorBag implements TranslatorInterface
{
public function trans($id, array $parameters = [], $domain = null, $locale = null): string
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string
{
}
}

View File

@ -18,7 +18,6 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\Service\ResetInterface;
/**
@ -129,12 +128,8 @@ class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterfa
/**
* {@inheritdoc}
*/
public function dispatch($event, string $eventName = null): object
public function dispatch(object $event, string $eventName = null): object
{
if (!\is_object($event)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s::dispatch()" must be an object, %s given.', EventDispatcherInterface::class, \gettype($event)));
}
$eventName = $eventName ?? \get_class($event);
if (null === $this->callStack) {
@ -143,7 +138,7 @@ class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterfa
$currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : '';
if (null !== $this->logger && ($event instanceof Event || $event instanceof StoppableEventInterface) && $event->isPropagationStopped()) {
if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
$this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));
}

View File

@ -15,7 +15,6 @@ use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\VarDumper\Caster\ClassStub;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @author Fabien Potencier <fabien@symfony.com>
@ -121,7 +120,7 @@ final class WrappedListener
$e->stop();
}
if (($event instanceof Event || $event instanceof StoppableEventInterface) && $event->isPropagationStopped()) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
$this->stoppedPropagation = true;
}
}

View File

@ -13,7 +13,6 @@ namespace Symfony\Component\EventDispatcher;
use Psr\EventDispatcher\StoppableEventInterface;
use Symfony\Component\EventDispatcher\Debug\WrappedListener;
use Symfony\Contracts\EventDispatcher\Event;
/**
* The EventDispatcherInterface is the central point of Symfony's event listener system.
@ -46,12 +45,8 @@ class EventDispatcher implements EventDispatcherInterface
/**
* {@inheritdoc}
*/
public function dispatch($event, string $eventName = null): object
public function dispatch(object $event, string $eventName = null): object
{
if (!\is_object($event)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s::dispatch()" must be an object, %s given.', EventDispatcherInterface::class, \gettype($event)));
}
$eventName = $eventName ?? \get_class($event);
if (null !== $this->optimized && null !== $eventName) {
@ -226,7 +221,7 @@ class EventDispatcher implements EventDispatcherInterface
*/
protected function callListeners(iterable $listeners, string $eventName, object $event)
{
$stoppable = $event instanceof Event || $event instanceof StoppableEventInterface;
$stoppable = $event instanceof StoppableEventInterface;
foreach ($listeners as $listener) {
if ($stoppable && $event->isPropagationStopped()) {

View File

@ -22,11 +22,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEvent
*/
interface EventDispatcherInterface extends ContractsEventDispatcherInterface
{
/**
* {@inheritdoc}
*/
public function dispatch($event, string $eventName = null): object;
/**
* Adds an event listener that listens on the specified events.
*

View File

@ -28,7 +28,7 @@ class ImmutableEventDispatcher implements EventDispatcherInterface
/**
* {@inheritdoc}
*/
public function dispatch($event, string $eventName = null): object
public function dispatch(object $event, string $eventName = null): object
{
return $this->dispatcher->dispatch($event, $eventName);
}

View File

@ -17,7 +17,7 @@
],
"require": {
"php": "^7.2.9",
"symfony/event-dispatcher-contracts": "^1.1|^2"
"symfony/event-dispatcher-contracts": "^2"
},
"require-dev": {
"symfony/dependency-injection": "^4.4|^5.0",
@ -33,7 +33,7 @@
},
"provide": {
"psr/event-dispatcher-implementation": "1.0",
"symfony/event-dispatcher-implementation": "1.1"
"symfony/event-dispatcher-implementation": "2.0"
},
"suggest": {
"symfony/dependency-injection": "",

View File

@ -23,7 +23,7 @@
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0",
"symfony/property-access": "^5.0",
"symfony/service-contracts": "~1.1"
"symfony/service-contracts": "^1.1|^2"
},
"require-dev": {
"doctrine/collections": "~1.0",

View File

@ -20,7 +20,7 @@
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^1.1|^2.0"
"symfony/translation-contracts": "^1.1|^2"
},
"autoload": {
"psr-4": { "Symfony\\Component\\String\\": "" },

View File

@ -47,9 +47,9 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
{
$trans = $this->translator->trans($id, $parameters, $domain, $locale);
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters);
return $trans;
@ -58,7 +58,7 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function setLocale(string $locale)
{
$this->translator->setLocale($locale);
}
@ -119,13 +119,12 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
return $this->messages;
}
private function collectMessage(?string $locale, ?string $domain, ?string $id, string $translation, ?array $parameters = [])
private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = [])
{
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->translator->getCatalogue($locale);
$locale = $catalogue->getLocale();
$fallbackLocale = null;

View File

@ -44,9 +44,9 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
{
$trans = $this->translator->trans($id, $parameters, $domain, $locale);
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);
$this->log($id, $domain, $locale);
return $trans;
@ -55,7 +55,7 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function setLocale(string $locale)
{
$prev = $this->translator->getLocale();
$this->translator->setLocale($locale);
@ -107,13 +107,12 @@ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface,
/**
* Logs for missing translations.
*/
private function log(?string $id, ?string $domain, ?string $locale)
private function log(string $id, ?string $domain, ?string $locale)
{
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
$catalogue = $this->translator->getCatalogue($locale);
if ($catalogue->defines($id, $domain)) {
return;

View File

@ -144,7 +144,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function setLocale(string $locale)
{
$this->assertValidLocale($locale);
$this->locale = $locale;
@ -190,9 +190,9 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
{
if ('' === $id = (string) $id) {
if (null === $id || '' === $id) {
return '';
}

View File

@ -18,7 +18,7 @@
"require": {
"php": "^7.2.9",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^1.1.6|^2"
"symfony/translation-contracts": "^2"
},
"require-dev": {
"symfony/config": "^4.4|^5.0",
@ -40,7 +40,7 @@
"symfony/yaml": "<4.4"
},
"provide": {
"symfony/translation-implementation": "1.0"
"symfony/translation-implementation": "2.0"
},
"suggest": {
"symfony/config": "",

View File

@ -16,7 +16,7 @@
}
],
"require": {
"php": "^7.1.3",
"php": "^7.2.9",
"psr/cache": "^1.0"
},
"suggest": {
@ -28,7 +28,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
"dev-master": "2.0-dev"
}
}
}

View File

@ -13,84 +13,42 @@ namespace Symfony\Contracts\EventDispatcher;
use Psr\EventDispatcher\StoppableEventInterface;
if (interface_exists(StoppableEventInterface::class)) {
/**
* Event is the base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class Event implements StoppableEventInterface
{
private $propagationStopped = false;
/**
* Event is the base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
* {@inheritdoc}
*/
class Event implements StoppableEventInterface
public function isPropagationStopped(): bool
{
private $propagationStopped = false;
/**
* Returns whether further event listeners should be triggered.
*/
public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
public function stopPropagation(): void
{
$this->propagationStopped = true;
}
return $this->propagationStopped;
}
} else {
/**
* Event is the base class for classes containing event data.
* Stops the propagation of the event to further event listeners.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*
* @author Guilherme Blanco <guilhermeblanco@hotmail.com>
* @author Jonathan Wage <jonwage@gmail.com>
* @author Roman Borschel <roman@code-factory.org>
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
class Event
public function stopPropagation(): void
{
private $propagationStopped = false;
/**
* Returns whether further event listeners should be triggered.
*/
public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
public function stopPropagation(): void
{
$this->propagationStopped = true;
}
$this->propagationStopped = true;
}
}

View File

@ -13,46 +13,19 @@ namespace Symfony\Contracts\EventDispatcher;
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
if (interface_exists(PsrEventDispatcherInterface::class)) {
/**
* Allows providing hooks on domain-specific lifecycles by dispatching events.
*/
interface EventDispatcherInterface extends PsrEventDispatcherInterface
{
/**
* Allows providing hooks on domain-specific lifecycles by dispatching events.
* Dispatches an event to all registered listeners.
*
* @param object $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
*/
interface EventDispatcherInterface extends PsrEventDispatcherInterface
{
/**
* Dispatches an event to all registered listeners.
*
* For BC with Symfony 4, the $eventName argument is not declared explicitly on the
* signature of the method. Implementations that are not bound by this BC constraint
* MUST declare it explicitly, as allowed by PHP.
*
* @param object $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
*/
public function dispatch($event/*, string $eventName = null*/);
}
} else {
/**
* Allows providing hooks on domain-specific lifecycles by dispatching events.
*/
interface EventDispatcherInterface
{
/**
* Dispatches an event to all registered listeners.
*
* For BC with Symfony 4, the $eventName argument is not declared explicitly on the
* signature of the method. Implementations that are not bound by this BC constraint
* MUST declare it explicitly, as allowed by PHP.
*
* @param object $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
*/
public function dispatch($event/*, string $eventName = null*/);
}
public function dispatch(object $event, string $eventName = null): object;
}

View File

@ -16,10 +16,10 @@
}
],
"require": {
"php": "^7.1.3"
"php": "^7.2.9",
"psr/event-dispatcher": "^1"
},
"suggest": {
"psr/event-dispatcher": "",
"symfony/event-dispatcher-implementation": ""
},
"autoload": {
@ -28,7 +28,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
"dev-master": "2.0-dev"
}
}
}

View File

@ -21,8 +21,6 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
* MUST be thrown by the destructor unless one was already thrown by another method.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @experimental in 1.1
*/
interface ChunkInterface
{

View File

@ -20,8 +20,6 @@ use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
* @see HttpClientTestCase for a reference test suite
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @experimental in 1.1
*/
interface HttpClientInterface
{

View File

@ -22,8 +22,6 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
* A (lazily retrieved) HTTP response.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @experimental in 1.1
*/
interface ResponseInterface
{

View File

@ -15,8 +15,6 @@ namespace Symfony\Contracts\HttpClient;
* Yields response chunks, returned by HttpClientInterface::stream().
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @experimental in 1.1
*/
interface ResponseStreamInterface extends \Iterator
{

View File

@ -16,7 +16,7 @@
}
],
"require": {
"php": "^7.1.3"
"php": "^7.2.9"
},
"suggest": {
"symfony/http-client-implementation": ""
@ -27,7 +27,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
"dev-master": "2.0-dev"
}
}
}

View File

@ -16,7 +16,7 @@
}
],
"require": {
"php": "^7.1.3",
"php": "^7.2.9",
"psr/container": "^1.0"
},
"suggest": {
@ -28,7 +28,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
"dev-master": "2.0-dev"
}
}
}

View File

@ -20,7 +20,7 @@ interface LocaleAwareInterface
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function setLocale($locale);
public function setLocale(string $locale);
/**
* Returns the current locale.

View File

@ -61,5 +61,5 @@ interface TranslatorInterface
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null);
public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null);
}

View File

@ -25,9 +25,9 @@ trait TranslatorTrait
/**
* {@inheritdoc}
*/
public function setLocale($locale)
public function setLocale(string $locale)
{
$this->locale = (string) $locale;
$this->locale = $locale;
}
/**
@ -41,9 +41,9 @@ trait TranslatorTrait
/**
* {@inheritdoc}
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null)
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null): string
{
if ('' === $id = (string) $id) {
if (null === $id || '' === $id) {
return '';
}
@ -52,7 +52,7 @@ trait TranslatorTrait
}
$number = (float) $parameters['%count%'];
$locale = (string) $locale ?: $this->getLocale();
$locale = $locale ?: $this->getLocale();
$parts = [];
if (preg_match('/^\|++$/', $id)) {

View File

@ -16,7 +16,7 @@
}
],
"require": {
"php": "^7.1.3"
"php": "^7.2.9"
},
"suggest": {
"symfony/translation-implementation": ""
@ -27,7 +27,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
"dev-master": "2.0-dev"
}
}
}

View File

@ -16,9 +16,10 @@
}
],
"require": {
"php": "^7.1.3",
"php": "^7.2.9",
"psr/cache": "^1.0",
"psr/container": "^1.0"
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0"
},
"require-dev": {
"symfony/polyfill-intl-idn": "^1.10"
@ -31,7 +32,6 @@
"symfony/translation-contracts": "self.version"
},
"suggest": {
"psr/event-dispatcher": "When using the EventDispatcher contracts",
"symfony/cache-implementation": "",
"symfony/event-dispatcher-implementation": "",
"symfony/http-client-implementation": "",
@ -47,7 +47,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
"dev-master": "2.0-dev"
}
}
}