1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.0.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\ORM\Rule;
16:
17: use Cake\Datasource\EntityInterface;
18:
19: /**
20: * Checks that a list of fields from an entity are unique in the table
21: */
22: class IsUnique
23: {
24:
25: /**
26: * The list of fields to check
27: *
28: * @var array
29: */
30: protected $_fields;
31:
32: /**
33: * The options to use.
34: *
35: * @var array
36: */
37: protected $_options;
38:
39: /**
40: * Constructor.
41: *
42: * ### Options
43: *
44: * - `allowMultipleNulls` Set to false to disallow multiple null values in
45: * multi-column unique rules. By default this is `true` to emulate how SQL UNIQUE
46: * keys work.
47: *
48: * @param array $fields The list of fields to check uniqueness for
49: * @param array $options The additional options for this rule.
50: */
51: public function __construct(array $fields, array $options = [])
52: {
53: $this->_fields = $fields;
54: $this->_options = $options + ['allowMultipleNulls' => true];
55: }
56:
57: /**
58: * Performs the uniqueness check
59: *
60: * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields
61: * where the `repository` key is required.
62: * @param array $options Options passed to the check,
63: * @return bool
64: */
65: public function __invoke(EntityInterface $entity, array $options)
66: {
67: if (!$entity->extract($this->_fields, true)) {
68: return true;
69: }
70: $allowMultipleNulls = $this->_options['allowMultipleNulls'];
71:
72: $alias = $options['repository']->getAlias();
73: $conditions = $this->_alias($alias, $entity->extract($this->_fields), $allowMultipleNulls);
74: if ($entity->isNew() === false) {
75: $keys = (array)$options['repository']->getPrimaryKey();
76: $keys = $this->_alias($alias, $entity->extract($keys), $allowMultipleNulls);
77: if (array_filter($keys, 'strlen')) {
78: $conditions['NOT'] = $keys;
79: }
80: }
81:
82: return !$options['repository']->exists($conditions);
83: }
84:
85: /**
86: * Add a model alias to all the keys in a set of conditions.
87: *
88: * Null values will be omitted from the generated conditions,
89: * as SQL UNIQUE indexes treat `NULL != NULL`
90: *
91: * @param string $alias The alias to add.
92: * @param array $conditions The conditions to alias.
93: * @param bool $multipleNulls Whether or not to allow multiple nulls.
94: * @return array
95: */
96: protected function _alias($alias, $conditions, $multipleNulls)
97: {
98: $aliased = [];
99: foreach ($conditions as $key => $value) {
100: if ($multipleNulls) {
101: $aliased["$alias.$key"] = $value;
102: } else {
103: $aliased["$alias.$key IS"] = $value;
104: }
105: }
106:
107: return $aliased;
108: }
109: }
110: