diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c22dd6c1..5109833f 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -22,16 +22,35 @@ jobs: - "8.0" # disabled for now as phpspec/prophecy does not allow 8.1 # - "8.1" + dependencies: [highest] + include: + - php-version: "7.2" + dependencies: lowest + - php-version: "8.0" + dependencies: lowest steps: - name: "Checkout" uses: "actions/checkout@v2" - - name: "Install PHP 7+" + - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: "none" php-version: "${{ matrix.php-version }}" + extensions: mongodb, redis, amqp + + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + + - name: Runs Elasticsearch + uses: elastic/elastic-github-actions/elasticsearch@master + with: + stack-version: 7.6.0 - name: Get composer cache directory id: composercache @@ -44,9 +63,13 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- + - name: "Handle lowest dependencies update" + if: "contains(matrix.dependencies, 'lowest')" + run: "echo \"COMPOSER_FLAGS=$COMPOSER_FLAGS --prefer-lowest\" >> $GITHUB_ENV" + - name: "Install latest dependencies" run: | composer update ${{ env.COMPOSER_FLAGS }} - name: "Run tests" - run: "composer test" + run: "composer exec phpunit -- --verbose" diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 1fe45df7..d343ffec 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,16 +1,19 @@ parameters: - level: 3 + level: 5 + + treatPhpDocTypesAsCertain: false paths: - src/ # - tests/ - ignoreErrors: - '#zend_monitor_|ZEND_MONITOR_#' - - '#RollbarNotifier#' - - '#Predis\\Client#' - '#^Cannot call method ltrim\(\) on int\|false.$#' - - '#^Access to an undefined property Raven_Client::\$context.$#' - '#MongoDB\\(Client|Collection)#' - - '#Gelf\\IMessagePublisher#' + - message: '#Return type \(string\) of method Monolog\\Formatter\\LineFormatter::normalizeException\(\) should be compatible with return type \(array\) of method Monolog\\Formatter\\NormalizerFormatter::normalizeException\(\)#' + paths: + - src/Monolog/Formatter/LineFormatter.php + - message: '#Method Monolog\\Handler\\LogglyHandler::loadCurlHandle\(\) never returns resource so it can be removed from the return typehint.#' + paths: + - src/Monolog/Handler/LogglyHandler.php diff --git a/src/Monolog/ErrorHandler.php b/src/Monolog/ErrorHandler.php index 89381b9a..3cf351a7 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -58,6 +58,7 @@ class ErrorHandler */ public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self { + /** @phpstan-ignore-next-line */ $handler = new static($logger); if ($errorLevelMap !== false) { $handler->registerErrorHandler($errorLevelMap); @@ -74,7 +75,9 @@ class ErrorHandler public function registerExceptionHandler($levelMap = [], $callPrevious = true): self { - $prev = set_exception_handler([$this, 'handleException']); + $prev = set_exception_handler(function (\Throwable $e): void { + $this->handleException($e); + }); $this->uncaughtExceptionLevelMap = $levelMap; foreach ($this->defaultExceptionLevelMap() as $class => $level) { if (!isset($this->uncaughtExceptionLevelMap[$class])) { @@ -145,11 +148,7 @@ class ErrorHandler ]; } - /** - * @private - * @param \Exception $e - */ - public function handleException($e) + private function handleException(\Throwable $e) { $level = LogLevel::ERROR; foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) { diff --git a/src/Monolog/Formatter/GelfMessageFormatter.php b/src/Monolog/Formatter/GelfMessageFormatter.php index 271628a3..95d7c1d8 100644 --- a/src/Monolog/Formatter/GelfMessageFormatter.php +++ b/src/Monolog/Formatter/GelfMessageFormatter.php @@ -134,6 +134,7 @@ class GelfMessageFormatter extends NormalizerFormatter $message->setAdditional($this->contextPrefix . $key, $val); } + /** @phpstan-ignore-next-line */ if (null === $message->getFile() && isset($record['context']['exception']['file'])) { if (preg_match("/^(.+):([0-9]+)$/", $record['context']['exception']['file'], $matches)) { $message->setFile($matches[1]); diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index c367baa6..59d9f0a0 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -63,8 +63,6 @@ class JsonFormatter extends NormalizerFormatter /** * {@inheritdoc} - * - * @suppress PhanTypeComparisonToArray */ public function format(array $record): string { diff --git a/src/Monolog/Formatter/LineFormatter.php b/src/Monolog/Formatter/LineFormatter.php index c63b39d3..de27f2f7 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -126,9 +126,6 @@ class LineFormatter extends NormalizerFormatter return $this->replaceNewlines($this->convertToString($value)); } - /** - * @suppress PhanParamSignatureMismatch - */ protected function normalizeException(\Throwable $e, int $depth = 0): string { $str = $this->formatException($e); diff --git a/src/Monolog/Formatter/MongoDBFormatter.php b/src/Monolog/Formatter/MongoDBFormatter.php index 9d41a505..d85c58c6 100644 --- a/src/Monolog/Formatter/MongoDBFormatter.php +++ b/src/Monolog/Formatter/MongoDBFormatter.php @@ -118,7 +118,7 @@ class MongoDBFormatter implements FormatterInterface private function getMongoDbDateTime(\DateTimeInterface $value): UTCDateTime { - return new UTCDateTime((int) (string) floor($value->format('U.u') * 1000)); + return new UTCDateTime((int) floor(((float) $value->format('U.u')) * 1000)); } /** @@ -130,7 +130,7 @@ class MongoDBFormatter implements FormatterInterface */ private function legacyGetMongoDbDateTime(\DateTimeInterface $value): UTCDateTime { - $milliseconds = floor($value->format('U.u') * 1000); + $milliseconds = floor(((float) $value->format('U.u')) * 1000); $milliseconds = (PHP_INT_SIZE == 8) //64-bit OS? ? (int) $milliseconds diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 41968310..07f78956 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -24,10 +24,14 @@ class NormalizerFormatter implements FormatterInterface { public const SIMPLE_DATE = "Y-m-d\TH:i:sP"; + /** @var string */ protected $dateFormat; + /** @var int */ protected $maxNormalizeDepth = 9; + /** @var int */ protected $maxNormalizeItemCount = 1000; + /** @var int */ private $jsonEncodeOptions = Utils::DEFAULT_JSON_FLAGS; /** @@ -159,12 +163,7 @@ class NormalizerFormatter implements FormatterInterface $value = $data->__toString(); } else { // the rest is normalized by json encoding and decoding it - $encoded = $this->toJson($data, true); - if ($encoded === false) { - $value = 'JSON_ERROR'; - } else { - $value = json_decode($encoded, true); - } + $value = json_decode($this->toJson($data, true), true); } return [Utils::getClass($data) => $value]; @@ -248,12 +247,12 @@ class NormalizerFormatter implements FormatterInterface return $date->format($this->dateFormat); } - public function addJsonEncodeOption($option) + public function addJsonEncodeOption(int $option) { $this->jsonEncodeOptions |= $option; } - public function removeJsonEncodeOption($option) + public function removeJsonEncodeOption(int $option) { $this->jsonEncodeOptions &= ~$option; } diff --git a/src/Monolog/Formatter/ScalarFormatter.php b/src/Monolog/Formatter/ScalarFormatter.php index 8d560e77..17402041 100644 --- a/src/Monolog/Formatter/ScalarFormatter.php +++ b/src/Monolog/Formatter/ScalarFormatter.php @@ -33,13 +33,13 @@ class ScalarFormatter extends NormalizerFormatter /** * @param mixed $value - * @return mixed + * @return string|int|bool|null */ protected function normalizeValue($value) { $normalized = $this->normalize($value); - if (is_array($normalized) || is_object($normalized)) { + if (is_array($normalized)) { return $this->toJson($normalized, true); } diff --git a/src/Monolog/Formatter/WildfireFormatter.php b/src/Monolog/Formatter/WildfireFormatter.php index 2d96739e..05919d84 100644 --- a/src/Monolog/Formatter/WildfireFormatter.php +++ b/src/Monolog/Formatter/WildfireFormatter.php @@ -105,7 +105,7 @@ class WildfireFormatter extends NormalizerFormatter /** * {@inheritdoc} - * @suppress PhanTypeMismatchReturn + * @return int|bool|string|null|array|object */ protected function normalize($data, int $depth = 0) { diff --git a/src/Monolog/Handler/DynamoDbHandler.php b/src/Monolog/Handler/DynamoDbHandler.php index 961d7acf..29f340a0 100644 --- a/src/Monolog/Handler/DynamoDbHandler.php +++ b/src/Monolog/Handler/DynamoDbHandler.php @@ -53,6 +53,7 @@ class DynamoDbHandler extends AbstractProcessingHandler */ public function __construct(DynamoDbClient $client, string $table, $level = Logger::DEBUG, bool $bubble = true) { + /** @phpstan-ignore-next-line */ if (defined('Aws\Sdk::VERSION') && version_compare(Sdk::VERSION, '3.0', '>=')) { $this->version = 3; $this->marshaler = new Marshaler; diff --git a/src/Monolog/Handler/FilterHandler.php b/src/Monolog/Handler/FilterHandler.php index 735fdfbd..2d7a648e 100644 --- a/src/Monolog/Handler/FilterHandler.php +++ b/src/Monolog/Handler/FilterHandler.php @@ -159,9 +159,14 @@ class FilterHandler extends Handler implements ProcessableHandlerInterface, Rese */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { - $this->getHandler()->setFormatter($formatter); + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); - return $this; + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } /** @@ -169,7 +174,12 @@ class FilterHandler extends Handler implements ProcessableHandlerInterface, Rese */ public function getFormatter(): FormatterInterface { - return $this->getHandler()->getFormatter(); + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } public function reset() diff --git a/src/Monolog/Handler/FingersCrossedHandler.php b/src/Monolog/Handler/FingersCrossedHandler.php index 7424b108..a63fe148 100644 --- a/src/Monolog/Handler/FingersCrossedHandler.php +++ b/src/Monolog/Handler/FingersCrossedHandler.php @@ -202,9 +202,14 @@ class FingersCrossedHandler extends Handler implements ProcessableHandlerInterfa */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { - $this->getHandler()->setFormatter($formatter); + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); - return $this; + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } /** @@ -212,6 +217,11 @@ class FingersCrossedHandler extends Handler implements ProcessableHandlerInterfa */ public function getFormatter(): FormatterInterface { - return $this->getHandler()->getFormatter(); + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } } diff --git a/src/Monolog/Handler/FormattableHandlerTrait.php b/src/Monolog/Handler/FormattableHandlerTrait.php index 00140b4e..3dbf0f65 100644 --- a/src/Monolog/Handler/FormattableHandlerTrait.php +++ b/src/Monolog/Handler/FormattableHandlerTrait.php @@ -22,13 +22,12 @@ use Monolog\Formatter\LineFormatter; trait FormattableHandlerTrait { /** - * @var FormatterInterface + * @var ?FormatterInterface */ protected $formatter; /** * {@inheritdoc} - * @suppress PhanTypeMismatchReturn */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { diff --git a/src/Monolog/Handler/HandlerWrapper.php b/src/Monolog/Handler/HandlerWrapper.php index 3f2c7a1a..e94028be 100644 --- a/src/Monolog/Handler/HandlerWrapper.php +++ b/src/Monolog/Handler/HandlerWrapper.php @@ -128,7 +128,7 @@ class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, F public function reset() { if ($this->handler instanceof ResettableInterface) { - return $this->handler->reset(); + $this->handler->reset(); } } } diff --git a/src/Monolog/Handler/LogglyHandler.php b/src/Monolog/Handler/LogglyHandler.php index c8befc28..19710d93 100644 --- a/src/Monolog/Handler/LogglyHandler.php +++ b/src/Monolog/Handler/LogglyHandler.php @@ -68,7 +68,7 @@ class LogglyHandler extends AbstractProcessingHandler protected function getCurlHandler(string $endpoint) { if (!array_key_exists($endpoint, $this->curlHandlers)) { - $this->curlHandlers[$endpoint] = $this->loadCurlHandler($endpoint); + $this->curlHandlers[$endpoint] = $this->loadCurlHandle($endpoint); } return $this->curlHandlers[$endpoint]; @@ -79,9 +79,9 @@ class LogglyHandler extends AbstractProcessingHandler * * @param string $endpoint * - * @return resource + * @return resource|\CurlHandle */ - private function loadCurlHandler(string $endpoint) + private function loadCurlHandle(string $endpoint) { $url = sprintf("https://%s/%s/%s/", static::HOST, $endpoint, $this->token); diff --git a/src/Monolog/Handler/MandrillHandler.php b/src/Monolog/Handler/MandrillHandler.php index 2a3b49c5..95c612a8 100644 --- a/src/Monolog/Handler/MandrillHandler.php +++ b/src/Monolog/Handler/MandrillHandler.php @@ -13,6 +13,7 @@ namespace Monolog\Handler; use Monolog\Logger; use Swift; +use Swift_Message; /** * MandrillHandler uses cURL to send the emails to the Mandrill API @@ -21,25 +22,27 @@ use Swift; */ class MandrillHandler extends MailHandler { + /** @var Swift_Message */ protected $message; + /** @var string */ protected $apiKey; /** - * @psalm-param Swift_Message|callable(string, array): Swift_Message $message + * @psalm-param Swift_Message|callable(): Swift_Message $message * - * @param string $apiKey A valid Mandrill API key - * @param callable|\Swift_Message $message An example message for real messages, only the body will be replaced - * @param string|int $level The minimum logging level at which this handler will be triggered - * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + * @param string $apiKey A valid Mandrill API key + * @param callable|Swift_Message $message An example message for real messages, only the body will be replaced + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not */ public function __construct(string $apiKey, $message, $level = Logger::ERROR, bool $bubble = true) { parent::__construct($level, $bubble); - if (!$message instanceof \Swift_Message && is_callable($message)) { + if (!$message instanceof Swift_Message && is_callable($message)) { $message = $message(); } - if (!$message instanceof \Swift_Message) { + if (!$message instanceof Swift_Message) { throw new \InvalidArgumentException('You must provide either a Swift_Message instance or a callable returning it'); } $this->message = $message; @@ -58,9 +61,11 @@ class MandrillHandler extends MailHandler $message = clone $this->message; $message->setBody($content, $mime); + /** @phpstan-ignore-next-line */ if (version_compare(Swift::VERSION, '6.0.0', '>=')) { $message->setDate(new \DateTimeImmutable()); } else { + /** @phpstan-ignore-next-line */ $message->setDate(time()); } diff --git a/src/Monolog/Handler/OverflowHandler.php b/src/Monolog/Handler/OverflowHandler.php index dbe9b22d..4c309865 100644 --- a/src/Monolog/Handler/OverflowHandler.php +++ b/src/Monolog/Handler/OverflowHandler.php @@ -131,9 +131,13 @@ class OverflowHandler extends AbstractHandler implements FormattableHandlerInter */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { - $this->handler->setFormatter($formatter); + if ($this->handler instanceof FormattableHandlerInterface) { + $this->handler->setFormatter($formatter); - return $this; + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); } /** @@ -141,6 +145,10 @@ class OverflowHandler extends AbstractHandler implements FormattableHandlerInter */ public function getFormatter(): FormatterInterface { - return $this->handler->getFormatter(); + if ($this->handler instanceof FormattableHandlerInterface) { + return $this->handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($this->handler).' does not support formatters.'); } } diff --git a/src/Monolog/Handler/PHPConsoleHandler.php b/src/Monolog/Handler/PHPConsoleHandler.php index aa94e613..94802d0b 100644 --- a/src/Monolog/Handler/PHPConsoleHandler.php +++ b/src/Monolog/Handler/PHPConsoleHandler.php @@ -93,9 +93,6 @@ class PHPConsoleHandler extends AbstractProcessingHandler return array_replace($this->options, $options); } - /** - * @suppress PhanTypeMismatchArgument - */ private function initConnector(?Connector $connector = null): Connector { if (!$connector) { diff --git a/src/Monolog/Handler/ProcessableHandlerTrait.php b/src/Monolog/Handler/ProcessableHandlerTrait.php index c4c38ec5..71d767be 100644 --- a/src/Monolog/Handler/ProcessableHandlerTrait.php +++ b/src/Monolog/Handler/ProcessableHandlerTrait.php @@ -27,7 +27,6 @@ trait ProcessableHandlerTrait /** * {@inheritdoc} - * @suppress PhanTypeMismatchReturn */ public function pushProcessor(callable $callback): HandlerInterface { diff --git a/src/Monolog/Handler/SamplingHandler.php b/src/Monolog/Handler/SamplingHandler.php index 6dcf5996..4c33d4a3 100644 --- a/src/Monolog/Handler/SamplingHandler.php +++ b/src/Monolog/Handler/SamplingHandler.php @@ -100,9 +100,14 @@ class SamplingHandler extends AbstractHandler implements ProcessableHandlerInter */ public function setFormatter(FormatterInterface $formatter): HandlerInterface { - $this->getHandler()->setFormatter($formatter); + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + $handler->setFormatter($formatter); - return $this; + return $this; + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } /** @@ -110,6 +115,11 @@ class SamplingHandler extends AbstractHandler implements ProcessableHandlerInter */ public function getFormatter(): FormatterInterface { - return $this->getHandler()->getFormatter(); + $handler = $this->getHandler(); + if ($handler instanceof FormattableHandlerInterface) { + return $handler->getFormatter(); + } + + throw new \UnexpectedValueException('The nested handler of type '.get_class($handler).' does not support formatters.'); } } diff --git a/src/Monolog/Handler/Slack/SlackRecord.php b/src/Monolog/Handler/Slack/SlackRecord.php index b74a22fb..6658d0d4 100644 --- a/src/Monolog/Handler/Slack/SlackRecord.php +++ b/src/Monolog/Handler/Slack/SlackRecord.php @@ -77,7 +77,7 @@ class SlackRecord private $excludeFields; /** - * @var FormatterInterface + * @var ?FormatterInterface */ private $formatter; @@ -226,7 +226,7 @@ class SlackRecord * * @param ?string $channel * - * @return SlackHandler + * @return static */ public function setChannel(?string $channel = null): self { @@ -240,7 +240,7 @@ class SlackRecord * * @param ?string $username * - * @return SlackHandler + * @return static */ public function setUsername(?string $username = null): self { diff --git a/src/Monolog/Handler/SwiftMailerHandler.php b/src/Monolog/Handler/SwiftMailerHandler.php index 6b3e4950..2c5c5dac 100644 --- a/src/Monolog/Handler/SwiftMailerHandler.php +++ b/src/Monolog/Handler/SwiftMailerHandler.php @@ -93,9 +93,11 @@ class SwiftMailerHandler extends MailHandler } $message->setBody($content, $mime); + /** @phpstan-ignore-next-line */ if (version_compare(Swift::VERSION, '6.0.0', '>=')) { $message->setDate(new \DateTimeImmutable()); } else { + /** @phpstan-ignore-next-line */ $message->setDate(time()); } diff --git a/src/Monolog/Handler/SyslogUdpHandler.php b/src/Monolog/Handler/SyslogUdpHandler.php index 4c8a6c0f..270c68ba 100644 --- a/src/Monolog/Handler/SyslogUdpHandler.php +++ b/src/Monolog/Handler/SyslogUdpHandler.php @@ -94,7 +94,7 @@ class SyslogUdpHandler extends AbstractSyslogHandler $hostname = '-'; } - if ($this->rfc === self::RFC3164) { + if ($this->rfc === self::RFC3164 && ($datetime instanceof \DateTimeImmutable || $datetime instanceof \DateTime)) { $datetime->setTimezone(new \DateTimeZone('UTC')); } $date = $datetime->format($this->dateFormats[$this->rfc]); diff --git a/src/Monolog/Handler/ZendMonitorHandler.php b/src/Monolog/Handler/ZendMonitorHandler.php index 78a886f1..34fe80fa 100644 --- a/src/Monolog/Handler/ZendMonitorHandler.php +++ b/src/Monolog/Handler/ZendMonitorHandler.php @@ -73,7 +73,7 @@ class ZendMonitorHandler extends AbstractProcessingHandler * Write to Zend Monitor Events * @param string $type Text displayed in "Class Name (custom)" field * @param string $message Text displayed in "Error String" - * @param mixed $formatted Displayed in Custom Variables tab + * @param array $formatted Displayed in Custom Variables tab * @param int $severity Set the event severity level (-1,0,1) */ protected function writeZendMonitorCustomEvent(string $type, string $message, array $formatted, int $severity): void diff --git a/src/Monolog/SignalHandler.php b/src/Monolog/SignalHandler.php index 4a79fe66..517ab549 100644 --- a/src/Monolog/SignalHandler.php +++ b/src/Monolog/SignalHandler.php @@ -41,9 +41,6 @@ class SignalHandler if ($callPrevious) { $handler = pcntl_signal_get_handler($signo); - if ($handler === false) { - return $this; - } $this->previousSignalHandler[$signo] = $handler; } else { unset($this->previousSignalHandler[$signo]); diff --git a/src/Monolog/Test/TestCase.php b/src/Monolog/Test/TestCase.php index ecb8907f..b996bbc9 100644 --- a/src/Monolog/Test/TestCase.php +++ b/src/Monolog/Test/TestCase.php @@ -49,9 +49,6 @@ class TestCase extends \PHPUnit\Framework\TestCase ]; } - /** - * @suppress PhanTypeMismatchReturn - */ protected function getIdentityFormatter(): FormatterInterface { $formatter = $this->createMock(FormatterInterface::class);