1<?php
2
3App::uses('CakeEventListener', 'Event');
4App::uses('CrudListener', 'Crud.Controller/Crud');
5
6/**
7 * Implements beforeRender event listener to set related models' lists to
8 * the view
9 *
10 * Licensed under The MIT License
11 * For full copyright and license information, please see the LICENSE.txt
12 */
13class RelatedModelsListener extends CrudListener {
14
15/**
16 * Gets the list of associated model lists to be fetched for an action
17 *
18 * @param string $action name of the action
19 * @return array
20 */
21	public function models($action = null) {
22		$settings = $this->_action($action)->relatedModels();
23		if ($settings === true) {
24			$ModelInstance = $this->_model();
25			return array_merge(
26				$ModelInstance->getAssociated('belongsTo'),
27				$ModelInstance->getAssociated('hasAndBelongsToMany')
28			);
29		}
30
31		if (empty($settings)) {
32			return array();
33		}
34
35		if (is_string($settings)) {
36			$settings = array($settings);
37		}
38
39		return $settings;
40	}
41
42/**
43 * Find and publish all related models to the view
44 * for an action
45 *
46 * @param NULL|string $action If NULL the current action will be used
47 * @return void
48 */
49	public function publishRelatedModels($action = null) {
50		$models = $this->models($action);
51
52		if (empty($models)) {
53			return;
54		}
55
56		$Controller = $this->_controller();
57
58		foreach ($models as $modelName) {
59			$associationType = $this->_getAssociationType($modelName);
60			$associatedModel = $this->_getModelInstance($modelName, $associationType);
61
62			$viewVar = Inflector::variable(Inflector::pluralize($associatedModel->alias));
63			if (array_key_exists($viewVar, $Controller->viewVars)) {
64				continue;
65			}
66
67			$query = $this->_getBaseQuery($associatedModel, $associationType);
68
69			$subject = $this->_trigger('beforeRelatedModel', compact('modelName', 'query', 'viewVar', 'associationType', 'associatedModel'));
70			$items = $this->_findRelatedItems($associatedModel, $subject->query);
71			$subject = $this->_trigger('afterRelatedModel', compact('modelName', 'items', 'viewVar', 'associationType', 'associatedModel'));
72
73			$Controller->set($subject->viewVar, $subject->items);
74		}
75	}
76
77/**
78 * Fetches related models' list and sets them to a variable for the view
79 *
80 * @codeCoverageIgnore
81 * @param CakeEvent $event
82 * @return void
83 */
84	public function beforeRender(CakeEvent $event) {
85		$this->publishRelatedModels();
86	}
87
88/**
89 * Execute the DB query to find the related items
90 *
91 * @param Model $Model
92 * @param array $query
93 * @return array
94 */
95	protected function _findRelatedItems(Model $Model, $query) {
96		if ($this->_hasTreeBehavior($Model)) {
97			return $Model->generateTreeList(
98				$query['conditions'],
99				$query['keyPath'],
100				$query['valuePath'],
101				$query['spacer'],
102				$query['recursive']
103			);
104		}
105
106		return $Model->find('list', $query);
107	}
108
109/**
110 * Get the base query to find the related items for an associated model
111 *
112 * @param Model $associatedModel
113 * @param string $associationType
114 * @return array
115 */
116	protected function _getBaseQuery(Model $associatedModel, $associationType = null) {
117		$query = array();
118
119		if ($associationType === 'belongsTo') {
120			$PrimaryModel = $this->_model();
121			$query['conditions'][] = $PrimaryModel->belongsTo[$associatedModel->alias]['conditions'];
122		}
123
124		if ($this->_hasTreeBehavior($associatedModel)) {
125			$TreeBehavior = $this->_getTreeBehavior($associatedModel);
126			$query = array(
127				'keyPath' => null,
128				'valuePath' => null,
129				'spacer' => '_',
130				'recursive' => $TreeBehavior->settings[$associatedModel->alias]['recursive']
131			);
132
133			if (empty($query['conditions'])) {
134				$query['conditions'][] = $TreeBehavior->settings[$associatedModel->alias]['scope'];
135			}
136		}
137
138		return $query;
139	}
140
141/**
142 * Returns model instance based on its name
143 *
144 * @param string $modelName
145 * @param string $associationType
146 * @return Model
147 */
148	protected function _getModelInstance($modelName, $associationType = null) {
149		$PrimaryModel = $this->_model();
150
151		if (isset($PrimaryModel->{$modelName})) {
152			return $PrimaryModel->{$modelName};
153		}
154
155		$Controller = $this->_controller();
156		if (isset($Controller->{$modelName}) && $Controller->{$modelName} instanceOf Model) {
157			return $Controller->{$modelName};
158		}
159
160		if ($associationType && !empty($PrimaryModel->{$associationType}[$modelName]['className'])) {
161			return $this->_classRegistryInit($PrimaryModel->{$associationType}[$modelName]['className']);
162		}
163
164		return $this->_classRegistryInit($modelName);
165	}
166
167/**
168 * Returns model's association type with controller's model
169 *
170 * @param string $modelName
171 * @return string|null Association type if found else null
172 */
173	protected function _getAssociationType($modelName) {
174		$associated = $this->_model()->getAssociated();
175		return isset($associated[$modelName]) ? $associated[$modelName] : null;
176	}
177
178/**
179 * Check if a model has the Tree behavior attached or not
180 *
181 * @codeCoverageIgnore
182 * @param Model $Model
183 * @return boolean
184 */
185	protected function _hasTreeBehavior(Model $Model) {
186		return $Model->Behaviors->attached('Tree');
187	}
188
189/**
190 * Get the TreeBehavior from a model
191 *
192 * @codeCoverageIgnore
193 * @param Model $Model
194 * @return TreeBehavior
195 */
196	protected function _getTreeBehavior(Model $Model) {
197		return $Model->Behaviors->Tree;
198	}
199
200/**
201 * Wrapper for ClassRegistry::init for easier testing
202 *
203 * @codeCoverageIgnore
204 * @return Model
205 */
206	protected function _classRegistryInit($modelName) {
207		return ClassRegistry::init($modelName);
208	}
209
210}
211