1: <?php
2:
3: namespace GeoIp2\WebService;
4:
5: use GeoIp2\Exception\GeoIp2Exception;
6: use GeoIp2\Exception\HttpException;
7: use GeoIp2\Exception\WebServiceException;
8: use GeoIp2\Model\City;
9: use GeoIp2\Model\CityIspOrg;
10: use GeoIp2\Model\Country;
11: use GeoIp2\Model\Omni;
12: use Guzzle\Http\Client as GuzzleClient;
13: use Guzzle\Common\Exception\RuntimeException;
14: use Guzzle\Http\Exception\ClientErrorResponseException;
15: use Guzzle\Http\Exception\ServerErrorResponseException;
16:
17: /**
18: * This class provides a client API for all the GeoIP2 web service's
19: * end points. The end points are Country, City, City/ISP/Org, and Omni. Each
20: * end point returns a different set of data about an IP address, with Country
21: * returning the least data and Omni the most.
22: *
23: * Each web service end point is represented by a different model class, and
24: * these model classes in turn contain multiple Record classes. The record
25: * classes have attributes which contain data about the IP address.
26: *
27: * If the web service does not return a particular piece of data for an IP
28: * address, the associated attribute is not populated.
29: *
30: * The web service may not return any information for an entire record, in
31: * which case all of the attributes for that record class will be empty.
32: *
33: * **Usage**
34: *
35: * The basic API for this class is the same for all of the web service end
36: * points. First you create a web service object with your MaxMind
37: * <code>$userId</code> and <code>$licenseKey</code>, then you call the method
38: * corresponding to a specific end point, passing it the IP address you want
39: * to look up.
40: *
41: * If the request succeeds, the method call will return a model class for
42: * the end point you called. This model in turn contains multiple record
43: * classes, each of which represents part of the data returned by the web
44: * service.
45: *
46: * If the request fails, the client class throws an exception.
47: *
48: * **Exceptions**
49: *
50: * For details on the possible errors returned by the web service itself, see
51: * {@link http://dev.maxmind.com/geoip2/geoip/web-services the GeoIP2 web
52: * service docs}.
53: *
54: * If the web service returns an explicit error document, this is thrown as a
55: * {@link \GeoIp2\Exception\WebServiceException}. If some other sort of
56: * transport error occurs, this is thrown as a {@link
57: * \GeoIp2\Exception\HttpException}. The difference is that the web service
58: * error includes an error message and error code delivered by the web
59: * service. The latter is thrown when some sort of unanticipated error occurs,
60: * such as the web service returning a 500 or an invalid error document.
61: *
62: * If the web service returns any status code besides 200, 4xx, or 5xx, this
63: * also becomes a {@link \GeoIp2\Exception\HttpException}.
64: *
65: * Finally, if the web service returns a 200 but the body is invalid, the
66: * client throws a {@link \GeoIp2\Exception\GeoIp2Exception}.
67: */
68: class Client
69: {
70: private $userId;
71: private $licenseKey;
72: private $languages;
73: private $host;
74: private $guzzleClient;
75:
76: /**
77: * Constructor.
78: *
79: * @param int $userId Your MaxMind user ID
80: * @param string $licenseKey Your MaxMind license key
81: * @param array $languages List of language codes to use in name property
82: * from most preferred to least preferred.
83: * @param string $host Optional host parameter
84: * @param object $guzzleClient Optional Guzzle client to use (to facilitate
85: * unit testing).
86: */
87: public function __construct(
88: $userId,
89: $licenseKey,
90: $languages = array('en'),
91: $host = 'geoip.maxmind.com',
92: $guzzleClient = null
93: ) {
94: $this->userId = $userId;
95: $this->licenseKey = $licenseKey;
96: $this->languages = $languages;
97: $this->host = $host;
98: // To enable unit testing
99: $this->guzzleClient = $guzzleClient;
100: }
101:
102: /**
103: * This method calls the GeoIP2 City endpoint.
104: *
105: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
106: * address is provided, the address that the web service is called
107: * from will be used.
108: *
109: * @return \GeoIp2\Model\City
110: *
111: * @throws \GeoIp2\Exception\GeoIp2Exception if there was a generic
112: * error processing your request.
113: * @throws \GeoIp2\Exception\HttpException if there was an HTTP transport
114: * error.
115: * @throws \GeoIp2\Exception\WebServiceException if an error was returned
116: * by MaxMind's GeoIP2 web service.
117: */
118: public function city($ipAddress = 'me')
119: {
120: return $this->responseFor('city', 'City', $ipAddress);
121: }
122:
123: /**
124: * This method calls the GeoIP2 Country endpoint.
125: *
126: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
127: * address is provided, the address that the web service is called
128: * from will be used.
129: *
130: * @return \GeoIp2\Model\Country
131: *
132: * @throws \GeoIp2\Exception\GeoIp2Exception if there was a generic
133: * error processing your request.
134: * @throws \GeoIp2\Exception\HttpException if there was an HTTP transport
135: * error.
136: * @throws \GeoIp2\Exception\WebServiceException if an error was returned
137: * by MaxMind's GeoIP2 web service.
138: */
139: public function country($ipAddress = 'me')
140: {
141: return $this->responseFor('country', 'Country', $ipAddress);
142: }
143:
144: /**
145: * This method calls the GeoIP2 City/ISP/Org endpoint.
146: *
147: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
148: * address is provided, the address that the web service is called
149: * from will be used.
150: *
151: * @return \GeoIp2\Model\CityIspOrg
152: *
153: * @throws \GeoIp2\Exception\GeoIp2Exception if there was a generic
154: * error processing your request.
155: * @throws \GeoIp2\Exception\HttpException if there was an HTTP transport
156: * error.
157: * @throws \GeoIp2\Exception\WebServiceException if an error was returned
158: * by MaxMind's GeoIP2 web service.
159: */
160: public function cityIspOrg($ipAddress = 'me')
161: {
162: return $this->responseFor('city_isp_org', 'CityIspOrg', $ipAddress);
163: }
164:
165: /**
166: * This method calls the GeoIP2 Omni endpoint.
167: *
168: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
169: * address is provided, the address that the web service is called
170: * from will be used.
171: *
172: * @return \GeoIp2\Model\Omni
173: *
174: * @throws \GeoIp2\Exception\GeoIp2Exception if there was a generic
175: * error processing your request.
176: * @throws \GeoIp2\Exception\HttpException if there was an HTTP transport
177: * error.
178: * @throws \GeoIp2\Exception\WebServiceException if an error was returned
179: * by MaxMind's GeoIP2 web service.
180: */
181: public function omni($ipAddress = 'me')
182: {
183: return $this->responseFor('omni', 'Omni', $ipAddress);
184: }
185:
186: private function responseFor($endpoint, $class, $ipAddress)
187: {
188: $uri = implode('/', array($this->baseUri(), $endpoint, $ipAddress));
189:
190: $client = $this->guzzleClient ?
191: $this->guzzleClient : new GuzzleClient();
192: $request = $client->get($uri, array('Accept' => 'application/json'));
193: $request->setAuth($this->userId, $this->licenseKey);
194: $ua = $request->getHeader('User-Agent');
195: $ua = "GeoIP2 PHP API ($ua)";
196: $request->setHeader('User-Agent', $ua);
197:
198: $response = null;
199: try {
200: $response = $request->send();
201: } catch (ClientErrorResponseException $e) {
202: $this->handle4xx($e->getResponse(), $uri);
203: } catch (ServerErrorResponseException $e) {
204: $this->handle5xx($e->getResponse(), $uri);
205: }
206:
207: if ($response && $response->isSuccessful()) {
208: $body = $this->handleSuccess($response, $uri);
209: $class = "GeoIp2\\Model\\" . $class;
210: return new $class($body, $this->languages);
211: } else {
212: $this->handleNon200($response, $uri);
213: }
214: }
215:
216: private function handleSuccess($response, $uri)
217: {
218: if ($response->getContentLength() == 0) {
219: throw new GeoIp2Exception(
220: "Received a 200 response for $uri but did not " .
221: "receive a HTTP body."
222: );
223: }
224:
225: try {
226: return $response->json();
227: } catch (RuntimeException $e) {
228: throw new GeoIp2Exception(
229: "Received a 200 response for $uri but could not decode " .
230: "the response as JSON: " . $e->getMessage()
231: );
232:
233: }
234: }
235:
236: private function handle4xx($response, $uri)
237: {
238: $status = $response->getStatusCode();
239:
240: $body = array();
241:
242: if ($response->getContentLength() > 0) {
243: if (strstr($response->getContentType(), 'json')) {
244: try {
245: $body = $response->json();
246: if (!isset($body['code']) || !isset($body['error'])) {
247: throw new GeoIp2Exception(
248: 'Response contains JSON but it does not specify ' .
249: 'code or error keys: ' . $response->getBody()
250: );
251: }
252: } catch (RuntimeException $e) {
253: throw new HttpException(
254: "Received a $status error for $uri but it did not " .
255: "include the expected JSON body: " .
256: $e->getMessage(),
257: $status,
258: $uri
259: );
260: }
261: } else {
262: throw new HttpException(
263: "Received a $status error for $uri with the " .
264: "following body: " . $response->getBody(),
265: $status,
266: $uri
267: );
268: }
269: } else {
270: throw new HttpException(
271: "Received a $status error for $uri with no body",
272: $status,
273: $uri
274: );
275: }
276:
277: throw new WebServiceException(
278: $body['error'],
279: $body['code'],
280: $status,
281: $uri
282: );
283: }
284:
285: private function handle5xx($response, $uri)
286: {
287: $status = $response->getStatusCode();
288:
289: throw new HttpException(
290: "Received a server error ($status) for $uri",
291: $status,
292: $uri
293: );
294: }
295:
296: private function handleNon200($response, $uri)
297: {
298: $status = $response->getStatusCode();
299:
300: throw new HttpException(
301: "Received a very surprising HTTP status " .
302: "($status) for $uri",
303: $status,
304: $uri
305: );
306: }
307:
308: private function baseUri()
309: {
310: return 'https://' . $this->host . '/geoip/v2.0';
311: }
312: }
313: