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