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\Association;
16:
17: use Cake\Database\Expression\IdentifierExpression;
18: use Cake\Datasource\EntityInterface;
19: use Cake\ORM\Association;
20: use Cake\ORM\Association\Loader\SelectLoader;
21: use Cake\ORM\Table;
22: use Cake\Utility\Inflector;
23: use RuntimeException;
24:
25: /**
26: * Represents an 1 - N relationship where the source side of the relation is
27: * related to only one record in the target table.
28: *
29: * An example of a BelongsTo association would be Article belongs to Author.
30: */
31: class BelongsTo extends Association
32: {
33:
34: /**
35: * Valid strategies for this type of association
36: *
37: * @var array
38: */
39: protected $_validStrategies = [
40: self::STRATEGY_JOIN,
41: self::STRATEGY_SELECT
42: ];
43:
44: /**
45: * Gets the name of the field representing the foreign key to the target table.
46: *
47: * @return string
48: */
49: public function getForeignKey()
50: {
51: if ($this->_foreignKey === null) {
52: $this->_foreignKey = $this->_modelKey($this->getTarget()->getAlias());
53: }
54:
55: return $this->_foreignKey;
56: }
57:
58: /**
59: * Handle cascading deletes.
60: *
61: * BelongsTo associations are never cleared in a cascading delete scenario.
62: *
63: * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete.
64: * @param array $options The options for the original delete.
65: * @return bool Success.
66: */
67: public function cascadeDelete(EntityInterface $entity, array $options = [])
68: {
69: return true;
70: }
71:
72: /**
73: * Returns default property name based on association name.
74: *
75: * @return string
76: */
77: protected function _propertyName()
78: {
79: list(, $name) = pluginSplit($this->_name);
80:
81: return Inflector::underscore(Inflector::singularize($name));
82: }
83:
84: /**
85: * Returns whether or not the passed table is the owning side for this
86: * association. This means that rows in the 'target' table would miss important
87: * or required information if the row in 'source' did not exist.
88: *
89: * @param \Cake\ORM\Table $side The potential Table with ownership
90: * @return bool
91: */
92: public function isOwningSide(Table $side)
93: {
94: return $side === $this->getTarget();
95: }
96:
97: /**
98: * Get the relationship type.
99: *
100: * @return string
101: */
102: public function type()
103: {
104: return self::MANY_TO_ONE;
105: }
106:
107: /**
108: * Takes an entity from the source table and looks if there is a field
109: * matching the property name for this association. The found entity will be
110: * saved on the target table for this association by passing supplied
111: * `$options`
112: *
113: * @param \Cake\Datasource\EntityInterface $entity an entity from the source table
114: * @param array $options options to be passed to the save method in the target table
115: * @return bool|\Cake\Datasource\EntityInterface false if $entity could not be saved, otherwise it returns
116: * the saved entity
117: * @see \Cake\ORM\Table::save()
118: */
119: public function saveAssociated(EntityInterface $entity, array $options = [])
120: {
121: $targetEntity = $entity->get($this->getProperty());
122: if (empty($targetEntity) || !($targetEntity instanceof EntityInterface)) {
123: return $entity;
124: }
125:
126: $table = $this->getTarget();
127: $targetEntity = $table->save($targetEntity, $options);
128: if (!$targetEntity) {
129: return false;
130: }
131:
132: $properties = array_combine(
133: (array)$this->getForeignKey(),
134: $targetEntity->extract((array)$this->getBindingKey())
135: );
136: $entity->set($properties, ['guard' => false]);
137:
138: return $entity;
139: }
140:
141: /**
142: * Returns a single or multiple conditions to be appended to the generated join
143: * clause for getting the results on the target table.
144: *
145: * @param array $options list of options passed to attachTo method
146: * @return array
147: * @throws \RuntimeException if the number of columns in the foreignKey do not
148: * match the number of columns in the target table primaryKey
149: */
150: protected function _joinCondition($options)
151: {
152: $conditions = [];
153: $tAlias = $this->_name;
154: $sAlias = $this->_sourceTable->getAlias();
155: $foreignKey = (array)$options['foreignKey'];
156: $bindingKey = (array)$this->getBindingKey();
157:
158: if (count($foreignKey) !== count($bindingKey)) {
159: if (empty($bindingKey)) {
160: $msg = 'The "%s" table does not define a primary key. Please set one.';
161: throw new RuntimeException(sprintf($msg, $this->getTarget()->getTable()));
162: }
163:
164: $msg = 'Cannot match provided foreignKey for "%s", got "(%s)" but expected foreign key for "(%s)"';
165: throw new RuntimeException(sprintf(
166: $msg,
167: $this->_name,
168: implode(', ', $foreignKey),
169: implode(', ', $bindingKey)
170: ));
171: }
172:
173: foreach ($foreignKey as $k => $f) {
174: $field = sprintf('%s.%s', $tAlias, $bindingKey[$k]);
175: $value = new IdentifierExpression(sprintf('%s.%s', $sAlias, $f));
176: $conditions[$field] = $value;
177: }
178:
179: return $conditions;
180: }
181:
182: /**
183: * {@inheritDoc}
184: *
185: * @return \Closure
186: */
187: public function eagerLoader(array $options)
188: {
189: $loader = new SelectLoader([
190: 'alias' => $this->getAlias(),
191: 'sourceAlias' => $this->getSource()->getAlias(),
192: 'targetAlias' => $this->getTarget()->getAlias(),
193: 'foreignKey' => $this->getForeignKey(),
194: 'bindingKey' => $this->getBindingKey(),
195: 'strategy' => $this->getStrategy(),
196: 'associationType' => $this->type(),
197: 'finder' => [$this, 'find']
198: ]);
199:
200: return $loader->buildEagerLoader($options);
201: }
202: }
203: