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