Completed
Branch breakout (13405a)
by Paul
02:10
created

SIP2Client::setDefault()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
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
138 3
        $this->logger->info("SIP2: result={$result}");
139
140
        // test message for CRC validity
141 3
        if (SIP2Response::checkCRC($result)) {
142 2
            $this->logger->debug("SIP2: Message from ACS passed CRC check");
143
        } else {
144
            //CRC check failed, we resend the request
145 2
            if ($depth < $this->maxretry) {
146 2
                $depth++;
147 2
                $this->logger->warning("SIP2: Message failed CRC check, retry {$depth})");
148 2
                $result = $this->getRawResponse($request, $depth);
149
            } else {
150 1
                $errMsg="SIP2: Failed to get valid CRC after {$this->maxretry} retries.";
151 1
                $this->logger->critical($errMsg);
152 1
                throw new RuntimeException($errMsg);
153
            }
154
        }
155
156 2
        return $result;
157
    }
158
159
    /**
160
     * Connect to ACS via SIP2
161
     *
162
     * The $bind parameter can be useful where a machine which has multiple outbound connections and its important
163
     * to control which one is used (normally because the remote SIP2 service is firewalled to particular IPs
164
     *
165
     * @param string $address ip:port of remote SIP2 service
166
     * @param string|null $bind local ip to bind socket to
167
     * @throws RuntimeException if connection cannot be established
168
     */
169 5
    public function connect($address, $bind = null)
170
    {
171 5
        $this->logger->debug("SIP2Client: Attempting connection to $address");
172
173 5
        $this->socket = $this->getSocketFactory()->createFromString($address, $scheme);
174
175
        try {
176 5
            if (!empty($bind)) {
177 1
                $this->logger->debug("SIP2Client: binding socket to $bind");
178 1
                $this->socket->bind($bind);
179
            }
180
181 5
            $this->socket->connect($address);
182 1
        } catch (\Exception $e) {
183 1
            $this->socket->close();
184 1
            $this->socket = null;
185 1
            $this->logger->error("SIP2Client: Failed to connect: " . $e->getMessage());
186 1
            throw new RuntimeException("Connection failure", 0, $e);
187
        }
188
189 4
        $this->logger->debug("SIP2Client: connected");
190 4
    }
191
192
    /**
193
     * Disconnect from ACS
194
     */
195 1
    public function disconnect()
196
    {
197 1
        $this->socket->close();
198 1
        $this->socket = null;
199 1
    }
200
}
201