1: <?php
2:
3: namespace Alpha\Model\Type;
4:
5: use Alpha\Util\Helper\Validator;
6: use Alpha\Util\Config\ConfigProvider;
7: use Alpha\Exception\IllegalArguementException;
8: use Alpha\Model\ActiveRecord;
9: use ReflectionClass;
10:
11: /**
12: * The Relation complex data type.
13: *
14: * @since 1.0
15: *
16: * @author John Collins <dev@alphaframework.org>
17: * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
18: * @copyright Copyright (c) 2015, John Collins (founder of Alpha Framework).
19: * All rights reserved.
20: *
21: * <pre>
22: * Redistribution and use in source and binary forms, with or
23: * without modification, are permitted provided that the
24: * following conditions are met:
25: *
26: * * Redistributions of source code must retain the above
27: * copyright notice, this list of conditions and the
28: * following disclaimer.
29: * * Redistributions in binary form must reproduce the above
30: * copyright notice, this list of conditions and the
31: * following disclaimer in the documentation and/or other
32: * materials provided with the distribution.
33: * * Neither the name of the Alpha Framework nor the names
34: * of its contributors may be used to endorse or promote
35: * products derived from this software without specific
36: * prior written permission.
37: *
38: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
39: * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
40: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
41: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
42: * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
43: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
44: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
45: * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
46: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
47: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
48: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
49: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
50: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51: * </pre>
52: */
53: class Relation extends Type implements TypeInterface
54: {
55: /**
56: * The name of the business object class which this class is related to.
57: *
58: * @var string
59: *
60: * @since 1.0
61: */
62: private $relatedClass;
63:
64: /**
65: * The name of the fields of the business object class by which this class is related by.
66: *
67: * @var string
68: *
69: * @since 1.0
70: */
71: private $relatedClassField;
72:
73: /**
74: * The name of the field from the related business object class which is displayed by the selection widget.
75: *
76: * @var string
77: *
78: * @since 1.0
79: */
80: private $relatedClassDisplayField;
81:
82: /**
83: * An array of fields to use the values of while rendering related display values via the selection widget.
84: *
85: * @var array
86: *
87: * @since 1.0
88: */
89: private $relatedClassHeaderFields = array();
90:
91: /**
92: * The name of the business object class on the left of a MANY-TO-MANY relation.
93: *
94: * @var string
95: *
96: * @since 1.0
97: */
98: private $relatedClassLeft;
99:
100: /**
101: * The name of the field from the related business object class on the left of a
102: * MANY-TO-MANY relation which is displayed by the selection widget.
103: *
104: * @var string
105: *
106: * @since 1.0
107: */
108: private $relatedClassLeftDisplayField;
109:
110: /**
111: * The name of the business object class on the right of a MANY-TO-MANY relation.
112: *
113: * @var string
114: *
115: * @since 1.0
116: */
117: private $relatedClassRight;
118:
119: /**
120: * The name of the field from the related business object class on the right of a
121: * MANY-TO-MANY relation which is displayed by the selection widget.
122: *
123: * @var string
124: *
125: * @since 1.0
126: */
127: private $relatedClassRightDisplayField;
128:
129: /**
130: * The type of relation ('MANY-TO-ONE' or 'ONE-TO-MANY' or 'ONE-TO-ONE' or 'MANY-TO-MANY').
131: *
132: * @var string
133: *
134: * @since 1.0
135: */
136: private $relationType;
137:
138: /**
139: * In the case of MANY-TO-MANY relationship, a lookup object will be required.
140: *
141: * @var Alpha\Model\Type\RelationLookup
142: *
143: * @since 1.0
144: */
145: private $lookup;
146:
147: /**
148: * In the case of MANY-TO-MANY relationship, this transient array can be used to hold the OIDs of related records.
149: *
150: * @var array
151: *
152: * @since 2.0
153: */
154: private $OIDs = array();
155:
156: /**
157: * When building a relation with the TagObject BO, set this to the name of the tagged class.
158: *
159: * @var string
160: *
161: * @since 1.0
162: */
163: private $taggedClass;
164:
165: /**
166: * An array of the allowable relationship types ('MANY-TO-ONE' or 'ONE-TO-MANY' or 'ONE-TO-ONE' or 'MANY-TO-MANY').
167: *
168: * @var array
169: *
170: * @since 1.0
171: */
172: private $allowableRelationTypes = array('MANY-TO-ONE', 'ONE-TO-MANY', 'ONE-TO-ONE', 'MANY-TO-MANY');
173:
174: /**
175: * The object ID (OID) value of the related object. In the special case of a MANY-TO-MANY
176: * relation, contains the OID of the object on the current, accessing side. Can contain NULL.
177: *
178: * @var mixed
179: *
180: * @since 1.0
181: */
182: private $value = null;
183:
184: /**
185: * The validation rule for the Relation type.
186: *
187: * @var string
188: *
189: * @since 1.0
190: */
191: private $validationRule;
192:
193: /**
194: * The error message for the Relation type when validation fails.
195: *
196: * @var string
197: *
198: * @since 1.0
199: */
200: protected $helper;
201:
202: /**
203: * The size of the value for the this Relation.
204: *
205: * @var int
206: *
207: * @since 1.0
208: */
209: private $size = 11;
210:
211: /**
212: * The absolute maximum size of the value for the this Relation.
213: *
214: * @var int
215: *
216: * @since 1.0
217: */
218: const MAX_SIZE = 11;
219:
220: /**
221: * Constructor.
222: *
223: * @since 1.0
224: */
225: public function __construct()
226: {
227: $this->validationRule = Validator::REQUIRED_INTEGER;
228: $this->helper = ' not a valid Relation value! A maximum of '.$this->size.' characters is allowed.';
229: }
230:
231: /**
232: * Set the name of the business object class that this class is related to.
233: *
234: * @param string $RC
235: * @param string $side Only required for MANY-TO-MANY relations
236: *
237: * @since 1.0
238: *
239: * @throws Alpha\Exception\IllegalArguementException
240: */
241: public function setRelatedClass($RC, $side = '')
242: {
243: if (in_array($RC, ActiveRecord::getBOClassNames())) {
244: switch ($side) {
245: case '':
246: $this->relatedClass = $RC;
247: break;
248: case 'left':
249: $this->relatedClassLeft = $RC;
250: break;
251: case 'right':
252: $this->relatedClassRight = $RC;
253: break;
254: default:
255: throw new IllegalArguementException('The side paramter ['.$RC.'] is not valid!');
256: }
257: } else {
258: throw new IllegalArguementException('The class ['.$RC.'] is not defined anywhere!');
259: }
260: }
261:
262: /**
263: * Get the name of the business object class that this class is related to.
264: *
265: * @param string $RC
266: *
267: * @return string
268: *
269: * @since 1.0
270: *
271: * @throws Alpha\Exception\IllegalArguementException
272: */
273: public function getRelatedClass($side = '')
274: {
275: switch ($side) {
276: case '':
277: return $this->relatedClass;
278: break;
279: case 'left':
280: return $this->relatedClassLeft;
281: break;
282: case 'right':
283: return $this->relatedClassRight;
284: break;
285: default:
286: throw new IllegalArguementException('The side paramter ['.$RC.'] is not valid!');
287:
288: return '';
289: }
290: }
291:
292: /**
293: * Setter for the field of the related class.
294: *
295: * @param string $RCF
296: *
297: * @since 1.0
298: *
299: * @throws Alpha\Exception\IllegalArguementException
300: */
301: public function setRelatedClassField($RCF)
302: {
303: // use reflection to sure the related class has the field $RCF
304: $reflection = new ReflectionClass($this->relatedClass);
305: $properties = $reflection->getProperties();
306: $fieldFound = false;
307:
308: foreach ($properties as $propObj) {
309: if ($RCF == $propObj->name) {
310: $fieldFound = true;
311: break;
312: }
313: }
314:
315: if ($fieldFound) {
316: $this->relatedClassField = $RCF;
317: } else {
318: throw new IllegalArguementException('The field ['.$RCF.'] was not found in the class ['.$this->relatedClass.']');
319: }
320: }
321:
322: /**
323: * Getter for the field of the related class.
324: *
325: * @return string
326: *
327: * @since 1.0
328: */
329: public function getRelatedClassField()
330: {
331: return $this->relatedClassField;
332: }
333:
334: /**
335: * Setter for ONE-TO-MANY relations, which sets the header fields to
336: * render from the related class.
337: *
338: * @param array $fieldNames
339: *
340: * @since 1.0
341: */
342: public function setRelatedClassHeaderFields($fieldNames)
343: {
344: $this->relatedClassHeaderFields = $fieldNames;
345: }
346:
347: /**
348: * Getter for the selection widget field headings of the related class.
349: *
350: * @return array
351: *
352: * @since 1.0
353: */
354: public function getRelatedClassHeaderFields()
355: {
356: return $this->relatedClassHeaderFields;
357: }
358:
359: /**
360: * Setter for the display field from the related class.
361: *
362: * @param string $RCDF
363: * @param string $side Only required for MANY-TO-MANY relations
364: *
365: * @since 1.0
366: *
367: * @throws Alpha\Exception\IllegalArguementException
368: */
369: public function setRelatedClassDisplayField($RCDF, $side = '')
370: {
371: switch ($side) {
372: case '':
373: $this->relatedClassDisplayField = $RCDF;
374: break;
375: case 'left':
376: $this->relatedClassLeftDisplayField = $RCDF;
377: break;
378: case 'right':
379: $this->relatedClassRightDisplayField = $RCDF;
380: break;
381: default:
382: throw new IllegalArguementException('The side paramter ['.$RC.'] is not valid!');
383: }
384: }
385:
386: /**
387: * Getter for the display field from the related class.
388: *
389: * @param string $side Only required for MANY-TO-MANY relations
390: *
391: * @return string
392: *
393: * @since 1.0
394: *
395: * @throws Alpha\Exception\IllegalArguementException
396: */
397: public function getRelatedClassDisplayField($side = '')
398: {
399: switch ($side) {
400: case '':
401: return $this->relatedClassDisplayField;
402: break;
403: case 'left':
404: return $this->relatedClassLeftDisplayField;
405: break;
406: case 'right':
407: return $this->relatedClassRightDisplayField;
408: break;
409: default:
410: throw new IllegalArguementException('The side paramter ['.$RC.'] is not valid!');
411:
412: return '';
413: }
414: }
415:
416: /**
417: * Setter for the relation type.
418: *
419: * @param string $RT
420: *
421: * @throws Alpha\Exception\IllegalArguementException
422: * @throws Alpha\Exception\FailedLookupCreateException
423: *
424: * @since 1.0
425: */
426: public function setRelationType($RT)
427: {
428: if (in_array($RT, $this->allowableRelationTypes)) {
429: $this->relationType = $RT;
430: if ($RT == 'MANY-TO-MANY') {
431: try {
432: $this->lookup = new RelationLookup($this->relatedClassLeft, $this->relatedClassRight);
433: } catch (FailedLookupCreateException $flce) {
434: throw $flce;
435: } catch (IllegalArguementException $iae) {
436: throw $iae;
437: }
438: }
439: } else {
440: throw new IllegalArguementException('Relation type of ['.$RT.'] is invalid!');
441: }
442: }
443:
444: /**
445: * Getter for the relation type.
446: *
447: * @return string
448: *
449: * @since 1.0
450: */
451: public function getRelationType()
452: {
453: return $this->relationType;
454: }
455:
456: /**
457: * Setter for the value (OID of related object) of this relation.
458: *
459: * @param int $val
460: *
461: * @since 1.0
462: *
463: * @throws Alpha\Exception\IllegalArguementException
464: */
465: public function setValue($val)
466: {
467: if (empty($val)) {
468: $this->value = null;
469: } else {
470: if (!Validator::isInteger($val)) {
471: throw new IllegalArguementException("[$val]".$this->helper);
472: }
473:
474: if (mb_strlen($val) <= $this->size) {
475: $this->value = str_pad($val, 11, '0', STR_PAD_LEFT);
476: } else {
477: throw new IllegalArguementException("[$val]".$this->helper);
478: }
479: }
480: }
481:
482: /**
483: * Getter for the array of OIDs used by MANY-TO-MANY instances.
484: *
485: * @return array
486: *
487: * @since 2.0
488: */
489: public function getRelatedOIDs()
490: {
491: return $this->OIDs;
492: }
493:
494: /**
495: * Setter for the array of OIDs used by MANY-TO-MANY instances.
496: *
497: * @param array $OIDs
498: *
499: * @since 2.0
500: *
501: * @throws Alpha\Exception\IllegalArguementException
502: */
503: public function setRelatedOIDs($OIDs)
504: {
505: if (is_array($OIDs)) {
506: $this->OIDs = $OIDs;
507: } else {
508: throw new IllegalArguementException('An array must be provided to setRelatedOIDs()!');
509: }
510: }
511:
512: /**
513: * Getter for the Relation value.
514: *
515: * @return mixed
516: *
517: * @since 1.0
518: */
519: public function getValue()
520: {
521: return $this->value;
522: }
523:
524: /**
525: * Get the validation rule.
526: *
527: * @return string
528: *
529: * @since 1.0
530: */
531: public function getRule()
532: {
533: return $this->validationRule;
534: }
535:
536: /**
537: * Setter to override the default validation rule.
538: *
539: * @param string $rule
540: *
541: * @since 1.0
542: */
543: public function setRule($rule)
544: {
545: $this->validationRule = $rule;
546: }
547:
548: /**
549: * Getter for the display value of the related class field. In the case of a
550: * MANY-TO-MANY Relation, a comma-seperated sorted list of values is returned.
551: *
552: * @param string $accessingClassName Used to indicate the reading side when accessing from MANY-TO-MANY relation (leave blank for other relation types)
553: *
554: * @return string
555: *
556: * @since 1.0
557: *
558: * @throws Alpha\Exception\IllegalArguementException
559: */
560: public function getRelatedClassDisplayFieldValue($accessingClassName = '')
561: {
562: if ($this->relationType == 'MANY-TO-MANY') {
563: /*
564: * 1. Use RelationLookup to get OIDs of related objects
565: * 2. Load related objects
566: * 3. Access the value of the field on the object to build the
567: * comma-seperated list.
568: */
569: if (empty($this->lookup)) {
570: throw new IllegalArguementException('Tried to load related MANY-TO-MANY fields but no RelationLookup set on the Relation object!');
571: }
572:
573: if (empty($accessingClassName)) {
574: throw new IllegalArguementException('Tried to load related MANY-TO-MANY fields but no accessingClassName parameter set on the call to getRelatedClassDisplayFieldValue!');
575: }
576:
577: // load objects on the right from accessing on the left
578: if ($accessingClassName == $this->relatedClassLeft) {
579: $obj = new $this->relatedClassRight();
580:
581: $lookupObjects = $this->lookup->loadAllByAttribute('leftID', $this->value);
582:
583: $values = array();
584: foreach ($lookupObjects as $lookupObject) {
585: $obj->load($lookupObject->get('rightID'));
586: array_push($values, $obj->get($this->relatedClassRightDisplayField));
587: }
588: // sort array, then return as comma-seperated string
589: asort($values);
590:
591: return implode(',', $values);
592: }
593: // load objects on the left from accessing on the right
594: if ($accessingClassName == $this->relatedClassRight) {
595: $obj = new $this->relatedClassLeft();
596:
597: $lookupObjects = $this->lookup->loadAllByAttribute('rightID', $this->value);
598:
599: $values = array();
600: foreach ($lookupObjects as $lookupObject) {
601: $obj->load($lookupObject->get('leftID'));
602: array_push($values, $obj->get($this->relatedClassLeftDisplayField));
603: }
604: // sort array, then return as comma-seperated string
605: asort($values);
606:
607: return implode(',', $values);
608: }
609: } else {
610: $obj = new $this->relatedClass();
611: // making sure we have an object to load
612: if (empty($this->value) || $this->value == '00000000000') {
613: return '';
614: } else {
615: $obj->load($this->value);
616:
617: return $obj->get($this->relatedClassDisplayField);
618: }
619: }
620: }
621:
622: /**
623: * For one-to-many and many-to-many relations, get the objects on the other side.
624: *
625: * string $accessingClassName Used to indicate the reading side when accessing from MANY-TO-MANY relation (leave blank for other relation types)
626: *
627: * @return array
628: *
629: * @since 1.0
630: *
631: * @throws Alpha\Exception\IllegalArguementException
632: */
633: public function getRelatedObjects($accessingClassName = '')
634: {
635: $config = ConfigProvider::getInstance();
636:
637: if ($this->relationType == 'ONE-TO-MANY') {
638: if ($this->getValue() == '') { // if the value is empty, then return an empty array
639: return array();
640: }
641:
642: $obj = new $this->relatedClass();
643: if ($this->relatedClass == 'Alpha\Model\Tag') {
644: $objects = $obj->loadTags($this->taggedClass, $this->getValue());
645: } else {
646: $objects = $obj->loadAllByAttribute($this->getRelatedClassField(), $this->getValue());
647: }
648:
649: return $objects;
650: } else { // MANY-TO-MANY
651: if (empty($this->lookup)) {
652: throw new IllegalArguementException('Tried to load related MANY-TO-MANY objects but no RelationLookup set on the Relation object!');
653: }
654:
655: if (empty($accessingClassName)) {
656: throw new IllegalArguementException('Tried to load related MANY-TO-MANY objects but no accessingClassName parameter set on the call to getRelatedObjects!');
657: }
658:
659: $objects = array();
660:
661: // load objects on the right from accessing on the left
662: if ($accessingClassName == $this->relatedClassLeft) {
663: $lookupObjects = $this->lookup->loadAllByAttribute('leftID', $this->value);
664:
665: foreach ($lookupObjects as $lookupObject) {
666: $obj = new $this->relatedClassRight();
667: $obj->load($lookupObject->get('rightID'));
668: array_push($objects, $obj);
669: }
670: }
671: // load objects on the left from accessing on the right
672: if ($accessingClassName == $this->relatedClassRight && count($objects) == 0) {
673: $lookupObjects = $this->lookup->loadAllByAttribute('rightID', $this->value);
674:
675: foreach ($lookupObjects as $lookupObject) {
676: $obj = new $this->relatedClassLeft();
677: $obj->load($lookupObject->get('leftID'));
678: array_push($objects, $obj);
679: }
680: }
681:
682: return $objects;
683: }
684: }
685:
686: /**
687: * For one-to-one relations, get the object on the other side.
688: *
689: * @return array
690: *
691: * @since 1.0
692: *
693: * @throws Alpha\Model\Type\IllegalArguementException
694: */
695: public function getRelatedObject()
696: {
697: if (!class_exists($this->relatedClass)) {
698: throw new IllegalArguementException('Could not load the definition for the BO class ['.$this->relatedClass.']');
699: }
700:
701: $obj = new $this->relatedClass();
702: $obj->loadByAttribute($this->getRelatedClassField(), $this->getValue());
703:
704: return $obj;
705: }
706:
707: /**
708: * Get the allowable size of the Relation in the database field.
709: *
710: * @return int
711: *
712: * @since 1.0
713: */
714: public function getSize()
715: {
716: return $this->size;
717: }
718:
719: /**
720: * Get the lookup object if available (only on MANY-TO-MANY relations, null otherwise).
721: *
722: * @return RelationLookup
723: *
724: * @since 1.0
725: */
726: public function getLookup()
727: {
728: return $this->lookup;
729: }
730:
731: /**
732: * Gets the side ('left' or 'right') of the passed classname on the current Relation object.
733: *
734: * @param string $BOClassname
735: *
736: * @return string
737: *
738: * @since 1.0
739: *
740: * @throws Alpha\Model\Type\IllegalArguementException
741: */
742: public function getSide($BOClassname)
743: {
744: if ($BOClassname == $this->relatedClassLeft) {
745: return 'left';
746: } elseif ($BOClassname == $this->relatedClassRight) {
747: return 'right';
748: } else {
749: throw new IllegalArguementException('Error trying to determine the MANY-TO-MANY relationship side for the classname ['.$BOClassname.']');
750: }
751: }
752:
753: /**
754: * Set the taggedClass property to the name of the tagged class when building relations
755: * to the TagObject BO.
756: *
757: * @param $taggedClass
758: *
759: * @since 1.0
760: */
761: public function setTaggedClass($taggedClass)
762: {
763: $this->taggedClass = $taggedClass;
764: }
765: }
766: