Passed
Push — master ( 53ff33...38fc19 )
by Paul
07:45
created

SIP2Client::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 2
c 2
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace lordelph\SIP2;
4
5
/**
6
 * SIP2Client Class
7
 *
8
 * This class provides a method of communicating with an Integrated
9
 * Library System using 3M's SIP2 standard.
10
 *
11
 * @licence    https://opensource.org/licenses/MIT
12
 * @copyright  John Wohlers <[email protected]>
13
 * @copyright  Paul Dixon <[email protected]>
14
 */
15
16
use lordelph\SIP2\Exception\RuntimeException;
17
use lordelph\SIP2\Request\SIP2Request;
18
use lordelph\SIP2\Response\SIP2Response;
19
use Psr\Log\LoggerAwareInterface;
20
use Psr\Log\LoggerAwareTrait;
21
use Psr\Log\LoggerInterface;
22
use Psr\Log\NullLogger;
23
use Socket\Raw\Factory;
24
use Socket\Raw\Socket;
25
26
/**
27
 * SIP2Client provides a simple client for SIP2 library services
28
 *
29
 * In the specification, and the comments below, 'SC' (or Self Check) denotes the client, and ACS (or Automated
30
 * Circulation System) denotes the server.
31
 */
32
class SIP2Client implements LoggerAwareInterface
33
{
34
    use LoggerAwareTrait;
35
36
    //-----------------------------------------------------
37
    // request options
38
    //-----------------------------------------------------
39
40
    /**
41
     * @var array name=>value request defaults used for every request
42
     */
43
    private $default=[];
44
45
    //-----------------------------------------------------
46
    // connection handling
47
    //-----------------------------------------------------
48
49
    /** @var int maximum number of resends in the event of CRC failure */
50
    public $maxretry = 3;
51
52
    /** @var Socket */
53
    private $socket;
54
55
    /** @var Factory injectable factory for creating socket connections */
56
    private $socketFactory;
57
58
    /**
59
     * Constructor allows you to provide a PSR-3 logger, but you can also use the setLogger method
60
     * later on.
61
     *
62
     * @param LoggerInterface|null $logger
63
     */
64 5
    public function __construct(LoggerInterface $logger = null)
65
    {
66 5
        $this->logger = $logger ?? new NullLogger();
67 5
        $this->setDefault('InstitutionId', 'WohlersSIP');
68 5
    }
69
70 5
    public function setDefault($name, $value)
71
    {
72 5
        $this->default[$name] = $value;
73 5
    }
74
75
    /**
76
     * Allows an alternative socket factory to be injected. The allows us to
77
     * mock socket connections for testing
78
     *
79
     * @param Factory $factory
80
     */
81 5
    public function setSocketFactory(Factory $factory)
82
    {
83 5
        $this->socketFactory = $factory;
84 5
    }
85
86
    /**
87
     * Get the current socket factory, creating a default one if necessary
88
     * @return Factory
89
     */
90 5
    private function getSocketFactory()
91
    {
92 5
        if (is_null($this->socketFactory)) {
93
            $this->socketFactory = new Factory(); //@codeCoverageIgnore
94
        }
95 5
        return $this->socketFactory;
96
    }
97
98
99
    /**
100
     * @param SIP2Request $request
101
     * @return SIP2Response
102
     * @throws RuntimeException if server fails to produce a valid response
103
     */
104 3
    public function sendRequest(SIP2Request $request) : SIP2Response
105
    {
106 3
        foreach ($this->default as $name => $value) {
107 3
            $request->setDefault($name, $value);
108
        }
109
110 3
        $raw = $this->getRawResponse($request);
111 2
        return SIP2Response::parse($raw);
112
    }
113
114 3
    private function getRawResponse(SIP2Request $request, $depth = 0)
115
    {
116 3
        $result = '';
117 3
        $terminator = '';
118
119 3
        $message = $request->getMessageString();
120
121 3
        $this->logger->debug('SIP2: Sending SIP2 request '.trim($message));
122 3
        $this->socket->write($message);
123
124 3
        $this->logger->debug('SIP2: Request Sent, Reading response');
125
126 3
        while ($terminator != "\x0D") {
127
            //@codeCoverageIgnoreStart
128
            try {
129
                $terminator = $this->socket->recv(1, 0);
130
            } catch (\Exception $e) {
131
                break;
132
            }
133
            //@codeCoverageIgnoreEnd
134
135 3
            $result = $result . $terminator;
136
        }
137
        //if the remote end uses CRLF as a terminator, we might have a stray LF in our buffer - by trimming
138
        //the responses, we ensure those don't upset us
139 3
        $result=trim($result);
140
141 3
        $this->logger->info("SIP2: result={$result}");
142
143
        // test message for CRC validity
144 3
        if (SIP2Response::checkCRC($result)) {
145 2
            $this->logger->debug("SIP2: Message from ACS passed CRC check");
146
        } else {
147
            //CRC check failed, we resend the request
148 2
            if ($depth < $this->maxretry) {
149 2
                $depth++;
150 2
                $this->logger->warning("SIP2: Message failed CRC check, retry {$depth})");
151 2
                $result = $this->getRawResponse($request, $depth);
152
            } else {
153 1
                $errMsg="SIP2: Failed to get valid CRC after {$this->maxretry} retries.";
154 1
                $this->logger->critical($errMsg);
155 1
                throw new RuntimeException($errMsg);
156
            }
157
        }
158
159 2
        return $result;
160
    }
161
162
    /**
163
     * Connect to ACS via SIP2
164
     *
165
     * The $bind parameter can be useful where a machine which has multiple outbound connections and its important
166
     * to control which one is used (normally because the remote SIP2 service is firewalled to particular IPs
167
     *
168
     * @param string $address ip:port of remote SIP2 service
169
     * @param string|null $bind local ip to bind socket to
170
     * @param int $timeout number of seconds to allow for connection to succeed
171
     */
172 5
    public function connect($address, $bind = null, $timeout = 15)
173
    {
174 5
        $this->logger->debug("SIP2Client: Attempting connection to $address");
175
176
        try {
177 5
            $this->socket = $this->createClient($address, $bind, $timeout);
178 1
        } catch (\Exception $e) {
179 1
            $this->socket = null;
180 1
            $this->logger->error("SIP2Client: Failed to connect: " . $e->getMessage());
181 1
            throw new RuntimeException("Connection failure", 0, $e);
182
        }
183
184 4
        $this->logger->debug("SIP2Client: connected");
185 4
    }
186
187
    /**
188
     * A 'missing' factory method which allows us to establish a socket bound to a particular interface
189
     * @param $address
190
     * @param $bind
191
     * @param $timeout
192
     * @return Socket
193
     * @throws \Exception
194
     */
195 5
    private function createClient($address, $bind, $timeout) : Socket
196
    {
197 5
        $factory = $this->getSocketFactory();
198
199 5
        $socket = $factory->createFromString($address, $scheme);
200
201
        try {
202 5
            if (!empty($bind)) {
203 1
                $this->logger->debug("SIP2Client: binding socket to $bind");
204 1
                $socket->bind($bind);
205
            }
206
207 5
            if ($timeout === null) {
208
                $socket->connect($address);
209
            } else {
210
                // connectTimeout enables non-blocking mode, so turn blocking on again
211 5
                $socket->connectTimeout($address, $timeout);
212 4
                $socket->setBlocking(true);
213
            }
214 1
        } catch (\Exception $e) {
215 1
            $socket->close();
216 1
            throw $e;
217
        }
218
219 4
        return $socket;
220
    }
221
222
223
    /**
224
     * Disconnect from ACS
225
     */
226 1
    public function disconnect()
227
    {
228 1
        $this->socket->close();
229 1
        $this->socket = null;
230 1
    }
231
}
232