diff --git a/composer.json b/composer.json index 24d459eaa6..b313b4d2d9 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "php-parallel-lint/php-parallel-lint": "^1.0", - "phpstan/phpstan": "^1.9", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", "symfony/phpunit-bridge": "^7.0" }, "license": "GPL-3.0", diff --git a/composer.lock b/composer.lock index d3f9bede98..8ce2a2ec61 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aa3824997321f777a508067a9e49578a", + "content-hash": "c6379e20ca1bebc6474e12633f775688", "packages": [ { "name": "bcosca/fatfree-core", @@ -4170,20 +4170,20 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.11", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" + "reference": "6c98c7600fc717b2c78c11ef60040d5b1e359c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", - "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6c98c7600fc717b2c78c11ef60040d5b1e359c82", + "reference": "6c98c7600fc717b2c78c11ef60040d5b1e359c82", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -4224,7 +4224,54 @@ "type": "github" } ], - "time": "2024-11-17T14:08:01+00:00" + "time": "2024-11-17T14:17:00+00:00" + }, + { + "name": "phpstan/phpstan-deprecation-rules", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", + "reference": "81833b5787e2e8f451b31218875e29e4ed600ab2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/81833b5787e2e8f451b31218875e29e4ed600ab2", + "reference": "81833b5787e2e8f451b31218875e29e4ed600ab2", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", + "support": { + "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/2.0.0" + }, + "time": "2024-10-26T16:04:11+00:00" }, { "name": "psr/event-dispatcher", diff --git a/phpstan.neon b/phpstan.neon index 7341144d01..e368a6c720 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -27,3 +27,6 @@ parameters: typeAliases: SpoutParameterInfo: 'array{title: string, type: spouts\Parameter::TYPE_*, default: string, required: bool, validation: array, values?: array}' SpoutParameters: 'array' + +includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon diff --git a/src/controllers/Index.php b/src/controllers/Index.php index 343bbc9eca..a920b26c89 100644 --- a/src/controllers/Index.php +++ b/src/controllers/Index.php @@ -109,7 +109,7 @@ public function home(): void { * @return array{entries: array}>, hasMore: bool} html with items */ private function loadItems(array $params, array $tags) { - $options = ItemOptions::fromUser($params); + $options = new ItemOptions($params); $entries = []; foreach ($this->itemsDao->get($options) as $item) { $entries[] = $this->viewHelper->preprocessEntry($item, $this->tagsController, $tags, $options->search); diff --git a/src/controllers/Items.php b/src/controllers/Items.php index ff8c6cefc5..ce8316b297 100644 --- a/src/controllers/Items.php +++ b/src/controllers/Items.php @@ -149,7 +149,7 @@ public function listItems(): void { $this->authentication->needsLoggedInOrPublicMode(); // parse params - $options = ItemOptions::fromUser($_GET); + $options = new ItemOptions($_GET); // get items $items = $this->itemsDao->get($options); diff --git a/src/controllers/Items/Sync.php b/src/controllers/Items/Sync.php index c671231be0..e808afce00 100644 --- a/src/controllers/Items/Sync.php +++ b/src/controllers/Items/Sync.php @@ -115,9 +115,29 @@ public function sync(): void { public function updateStatuses(): void { $this->authentication->needsLoggedIn(); - if (isset($_POST['updatedStatuses']) - && is_array($_POST['updatedStatuses'])) { - $this->itemsDao->bulkStatusUpdate($_POST['updatedStatuses']); + if (isset($_POST['updatedStatuses'])) { + $updatedStatuses = $_POST['updatedStatuses']; + if (!is_array($updatedStatuses)) { + $this->view->jsonError(['updatedStatuses' => 'not an array']); + } + + foreach ($updatedStatuses as $index => $status) { + if (!is_array($status)) { + $this->view->jsonError(["updatedStatuses[$index]" => 'not an array']); + } + + if (!array_key_exists('id', $status)) { + $this->view->jsonError(["updatedStatuses[$index]" => 'missing id argument']); + } + + $id = (int) $status['id']; + if ((string) $id !== $status['id']) { + $this->view->jsonError(["updatedStatuses[$index]['id']" => 'not a number']); + } + $status['id'] = $id; + } + + $this->itemsDao->bulkStatusUpdate($updatedStatuses); } $this->sync(); diff --git a/src/controllers/Rss.php b/src/controllers/Rss.php index b460baaa13..f43a599705 100644 --- a/src/controllers/Rss.php +++ b/src/controllers/Rss.php @@ -50,7 +50,7 @@ public function rss(): void { $lastSourceId = 0; $lastSourceName = ''; - $options = ItemOptions::fromUser($_GET); + $options = new ItemOptions($_GET); // get items $newestEntryDate = null; diff --git a/src/daos/ItemOptions.php b/src/daos/ItemOptions.php index 0683707fdd..3364f493d0 100644 --- a/src/daos/ItemOptions.php +++ b/src/daos/ItemOptions.php @@ -11,96 +11,114 @@ */ final class ItemOptions { /** @readonly */ - public ?int $offset = 0; + public ?int $offset; /** @readonly */ - public ?string $search = null; + public ?string $search; /** * Maximum number of items to fetch from the database (unbounded) * * @readonly */ - public ?int $pageSize = null; + public ?int $pageSize; /** @readonly */ - public ?DateTime $fromDatetime = null; + public ?DateTime $fromDatetime; /** @readonly */ - public ?int $fromId = null; + public ?int $fromId; /** @readonly */ - public ?DateTime $updatedSince = null; + public ?DateTime $updatedSince; /** @readonly */ - public ?string $tag = null; + public ?string $tag; /** * @var 'starred'|'unread'|null * * @readonly */ - public ?string $filter = null; + public ?string $filter; /** @readonly */ - public ?int $source = null; + public ?int $source; - /** @var int[] @readonly */ - public array $extraIds = []; + /** + * @var int[] + * + * @readonly + */ + public array $extraIds; /** * Creates new ItemOptions object ensuring the values are proper types. * * @param array $data - * - * @return static */ - public static function fromUser(array $data): self { - $options = new static(); - + public function __construct(array $data) { if (isset($data['offset']) && is_numeric($data['offset'])) { - $options->offset = (int) $data['offset']; + $this->offset = (int) $data['offset']; + } else { + $this->offset = 0; } if (isset($data['search']) && is_string($data['search']) && strlen($search = trim($data['search'])) > 0) { - $options->search = $search; + $this->search = $search; + } else { + $this->search = null; } if (isset($data['items']) && is_numeric($data['items'])) { - $options->pageSize = (int) $data['items']; + $this->pageSize = (int) $data['items']; + } else { + $this->pageSize = null; } if (isset($data['fromDatetime']) && is_string($data['fromDatetime']) && strlen($data['fromDatetime']) > 0) { - $options->fromDatetime = new DateTime($data['fromDatetime']); + $this->fromDatetime = new DateTime($data['fromDatetime']); + } else { + $this->fromDatetime = null; } if (isset($data['fromId']) && is_numeric($data['fromId'])) { - $options->fromId = (int) $data['fromId']; + $this->fromId = (int) $data['fromId']; + } else { + $this->fromId = null; } if (isset($data['updatedsince']) && is_string($data['updatedsince']) && strlen($data['updatedsince']) > 0) { - $options->updatedSince = new DateTime($data['updatedsince']); + $this->updatedSince = new DateTime($data['updatedsince']); + } else { + $this->updatedSince = null; } if (isset($data['tag']) && is_string($data['tag']) && strlen($tag = trim($data['tag'])) > 0) { - $options->tag = $tag; + $this->tag = $tag; + } else { + $this->tag = null; } if (isset($data['type']) && is_string($data['type']) && in_array($filter = trim($data['type']), ['starred', 'unread'], true)) { - $options->filter = $filter; + $this->filter = $filter; + } else { + $this->filter = null; } if (isset($data['source']) && is_numeric($data['source'])) { - $options->source = (int) $data['source']; + $this->source = (int) $data['source']; + } else { + $this->source = null; } if (isset($data['extraIds']) && is_array($data['extraIds'])) { - $options->extraIds = array_map( + $this->extraIds = array_map( fn($val) => (int) $val, $data['extraIds'] ); + } else { + $this->extraIds = []; } - - return $options; } } diff --git a/src/daos/Sources.php b/src/daos/Sources.php index c334e58cc1..8913e15334 100644 --- a/src/daos/Sources.php +++ b/src/daos/Sources.php @@ -152,6 +152,7 @@ public function validate(string $title, string $spout, array $params) { } $validation = $spout->params[$id]['validation']; + // @phpstan-ignore-next-line function.alreadyNarrowedType (User can create their own spouts so we cannot necessarily trust PHPDoc.) if (!is_array($validation)) { $validation = [$validation]; } diff --git a/src/daos/mysql/Items.php b/src/daos/mysql/Items.php index bc31802553..061c52f184 100644 --- a/src/daos/mysql/Items.php +++ b/src/daos/mysql/Items.php @@ -569,55 +569,53 @@ public function statuses(DateTime $since): array { public function bulkStatusUpdate(array $statuses): void { $sql = []; foreach ($statuses as $status) { - if (array_key_exists('id', $status)) { - $id = (int) $status['id']; - if ($id > 0) { - $statusUpdate = null; - - // sanitize statuses - foreach (['unread', 'starred'] as $sk) { - if (array_key_exists($sk, $status)) { - if ($status[$sk] == 'true') { - $statusUpdate = [ - 'sk' => $sk, - 'sql' => static::$stmt::isTrue($sk), - ]; - } elseif ($status[$sk] == 'false') { - $statusUpdate = [ - 'sk' => $sk, - 'sql' => static::$stmt::isFalse($sk), - ]; - } + $id = (int) $status['id']; + if ($id > 0) { + $statusUpdate = null; + + // sanitize statuses + foreach (['unread', 'starred'] as $sk) { + if (array_key_exists($sk, $status)) { + if ($status[$sk] == 'true') { + $statusUpdate = [ + 'sk' => $sk, + 'sql' => static::$stmt::isTrue($sk), + ]; + } elseif ($status[$sk] == 'false') { + $statusUpdate = [ + 'sk' => $sk, + 'sql' => static::$stmt::isFalse($sk), + ]; } } + } - // sanitize update time - if (array_key_exists('datetime', $status)) { - $updateDate = new DateTime($status['datetime']); - } else { - $updateDate = null; - } + // sanitize update time + if (array_key_exists('datetime', $status)) { + $updateDate = new DateTime($status['datetime']); + } else { + $updateDate = null; + } - if ($statusUpdate !== null && $updateDate !== null) { - $sk = $statusUpdate['sk']; - if (array_key_exists($id, $sql)) { - // merge status updates for the same entry and - // ensure all saved status updates have been made - // after the last server update for this entry. - if (!array_key_exists($sk, $sql[$id]['updates']) - || $updateDate > $sql[$id]['datetime']) { - $sql[$id]['updates'][$sk] = $statusUpdate['sql']; - } - if ($updateDate < $sql[$id]['datetime']) { - $sql[$id]['datetime'] = $updateDate; - } - } else { - // create new status update - $sql[$id] = [ - 'updates' => [$sk => $statusUpdate['sql']], - 'datetime' => $updateDate->format('Y-m-d H:i:s'), - ]; + if ($statusUpdate !== null && $updateDate !== null) { + $sk = $statusUpdate['sk']; + if (array_key_exists($id, $sql)) { + // merge status updates for the same entry and + // ensure all saved status updates have been made + // after the last server update for this entry. + if (!array_key_exists($sk, $sql[$id]['updates']) + || $updateDate > $sql[$id]['datetime']) { + $sql[$id]['updates'][$sk] = $statusUpdate['sql']; } + if ($updateDate < $sql[$id]['datetime']) { + $sql[$id]['datetime'] = $updateDate; + } + } else { + // create new status update + $sql[$id] = [ + 'updates' => [$sk => $statusUpdate['sql']], + 'datetime' => $updateDate->format('Y-m-d H:i:s'), + ]; } } } diff --git a/src/helpers/FeedReader.php b/src/helpers/FeedReader.php index 6c4a57ffce..6c45833e75 100644 --- a/src/helpers/FeedReader.php +++ b/src/helpers/FeedReader.php @@ -30,7 +30,7 @@ public function __construct( WebClient::class => $webClient, ]); - $this->simplepie->set_file_class(SimplePieFileGuzzle::class); + $this->simplepie->get_registry()->register(\SimplePie\File::class, SimplePieFileGuzzle::class, true); $this->simplepie->set_autodiscovery_level(SimplePie::LOCATOR_AUTODISCOVERY | SimplePie::LOCATOR_LOCAL_EXTENSION | SimplePie::LOCATOR_LOCAL_BODY); $this->simplepie->set_useragent($webClient->getUserAgent()); } @@ -48,6 +48,7 @@ public function load(string $url): array { @$this->simplepie->init(); // on error retry with force_feed + // @phpstan-ignore-next-line notIdentical.alwaysTrue (The upstream type is incorrect: https://github.com/simplepie/simplepie/pull/903) if ($this->simplepie->error() !== null) { @$this->simplepie->set_autodiscovery_level(SimplePie::LOCATOR_NONE); @$this->simplepie->force_feed(true); diff --git a/src/helpers/ImageUtils.php b/src/helpers/ImageUtils.php index acef4a2819..c61020b04a 100644 --- a/src/helpers/ImageUtils.php +++ b/src/helpers/ImageUtils.php @@ -64,11 +64,12 @@ public static function parseShortcutIcons(string $html): array { $icons = []; $i = 0; foreach ($links as $link) { - if (preg_match('#\shref=(?:("|\')(?P.+)\1|(?P[^\s"\'=><`]+?))#iU', $link[0], $href)) { + if (preg_match('#\shref=(?:("|\')(?P.+)\1|(?P[^\s"\'=><`]+?))#iU', $link[0], $href, PREG_UNMATCHED_AS_NULL)) { + // For PHPStan: The capture groups are all the alternatives so it cannot be null. + /** @var non-empty-string */ + $url = $href['uq_url'] ?? $href['url']; $icons[] = [ - // Unmatched group should return an empty string but they are missing. - // https://bugs.php.net/bug.php?id=50887 - 'url' => isset($href['uq_url']) && $href['uq_url'] !== '' ? $href['uq_url'] : $href['url'], + 'url' => $url, // Sizes are only used by Apple. // https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html 'sizes' => preg_match('#\ssizes=("|\'|)(?P[0-9\.]+)x\2\1#i', $link[0], $sizes) diff --git a/src/helpers/WebClient.php b/src/helpers/WebClient.php index e028f6bec6..2f39c663f4 100644 --- a/src/helpers/WebClient.php +++ b/src/helpers/WebClient.php @@ -33,42 +33,7 @@ public function __construct(Configuration $configuration, Logger $logger) { */ public function getHttpClient(): GuzzleHttp\Client { if ($this->httpClient === null) { - $stack = HandlerStack::create(); - $stack->push(new GuzzleTranscoder()); - - if ($this->configuration->loggerLevel === Configuration::LOGGER_LEVEL_DEBUG) { - if ($this->configuration->debug === 0) { - $logFormat = GuzzleHttp\MessageFormatter::SHORT; - } elseif ($this->configuration->debug === 1) { - $logFormat = ">>>>>>>>\n{req_headers}\n<<<<<<<<\n{res_headers}\n--------\n{error}"; - } else { - $logFormat = GuzzleHttp\MessageFormatter::DEBUG; - } - - $logger = GuzzleHttp\Middleware::log( - $this->logger, - new GuzzleHttp\MessageFormatter($logFormat), - \Psr\Log\LogLevel::DEBUG - ); - $stack->push($logger); - } - - $httpClient = new GuzzleHttp\Client([ - 'allow_redirects' => [ - 'track_redirects' => true, - ], - 'headers' => [ - 'User-Agent' => self::getUserAgent(), - ], - 'handler' => $stack, - 'timeout' => 60, // seconds - 'curl' => [ - // Guzzle will not send Accept-Encoding by default. - // https://github.com/guzzle/guzzle/pull/3215 - // Delegate choosing compression method to curl. - \CURLOPT_ENCODING => '', - ], - ]); + $httpClient = new GuzzleHttp\Client($this->createHttpClientConfig()); $this->httpClient = $httpClient; } @@ -76,6 +41,50 @@ public function getHttpClient(): GuzzleHttp\Client { return $this->httpClient; } + /** + * Create a config for HTTP client for use by spouts. + * + * @return array $config + */ + public function createHttpClientConfig(): array { + $stack = HandlerStack::create(); + $stack->push(new GuzzleTranscoder()); + + if ($this->configuration->loggerLevel === Configuration::LOGGER_LEVEL_DEBUG) { + if ($this->configuration->debug === 0) { + $logFormat = GuzzleHttp\MessageFormatter::SHORT; + } elseif ($this->configuration->debug === 1) { + $logFormat = ">>>>>>>>\n{req_headers}\n<<<<<<<<\n{res_headers}\n--------\n{error}"; + } else { + $logFormat = GuzzleHttp\MessageFormatter::DEBUG; + } + + $logger = GuzzleHttp\Middleware::log( + $this->logger, + new GuzzleHttp\MessageFormatter($logFormat), + \Psr\Log\LogLevel::DEBUG + ); + $stack->push($logger); + } + + return [ + 'allow_redirects' => [ + 'track_redirects' => true, + ], + 'headers' => [ + 'User-Agent' => self::getUserAgent(), + ], + 'handler' => $stack, + 'timeout' => 60, // seconds + 'curl' => [ + // Guzzle will not send Accept-Encoding by default. + // https://github.com/guzzle/guzzle/pull/3215 + // Delegate choosing compression method to curl. + \CURLOPT_ENCODING => '', + ], + ]; + } + /** * get the user agent to use for web based spouts * diff --git a/src/spouts/Item.php b/src/spouts/Item.php index 7d5b13b09e..82b2806dbb 100644 --- a/src/spouts/Item.php +++ b/src/spouts/Item.php @@ -243,10 +243,17 @@ public function getExtraData() { * @return Item */ public function withExtraData($extraData): Item { - /** @var Item */ // For PHPStan - $modified = clone $this; - $modified->extraData = $extraData; - - return $modified; + return new self( + $this->id, + $this->title, + $this->content, + $this->thumbnail, + $this->icon, + $this->link, + $this->date, + $this->author, + // Replace with a new value (possibly using a different type). + $extraData + ); } } diff --git a/src/spouts/twitter/TwitterV1ApiClient.php b/src/spouts/twitter/TwitterV1ApiClient.php index 3e270cfea9..1f1ad9a025 100644 --- a/src/spouts/twitter/TwitterV1ApiClient.php +++ b/src/spouts/twitter/TwitterV1ApiClient.php @@ -237,30 +237,36 @@ private static function formatEntities(stdClass $groupedEntities): array { /** @var int $start */ $start = $entity->indices[0]; $end = $entity->indices[1]; - if ($type === 'hashtags') { - $result[$start] = [ - 'text' => '#' . $entity->text, - 'url' => 'https://twitter.com/hashtag/' . urlencode($entity->text), - 'end' => $end, - ]; - } elseif ($type === 'symbols') { - $result[$start] = [ - 'text' => '$' . $entity->text, - 'url' => 'https://twitter.com/search?q=%24' . urlencode($entity->text), - 'end' => $end, - ]; - } elseif ($type === 'user_mentions') { - $result[$start] = [ - 'text' => '@' . $entity->screen_name, - 'url' => 'https://twitter.com/' . urlencode($entity->screen_name), - 'end' => $end, - ]; - } elseif ($type === 'urls' || $type === 'media') { - $result[$start] = [ - 'text' => $entity->display_url, - 'url' => $entity->expanded_url, - 'end' => $end, - ]; + switch ($type) { + case 'hashtags': + $result[$start] = [ + 'text' => '#' . $entity->text, + 'url' => 'https://twitter.com/hashtag/' . urlencode($entity->text), + 'end' => $end, + ]; + break; + case 'symbols': + $result[$start] = [ + 'text' => '$' . $entity->text, + 'url' => 'https://twitter.com/search?q=%24' . urlencode($entity->text), + 'end' => $end, + ]; + break; + case 'user_mentions': + $result[$start] = [ + 'text' => '@' . $entity->screen_name, + 'url' => 'https://twitter.com/' . urlencode($entity->screen_name), + 'end' => $end, + ]; + break; + case 'urls': + case 'media': + $result[$start] = [ + 'text' => $entity->display_url, + 'url' => $entity->expanded_url, + 'end' => $end, + ]; + break; } } } diff --git a/src/spouts/twitter/TwitterV1ApiClientFactory.php b/src/spouts/twitter/TwitterV1ApiClientFactory.php index 937d49a2a8..3e99c5f393 100644 --- a/src/spouts/twitter/TwitterV1ApiClientFactory.php +++ b/src/spouts/twitter/TwitterV1ApiClientFactory.php @@ -32,8 +32,7 @@ public function create( ): TwitterV1ApiClient { $access_token_used = !empty($accessToken) && !empty($accessTokenSecret); - $oldClient = $this->webClient->getHttpClient(); - $config = $oldClient->getConfig(); + $config = $this->webClient->createHttpClientConfig(); $config['base_uri'] = 'https://api.twitter.com/1.1/'; $config['auth'] = 'oauth'; @@ -43,7 +42,6 @@ public function create( 'token' => $access_token_used ? $accessToken : '', 'token_secret' => $access_token_used ? $accessTokenSecret : '', ]); - $config['handler'] = clone $config['handler']; // we do not want to contaminate other spouts $config['handler']->push($middleware); $httpClient = new GuzzleHttp\Client($config); diff --git a/tests/Spouts/TwitterTest.php b/tests/Spouts/TwitterTest.php index b78d21f94a..0ec03b8149 100644 --- a/tests/Spouts/TwitterTest.php +++ b/tests/Spouts/TwitterTest.php @@ -4,7 +4,6 @@ namespace Tests\Spouts; -use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; @@ -32,9 +31,9 @@ private function getResourceData(string $fileName): string { private function makeSpout($spout, array $responses): spout { $mock = new MockHandler($responses); $stack = HandlerStack::create($mock); - $httpClient = new Client([ + $httpClientConfig = [ 'handler' => $stack, - ]); + ]; $container = new Container(); $container->setDefaults(['shared' => false]); @@ -45,9 +44,9 @@ private function makeSpout($spout, array $responses): spout { ->setShared(true) ; $container - ->register(WebClient::class, function() use ($httpClient) { + ->register(WebClient::class, function() use ($httpClientConfig) { $stub = $this->createMock(WebClient::class); - $stub->method('getHttpClient')->willReturn($httpClient); + $stub->method('createHttpClientConfig')->willReturn($httpClientConfig); return $stub; })