hendrikmaus/spas

View on GitHub
src/Request/RequestProcessor.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php

namespace Hmaus\Spas\Request;

use GuzzleHttp\Psr7\Response;
use GuzzleHttp\UriTemplate;
use Hmaus\Spas\Event\AfterEach;
use Hmaus\Spas\Event\BeforeEach;
use Hmaus\Spas\Formatter\FormatterService;
use Hmaus\Spas\Parser\ParsedRequest;
use Hmaus\Spas\Parser\SpasResponse;
use Hmaus\Spas\Request\Result\ExceptionHandler;
use Hmaus\Spas\Request\Result\ProcessorReport;
use Hmaus\Spas\Validation\ValidatorService;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\HeaderBag;

class RequestProcessor
{
    /**
     * Amount of max seconds spas will wait until re-triggering a pollabe resource
     * @type int
     */
    const RETRY_AFTER_THRESHHOLD = 2;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var InputInterface
     */
    private $input;

    /**
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    /**
     * @var ValidatorService
     */
    private $validator;

    /**
     * @var HttpClient
     */
    private $http;

    /**
     * @var ExceptionHandler
     */
    private $exceptionHandler;

    /**
     * @var FormatterService
     */
    private $formatterService;

    /**
     * @var FilterHandler
     */
    private $filterHandler;

    /**
     * @var ProcessorReport
     */
    private $report;

    public function __construct(
        InputInterface $input,
        LoggerInterface $logger,
        EventDispatcherInterface $dispatcher,
        ValidatorService $validatorService,
        HttpClient $http,
        ExceptionHandler $exceptionHandler,
        FormatterService $formatterService,
        FilterHandler $filterHandler,
        ProcessorReport $processorReport
    )
    {
        $this->logger = $logger;
        $this->dispatcher = $dispatcher;
        $this->validator = $validatorService;
        $this->http = $http;
        $this->exceptionHandler = $exceptionHandler;
        $this->formatterService = $formatterService;
        $this->input = $input;
        $this->filterHandler = $filterHandler;
        $this->report = $processorReport;
    }

    /**
     * Process each individual transaction
     * @param ParsedRequest $request
     */
    public function process(ParsedRequest $request)
    {
        if ($this->shouldExit($request)) {
            return;
        }

        $this->beginLogBlock($request);
        $this->dispatcher->dispatch(BeforeEach::NAME, new BeforeEach($request));
        $this->filterHandler->filter($request);
        $request->setBaseUrl($this->input->getOption('base_uri'));
        $request->setHref((new UriTemplate())->expand($request->getUriTemplate(), $request->getParams()->all()));

        if (!$request->isEnabled()) {
            $this->report->disabled();
            $this->logger->info('Disabled');
            $this->dispatcher->dispatch(AfterEach::NAME, new AfterEach($request));
            $this->endLogBlock();
            return;
        }

        try {
            $this->printRequest($request);
            $this->addActualResponse($this->doRequest($request), $request);
            $this->dispatcher->dispatch(AfterEach::NAME, new AfterEach($request));
            $this->validator->validate($request);
            $this->printErrorResponse($request);
            $this->printValidatorReport();
            $this->validator->reset();
        } catch (\Exception $exception) {
            $this->dispatcher->dispatch(AfterEach::NAME, new AfterEach($request));
            $this->report->failed();
            $this->exceptionHandler->handle($exception);
        }

        $this->checkforRepetition($request);
        $this->report->processed($request->getName());
    }

    /**
     * Report getter
     * @return ProcessorReport
     */
    public function getReport()
    {
        return $this->report;
    }

    /**
     * Do request and handle polling for resources that return HTTP Retry-After header
     *
     * @param ParsedRequest $request
     * @param int $attempt How often the request was repeated
     * @return \Psr\Http\Message\ResponseInterface
     */
    private function doRequest(ParsedRequest $request, $attempt = 0)
    {
        $attempt++;

        $pollingThreshhold = $this->input->getOption('polling_count') - 1;

        $response = $this->http->request($request);
        $this->printResponse($response);

        if (!$response->hasHeader('retry-after')) {
            return $response;
        }

        if ($attempt > $pollingThreshhold) {
            $this->logger->warning(
                'Retried {0} time(s), validating last response',
                [$pollingThreshhold + 1]
            );
            return $response;
        }

        $retryAfter = $response->getHeader('retry-after');
        $retryAfter = array_pop($retryAfter);

        if (!is_numeric($retryAfter)) {
            // todo spas could try to work with dates
            $this->logger->warning('Retry-After header contains a value spas cannot work with:');
            $this->logger->warning('  {0}', [$retryAfter]);
            $this->logger->warning('Validating last response');

            return $response;
        }

        $retryAfter = (int)$retryAfter;

        if ($retryAfter > static::RETRY_AFTER_THRESHHOLD) {
            $this->logger->warning(
                'Retry-After header wants to wait longer than spas\' threshhold of %d seconds', [
                static::RETRY_AFTER_THRESHHOLD
            ]);
            return $response;
        }

        $this->logger->info('');
        $this->logger->info('Waiting {0} second(s) until next try', [$retryAfter]);

        usleep($retryAfter * 1000000);

        return $this->doRequest($request, $attempt);
    }

