1: <?php
2: /**
3: * @copyright 2012-2013 Rackspace Hosting, Inc.
4: * See COPYING for licensing information
5: * @package phpOpenCloud
6: * @version 1.0
7: * @author Glen Campbell <glen.campbell@rackspace.com>
8: * @author Jamie Hannaford <jamie.hannaford@rackspace.com>
9: */
10:
11: namespace OpenCloud\Common;
12:
13: use OpenCloud\Common\Lang;
14: use OpenCloud\Common\Exceptions\AttributeError;
15: use OpenCloud\Common\Exceptions\JsonError;
16: use OpenCloud\Common\Exceptions\UrlError;
17:
18: /**
19: * The root class for all other objects used or defined by this SDK.
20: *
21: * It contains common code for error handling as well as service functions that
22: * are useful. Because it is an abstract class, it cannot be called directly,
23: * and it has no publicly-visible properties.
24: */
25: abstract class Base
26: {
27:
28: private $http_headers = array();
29: private $_errors = array();
30:
31: /**
32: * Debug status.
33: *
34: * @var LoggerInterface
35: * @access private
36: */
37: private $logger;
38:
39: /**
40: * Sets the Logger object.
41: *
42: * @param \OpenCloud\Common\Log\LoggerInterface $logger
43: */
44: public function setLogger(Log\LoggerInterface $logger)
45: {
46: $this->logger = $logger;
47: }
48:
49: /**
50: * Returns the Logger object.
51: *
52: * @return \OpenCloud\Common\Log\AbstractLogger
53: */
54: public function getLogger()
55: {
56: if (null === $this->logger) {
57: $this->setLogger(new Log\Logger);
58: }
59: return $this->logger;
60: }
61:
62: /**
63: * Returns the URL of the service/object
64: *
65: * The assumption is that nearly all objects will have a URL; at this
66: * base level, it simply throws an exception to enforce the idea that
67: * subclasses need to define this method.
68: *
69: * @throws UrlError
70: */
71: public function url($subresource = '')
72: {
73: throw new UrlError(Lang::translate(
74: 'URL method must be overridden in class definition'
75: ));
76: }
77:
78: /**
79: * Populates the current object based on an unknown data type.
80: *
81: * @param array|object|string|integer $info
82: * @throws Exceptions\InvalidArgumentError
83: */
84: public function populate($info, $setObjects = true)
85: {
86: if (is_string($info) || is_integer($info)) {
87:
88: // If the data type represents an ID, the primary key is set
89: // and we retrieve the full resource from the API
90: $this->{$this->primaryKeyField()} = (string) $info;
91: $this->refresh($info);
92:
93: } elseif (is_object($info) || is_array($info)) {
94:
95: foreach($info as $key => $value) {
96:
97: if ($key == 'metadata' || $key == 'meta') {
98:
99: if (empty($this->metadata) || !$this->metadata instanceof Metadata) {
100: $this->metadata = new Metadata;
101: }
102:
103: // Metadata
104: $this->$key->setArray($value);
105:
106: } elseif (!empty($this->associatedResources[$key]) && $setObjects === true) {
107:
108: // Associated resource
109: try {
110: $resource = $this->service()->resource($this->associatedResources[$key], $value);
111: $resource->setParent($this);
112: $this->$key = $resource;
113: } catch (Exception\ServiceException $e) {}
114:
115: } elseif (!empty($this->associatedCollections[$key]) && $setObjects === true) {
116:
117: // Associated collection
118: try {
119: $this->$key = $this->service()->resourceList($this->associatedCollections[$key], null, $this);
120: } catch (Exception\ServiceException $e) {}
121:
122: } else {
123:
124: // Normal key/value pair
125: $this->$key = $value;
126: }
127: }
128: } elseif (null !== $info) {
129: throw new Exceptions\InvalidArgumentError(sprintf(
130: Lang::translate('Argument for [%s] must be string or object'),
131: get_class()
132: ));
133: }
134: }
135:
136: /**
137: * Sets extended attributes on an object and validates them
138: *
139: * This function is provided to ensure that attributes cannot
140: * arbitrarily added to an object. If this function is called, it
141: * means that the attribute is not defined on the object, and thus
142: * an exception is thrown.
143: *
144: * @codeCoverageIgnore
145: *
146: * @param string $property the name of the attribute
147: * @param mixed $value the value of the attribute
148: * @return void
149: */
150: public function __set($property, $value)
151: {
152: $this->setProperty($property, $value);
153: }
154:
155: /**
156: * Sets an extended (unrecognized) property on the current object
157: *
158: * If RAXSDK_STRICT_PROPERTY_CHECKS is TRUE, then the prefix of the
159: * property name must appear in the $prefixes array, or else an
160: * exception is thrown.
161: *
162: * @param string $property the property name
163: * @param mixed $value the value of the property
164: * @param array $prefixes optional list of supported prefixes
165: * @throws \OpenCloud\AttributeError if strict checks are on and
166: * the property prefix is not in the list of prefixes.
167: */
168: public function setProperty($property, $value, array $prefixes = array())
169: {
170: // if strict checks are off, go ahead and set it
171: if (!RAXSDK_STRICT_PROPERTY_CHECKS
172: || $this->checkAttributePrefix($property, $prefixes)
173: ) {
174: $this->$property = $value;
175: } else {
176: // if that fails, then throw the exception
177: throw new AttributeError(sprintf(
178: Lang::translate('Unrecognized attribute [%s] for [%s]'),
179: $property,
180: get_class($this)
181: ));
182: }
183: }
184:
185: /**
186: * Converts an array of key/value pairs into a single query string
187: *
188: * For example, array('A'=>1,'B'=>2) would become 'A=1&B=2'.
189: *
190: * @param array $arr array of key/value pairs
191: * @return string
192: */
193: public function makeQueryString($array)
194: {
195: $queryString = '';
196:
197: foreach($array as $key => $value) {
198: if ($queryString) {
199: $queryString .= '&';
200: }
201: $queryString .= urlencode($key) . '=' . urlencode($this->to_string($value));
202: }
203:
204: return $queryString;
205: }
206:
207: /**
208: * Checks the most recent JSON operation for errors
209: *
210: * This function should be called after any `json_*()` function call.
211: * This ensures that nasty JSON errors are detected and the proper
212: * exception thrown.
213: *
214: * Example:
215: * `$obj = json_decode($string);`
216: * `if (check_json_error()) do something ...`
217: *
218: * @return boolean TRUE if an error occurred, FALSE if none
219: * @throws JsonError
220: *
221: * @codeCoverageIgnore
222: */
223: public function checkJsonError()
224: {
225: switch (json_last_error()) {
226: case JSON_ERROR_NONE:
227: return;
228: case JSON_ERROR_DEPTH:
229: $jsonError = 'JSON error: The maximum stack depth has been exceeded';
230: break;
231: case JSON_ERROR_STATE_MISMATCH:
232: $jsonError = 'JSON error: Invalid or malformed JSON';
233: break;
234: case JSON_ERROR_CTRL_CHAR:
235: $jsonError = 'JSON error: Control character error, possibly incorrectly encoded';
236: break;
237: case JSON_ERROR_SYNTAX:
238: $jsonError = 'JSON error: Syntax error';
239: break;
240: case JSON_ERROR_UTF8:
241: $jsonError = 'JSON error: Malformed UTF-8 characters, possibly incorrectly encoded';
242: break;
243: default:
244: $jsonError = 'Unexpected JSON error';
245: break;
246: }
247:
248: if (isset($jsonError)) {
249: throw new JsonError(Lang::translate($jsonError));
250: }
251: }
252:
253: /**
254: * Returns a class that implements the HttpRequest interface.
255: *
256: * This can be stubbed out for unit testing and avoid making live calls.
257: */
258: public function getHttpRequestObject($url, $method = 'GET', array $options = array())
259: {
260: return new Request\Curl($url, $method, $options);
261: }
262:
263: /**
264: * Checks the attribute $property and only permits it if the prefix is
265: * in the specified $prefixes array
266: *
267: * This is to support extension namespaces in some services.
268: *
269: * @param string $property the name of the attribute
270: * @param array $prefixes a list of prefixes
271: * @return boolean TRUE if valid; FALSE if not
272: */
273: private function checkAttributePrefix($property, array $prefixes = array())
274: {
275: $prefix = strstr($property, ':', true);
276:
277: if (in_array($prefix, $prefixes)) {
278: return true;
279: } else {
280: return false;
281: }
282: }
283:
284: /**
285: * Converts a value to an HTTP-displayable string form
286: *
287: * @param mixed $x a value to convert
288: * @return string
289: */
290: private function to_string($x)
291: {
292: if (is_bool($x) && $x) {
293: return 'True';
294: } elseif (is_bool($x)) {
295: return 'False';
296: } else {
297: return (string) $x;
298: }
299: }
300:
301: }
302: