1<?php 2/* 3 * Copyright 2011 Google 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 18namespace Google; 19 20use Google\Exception as GoogleException; 21use ReflectionObject; 22use ReflectionProperty; 23use stdClass; 24 25/** 26 * This class defines attributes, valid values, and usage which is generated 27 * from a given json schema. 28 * http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5 29 * 30 */ 31class Model implements \ArrayAccess 32{ 33 /** 34 * If you need to specify a NULL JSON value, use Google\Model::NULL_VALUE 35 * instead - it will be replaced when converting to JSON with a real null. 36 */ 37 const NULL_VALUE = "{}gapi-php-null"; 38 protected $internal_gapi_mappings = array(); 39 protected $modelData = array(); 40 protected $processed = array(); 41 42 /** 43 * Polymorphic - accepts a variable number of arguments dependent 44 * on the type of the model subclass. 45 */ 46 final public function __construct() 47 { 48 if (func_num_args() == 1 && is_array(func_get_arg(0))) { 49 // Initialize the model with the array's contents. 50 $array = func_get_arg(0); 51 $this->mapTypes($array); 52 } 53 $this->gapiInit(); 54 } 55 56 /** 57 * Getter that handles passthrough access to the data array, and lazy object creation. 58 * @param string $key Property name. 59 * @return mixed The value if any, or null. 60 */ 61 public function __get($key) 62 { 63 $keyType = $this->keyType($key); 64 $keyDataType = $this->dataType($key); 65 if ($keyType && !isset($this->processed[$key])) { 66 if (isset($this->modelData[$key])) { 67 $val = $this->modelData[$key]; 68 } elseif ($keyDataType == 'array' || $keyDataType == 'map') { 69 $val = array(); 70 } else { 71 $val = null; 72 } 73 74 if ($this->isAssociativeArray($val)) { 75 if ($keyDataType && 'map' == $keyDataType) { 76 foreach ($val as $arrayKey => $arrayItem) { 77 $this->modelData[$key][$arrayKey] = 78 new $keyType($arrayItem); 79 } 80 } else { 81 $this->modelData[$key] = new $keyType($val); 82 } 83 } else if (is_array($val)) { 84 $arrayObject = array(); 85 foreach ($val as $arrayIndex => $arrayItem) { 86 $arrayObject[$arrayIndex] = new $keyType($arrayItem); 87 } 88 $this->modelData[$key] = $arrayObject; 89 } 90 $this->processed[$key] = true; 91 } 92 93 return isset($this->modelData[$key]) ? $this->modelData[$key] : null; 94 } 95 96 /** 97 * Initialize this object's properties from an array. 98 * 99 * @param array $array Used to seed this object's properties. 100 * @return void 101 */ 102 protected function mapTypes($array) 103 { 104 // Hard initialise simple types, lazy load more complex ones. 105 foreach ($array as $key => $val) { 106 if ($keyType = $this->keyType($key)) { 107 $dataType = $this->dataType($key); 108 if ($dataType == 'array' || $dataType == 'map') { 109 $this->$key = array(); 110 foreach ($val as $itemKey => $itemVal) { 111 if ($itemVal instanceof $keyType) { 112 $this->{$key}[$itemKey] = $itemVal; 113 } else { 114 $this->{$key}[$itemKey] = new $keyType($itemVal); 115 } 116 } 117 } elseif ($val instanceof $keyType) { 118 $this->$key = $val; 119 } else { 120 $this->$key = new $keyType($val); 121 } 122 unset($array[$key]); 123 } elseif (property_exists($this, $key)) { 124 $this->$key = $val; 125 unset($array[$key]); 126 } elseif (property_exists($this, $camelKey = $this->camelCase($key))) { 127 // This checks if property exists as camelCase, leaving it in array as snake_case 128 // in case of backwards compatibility issues. 129 $this->$camelKey = $val; 130 } 131 } 132 $this->modelData = $array; 133 } 134 135 /** 136 * Blank initialiser to be used in subclasses to do post-construction initialisation - this 137 * avoids the need for subclasses to have to implement the variadics handling in their 138 * constructors. 139 */ 140 protected function gapiInit() 141 { 142 return; 143 } 144 145 /** 146 * Create a simplified object suitable for straightforward 147 * conversion to JSON. This is relatively expensive 148 * due to the usage of reflection, but shouldn't be called 149 * a whole lot, and is the most straightforward way to filter. 150 */ 151 public function toSimpleObject() 152 { 153 $object = new stdClass(); 154 155 // Process all other data. 156 foreach ($this->modelData as $key => $val) { 157 $result = $this->getSimpleValue($val); 158 if ($result !== null) { 159 $object->$key = $this->nullPlaceholderCheck($result); 160 } 161 } 162 163 // Process all public properties. 164 $reflect = new ReflectionObject($this); 165 $props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC); 166 foreach ($props as $member) { 167 $name = $member->getName(); 168 $result = $this->getSimpleValue($this->$name); 169 if ($result !== null) { 170 $name = $this->getMappedName($name); 171 $object->$name = $this->nullPlaceholderCheck($result); 172 } 173 } 174 175 return $object; 176 } 177 178 /** 179 * Handle different types of values, primarily 180 * other objects and map and array data types. 181 */ 182 private function getSimpleValue($value) 183 { 184 if ($value instanceof Model) { 185 return $value->toSimpleObject(); 186 } else if (is_array($value)) { 187 $return = array(); 188 foreach ($value as $key => $a_value) { 189 $a_value = $this->getSimpleValue($a_value); 190 if ($a_value !== null) { 191 $key = $this->getMappedName($key); 192 $return[$key] = $this->nullPlaceholderCheck($a_value); 193 } 194 } 195 return $return; 196 } 197 return $value; 198 } 199 200 /** 201 * Check whether the value is the null placeholder and return true null. 202 */ 203 private function nullPlaceholderCheck($value) 204 { 205 if ($value === self::NULL_VALUE) { 206 return null; 207 } 208 return $value; 209 } 210 211 /** 212 * If there is an internal name mapping, use that. 213 */ 214 private function getMappedName($key) 215 { 216 if (isset($this->internal_gapi_mappings, $this->internal_gapi_mappings[$key])) { 217 $key = $this->internal_gapi_mappings[$key]; 218 } 219 return $key; 220 } 221 222 /** 223 * Returns true only if the array is associative. 224 * @param array $array 225 * @return bool True if the array is associative. 226 */ 227 protected function isAssociativeArray($array) 228 { 229 if (!is_array($array)) { 230 return false; 231 } 232 $keys = array_keys($array); 233 foreach ($keys as $key) { 234 if (is_string($key)) { 235 return true; 236 } 237 } 238 return false; 239 } 240 241 /** 242 * Verify if $obj is an array. 243 * @throws \Google\Exception Thrown if $obj isn't an array. 244 * @param array $obj Items that should be validated. 245 * @param string $method Method expecting an array as an argument. 246 */ 247 public function assertIsArray($obj, $method) 248 { 249 if ($obj && !is_array($obj)) { 250 throw new GoogleException( 251 "Incorrect parameter type passed to $method(). Expected an array." 252 ); 253 } 254 } 255 256 /** @return bool */ 257 #[\ReturnTypeWillChange] 258 public function offsetExists($offset) 259 { 260 return isset($this->$offset) || isset($this->modelData[$offset]); 261 } 262 263 /** @return mixed */ 264 #[\ReturnTypeWillChange] 265 public function offsetGet($offset) 266 { 267 return isset($this->$offset) ? 268 $this->$offset : 269 $this->__get($offset); 270 } 271 272 /** @return void */ 273 #[\ReturnTypeWillChange] 274 public function offsetSet($offset, $value) 275 { 276 if (property_exists($this, $offset)) { 277 $this->$offset = $value; 278 } else { 279 $this->modelData[$offset] = $value; 280 $this->processed[$offset] = true; 281 } 282 } 283 284 /** @return void */ 285 #[\ReturnTypeWillChange] 286 public function offsetUnset($offset) 287 { 288 unset($this->modelData[$offset]); 289 } 290 291 protected function keyType($key) 292 { 293 $keyType = $key . "Type"; 294 295 // ensure keyType is a valid class 296 if (property_exists($this, $keyType) && class_exists($this->$keyType)) { 297 return $this->$keyType; 298 } 299 } 300 301 protected function dataType($key) 302 { 303 $dataType = $key . "DataType"; 304 305 if (property_exists($this, $dataType)) { 306 return $this->$dataType; 307 } 308 } 309 310 public function __isset($key) 311 { 312 return isset($this->modelData[$key]); 313 } 314 315 public function __unset($key) 316 { 317 unset($this->modelData[$key]); 318 } 319 320 /** 321 * Convert a string to camelCase 322 * @param string $value 323 * @return string 324 */ 325 private function camelCase($value) 326 { 327 $value = ucwords(str_replace(array('-', '_'), ' ', $value)); 328 $value = str_replace(' ', '', $value); 329 $value[0] = strtolower($value[0]); 330 return $value; 331 } 332} 333