    /**
     * @param $response
     * @codeCoverageIgnore
     */
    private function printResponse(Response $response)
    {
        $this->logger->info('{0} {1}', [$response->getStatusCode(), $response->getReasonPhrase()]);
    }

    /**
     * @param ParsedRequest $request
     * @codeCoverageIgnore
     */
    private function printRequest(ParsedRequest $request)
    {
        $maxPrintLength = 70;

        if (strlen($request->getHref()) > $maxPrintLength) {
            $this->logger->info('{0} {1}...', [
                $request->getMethod(),
                substr($request->getHref(), 0, $maxPrintLength)
            ]);
        } else {
            $this->logger->info('{0} {1}', [
                $request->getMethod(),
                $request->getHref()
            ]);
        }
    }

    /**
     * Log validation result to the console
     */
    private function printValidatorReport()
    {
        if (!$this->validator->isValid()) {
            $this->report->failed();

            $formatter = $this
                ->formatterService
                ->getFormatterByContentType(
                    $this->validator->getContentType()
                );

            $this->logger->error(
                $formatter->format(
                    $this->validator->getReport()
                )
            );
        } else {
            $this->report->passed();
            $this->logger->info('Passed');
        }
    }

    /**
     * If the validator found issues, this helper prints what the server has to say
     *
     * @param ParsedRequest $request
     */
    private function printErrorResponse(ParsedRequest $request)
    {
        if ($this->validator->isValid()) {
            return;
        }

        $response    = $request->getActualResponse();
        $contentType = $response->getHeaders()->get('content-type');
        $body        = $response->getBody();
        $formatter   = $this->formatterService->getFormatterByContentType($contentType);

        $this
            ->logger
            ->error(
                $formatter->format($body)
            );
    }

    /**
     * Print the visual start of a request being processed
     * @param ParsedRequest $request
     */
    private function beginLogBlock(ParsedRequest $request)
    {
        $this->logger->info('');
        $this->logger->info('-----------------');
        $this->logger->info($request->getName());
    }

    /**
     * Print the visual end of a processed request
     */
    private function endLogBlock()
    {
        $this->logger->info('-----------------');
    }

    /**
     * @param ParsedRequest $request
     */
    private function checkforRepetition(ParsedRequest $request)
    {
        $repetitionConfig = $request->getRepetitionConfig();
        $shouldRepeat     = $repetitionConfig->repeat;
        $repeatedEnough   = $repetitionConfig->count >= $repetitionConfig->times;

        if (!$shouldRepeat) {
            $this->endLogBlock();
            return;
        }

        if (!$repeatedEnough) {
            $repetitionConfig->count += 1;
            $this->logger->info('Repeating request');
            $this->endLogBlock();
            $this->process($request);
            return;
        }

        $this->endLogBlock();
    }

    /**
     * @param ParsedRequest $request
     * @return bool
     */
    private function shouldExit(ParsedRequest $request): bool
    {
        $wasProcessed = $this->report->wasProcessed($request->getName());
        $onlyRunHappyCaseTransactions = $this->input->getOption('all_transactions') == null;

        return $wasProcessed && $onlyRunHappyCaseTransactions;
    }

    /**
     * Map guzzle response to a ParsedResponse and add it to the request
     * This way, hooks can work with the results of a request
     * @param ResponseInterface $response
     * @param ParsedRequest $request
     */
    private function addActualResponse(ResponseInterface $response, ParsedRequest $request)
    {
        $parsedResponse = new SpasResponse();
        $parsedResponse->setBody($response->getBody()->getContents());
        $parsedResponse->setHeaders(new HeaderBag($response->getHeaders()));
        $parsedResponse->setStatusCode($response->getStatusCode());
        $parsedResponse->setReasonPhrase($response->getReasonPhrase());

        $request->setActualResponse($parsedResponse);
    }
}