Test Setup Failed
Branch 0.x (5da7af)
by Pavel
13:48
created

LocatorChain::poll()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 37
rs 9.328
c 0
b 0
f 0
cc 4
nc 6
nop 2
1
<?php
2
3
/*
4
 * This file is part of the Veslo project <https://github.com/symfony-doge/veslo>.
5
 *
6
 * (C) 2019 Pavel Petrov <[email protected]>.
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * @license https://opensource.org/licenses/GPL-3.0 GPL-3.0
12
 */
13
14
declare(strict_types=1);
15
16
namespace Veslo\AppBundle\Http\Proxy;
17
18
use Ds\PriorityQueue;
19
use Exception;
20
use Psr\Log\LoggerInterface;
21
use RuntimeException;
22
23
/**
24
 * Aggregates proxy locators and polls each of them one by one until proxy list is returned
25
 *
26
 * Note: although it is technically possible to use LocatorChain instance as a part of other locator, this class
27
 * is not directly designed to be nested one (so it is placed outside of locator's namespace and named unconventionally)
28
 */
29
class LocatorChain implements LocatorInterface
30
{
31
    /**
32
     * Dispatches a failed poll message for locators marked by "isImportant" flag
33
     *
34
     * @var LoggerInterface
35
     */
36
    private $logger;
37
38
    /**
39
     * Priority queue with proxy locators
40
     *
41
     * It is not really needed to use data structures from php-ds extension in this context due to low objects count,
42
     * it's mostly for learning purposes... also we have a little bonus - priority managing is delegated to the C-layer
43
     * instead of PHP code, so a compiler pass can be simplified.
44
     *
45
     * @var PriorityQueue<LocatorInterface>
46
     *
47
     * @see https://github.com/php-ds
48
     */
49
    private $proxyLocators;
50
51
    /**
52
     * Cached polling result for current process
53
     * (otherwise we should call a `PriorityQueue::copy()` and it can be redundant)
54
     *
55
     * @var string[]
56
     */
57
    private $_proxyList;
58
59
    /**
60
     * LocatorChain constructor.
61
     *
62
     * @param LoggerInterface $logger Dispatches a failed poll message for locators marked by "isImportant" flag
63
     */
64
    public function __construct(LoggerInterface $logger)
65
    {
66
        $this->logger = $logger;
67
68
        $this->proxyLocators = new PriorityQueue();
69
        $this->_proxyList    = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,string> of property $_proxyList.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
70
    }
71
72
    /**
73
     * Adds a proxy locator to the list for polling
74
     *
75
     * @param LocatorInterface $proxyLocator Provides a list with proxies available for http requests
76
     * @param int              $priority     Locator priority in polling loop
77
     * @param bool             $isImportant  Whenever a critical message should be raised if locator fails to locate a
78
     *                                       proxy list
79
     *
80
     * @return void
81
     */
82
    public function addLocator(LocatorInterface $proxyLocator, int $priority, bool $isImportant = false): void
83
    {
84
        if (null !== $this->_proxyList) {
85
            throw new RuntimeException('Unable to add a new locator after locate() execution.');
86
        }
87
88
        $this->proxyLocators->push([$proxyLocator, ['isImportant' => $isImportant]], $priority);
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     *
94
     * @see PriorityQueue::getIterator()
95
     */
96
    public function locate(): iterable
97
    {
98
        if (null !== $this->_proxyList) {
99
            return $this->_proxyList;
100
        }
101
102
        foreach ($this->proxyLocators as list($proxyLocator, $pollParameters)) {
103
            $proxyList = $this->poll($proxyLocator, $pollParameters);
104
105
            if (0 < count($proxyList)) {
106
                return $this->_proxyList = $proxyList;
107
            }
108
        }
109
110
        return $this->_proxyList = [];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->_proxyList = array(); (array) is incompatible with the return type declared by the interface Veslo\AppBundle\Http\Pro...ocatorInterface::locate of type Veslo\AppBundle\Http\Proxy\iterable|string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
111
    }
112
113
    /**
114
     * Polls locator for a proxy list
115
     *
116
     * @param LocatorInterface $proxyLocator Provides a list with proxies available for http requests
117
     * @param array            $parameters   Parameters for locator polling
118
     *
119
     * @return string[]
120
     */
121
    private function poll(LocatorInterface $proxyLocator, array $parameters): array
122
    {
123
        $locatorClass = get_class($proxyLocator);
124
        $isImportant  = $parameters['isImportant'];
125
        $pollContext  = ['locatorClass' => $locatorClass, 'isImportant' => $isImportant];
126
127
        $this->logger->debug('Polling locator for a proxy list.', $pollContext);
128
129
        $proxyList = [];
130
131
        try {
132
            $proxyList = $proxyLocator->locate();
133
        } catch (Exception $e) {
134
            $message          = $e->getMessage();
135
            $pollContextError = array_merge(['message' => $message], $pollContext);
136
137
            $this->logger->error(
138
                'An error has been occurred while polling locator for a proxy list.',
139
                $pollContextError
140
            );
141
        }
142
143
        $isProxyListFound = (0 < count($proxyList));
144
145
        if ($isProxyListFound) {
146
            $pollContextFound = array_merge(['proxies' => $proxyList], $pollContext);
147
            $this->logger->debug('Proxy list has been located.', $pollContextFound);
148
149
            return $proxyList;
150
        }
151
152
        if ($isImportant) {
153
            $this->logger->critical("Proxy locator with 'isImportant' flag didn't provide a proxy list.", $pollContext);
154
        }
155
156
        return [];
157
    }
158
}
159