      1: <?php
      2: /**
      3:  * Copyright 2012-2014 Rackspace US, Inc.
      4:  *
      5:  * Licensed under the Apache License, Version 2.0 (the "License");
      6:  * you may not use this file except in compliance with the License.
      7:  * You may obtain a copy of the License at
      8:  *
      9:  * http://www.apache.org/licenses/LICENSE-2.0
     10:  *
     11:  * Unless required by applicable law or agreed to in writing, software
     12:  * distributed under the License is distributed on an "AS IS" BASIS,
     13:  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14:  * See the License for the specific language governing permissions and
     15:  * limitations under the License.
     16:  */
     17:
     18: namespace OpenCloud\Common;
     19:
     20: use OpenCloud\Common\Collection\ResourceIterator;
     21: use OpenCloud\Common\Constants\Header as HeaderConst;
     22: use OpenCloud\Common\Constants\Mime as MimeConst;
     23: use OpenCloud\Common\Exceptions\JsonError;
     24: use Psr\Log\LoggerInterface;
     25:
     26: /**
     27:  * The root class for all other objects used or defined by this SDK.
     28:  *
     29:  * It contains common code for error handling as well as service functions that
     30:  * are useful. Because it is an abstract class, it cannot be called directly,
     31:  * and it has no publicly-visible properties.
     32:  */
     33: abstract class Base
     34: {
     35:     /**
     36:      * Holds all the properties added by overloading.
     37:      *
     38:      * @var array
     39:      */
     40:     private $properties = array();
     41:
     42:     /**
     43:      * The logger instance
     44:      *
     45:      * @var LoggerInterface
     46:      */
     47:     private $logger;
     48:
     49:     /**
     50:      * The aliases configure for the properties of the instance.
     51:      *
     52:      * @var array
     53:      */
     54:     protected $aliases = array();
     55:
     56:     /**
     57:      * @return static
     58:      */
     59:     public static function getInstance()
     60:     {
     61:         return new static();
     62:     }
     63:
     64:     /**
     65:      * Intercept non-existent method calls for dynamic getter/setter functionality.
     66:      *
     67:      * @param $method
     68:      * @param $args
     69:      * @throws Exceptions\RuntimeException
     70:      */
     71:     public function __call($method, $args)
     72:     {
     73:         $prefix = substr($method, 0, 3);
     74:
     75:         // Get property - convert from camel case to underscore
     76:         $property = lcfirst(substr($method, 3));
     77:
     78:         // Only do these methods on properties which exist
     79:         if ($this->propertyExists($property) && $prefix == 'get') {
     80:             return $this->getProperty($property);
     81:         }
     82:
     83:         // Do setter
     84:         if ($this->propertyExists($property) && $prefix == 'set') {
     85:             return $this->setProperty($property, $args[0]);
     86:         }
     87:
     88:         throw new Exceptions\RuntimeException(sprintf(
     89:             'No method %s::%s()',
     90:             get_class($this),
     91:             $method
     92:         ));
     93:     }
     94:
     95:     /**
     96:      * We can set a property under three conditions:
     97:      *
     98:      * 1. If it has a concrete setter: setProperty()
     99:      * 2. If the property exists
    100:      * 3. If the property name's prefix is in an approved list
    101:      *
    102:      * @param  mixed $property
    103:      * @param  mixed $value
    104:      * @return mixed
    105:      */
    106:     protected function setProperty($property, $value)
    107:     {
    108:         $setter = 'set' . $this->toCamel($property);
    109:
    110:         if (method_exists($this, $setter)) {
    111:             return call_user_func(array($this, $setter), $value);
    112:         } elseif (false !== ($propertyVal = $this->propertyExists($property))) {
    113:
    114:             // Are we setting a public or private property?
    115:             if ($this->isAccessible($propertyVal)) {
    116:                 $this->$propertyVal = $value;
    117:             } else {
    118:                 $this->properties[$propertyVal] = $value;
    119:             }
    120:
    121:             return $this;
    122:         } else {
    123:
    124:             $this->getLogger()->warning(
    125:                 'Attempted to set {property} with value {value}, but the'
    126:                 . ' property has not been defined. Please define first.',
    127:                 array(
    128:                     'property' => $property,
    129:                     'value'    => print_r($value, true)
    130:                 )
    131:             );
    132:         }
    133:     }
    134:
    135:     /**
    136:      * Basic check to see whether property exists.
    137:      *
    138:      * @param string $property   The property name being investigated.
    139:      * @param bool   $allowRetry If set to TRUE, the check will try to format the name in underscores because
    140:      *                           there are sometimes discrepancies between camelCaseNames and underscore_names.
    141:      * @return bool
    142:      */
    143:     protected function propertyExists($property, $allowRetry = true)
    144:     {
    145:         if (!property_exists($this, $property) && !$this->checkAttributePrefix($property)) {
    146:             // Convert to under_score and retry
    147:             if ($allowRetry) {
    148:                 return $this->propertyExists($this->toUnderscores($property), false);
    149:             } else {
    150:                 $property = false;
    151:             }
    152:         }
    153:
    154:         return $property;
    155:     }
    156:
    157:     /**
    158:      * Convert a string to camelCase format.
    159:      *
    160:      * @param       $string
    161:      * @param  bool $capitalise Optional flag which allows for word capitalization.
    162:      * @return mixed
    163:      */
    164:     public function toCamel($string, $capitalise = true)
    165:     {
    166:         if ($capitalise) {
    167:             $string = ucfirst($string);
    168:         }
    169:
    170:         return preg_replace_callback('/_([a-z])/', function ($char) {
    171:             return strtoupper($char[1]);
    172:         }, $string);
    173:     }
    174:
    175:     /**
    176:      * Convert string to underscore format.
    177:      *
    178:      * @param $string
    179:      * @return mixed
    180:      */
    181:     public function toUnderscores($string)
    182:     {
    183:         $string = lcfirst($string);
    184:
    185:         return preg_replace_callback('/([A-Z])/', function ($char) {
    186:             return "_" . strtolower($char[1]);
    187:         }, $string);
    188:     }
    189:
    190:     /**
    191:      * Does the property exist in the object variable list (i.e. does it have public or protected visibility?)
    192:      *
    193:      * @param $property
    194:      * @return bool
    195:      */
    196:     private function isAccessible($property)
    197:     {
    198:         return array_key_exists($property, get_object_vars($this));
    199:     }
    200:
    201:     /**
    202:      * Checks the attribute $property and only permits it if the prefix is
    203:      * in the specified $prefixes array
    204:      *
    205:      * This is to support extension namespaces in some services.
    206:      *
    207:      * @param string $property the name of the attribute
    208:      * @return boolean
    209:      */
    210:     private function checkAttributePrefix($property)
    211:     {
    212:         if (!method_exists($this, 'getService')) {
    213:             return false;
    214:         }
    215:         $prefix = strstr($property, ':', true);
    216:
    217:         return in_array($prefix, $this->getService()->namespaces());
    218:     }
    219:
    220:     /**
    221:      * Grab value out of the data array.
    222:      *
    223:      * @param string $property
    224:      * @return mixed
    225:      */
    226:     protected function getProperty($property)
    227:     {
    228:         if (array_key_exists($property, $this->properties)) {
    229:             return $this->properties[$property];
    230:         } elseif (array_key_exists($this->toUnderscores($property), $this->properties)) {
    231:             return $this->properties[$this->toUnderscores($property)];
    232:         } elseif (method_exists($this, 'get' . ucfirst($property))) {
    233:             return call_user_func(array($this, 'get' . ucfirst($property)));
    234:         } elseif (false !== ($propertyVal = $this->propertyExists($property)) && $this->isAccessible($propertyVal)) {
    235:             return $this->$propertyVal;
    236:         }
    237:
    238:         return null;
    239:     }
    240:
    241:     /**
    242:      * Sets the logger.
    243:      *
    244:      * @param Log\LoggerInterface $logger
    245:      * @return $this
    246:      */
    247:     public function setLogger(Log\LoggerInterface $logger)
    248:     {
    249:         $this->logger = $logger;
    250:
    251:         return $this;
    252:     }
    253:
    254:     /**
    255:      * Returns the Logger object.
    256:      *
    257:      * @return \OpenCloud\Common\Log\AbstractLogger
    258:      */
    259:     public function getLogger()
    260:     {
    261:         if (null === $this->logger) {
    262:             $this->setLogger(new Log\Logger);
    263:         }
    264:
    265:         return $this->logger;
    266:     }
    267:
    268:     /**
    269:      * @deprecated
    270:      */
    271:     public function url($path = null, array $query = array())
    272:     {
    273:         return $this->getUrl($path, $query);
    274:     }
    275:
    276:     /**
    277:      * Populates the current object based on an unknown data type.
    278:      *
    279:      * @param  mixed $info
    280:      * @param        bool
    281:      * @throws Exceptions\InvalidArgumentError
    282:      */
    283:     public function populate($info, $setObjects = true)
    284:     {
    285:         if (is_string($info) || is_integer($info)) {
    286:
    287:             $this->setProperty($this->primaryKeyField(), $info);
    288:             $this->refresh($info);
    289:         } elseif (is_object($info) || is_array($info)) {
    290:
    291:             foreach ($info as $key => $value) {
    292:
    293:                 if ($key == 'metadata' || $key == 'meta') {
    294:
    295:                     // Try retrieving existing value
    296:                     if (null === ($metadata = $this->getProperty($key))) {
    297:                         // If none exists, create new object
    298:                         $metadata = new Metadata;
    299:                     }
    300:
    301:                     // Set values for metadata
    302:                     $metadata->setArray($value);
    303:
    304:                     // Set object property
    305:                     $this->setProperty($key, $metadata);
    306:                 } elseif (!empty($this->associatedResources[$key]) && $setObjects === true) {
    307:
    308:                     // Associated resource
    309:                     try {
    310:
    311:                         $resource = $this->getService()->resource($this->associatedResources[$key], $value);
    312:                         $resource->setParent($this);
    313:
    314:                         $this->setProperty($key, $resource);
    315:                     } catch (Exception\ServiceException $e) {
    316:                     }
    317:                 } elseif (!empty($this->associatedCollections[$key]) && $setObjects === true) {
    318:
    319:                     // Associated collection
    320:                     try {
    321:
    322:                         $className = $this->associatedCollections[$key];
    323:                         $options = $this->makeResourceIteratorOptions($className);
    324:                         $iterator = ResourceIterator::factory($this, $options, $value);
    325:
    326:                         $this->setProperty($key, $iterator);
    327:                     } catch (Exception\ServiceException $e) {
    328:                     }
    329:                 } elseif (!empty($this->aliases[$key])) {
    330:
    331:                     // Sometimes we might want to preserve camelCase
    332:                     // or covert `rax-bandwidth:bandwidth` to `raxBandwidth`
    333:                     $this->setProperty($this->aliases[$key], $value);
    334:                 } else {
    335:                     // Normal key/value pair
    336:                     $this->setProperty($key, $value);
    337:                 }
    338:             }
    339:         } elseif (null !== $info) {
    340:             throw new Exceptions\InvalidArgumentError(sprintf(
    341:                 Lang::translate('Argument for [%s] must be string or object'),
    342:                 get_class()
    343:             ));
    344:         }
    345:     }
    346:
    347:     /**
    348:      * Checks the most recent JSON operation for errors.
    349:      *
    350:      * @throws Exceptions\JsonError
    351:      * @codeCoverageIgnore
    352:      */
    353:     public static function checkJsonError()
    354:     {
    355:         switch (json_last_error()) {
    356:             case JSON_ERROR_NONE:
    357:                 return;
    358:             case JSON_ERROR_DEPTH:
    359:                 $jsonError = 'JSON error: The maximum stack depth has been exceeded';
    360:                 break;
    361:             case JSON_ERROR_STATE_MISMATCH:
    362:                 $jsonError = 'JSON error: Invalid or malformed JSON';
    363:                 break;
    364:             case JSON_ERROR_CTRL_CHAR:
    365:                 $jsonError = 'JSON error: Control character error, possibly incorrectly encoded';
    366:                 break;
    367:             case JSON_ERROR_SYNTAX:
    368:                 $jsonError = 'JSON error: Syntax error';
    369:                 break;
    370:             case JSON_ERROR_UTF8:
    371:                 $jsonError = 'JSON error: Malformed UTF-8 characters, possibly incorrectly encoded';
    372:                 break;
    373:             default:
    374:                 $jsonError = 'Unexpected JSON error';
    375:                 break;
    376:         }
    377:
    378:         if (isset($jsonError)) {
    379:             throw new JsonError(Lang::translate($jsonError));
    380:         }
    381:     }
    382:
    383:     public static function generateUuid()
    384:     {
    385:         return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
    386:             // 32 bits for "time_low"
    387:             mt_rand(0, 0xffff), mt_rand(0, 0xffff),
    388:
    389:             // 16 bits for "time_mid"
    390:             mt_rand(0, 0xffff),
    391:
    392:             // 16 bits for "time_hi_and_version",
    393:             // four most significant bits holds version number 4
    394:             mt_rand(0, 0x0fff) | 0x4000,
    395:
    396:             // 16 bits, 8 bits for "clk_seq_hi_res",
    397:             // 8 bits for "clk_seq_low",
    398:             // two most significant bits holds zero and one for variant DCE1.1
    399:             mt_rand(0, 0x3fff) | 0x8000,
    400:
    401:             // 48 bits for "node"
    402:             mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
    403:         );
    404:     }
    405:
    406:     public function makeResourceIteratorOptions($resource)
    407:     {
    408:         $options = array('resourceClass' => $this->stripNamespace($resource));
    409:
    410:         if (method_exists($resource, 'jsonCollectionName')) {
    411:             $options['key.collection'] = $resource::jsonCollectionName();
    412:         }
    413:
    414:         if (method_exists($resource, 'jsonCollectionElement')) {
    415:             $options['key.collectionElement'] = $resource::jsonCollectionElement();
    416:         }
    417:
    418:         return $options;
    419:     }
    420:
    421:     public function stripNamespace($namespace)
    422:     {
    423:         $array = explode('\\', $namespace);
    424:
    425:         return end($array);
    426:     }
    427:
    428:     protected static function getJsonHeader()
    429:     {
    430:         return array(HeaderConst::CONTENT_TYPE => MimeConst::JSON);
    431:     }
    432: }
    433: