1
2/*
3 +------------------------------------------------------------------------+
4 | Phalcon Framework                                                      |
5 +------------------------------------------------------------------------+
6 | Copyright (c) 2011-2017 Phalcon Team (https://phalconphp.com)          |
7 +------------------------------------------------------------------------+
8 | This source file is subject to the New BSD License that is bundled     |
9 | with this package in the file LICENSE.txt.                             |
10 |                                                                        |
11 | If you did not receive a copy of the license and are unable to         |
12 | obtain it through the world-wide-web, please send an email             |
13 | to license@phalconphp.com so we can send you a copy immediately.       |
14 +------------------------------------------------------------------------+
15 | Authors: Andres Gutierrez <andres@phalconphp.com>                      |
16 |          Eduar Carvajal <eduar@phalconphp.com>                         |
17 +------------------------------------------------------------------------+
18 */
19
20namespace Phalcon\Mvc\Model\Validator;
21
22use Phalcon\Mvc\Model;
23use Phalcon\Mvc\EntityInterface;
24use Phalcon\Mvc\Model\Exception;
25use Phalcon\Mvc\Model\Validator;
26
27/**
28 * Phalcon\Mvc\Model\Validator\Uniqueness
29 *
30 * Validates that a field or a combination of a set of fields are not
31 * present more than once in the existing records of the related table
32 *
33 * This validator is only for use with Phalcon\Mvc\Collection. If you are using
34 * Phalcon\Mvc\Model, please use the validators provided by Phalcon\Validation.
35 *
36 *<code>
37 * use Phalcon\Mvc\Collection;
38 * use Phalcon\Mvc\Model\Validator\Uniqueness;
39 *
40 * class Subscriptors extends Collection
41 * {
42 *     public function validation()
43 *     {
44 *         $this->validate(
45 *             new Uniqueness(
46 *                 [
47 *                     "field"   => "email",
48 *                     "message" => "Value of field 'email' is already present in another record",
49 *                 ]
50 *             )
51 *         );
52 *
53 *         if ($this->validationHasFailed() === true) {
54 *             return false;
55 *         }
56 *     }
57 * }
58 *</code>
59 *
60 * @deprecated 3.1.0
61 * @see Phalcon\Validation\Validator\Uniqueness
62 */
63class Uniqueness extends Validator
64{
65	/**
66	 * Executes the validator
67	 */
68	public function validate(<EntityInterface> record) -> boolean
69	{
70		var field, dependencyInjector, metaData, message, bindTypes, bindDataTypes,
71			columnMap, conditions, bindParams, number, composeField, columnField,
72			bindType, primaryField, attributeField, params, className, replacePairs;
73
74		let dependencyInjector = record->getDI();
75		let metaData = dependencyInjector->getShared("modelsMetadata");
76
77		/**
78		 * PostgreSQL check if the compared constant has the same type as the
79		 * column, so we make cast to the data passed to match those column types
80		 */
81		let bindTypes = [];
82		let bindDataTypes = metaData->getBindTypes(record);
83
84		if globals_get("orm.column_renaming") {
85			let columnMap = metaData->getReverseColumnMap(record);
86		} else {
87			let columnMap = null;
88		}
89
90		let conditions = [];
91		let bindParams = [];
92		let number = 0;
93
94		let field = this->getOption("field");
95		if typeof field == "array" {
96
97			/**
98			 * The field can be an array of values
99			 */
100			for composeField in field {
101
102				/**
103				 * The reversed column map is used in the case to get real column name
104				 */
105				if typeof columnMap == "array" {
106					if !fetch columnField, columnMap[composeField] {
107						throw new Exception("Column '" . composeField . "' isn't part of the column map");
108					}
109				} else {
110					let columnField = composeField;
111				}
112
113				/**
114				 * Some database systems require that we pass the values using bind casting
115				 */
116				if !fetch bindType, bindDataTypes[columnField] {
117					throw new Exception("Column '" . columnField . "' isn't part of the table columns");
118				}
119
120				/**
121				 * The attribute could be "protected" so we read using "readattribute"
122				 */
123				let conditions[] = "[" . composeField . "] = ?" . number;
124				let bindParams[] = record->readAttribute(composeField);
125				let bindTypes[] = bindType;
126
127				let number++;
128			}
129
130		} else {
131
132			/**
133			 * The reversed column map is used in the case to get real column name
134			 */
135			if typeof columnMap == "array" {
136				if !fetch columnField, columnMap[field] {
137					throw new Exception("Column '" . field . "' isn't part of the column map");
138				}
139			} else {
140				let columnField = field;
141			}
142
143			/**
144			 * Some database systems require that we pass the values using bind casting
145			 */
146			if !fetch bindType, bindDataTypes[columnField] {
147				throw new Exception("Column '" . columnField . "' isn't part of the table columns");
148			}
149
150			/**
151			 * We're checking the uniqueness with only one field
152			 */
153			let conditions[] = "[" . field . "] = ?0";
154			let bindParams[] = record->readAttribute(field);
155			let bindTypes[]  = bindType;
156
157			let number++;
158		}
159
160		/**
161		 * If the operation is update, there must be values in the object
162		 */
163		if record->getOperationMade() == Model::OP_UPDATE {
164
165			/**
166			 * We build a query with the primary key attributes
167			 */
168			if globals_get("orm.column_renaming") {
169				let columnMap = metaData->getColumnMap(record);
170			} else {
171				let columnMap = null;
172			}
173
174			for primaryField in metaData->getPrimaryKeyAttributes(record) {
175
176				if !fetch bindType, bindDataTypes[primaryField] {
177					throw new Exception("Column '" . primaryField . "' isn't part of the table columns");
178				}
179
180				/**
181				 * Rename the column if there is a column map
182				 */
183				if typeof columnMap == "array" {
184					if !fetch attributeField, columnMap[primaryField] {
185						throw new Exception("Column '" . primaryField . "' isn't part of the column map");
186					}
187				} else {
188					let attributeField = primaryField;
189				}
190
191				/**
192				 * Create a condition based on the renamed primary key
193				 */
194				let conditions[] = "[" . attributeField . "] <> ?" . number;
195				let bindParams[] = record->readAttribute(primaryField);
196				let bindTypes[] = bindType;
197
198				let number++;
199			}
200		}
201
202		/**
203		 * We don't trust the user, so we pass the parameters as bound parameters
204		 */
205		let params = [];
206		let params["di"] = dependencyInjector;
207		let params["conditions"] = join(" AND ", conditions);
208		let params["bind"] = bindParams;
209		let params["bindTypes"] = bindTypes;
210
211		let className = get_class(record);
212
213		/**
214		 * Check if the record does exist using a standard count
215		 */
216		if {className}::count(params) != 0 {
217
218			/**
219			 * Check if the developer has defined a custom message
220			 */
221			let message = this->getOption("message");
222
223			if typeof field == "array" {
224				let replacePairs = [":fields": join(", ", field)];
225				if empty message {
226					let message = "Value of fields: :fields are already present in another record";
227				}
228			} else {
229				let replacePairs = [":field": field];
230				if empty message {
231					let message = "Value of field: ':field' is already present in another record";
232				}
233			}
234
235			/**
236			 * Append the message to the validator
237			 */
238			this->appendMessage(strtr(message, replacePairs), field, "Unique");
239			return false;
240		}
241
242		return true;
243	}
244}
245