reset(); } /** * Match request by its method. * * @param string $method * * @return self */ public function matchMethod(string $method): self { return $this->addMatcher(new MethodMatcher($method)); } /** * Match request by its scheme. * * @param string $scheme * * @return self */ public function matchScheme(string $scheme): self { return $this->addMatcher(new SchemeMatcher($scheme)); } /** * Matches request by hostname. * * @param string $host * * @return self */ public function matchHost(string $host): self { return $this->addMatcher(new HostMatcher($host)); } /** * Matches request by the port. * * @param int $port * * @return self */ public function matchPort(int $port): self { return $this->addMatcher(new PortMatcher($port)); } /** * Matches request by origin. * * @param string $origin * * @return self * @throws \RuntimeException */ public function matchOrigin(string $origin): self { $parsed = parse_url($origin); if (!is_array($parsed)) { throw new RuntimeException('Malformed origin: ' . $origin); } if (array_key_exists('scheme', $parsed) && !empty($parsed['scheme'])) { $this->matchScheme($parsed['scheme']); } if (array_key_exists('host', $parsed) && !empty($parsed['host'])) { $this->matchHost($parsed['host']); } if (array_key_exists('port', $parsed) && is_int($parsed['port']) && $parsed['port'] > 0) { $this->matchPort($parsed['port']); } return $this; } /** * Matches request by the whole URI. * * @param \Psr\Http\Message\UriInterface|string $uri * * @return self */ public function matchUri($uri): self { return $this->addMatcher(new UriMatcher($uri)); } /** * Matches request by the whole URI using regular expression. * * @param string $expression * @param int $flags * * @return self */ public function matchUriRegExp(string $expression, int $flags = 0): self { return $this->addMatcher(new RegExpUriMatcher($expression, $flags)); } /** * Matches request by header value or several values. Header can have other values which are not specified here. * @see PockBuilder::matchExactHeader() if you want to match exact header values. * * @param string $header * @param string|string[] $value * * @return self */ public function matchHeader(string $header, $value): self { return $this->addMatcher(new HeaderMatcher($header, $value)); } /** * Matches request by headers values or several values. Headers can have other values which are not specified here. * @see PockBuilder::matchExactHeaders() if you want to match exact headers collection. * * @param array $headers * * @return self */ public function matchHeaders(array $headers): self { return $this->addMatcher(new HeadersMatcher($headers)); } /** * Matches request by the exact header pattern or values. * * @param string $header * @param string|string[] $value * * @return self */ public function matchExactHeader(string $header, $value): self { return $this->addMatcher(new ExactHeaderMatcher($header, $value)); } /** * Matches request by headers values or several values. * Note: only host header will be dropped. Any other headers will not be excluded and can result in the problems * with the exact matching. * * @param array $headers * * @return self */ public function matchExactHeaders(array $headers): self { return $this->addMatcher(new ExactHeadersMatcher($headers)); } /** * Matches request by the unparsed header line. * * @param string $header * @param string $value * * @return self */ public function matchHeaderLine(string $header, string $value): self { return $this->addMatcher(new HeaderLineMatcher($header, $value)); } /** * Matches request by the unparsed header line using provided regular expression. * * @param string $header * @param string $pattern * * @return self */ public function matchHeaderLineRegexp(string $header, string $pattern): self { return $this->addMatcher(new HeaderLineRegexpMatcher($header, $pattern)); } /** * Match request by its path. Path with and without slash at the start will be treated as the same path. * It's not the same for the path with slash at the end of it. * * @param string $path * * @return self */ public function matchPath(string $path): self { return $this->addMatcher(new PathMatcher($path)); } /** * Match request by its path using regular expression. This matcher doesn't care about prefix slash * since it's pretty easy to do it using regular expression. * * @param string $expression * @param int $flags * * @return self */ public function matchPathRegExp(string $expression, int $flags = 0): self { return $this->addMatcher(new RegExpPathMatcher($expression, $flags)); } /** * Match request by its query. Request can contain other query variables. * @see PockBuilder::matchExactQuery() if you want to match an entire query string. * * @param array $query * * @return self */ public function matchQuery(array $query): self { return $this->addMatcher(new QueryMatcher($query)); } /** * Match request by its query using regular expression. * * @param string $expression * @param int $flags * * @return self */ public function matchQueryRegExp(string $expression, int $flags = 0): self { return $this->addMatcher(new RegExpQueryMatcher($expression, $flags)); } /** * Match request by its query. Additional query parameters aren't allowed. * * @param array $query * * @return self */ public function matchExactQuery(array $query): self { return $this->addMatcher(new ExactQueryMatcher($query)); } /** * Match request with form-data. * * @param array $formFields * * @return self */ public function matchFormData(array $formFields): self { return $this->addMatcher(new FormDataMatcher($formFields)); } /** * Match request with form-data. Additional fields aren't allowed. * * @param array $formFields * * @return self */ public function matchExactFormData(array $formFields): self { return $this->addMatcher(new ExactFormDataMatcher($formFields)); } /** * Match request multipart form data. Will not match the request if body is not multipart. * Uses third-party library to parse the data. * * @param callable $callback Accepts Riverline\MultiPartParser\StreamedPart as an argument, returns true if matched. * * @return self * @see https://github.com/Riverline/multipart-parser#usage */ public function matchMultipartFormData(callable $callback): self { return $this->addMatcher(new MultipartFormDataMatcher($callback)); } /** * Match entire request body. * * @param \Psr\Http\Message\StreamInterface|resource|string $data * * @return self */ public function matchBody($data): self { return $this->addMatcher(new BodyMatcher($data)); } /** * Match entire request body using provided regular expression. * * @param string $expression * @param int $flags * * @return self */ public function matchBodyRegExp(string $expression, int $flags = 0): self { return $this->addMatcher(new RegExpBodyMatcher($expression, $flags)); } /** * Match JSON request body. * * @param mixed $data * * @return self * @throws \Pock\Exception\JsonException */ public function matchJsonBody($data): self { return $this->addMatcher(new JsonBodyMatcher( self::jsonDecode( self::serializeJson($data) ?? '', true ) )); } /** * Match JSON request body against JSON string or array with data. * * @param array|string $data * * @return self * @throws \Pock\Exception\JsonException */ public function matchSerializedJsonBody($data): self { if (is_string($data)) { $data = self::jsonDecode($data, true); } return $this->addMatcher(new JsonBodyMatcher($data)); } /** * Match XML request body using raw XML data. * * **Note:** this method will fallback to the string comparison if ext-xsl is not available. * It also doesn't serializer values with available XML serializer. * Use PockBuilder::matchSerializedXmlBody if you want to execute available serializer. * * @param DOMDocument|\Psr\Http\Message\StreamInterface|resource|string $data * * @return self * @throws \Pock\Exception\XmlException * @see \Pock\PockBuilder::matchSerializedXmlBody() * */ public function matchXmlBody($data): self { return $this->addMatcher(new XmlBodyMatcher($data)); } /** * Match XML request body. * * This method will try to use available XML serializer before matching. * * @phpstan-ignore-next-line * @param string|array|object $data * * @return self * @throws \Pock\Exception\XmlException */ public function matchSerializedXmlBody($data): self { return $this->matchXmlBody(self::serializeXml($data) ?? ''); } /** * Match request using provided callback. Callback should receive RequestInterface and return boolean. * If returned value is true then request is matched. * * @param callable $callback Callable that accepts PSR-7 RequestInterface as it's first argument. * * @return self */ public function matchCallback(callable $callback): self { return $this->addMatcher(new CallbackRequestMatcher($callback)); } /** * Add custom matcher to the mock. * * @param \Pock\Matchers\RequestMatcherInterface $matcher * * @return self */ public function addMatcher(RequestMatcherInterface $matcher): self { $this->closePrevious(); $this->matcher->addMatcher($matcher); return $this; } /** * Repeat this mock provided amount of times. * For example, if you pass 2 as an argument mock will be able to handle two identical requests. * * @param int $hits * * @return self */ public function repeat(int $hits): self { $this->closePrevious(); if ($hits > 0) { $this->maxHits = $hits; } return $this; } /** * Always execute this mock if matched. Mock with this call will not be expired ever. * * @return self */ public function always(): self { $this->closePrevious(); $this->maxHits = -1; return $this; } /** * Match request only at Nth hit. Previous matches will not be executed. * * **Note:** There IS a catch if you use this with the equal mocks. The test Client will not register hit * for the second mock and the second mock will be executed at N+1 time. * * For example, if you try to send 5 requests with this mocks and log response codes: * ```php * $builder = new PockBuilder(); * * $builder->matchHost('example.com')->at(2)->reply(200); * $builder->matchHost('example.com')->at(4)->reply(201); * $builder->always()->reply(400); * ``` * * You will get this: 400, 400, 200, 400, 400, 201 * Instead of this: 400, 400, 200, 400, 201, 400 * * @param int $hit * * @return self */ public function at(int $hit): self { $this->closePrevious(); $this->matchAt = $hit - 1; return $this; } /** * Throw an exception when request is being sent. * * @param \Throwable $throwable * * @return self */ public function throwException(Throwable $throwable): self { $this->throwable = $throwable; return $this; } /** * Throw an ClientExceptionInterface instance with specified message * * @param string $message * * @return self */ public function throwClientException(string $message = 'Pock ClientException'): self { return $this->throwException(new PockClientException($message)); } /** * Throw an NetworkExceptionInterface instance with specified message * * @param string $message * * @return self */ public function throwNetworkException(string $message = 'Pock NetworkException'): self { return $this->throwException(new PockNetworkException($message)); } /** * Throw an RequestExceptionInterface instance with specified message * * @param string $message * * @return self */ public function throwRequestException(string $message = 'Pock RequestException'): self { return $this->throwException(new PockRequestException($message)); } /** * @param int $statusCode * * @return \Pock\PockResponseBuilder */ public function reply(int $statusCode = 200): PockResponseBuilder { if (null === $this->responseBuilder) { $this->responseBuilder = new PockResponseBuilder($statusCode); return $this->responseBuilder; } return $this->responseBuilder->withStatusCode($statusCode); } /** * Construct the response during request execution using provided ReplytFactoryInterface implementation. * * @param \Pock\Factory\ReplyFactoryInterface $factory * @see ReplyFactoryInterface */ public function replyWithFactory(ReplyFactoryInterface $factory): void { $this->replyFactory = $factory; } /** * Construct the response during request execution using provided callback. * * Callback should receive the same parameters as in the `ReplyFactoryInterface::createReply` method. * * @see ReplyFactoryInterface::createReply() * * @param callable $callback */ public function replyWithCallback(callable $callback): void { $this->replyWithFactory(new CallbackReplyFactory($callback)); } /** * Reply to the request using provided client. Can be used to send real network request. * * @param \Psr\Http\Client\ClientInterface $client * @SuppressWarnings(unused) */ public function replyWithClient(ClientInterface $client): void { $this->replyWithCallback(function ( RequestInterface $request, PockResponseBuilder $responseBuilder ) use ($client): ResponseInterface { return $client->sendRequest($request); }); } /** * Resets the builder. * * @return self */ public function reset(): self { $this->matcher = new MultipleMatcher(); $this->replyFactory = null; $this->responseBuilder = null; $this->throwable = null; $this->maxHits = 1; $this->matchAt = -1; $this->mocks = []; return $this; } /** * Sets fallback Client. It will be used if no request can be matched. * * @param \Psr\Http\Client\ClientInterface|null $fallbackClient * * @return self */ public function setFallbackClient(?ClientInterface $fallbackClient = null): self { $this->fallbackClient = $fallbackClient; return $this; } /** * @return \Pock\Client */ public function getClient(): Client { $this->closePrevious(); return new Client($this->mocks, $this->fallbackClient); } private function closePrevious(): void { if (null !== $this->responseBuilder || null !== $this->replyFactory || null !== $this->throwable) { if (0 === count($this->matcher)) { $this->matcher->addMatcher(new AnyRequestMatcher()); } $response = null; if (null !== $this->responseBuilder) { $response = $this->responseBuilder->getResponse(); } $this->mocks[] = new Mock( $this->matcher, $this->replyFactory, $response, $this->throwable, $this->maxHits, $this->matchAt ); $this->matcher = new MultipleMatcher(); $this->replyFactory = null; $this->responseBuilder = null; $this->throwable = null; $this->maxHits = 1; $this->matchAt = -1; } } }