1: <?php
2: /**
3: * The exception that is thrown by QType::Cast
4: * if an invalid cast is performed. InvalidCastException
5: * derives from CallerException, and therefore should be handled
6: * similar to how CallerExceptions are handled (e.g. IncrementOffset should
7: * be called whenever an InvalidCastException is caught and rethrown).
8: */
9: class QInvalidCastException extends QCallerException {
10: /**
11: * Constructor
12: * @param string $strMessage
13: * @param int $intOffset
14: */
15: public function __construct($strMessage, $intOffset = 2) {
16: parent::__construct($strMessage, $intOffset);
17: }
18: }
19:
20: /**
21: * Type Library to add some support for strongly named types.
22: *
23: * PHP does not support strongly named types. The QCubed type library
24: * and QCubed typing in general attempts to bring some structure to types
25: * when passing in values, properties, parameters to/from QCubed framework objects
26: * and methods.
27: *
28: * The Type library attempts to allow as much flexibility as possible to
29: * set and cast variables to other types, similar to how PHP does it natively,
30: * but simply adds a big more structure to it.
31: *
32: * For example, regardless if a variable is an integer, boolean, or string,
33: * QType::Cast will allow the flexibility of those values to interchange with
34: * each other with little to no issue.
35: *
36: * In addition to value objects (ints, bools, floats, strings), the Type library
37: * also supports object casting. While technically casting one object to another
38: * is not a true cast, QType::Cast does at least ensure that the tap being "casted"
39: * to is a legitamate subclass of the object being "cast". So if you have ParentClass,
40: * and you have a ChildClass that extends ParentClass,
41: * $objChildClass = new ChildClass();
42: * $objParentClass = new ParentClass();
43: * Type::Cast($objChildClass, 'ParentClass'); // is a legal cast
44: * Type::Cast($objParentClass, 'ChildClass'); // will throw an InvalidCastException
45: *
46: * For values, specifically int to string conversion, one different between
47: * QType::Cast and PHP (in order to add structure) is that if an integer contains
48: * alpha characters, PHP would normally allow that through w/o complaint, simply
49: * ignoring any numeric characters past the first alpha character. QType::Cast
50: * would instead throw an InvalidCastException to let the developer immedaitely
51: * know that something doesn't look right.
52: *
53: * In theory, the type library should maintain the same level of flexibility
54: * PHP developers are accostomed to, while providing a mechanism to limit
55: * careless coding errors and tough to figure out mistakes due to PHP's sometimes
56: * overly laxed type conversions.
57: */
58: abstract class QType {
59: /**
60: * This faux constructor method throws a caller exception.
61: * The Type object should never be instantiated, and this constructor
62: * override simply guarantees it.
63: *
64: * @throws QCallerException
65: * @return \QType
66: */
67: public final function __construct() {
68: throw new QCallerException('Type should never be instantiated. All methods and variables are publically statically accessible.');
69: }
70:
71: /** String Type */
72: const String = 'string';
73: /** Integer Type */
74: const Integer = 'integer';
75: /** Float Type */
76: const Float = 'double';
77: /** Boolean Type */
78: const Boolean = 'boolean';
79: /** Object Type */
80: const Object = 'object';
81: /** Array Type */
82: const ArrayType = 'array';
83: /** QDateTime type */
84: const DateTime = 'QDateTime';
85: /** Resource Type */
86: const Resource = 'resource';
87:
88: // Virtual types
89: const Association = 'association';
90: const ReverseReference = 'reverse_reference';
91:
92: const NoOp = 1;
93: const CheckOnly = 2;
94: const CastOnly = 3;
95: const CheckAndCast = 4;
96: private static $intBehaviour = QType::CheckAndCast;
97:
98: private static function CastObjectTo($objItem, $strType) {
99: try {
100: $objReflection = new ReflectionClass($objItem);
101: if ($objReflection->getName() == 'SimpleXMLElement') {
102: switch ($strType) {
103: case QType::String:
104: return (string) $objItem;
105: case QType::Integer:
106: try {
107: return QType::Cast((string) $objItem, QType::Integer);
108: } catch (QCallerException $objExc) {
109: $objExc->IncrementOffset();
110: throw $objExc;
111: }
112: case QType::Boolean:
113: $strItem = strtolower(trim((string) $objItem));
114: if (($strItem == 'false') ||
115: (!$strItem))
116: return false;
117: else
118: return true;
119: }
120: }
121:
122: if ($objItem instanceof $strType)
123: return $objItem;
124:
125: if ($strType == QType::String) {
126: return (string) $objItem; // invokes __toString() magic method
127: }
128: } catch (Exception $objExc) {
129: }
130:
131: throw new QInvalidCastException(sprintf('Unable to cast %s object to %s', $objReflection->getName(), $strType));
132: }
133:
134: private static function CastValueTo($mixItem, $strNewType) {
135: $strOriginalType = gettype($mixItem);
136:
137: switch (QType::TypeFromDoc($strNewType)) {
138: case QType::Boolean:
139: if ($strOriginalType == QType::Boolean)
140: return $mixItem;
141: if (is_null($mixItem))
142: return false;
143: if (strlen($mixItem) == 0)
144: return false;
145: if (strtolower($mixItem) == 'false')
146: return false;
147: settype($mixItem, $strNewType);
148: return $mixItem;
149:
150: case QType::Integer:
151: if($strOriginalType == QType::Boolean)
152: throw new QInvalidCastException(sprintf('Unable to cast %s value to %s: %s', $strOriginalType, $strNewType, $mixItem));
153: if (strlen($mixItem) == 0)
154: return null;
155: if ($strOriginalType == QType::Integer)
156: return $mixItem;
157:
158: // Check to make sure the value hasn't changed significantly
159: $intItem = $mixItem;
160: settype($intItem, $strNewType);
161: $mixTest = $intItem;
162: settype($mixTest, $strOriginalType);
163:
164: // If the value hasn't changed, it's safe to return the casted value
165: if ((string)$mixTest === (string)$mixItem)
166: return $intItem;
167:
168: // if casting changed the value, but we have a valid integer, return with a string cast
169: if (preg_match('/^-?\d+$/',$mixItem) === 1)
170: return (string)$mixItem;
171:
172: // any other scenarios is an invalid cast
173: throw new QInvalidCastException(sprintf('Unable to cast %s value to %s: %s', $strOriginalType, $strNewType, $mixItem));
174: case QType::Float:
175: if($strOriginalType == QType::Boolean)
176: throw new QInvalidCastException(sprintf('Unable to cast %s value to %s: %s', $strOriginalType, $strNewType, $mixItem));
177: if (strlen($mixItem) == 0)
178: return null;
179: if ($strOriginalType == QType::Float)
180: return $mixItem;
181:
182: if (!is_numeric($mixItem))
183: throw new QInvalidCastException(sprintf('Invalid float: %s', $mixItem));
184:
185: // Check to make sure the value hasn't changed significantly
186: $fltItem = $mixItem;
187: settype($fltItem, $strNewType);
188: $mixTest = $fltItem;
189: settype($mixTest, $strOriginalType);
190:
191: //account for any scientific notation that results
192: //find out what notation is currently being used
193: $i = strpos($mixItem, '.');
194: $precision = ($i === false) ? 0 : strlen($mixItem) - $i - 1;
195: //and represent the casted value the same way
196: $strTest = sprintf('%.' . $precision . 'f', $fltItem);
197:
198: // If the value hasn't changed, it's safe to return the casted value
199: if ((string)$strTest === (string)$mixItem)
200: return $fltItem;
201:
202: // the changed value could be the result of loosing precision. Return the original value with no cast
203: return $mixItem;
204:
205: case QType::String:
206: if ($strOriginalType == QType::String)
207: return $mixItem;
208:
209: // Check to make sure the value hasn't changed significantly
210: $strItem = $mixItem;
211: settype($strItem, $strNewType);
212: $mixTest = $strItem;
213: settype($mixTest, $strOriginalType);
214:
215: // Has it?
216: $blnSame = true;
217: if ($strOriginalType == QType::Float) {
218: // type conversion from float to string affects precision and can throw off the comparison
219: // so we need to use a comparison check using an epsilon value instead
220: //$epsilon = 1.0e-14; too small
221: $epsilon = 1.0e-11;
222: $diff = abs($mixItem - $mixTest);
223: if ($diff > $epsilon) {
224: $blnSame = false;
225: }
226: }
227: else {
228: if ($mixTest != $mixItem)
229: $blnSame = false;
230: }
231: if (!$blnSame)
232: //This is an invalid cast
233: throw new QInvalidCastException(sprintf('Unable to cast %s value to %s: %s', $strOriginalType, $strNewType, $mixItem));
234:
235: return $strItem;
236:
237: default:
238: throw new QInvalidCastException(sprintf('Unable to cast %s value to unknown type %s', $strOriginalType, $strNewType));
239: }
240: }
241:
242: /**
243: * Converts an array to array (without modification) or throws exception
244: *
245: * @param array $arrItem The array item to be converted
246: * @param string $strType Type to which this array has to be converted
247: *
248: * @return string
249: * @throws QInvalidCastException
250: */
251: private static function CastArrayTo($arrItem, $strType) {
252: if ($strType == QType::ArrayType) {
253: return $arrItem;
254: } else {
255: throw new QInvalidCastException(sprintf('Unable to cast Array to %s', $strType));
256: }
257: }
258:
259: /**
260: * This method can be used to change the casting behaviour of QType::Cast().
261: * By default QType::Cast() does lots of validation and type casting (using settype()).
262: * Depending on your application you may or may not need validation or casting or both.
263: * In these situations you can set the necessary behaviour by passing the appropriate constant to this function.
264: *
265: * @static
266: * @param int $intBehaviour one of the 4 constants QType::NoOp, QType::CastOnly, QType::CheckOnly, QType::CheckAndCast
267: * @return int the previous setting
268: */
269: public static function SetBehaviour($intBehaviour) {
270: $oldBehaviour = QType::$intBehaviour;
271: QType::$intBehaviour = $intBehaviour;
272: return $oldBehaviour;
273: }
274:
275: /**
276: * Used to cast a variable to another type. Allows for moderate
277: * support of strongly-named types.
278: * Will throw an exception if the cast fails, causes unexpected side effects,
279: * if attempting to cast an object to a value (or vice versa), or if an object
280: * is being cast to a class that isn't a subclass (e.g. parent). The exception
281: * thrown will be an InvalidCastException, which extends CallerException.
282: *
283: * @param mixed $mixItem the value, array or object that you want to cast
284: * @param string $strType the type to cast to. Can be a QType::XXX constant (e.g. QType::Integer), or the name of a Class
285: *
286: * @return mixed the passed in value/array/object that has been cast to strType
287: * @throws Exception|QCallerException|QInvalidCastException
288: */
289: public final static function Cast($mixItem, $strType) {
290: switch (QType::$intBehaviour) {
291: case QType::NoOp:
292: return $mixItem;
293: case QType::CastOnly:
294: throw new QCallerException("QType::CastOnly handling not yet implemented");
295: break;
296: case QType::CheckOnly:
297: throw new QCallerException("QType::CheckOnly handling not yet implemented");
298: break;
299: case QType::CheckAndCast:
300: break;
301: default:
302: throw new InvalidArgumentException();
303: break;
304: }
305: // Automatically Return NULLs
306: if (is_null($mixItem))
307: return null;
308:
309: // Figure out what PHP thinks the type is
310: $strPhpType = gettype($mixItem);
311:
312: switch ($strPhpType) {
313: case QType::Object:
314: try {
315: return QType::CastObjectTo($mixItem, $strType);
316: } catch (QCallerException $objExc) {
317: $objExc->IncrementOffset();
318: throw $objExc;
319: }
320:
321: case QType::String:
322: case QType::Integer:
323: case QType::Float:
324: case QType::Boolean:
325: try {
326: return QType::CastValueTo($mixItem, $strType);
327: } catch (QCallerException $objExc) {
328: $objExc->IncrementOffset();
329: throw $objExc;
330: }
331:
332: case QType::ArrayType:
333: try {
334: return QType::CastArrayTo($mixItem, $strType);
335: } catch (QCallerException $objExc) {
336: $objExc->IncrementOffset();
337: throw $objExc;
338: }
339:
340: case QType::Resource:
341: // Cannot Cast Resources
342: throw new QInvalidCastException('Resources cannot be cast');
343:
344: default:
345: // Could not determine type
346: throw new QInvalidCastException(sprintf('Unable to determine type of item to be cast: %s', $mixItem));
347: }
348: }
349:
350: /**
351: * Used by the QCubed Code Generator to allow for the code generation of
352: * the actual "QType::Xxx" constant, instead of the text of the constant,
353: * in generated code.
354: * It is rare for Constant to be used manually outside of Code Generation.
355: *
356: * @param string $strType the type to convert to 'constant' form
357: *
358: * @return string the text of the Text:Xxx Constant
359: * @throws QInvalidCastException
360: */
361: public final static function Constant($strType) {
362: switch ($strType) {
363: case QType::Object: return 'QType::Object';
364: case QType::String: return 'QType::String';
365: case QType::Integer: return 'QType::Integer';
366: case QType::Float: return 'QType::Float';
367: case QType::Boolean: return 'QType::Boolean';
368: case QType::ArrayType: return 'QType::ArrayType';
369: case QType::Resource: return 'QType::Resource';
370: case QType::DateTime: return 'QType::DateTime';
371:
372: default:
373: // Could not determine type
374: throw new QInvalidCastException(sprintf('Unable to determine type of item to lookup its constant: %s', $strType));
375: }
376: }
377:
378: public final static function TypeFromDoc($strType) {
379: switch (strtolower($strType)) {
380: case 'string':
381: case 'str':
382: return QType::String;
383:
384: case 'integer':
385: case 'int':
386: return QType::Integer;
387:
388: case 'float':
389: case 'flt':
390: case 'double':
391: case 'dbl':
392: case 'single':
393: case 'decimal':
394: return QType::Float;
395:
396: case 'bool':
397: case 'boolean':
398: case 'bit':
399: return QType::Boolean;
400:
401: case 'datetime':
402: case 'date':
403: case 'time':
404: case 'qdatetime':
405: return QType::DateTime;
406:
407: case 'null':
408: case 'void':
409: return 'void';
410:
411: default:
412: try {
413: $objReflection = new ReflectionClass($strType);
414: return $strType;
415: } catch (ReflectionException $objExc) {
416: throw new QInvalidCastException(sprintf('Unable to determine type of item from PHPDoc Comment to lookup its QType or Class: %s', $strType));
417: }
418: }
419: }
420:
421: /**
422: * Used by the QCubed Code Generator and QSoapService class to allow for the xml generation of
423: * the actual "s:type" Soap Variable types.
424: *
425: * @param string $strType the type to convert to 'constant' form
426: *
427: * @return string the text of the SOAP standard s:type variable type
428: * @throws QInvalidCastException
429: */
430: public final static function SoapType($strType) {
431: switch ($strType) {
432: case QType::String: return 'string';
433: case QType::Integer: return 'int';
434: case QType::Float: return 'float';
435: case QType::Boolean: return 'boolean';
436: case QType::DateTime: return 'dateTime';
437:
438: case QType::ArrayType:
439: case QType::Object:
440: case QType::Resource:
441: default:
442: // Could not determine type
443: throw new QInvalidCastException(sprintf('Unable to determine type of item to lookup its constant: %s', $strType));
444: }
445: }
446: /*
447: final public static function SoapArrayType($strType) {
448: try {
449: return sprintf('ArrayOf%s', ucfirst(QType::SoapType($strType)));
450: } catch (QInvalidCastException $objExc) {}
451: $objExc->IncrementOffset();
452: throw $objExc;
453: }
454: }
455:
456: final public static function AlterSoapComplexTypeArray(&$strComplexTypeArray, $strType) {
457: switch ($strType) {
458: case QType::String:
459: $strItemName = 'string';
460: break;
461: case QType::Integer:
462: $strItemName = 'int';
463: break;
464: case QType::Float:
465: $strItemName = 'float';
466: break;
467: case QType::Boolean:
468: $strItemName = 'boolean';
469: break;
470: case QType::DateTime:
471: $strItemName = 'dateTime';
472: break;
473:
474: case QType::ArrayType:
475: case QType::Object:
476: case QType::Resource:
477: default:
478: // Could not determine type
479: throw new QInvalidCastException(sprintf('Unable to determine type of item to lookup its constant: %s', $strType));
480: }
481:
482: $strArrayName = QType::SoapArrayType($strType);
483:
484: if (!array_key_exists($strArrayName, $strComplexTypeArray))
485: $strComplexTypeArray[$strArrayName] = sprintf(
486: '<s:complexType name="%s"><s:sequence>' .
487: '<s:element minOccurs="0" maxOccurs="unbounded" name="%s" type="%s"/>' .
488: '</s:sequence></s:complexType>',
489: QType::SoapArrayType($strType),
490: $strItemName,
491: QType::SoapType($strType));
492: }*/
493: }