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
|
|
|
} |