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: use Cake\ORM\Association;
19: use RuntimeException;
20:
21: /**
22: * Checks that the value provided in a field exists as the primary key of another
23: * table.
24: */
25: class ExistsIn
26: {
27:
28: /**
29: * The list of fields to check
30: *
31: * @var array
32: */
33: protected $_fields;
34:
35: /**
36: * The repository where the field will be looked for
37: *
38: * @var \Cake\Datasource\RepositoryInterface|\Cake\ORM\Association|string
39: */
40: protected $_repository;
41:
42: /**
43: * Options for the constructor
44: *
45: * @var array
46: */
47: protected $_options = [];
48:
49: /**
50: * Constructor.
51: *
52: * Available option for $options is 'allowNullableNulls' flag.
53: * Set to true to accept composite foreign keys where one or more nullable columns are null.
54: *
55: * @param string|array $fields The field or fields to check existence as primary key.
56: * @param \Cake\Datasource\RepositoryInterface|\Cake\ORM\Association|string $repository The repository where the field will be looked for,
57: * or the association name for the repository.
58: * @param array $options The options that modify the rules behavior.
59: * Options 'allowNullableNulls' will make the rule pass if given foreign keys are set to `null`.
60: * Notice: allowNullableNulls cannot pass by database columns set to `NOT NULL`.
61: */
62: public function __construct($fields, $repository, array $options = [])
63: {
64: $options += ['allowNullableNulls' => false];
65: $this->_options = $options;
66:
67: $this->_fields = (array)$fields;
68: $this->_repository = $repository;
69: }
70:
71: /**
72: * Performs the existence check
73: *
74: * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields
75: * @param array $options Options passed to the check,
76: * where the `repository` key is required.
77: * @throws \RuntimeException When the rule refers to an undefined association.
78: * @return bool
79: */
80: public function __invoke(EntityInterface $entity, array $options)
81: {
82: if (is_string($this->_repository)) {
83: if (!$options['repository']->hasAssociation($this->_repository)) {
84: throw new RuntimeException(sprintf(
85: "ExistsIn rule for '%s' is invalid. '%s' is not associated with '%s'.",
86: implode(', ', $this->_fields),
87: $this->_repository,
88: get_class($options['repository'])
89: ));
90: }
91: $repository = $options['repository']->getAssociation($this->_repository);
92: $this->_repository = $repository;
93: }
94:
95: $fields = $this->_fields;
96: $source = $target = $this->_repository;
97: $isAssociation = $target instanceof Association;
98: $bindingKey = $isAssociation ? (array)$target->getBindingKey() : (array)$target->getPrimaryKey();
99: $realTarget = $isAssociation ? $target->getTarget() : $target;
100:
101: if (!empty($options['_sourceTable']) && $realTarget === $options['_sourceTable']) {
102: return true;
103: }
104:
105: if (!empty($options['repository'])) {
106: $source = $options['repository'];
107: }
108: if ($source instanceof Association) {
109: $source = $source->getSource();
110: }
111:
112: if (!$entity->extract($this->_fields, true)) {
113: return true;
114: }
115:
116: if ($this->_fieldsAreNull($entity, $source)) {
117: return true;
118: }
119:
120: if ($this->_options['allowNullableNulls']) {
121: $schema = $source->getSchema();
122: foreach ($fields as $i => $field) {
123: if ($schema->getColumn($field) && $schema->isNullable($field) && $entity->get($field) === null) {
124: unset($bindingKey[$i], $fields[$i]);
125: }
126: }
127: }
128:
129: $primary = array_map(
130: [$target, 'aliasField'],
131: $bindingKey
132: );
133: $conditions = array_combine(
134: $primary,
135: $entity->extract($fields)
136: );
137:
138: return $target->exists($conditions);
139: }
140:
141: /**
142: * Checks whether or not the given entity fields are nullable and null.
143: *
144: * @param \Cake\Datasource\EntityInterface $entity The entity to check.
145: * @param \Cake\ORM\Table $source The table to use schema from.
146: * @return bool
147: */
148: protected function _fieldsAreNull($entity, $source)
149: {
150: $nulls = 0;
151: $schema = $source->getSchema();
152: foreach ($this->_fields as $field) {
153: if ($schema->getColumn($field) && $schema->isNullable($field) && $entity->get($field) === null) {
154: $nulls++;
155: }
156: }
157:
158: return $nulls === count($this->_fields);
159: }
160: }
161: