ValidForeignKeyBehavior   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
wmc 22
lcom 1
cbo 0
dl 0
loc 185
rs 10
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A setup() 0 7 2
A beforeValidate() 0 7 2
B validateAllForeignKeys() 0 37 5
C validForeignKey() 0 70 13
1
<?php
2
/**
3
 * ValidForeignKey Behavior
4
 *
5
 * Licensed under The MIT License.
6
 * For full copyright and license information, please see the LICENSE.txt
7
 * Redistributions of files must retain the above copyright notice.
8
 *
9
 * @copyright Marc Würth
10
 * @author Marc Würth <[email protected]>
11
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
12
 * @link https://github.com/ravage84/ValidForeignKeyBehavior
13
 */
14
15
App::uses('ModelBehavior', 'Model');
16
17
/**
18
 * A CakePHP behavior to add data validation for foreign keys
19
 *
20
 * Inspired by dogmatic69's and iamFIREcracker's solutions.
21
 * But instead of looping through all bound associations,
22
 * you can selectively validate one foreign key with this behavior.
23
 *
24
 * Provides two ways of validation:
25
 * 1. Validate all foreign keys
26
 * 2. Validate a certain foreign key
27
 *
28
 * 1. This needs to be set through the behavior settings
29
 * once per model (or only once app wide in AppModel).
30
 *
31
 * <code>
32
 * public $actsAs = array(
33
 *     'ValidForeignKeyBehavior.ValidForeignKey' => array(
34
 *         'autoValidate' => true,
35
 *         'errMsg' => '',
36
 *         'exclude' => array()
37
 *     )
38
 * );
39
 * </code>
40
 *
41
 * Keep in mind that this could be overly costly because
42
 * the keys will be checked every time the application
43
 * saves or updates records.
44
 * Also the same behavior can be achieved by using foreign key
45
 * constraints in your RDBMS.
46
 *
47
 * 2. This allows to only validate foreign keys selectively and
48
 * must be added just like a normal data validation rule.
49
 *
50
 * <code>
51
 * public $validate = array(
52
 *     'product_id' => array(
53
 *         'validForeignKey' => array(
54
 *         'rule' => array('validForeignKey', true),
55
 *         'message' => 'Product ID must exist.'
56
 *     ),
57
 * ));
58
 * </code>
59
 *
60
 * @link https://github.com/infinitas/infinitas/blob/v0.9b/Core/Libs/Model/Behavior/ValidationBehavior.php#L147-L185 dogmatic69's solution.
61
 * @link https://gist.github.com/iamFIREcracker/1307191 iamFIREcracker's solution.
62
 */
63
class ValidForeignKeyBehavior extends ModelBehavior {
64
65
/**
66
 * Default settings for a model that has this behavior attached
67
 *
68
 * 'autoValidate' = Check all foreign keys automatically.
69
 * Defaults to false, use the validForeignKey data validation rule instead.
70
 * 'errMsg' = The error message to display.
71
 * 'exclude = Fields you want to exclude from the validation.
72
 *
73
 * @var array
74
 */
75
	protected $_defaults = array(
76
		'autoValidate' => false,
77
		'errMsg' => 'The key/ID for %s must exist.',
78
		'exclude' => array(),
79
	);
80
81
/**
82
 * Setup the behavior
83
 *
84
 * Checks if the configuration settings are set in the model,
85
 * merges them with the the defaults.
86
 *
87
 * @param Model $model Model using this behavior
88
 * @param array $config Configuration settings for $model
89
 * @return void
90
 */
91
	public function setup(Model $model, $config = array()) {
92
		if (!isset($this->settings[$model->alias])) {
93
			$this->settings[$model->alias] = $this->_defaults;
94
		}
95
		$this->settings[$model->alias] = array_merge(
96
			$this->settings[$model->alias], (array)$config);
97
	}
98
99
/**
100
 * Adds the validateAllForeignKeys data validation rule dynamically
101
 *
102
 * If 'autoValidate' is set to true
103
 *
104
 * @param Model $model Model using this behavior
105
 * @param array $options Options passed from Model::save() (unused).
106
 * @return bool True if validate operation should continue, false to abort
107
 */
108
	public function beforeValidate(Model $model, $options = array()) {
109
		if ($this->settings[$model->alias]['autoValidate']) {
110
			$this->validateAllForeignKeys($model);
111
		}
112
113
		return parent::beforeValidate($model, $options);
114
	}
115
116
/**
117
 * Validate all foreign keys of the model.
118
 *
119
 * Only checks the keys, if they are present in the data of the model array.
120
 *
121
 * @param Model $model The model to test.
122
 * @return bool True if valid, else false.
123
 */
124
	public function validateAllForeignKeys(Model $model) {
125
		// Get the aliases of all associations as array('AliasName) => 'alias_id', ...)
126
		$returnForeignKey = create_function('$belongToAssociation', 'return $belongToAssociation["foreignKey"];');
127
		$aliases = array_map($returnForeignKey, $model->belongsTo);
128
129
		// Check the foreign keys of all associations
130
		foreach ($aliases as $alias => $foreignKeyField) {
131
			// Skip excluded fields
132
			if (in_array($foreignKeyField, $this->settings[$model->alias]['exclude'])) {
133
				continue;
134
			}
135
136
			// But only if we have data to validate
137
			if (isset($model->data[$model->alias]) &&
138
				array_key_exists($foreignKeyField, $model->data[$model->alias])) {
139
				$assocModel = $model->{$alias};
140
				$foreignKeyValue = $model->data[$model->alias][$foreignKeyField];
141
142
				// Since we don't know better, allow null
143
				$allowNull = true;
144
145
				$model->validator()->add($foreignKeyField, 'validateAllForeignKeys', array(
146
					'rule' => array(
147
						'validForeignKey',
148
						$allowNull,
149
						$assocModel->name,
150
						$assocModel->primaryKey,
151
						$foreignKeyValue
152
					),
153
					'message' => sprintf(
154
						$this->settings[$model->alias]['errMsg'],
155
						Inflector::humanize(Inflector::underscore($alias))
156
					),
157
				));
158
			}
159
		}
160
	}
161
162
/**
163
 * Validate if a key exists in the associated table.
164
 *
165
 * It is intentional that we don't introspect the model
166
 * as we do it in validateAllForeignKeys().
167
 *
168
 * @param Model $model The model to validate.
169
 * @param array $data The key/value pair to validate.
170
 * @param bool $allowNull If null is allowed (optional).
171
 * @param null|string $assocModelName The name of the associated model (optional).
172
 * @param string $assocFieldName The name of the associated field (optional).
173
 * @param null|string $assocFieldValue The value to validate instead (optional).
174
 * @return bool True if valid, else false.
175
 * @throws InvalidArgumentException If an invalid amount of arguments was supplied.
176
 */
177
	public function validForeignKey(Model $model, $data, $allowNull = false,
178
									$assocModelName = null, $assocFieldName = 'id',
179
									$assocFieldValue = null) {
180
		$fieldValueGiven = false;
181
182
		// Depending on how many parameters are configured/passed
183
		switch (func_num_args()) {
184
			case 3:
185
				// No additional parameter given
186
				$allowNull = false;
187
				$assocModelName = null;
188
				$assocFieldName = null;
189
				break;
190
			case 4:
191
				// $allowNull given
192
				$assocModelName = null;
193
				$assocFieldName = null;
194
				break;
195
			case 5:
196
				// $allowNull, $assocModelName given
197
				$assocFieldName = null;
198
				break;
199
			case 6:
200
				// $allowNull, $assocModelName, $assocFieldName given
201
				break;
202
			case 7:
203
				// $allowNull, $assocModelName, $assocFieldName, $assocFieldValue given
204
				$fieldValueGiven = true;
205
				break;
206
			default:
207
				throw new InvalidArgumentException('Invalid amount of arguments to validForeignKey().');
208
		}
209
210
		// If no field value was given, get it from $data array
211
		if (!$fieldValueGiven) {
212
			reset($data);
213
			$assocFieldValue = current($data);
214
		}
215
		// Treat null values depending on $allowNull
216
		if ($assocFieldValue === null) {
217
			if ($allowNull === true) {
218
				return true;
219
			}
220
221
			return false;
222
		}
223
		// Workaround datasources like ArraySource, which allows false as key,
224
		// would be converted to "" otherwise.
225
		if ($assocFieldValue === false) {
226
			$assocFieldValue = 0;
227
		}
228
229
		if ($assocModelName === null) {
230
			$assocModelName = key($data);
231
			$assocModelName = substr($assocModelName, 0, -3);
232
			$assocModelName = Inflector::camelize($assocModelName);
233
		}
234
		if ($assocFieldName === null) {
235
			$assocFieldName = 'id';
236
		}
237
		$foreignModel = ClassRegistry::init($assocModelName);
238
239
		$found = $foreignModel->find('count',
240
			array(
241
				'conditions' => array($assocFieldName => $assocFieldValue),
242
				'recursive' => -1
243
			)
244
		);
245
		return ($found > 0) ? true : false;
246
	}
247
}