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: * The list of fields to check
26: *
27: * @var string[]
28: */
29: protected $_fields;
30:
31: /**
32: * The options to use.
33: *
34: * @var array
35: */
36: protected $_options;
37:
38: /**
39: * Constructor.
40: *
41: * ### Options
42: *
43: * - `allowMultipleNulls` Set to false to disallow multiple null values in
44: * multi-column unique rules. By default this is `true` to emulate how SQL UNIQUE
45: * keys work.
46: *
47: * @param string[] $fields The list of fields to check uniqueness for
48: * @param array $options The additional options for this rule.
49: */
50: public function __construct(array $fields, array $options = [])
51: {
52: $this->_fields = $fields;
53: $this->_options = $options + ['allowMultipleNulls' => true];
54: }
55:
56: /**
57: * Performs the uniqueness check
58: *
59: * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields
60: * where the `repository` key is required.
61: * @param array $options Options passed to the check,
62: * @return bool
63: */
64: public function __invoke(EntityInterface $entity, array $options)
65: {
66: if (!$entity->extract($this->_fields, true)) {
67: return true;
68: }
69: $allowMultipleNulls = $this->_options['allowMultipleNulls'];
70:
71: $alias = $options['repository']->getAlias();
72: $conditions = $this->_alias($alias, $entity->extract($this->_fields), $allowMultipleNulls);
73: if ($entity->isNew() === false) {
74: $keys = (array)$options['repository']->getPrimaryKey();
75: $keys = $this->_alias($alias, $entity->extract($keys), $allowMultipleNulls);
76: if (array_filter($keys, 'strlen')) {
77: $conditions['NOT'] = $keys;
78: }
79: }
80:
81: return !$options['repository']->exists($conditions);
82: }
83:
84: /**
85: * Add a model alias to all the keys in a set of conditions.
86: *
87: * Null values will be omitted from the generated conditions,
88: * as SQL UNIQUE indexes treat `NULL != NULL`
89: *
90: * @param string $alias The alias to add.
91: * @param array $conditions The conditions to alias.
92: * @param bool $multipleNulls Whether or not to allow multiple nulls.
93: * @return array
94: */
95: protected function _alias($alias, $conditions, $multipleNulls)
96: {
97: $aliased = [];
98: foreach ($conditions as $key => $value) {
99: if ($multipleNulls) {
100: $aliased["$alias.$key"] = $value;
101: } else {
102: $aliased["$alias.$key IS"] = $value;
103: }
104: }
105:
106: return $aliased;
107: }
108: }
109: