1: <?php
2:
3: class ModelTraverser {
4:
5: const CACHE_KEY = '_modelTraverserCache';
6: const FIND_LAST_INSTANCE = 'LastInstance';
7: const FIND_ALL = 'All';
8: const NODE_TYPE_SELF = 'self';
9: const NODE_TYPE_FIELD = 'field';
10: const NODE_TYPE_HAS_ONE = 'has_one';
11: const NODE_TYPE_HAS_MANY = 'has_many';
12:
13: public static function schema(Model $model, $path) {
14: try {
15: if (!is_array($path)) {
16: $path = explode('.', $path);
17: }
18:
19: if (self::isField($model, $path[0])) {
20: if (count($path) == 1) {
21: return self::fieldSchema($model, $path[0]);
22: } else {
23: throw new Exception("Path continues, but reached a field.");
24: }
25: } else if (self::isBelongsToAssociation($model, $path[0]) ||
26: self::isHasOneAssociation($model, $path[0])) {
27: return self::schema($model->{$path[0]}, self::pathPopFirst($path));
28: } else {
29: throw new Exception("Term is not a field or association.");
30: }
31: } catch (Exception $ex) {
32: throw new ModelTraverserException($ex->getMessage(), compact('model','path'), $ex);
33: }
34: }
35:
36: 37: 38: 39: 40: 41: 42:
43: public static function value(Model $model, $row, $path) {
44: $stack = self::find($model, $row, $path);
45: $top = self::_pathLastNode($stack);
46: return $top['value'];
47: }
48:
49: public static function displayValue(Model $model, $row, $path) {
50: $stack = self::find($model, $row, $path);
51: $top = self::_pathLastNode($stack);
52: $pathLastPart = self::_pathLastNode($path);
53: $association = self::oneToManyAssociationByForeignKey($top['model'], $pathLastPart);
54: if ($association) {
55: array_pop($stack);
56: if (empty($stack)) {
57: $subTop = array(
58: 'model' => $model,
59: 'value' => $row,
60: );
61: }
62: else {
63: $subTop = self::_pathLastNode($stack);
64: }
65: $subPath = array($association, $subTop['model']->{$association}->displayField);
66: return self::value(
67: $subTop['model']
68: , $subTop['value']
69: , $subPath
70: );
71: } else {
72: return $top['value'];
73: }
74: }
75:
76: private static function oneToManyAssociationByForeignKey(Model $model, $foreignKey) {
77: foreach ($model->getAssociated() as $associationAlias => $type) {
78: switch ($type) {
79: case 'belongsTo':
80: case 'hasOne':
81: if ($model->{$type}[$associationAlias]['foreignKey'] == $foreignKey) {
82: return $associationAlias;
83: }
84: }
85: }
86:
87: return false;
88: }
89:
90: public static function find(Model $model, $row, $path) {
91: try {
92: if (!is_array($path)) {
93: $path = explode('.', $path);
94: }
95:
96: if (count($path) == 0) {
97: throw new Exception("Path size is zero.");
98: }
99: if (!is_array($row)) {
100: throw new Exception('$row is not a array');
101: }
102: $currentModel = $model;
103: $leftPath = $path;
104: $stack = array();
105: while (!empty($leftPath)) {
106: $currentNode = $leftPath[0];
107: $leftPath = self::pathPopFirst($leftPath);
108: $currentNodeType = self::_findCurrentNodeType($currentModel, $currentNode);
109: switch ($currentNodeType) {
110: case self::NODE_TYPE_FIELD:
111: $currentRow = self::_findField($currentModel, $row, $currentNode);
112: break;
113:
114: case self::NODE_TYPE_SELF:
115: $currentRow = self::_findSelf($currentModel, $row, $currentNode);
116: break;
117:
118: case self::NODE_TYPE_HAS_ONE:
119: $currentRow = self::_findHasOne($currentModel, $row, $currentNode);
120: $currentModel = $currentModel->{$currentNode};
121: break;
122:
123: case self::NODE_TYPE_HAS_MANY:
124: $currentRow = self::_findHasMany($currentModel, $row, $currentNode);
125: $currentModel = $currentModel->{$currentNode};
126: break;
127:
128: default:
129: $currentRow = new Exception("Current node type \"$currentNodeType\" not mapped");
130: break;
131: }
132: $stack[] = array(
133: 'value' => $currentRow,
134: 'model' => $currentModel,
135: );
136: }
137: return $stack;
138: } catch (Exception $ex) {
139: throw new ModelTraverserException($ex->getMessage(), compact('model', 'row', 'path', 'leftPath', 'stack', 'currentModel', 'currentRow', 'currentNode', 'currentNodeType'), $ex);
140: }
141: }
142:
143: private static function _findCurrentNodeType(\Model $model, $currentNode) {
144: if (self::isField($model, $currentNode)) {
145: return self::NODE_TYPE_FIELD;
146: } else if ($model->alias == $currentNode) {
147: return self::NODE_TYPE_SELF;
148: } else if (self::isBelongsToAssociation($model, $currentNode) ||
149: self::isHasOneAssociation($model, $currentNode)) {
150: return self::NODE_TYPE_HAS_ONE;
151: } else if (self::isHasManyAssociation($model, $currentNode)) {
152: return self::NODE_TYPE_HAS_MANY;
153: } else {
154: throw new Exception("Node \"{$currentNode}\" is not a field or association of \"{$model->name}\" (Alias: \"{$model->alias}\").");
155: }
156: }
157:
158: public static function _findField(Model $model, $row, $field, $required = true) {
159: if (array_key_exists($model->alias, $row) && array_key_exists($field, $row[$model->alias])) {
160: return $row[$model->alias][$field];
161: } else if (array_key_exists($field, $row)) {
162: return $row[$field];
163: } else if (empty($row[$model->alias][$model->primaryKey])) {
164: return null;
165: } else {
166: $findRow = $model->find('first', array(
167: 'recursive' => -1,
168: 'conditions' => array(
169: $model->alias . '.' . $model->primaryKey => $row[$model->alias][$model->primaryKey],
170: ),
171: ));
172: if (empty($findRow)) {
173: if ($required) {
174: throw new Exception("Row not found");
175: }
176: else {
177: return null;
178: }
179: }
180: if (array_key_exists($model->alias, $findRow) && array_key_exists($field, $findRow[$model->alias])) {
181: return $findRow[$model->alias][$field];
182: } else if ($required) {
183: throw new Exception("Field \"$field\" not found for \"{$model->name}\" record: " . print_r($findRow, true));
184: } else {
185: return null;
186: }
187: }
188: }
189:
190: public static function _findSelf(Model $model, $row) {
191: return array_key_exists($model->alias, $row) ?
192: $row[$model->alias] :
193: $row;
194: }
195:
196: public static function _findHasOne(Model $model, &$row, $alias) {
197: if (!array_key_exists($alias, $row)) {
198: if (self::isBelongsToAssociation($model, $alias)) {
199: $row[$alias] = self::findBelongsToInstance($model, $row, $alias);
200: }
201:
202: else {
203: $row[$alias] = self::findHasOneInstance($model, $alias, $row);
204: }
205: }
206: return $row[$alias];
207: }
208:
209: public static function _findHasMany(Model $model, &$row, $alias) {
210: if (!array_key_exists($alias, $row)) {
211: $row[$alias] = self::findHasManyInstance($model, $row, $alias);
212: }
213: return $row[$alias];
214: }
215:
216: private static function fieldSchema(Model $model, $name) {
217: if (($fieldSchema = $model->schema($name))) {
218: return $fieldSchema;
219: } else if (!empty($model->virtualFields[$name])) {
220: if (!empty($model->virtualFieldsSchema[$name])) {
221: return $model->virtualFieldsSchema[$name];
222: } else {
223: return array(
224: 'type' => 'string'
225: );
226: }
227: } else {
228: throw new Exception("Field not found: {$model->name}.{$name}.");
229: }
230: }
231:
232: private static function isField(Model $model, $name) {
233: if (!is_array($schema = $model->schema())) {
234: throw new Exception("{$model->name}->schema() do not returned a array. Returned: \"$schema\"" );
235: }
236: return in_array($name, array_keys($model->schema())) ||
237: !empty($model->virtualFields[$name]);
238: }
239:
240: private static function isBelongsToAssociation(Model $model, $alias) {
241: return in_array($alias, $model->getAssociated('belongsTo'));
242: }
243:
244: 245: 246: 247: 248: 249:
250: private static function isHasOneAssociation(Model $model, $alias) {
251: return in_array($alias, $model->getAssociated('hasOne'));
252: }
253:
254: private static function findBelongsToInstance(Model $model, $row, $alias) {
255: $ret = $model->{$alias}->find(
256: 'first', array(
257: 'conditions' => array(
258: "{$alias}.{$model->{$alias}->primaryKey}" => self::_findField($model, $row, $model->belongsTo[$alias]['foreignKey']),
259: ), 'recursive' => -1
260: )
261: );
262:
263: if (array_key_exists($alias, $ret)) {
264: return $ret[$alias];
265: } else {
266: return array();
267: }
268: }
269:
270: 271: 272: 273: 274: 275: 276: 277:
278: private static function findHasOneInstance(Model $model, $alias, $row) {
279: return $model->{$alias}->find(
280: 'first', array(
281: 'conditions' => array(
282: "{$alias}.{$model->hasOne[$alias]['foreignKey']}" => $row[$model->alias][$model->primaryKey]
283: ), 'recursive' => -1
284: )
285: );
286: }
287:
288: private static function isHasManyAssociation(Model $model, $alias) {
289: return in_array($alias, $model->getAssociated('hasMany'));
290: }
291:
292: private static function findHasManyInstance(Model $model, $row, $alias) {
293: if (!array_key_exists($alias, $model->hasMany)) {
294: throw new Exception("Model \"{$model->name}\" has no hasMany association \"$alias\"");
295: }
296: $records = $model->{$alias}->find(
297: 'all'
298: , array(
299: 'conditions' => array(
300: "{$alias}.{$model->hasMany[$alias]['foreignKey']}" => $row[$model->alias][$model->primaryKey]
301: )
302: , 'recursive' => -1
303: )
304: );
305: $ret = array();
306: foreach ($records as $record) {
307: $ret[] = $record[$alias];
308: }
309: return $ret;
310: }
311:
312: private static function pathPopFirst($path) {
313: $newPath = array();
314:
315: for ($i = 1; $i < count($path); ++$i) {
316: $newPath[] = $path[$i];
317: }
318:
319: return $newPath;
320: }
321:
322: 323: 324: 325: 326:
327: private static function _pathLastNode($path) {
328: $arrayPath = is_array($path) ?
329: $path :
330: explode('.', $path);
331: if (empty($arrayPath)) {
332: throw new ModelTraverserException("Path is empty", compact('path'));
333: }
334: end($arrayPath);
335: return current($arrayPath);
336: }
337:
338: }
339:
340: class ModelTraverserException extends Exception {
341:
342: 343: 344: 345: 346: 347: 348:
349: public function __construct($message, $params, $previous = null) {
350: parent::__construct(
351: $message . ' || ' . print_r(
352: self::_buildDebug($params)
353: , true
354: )
355: , 1
356: , $previous
357: );
358: }
359:
360: public static function debug($var) {
361: debug(self::_buildDebug($var));
362: }
363:
364: private static function _buildDebug($var) {
365: if ($var instanceof Model) {
366: return get_class($var) . " ({$var->name}/{$var->alias})";
367: } else if (is_object($var)) {
368: return 'Object ' . get_class($var);
369: } else if (is_array($var)) {
370: $ret = array();
371: foreach ($var as $key => $value) {
372: $ret[$key] = self::_buildDebug($value);
373: }
374: return $ret;
375: } else {
376: return $var;
377: }
378: }
379:
380: }
381: