1 | <?php |
||
2 | /** |
||
3 | * BEdita, API-first content management framework |
||
4 | * Copyright 2016 ChannelWeb Srl, Chialab Srl |
||
5 | * |
||
6 | * This file is part of BEdita: you can redistribute it and/or modify |
||
7 | * it under the terms of the GNU Lesser General Public License as published |
||
8 | * by the Free Software Foundation, either version 3 of the License, or |
||
9 | * (at your option) any later version. |
||
10 | * |
||
11 | * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details. |
||
12 | */ |
||
13 | |||
14 | namespace BEdita\Core\Model\Action; |
||
15 | |||
16 | use Cake\Collection\CollectionInterface; |
||
17 | use Cake\Database\Expression\QueryExpression; |
||
18 | use Cake\Datasource\EntityInterface; |
||
19 | use Cake\Datasource\Exception\InvalidPrimaryKeyException; |
||
20 | use Cake\Datasource\Exception\RecordNotFoundException; |
||
21 | use Cake\ORM\Association; |
||
22 | use Cake\ORM\Association\BelongsTo; |
||
23 | use Cake\ORM\Association\BelongsToMany; |
||
24 | use Cake\ORM\Association\HasMany; |
||
25 | use Cake\ORM\Association\HasOne; |
||
26 | use Cake\ORM\Query; |
||
27 | use Cake\ORM\Table; |
||
28 | use Cake\ORM\TableRegistry; |
||
29 | use Cake\Utility\Hash; |
||
30 | use Cake\Utility\Inflector; |
||
31 | |||
32 | /** |
||
33 | * Command to list entities associated to another entity. |
||
34 | * |
||
35 | * @since 4.0.0 |
||
36 | */ |
||
37 | class ListAssociatedAction extends BaseAction |
||
38 | { |
||
39 | /** |
||
40 | * Name of inverse association. |
||
41 | * |
||
42 | * @var string |
||
43 | */ |
||
44 | public const INVERSE_ASSOCIATION_NAME = '_InverseAssociation'; |
||
45 | |||
46 | /** |
||
47 | * Association. |
||
48 | * |
||
49 | * @var \Cake\ORM\Association |
||
50 | */ |
||
51 | protected $Association; |
||
52 | |||
53 | /** |
||
54 | * Action used for listing entities. |
||
55 | * |
||
56 | * @var \BEdita\Core\Model\Action\BaseAction |
||
57 | */ |
||
58 | protected $ListAction; |
||
59 | |||
60 | /** |
||
61 | * @inheritDoc |
||
62 | */ |
||
63 | protected function initialize(array $config) |
||
64 | { |
||
65 | $this->Association = $this->getConfig('association'); |
||
66 | |||
67 | $table = $this->Association->getTarget(); |
||
68 | $this->ListAction = new ListEntitiesAction(compact('table')); |
||
69 | } |
||
70 | |||
71 | /** |
||
72 | * Build conditions for primary key. |
||
73 | * |
||
74 | * @param \Cake\ORM\Table $table Table object. |
||
75 | * @param mixed $primaryKey Primary key. |
||
76 | * @return array |
||
77 | * @throws \Cake\Datasource\Exception\InvalidPrimaryKeyException Throws an exception if primary key is invalid. |
||
78 | */ |
||
79 | protected function primaryKeyConditions(Table $table, $primaryKey) |
||
80 | { |
||
81 | $primaryKeyFields = array_map([$table, 'aliasField'], (array)$table->getPrimaryKey()); |
||
82 | |||
83 | $primaryKey = (array)$primaryKey; |
||
84 | if (count($primaryKeyFields) !== count($primaryKey)) { |
||
85 | $primaryKey = $primaryKey ?: [null]; |
||
86 | $primaryKey = array_map(function ($key) { |
||
87 | return var_export($key, true); |
||
88 | }, $primaryKey); |
||
89 | |||
90 | throw new InvalidPrimaryKeyException(__( |
||
91 | 'Record not found in table "{0}" with primary key [{1}]', |
||
92 | $table->getTable(), |
||
93 | implode(', ', $primaryKey) |
||
94 | )); |
||
95 | } |
||
96 | |||
97 | return array_combine($primaryKeyFields, $primaryKey); |
||
98 | } |
||
99 | |||
100 | /** |
||
101 | * Check that the entity for which associated entities should be listed actually exists. |
||
102 | * |
||
103 | * @param array $data Data. |
||
104 | * @return void |
||
105 | * @throws \InvalidArgumentException Throws an exception if required option `primaryKey` is missing. |
||
106 | * @throws \Cake\Datasource\Exception\RecordNotFoundException Throws an exception if the record could not be found. |
||
107 | */ |
||
108 | protected function checkEntityExists(array $data) |
||
109 | { |
||
110 | if (empty($data['primaryKey'])) { |
||
111 | throw new \InvalidArgumentException(__d('bedita', 'Missing required option "{0}"', 'primaryKey')); |
||
112 | } |
||
113 | |||
114 | $source = $this->Association->getSource(); |
||
115 | $conditions = $this->primaryKeyConditions($source, $data['primaryKey']); |
||
116 | |||
117 | $existing = $source->find() |
||
118 | ->where($conditions) |
||
119 | ->count(); |
||
120 | if (!$existing) { |
||
121 | throw new RecordNotFoundException(__('Record not found in table "{0}"', $source->getTable())); |
||
122 | } |
||
123 | } |
||
124 | |||
125 | /** |
||
126 | * Build inverse association for joining. |
||
127 | * |
||
128 | * @return \Cake\ORM\Association |
||
129 | * @throws \LogicException Throws an exception if an Association of an unknown type is passed. |
||
130 | */ |
||
131 | protected function buildInverseAssociation() |
||
132 | { |
||
133 | $sourceTable = $this->Association->getTarget(); |
||
134 | $targetTable = TableRegistry::getTableLocator()->get(static::INVERSE_ASSOCIATION_NAME, [ |
||
135 | 'className' => $this->Association->getSource()->getRegistryAlias(), |
||
136 | ]); |
||
137 | $targetTable->setTable($this->Association->getSource()->getTable()); |
||
138 | $propertyName = Inflector::underscore(static::INVERSE_ASSOCIATION_NAME); |
||
139 | |||
140 | $options = compact('propertyName', 'sourceTable', 'targetTable'); |
||
141 | if ($this->Association instanceof HasOne || $this->Association instanceof HasMany) { |
||
142 | $options += [ |
||
143 | 'foreignKey' => $this->Association->getForeignKey(), |
||
144 | 'bindingKey' => $this->Association->getBindingKey(), |
||
145 | ]; |
||
146 | |||
147 | $association = new BelongsTo(static::INVERSE_ASSOCIATION_NAME, $options); |
||
148 | } elseif ($this->Association instanceof BelongsTo) { |
||
149 | $options += [ |
||
150 | 'foreignKey' => $this->Association->getForeignKey(), |
||
151 | 'bindingKey' => $this->Association->getBindingKey(), |
||
152 | ]; |
||
153 | |||
154 | $association = new HasMany(static::INVERSE_ASSOCIATION_NAME, $options); |
||
155 | } elseif ($this->Association instanceof BelongsToMany) { |
||
156 | $options += [ |
||
157 | 'through' => $this->Association->junction()->getRegistryAlias(), |
||
158 | 'foreignKey' => $this->Association->getTargetForeignKey(), |
||
159 | 'targetForeignKey' => $this->Association->getForeignKey(), |
||
160 | 'conditions' => $this->Association->getConditions(), |
||
161 | ]; |
||
162 | |||
163 | $association = new BelongsToMany(static::INVERSE_ASSOCIATION_NAME, $options); |
||
164 | } else { |
||
165 | throw new \LogicException(sprintf('Unknown association type "%s"', get_class($this->Association))); |
||
166 | } |
||
167 | |||
168 | return $sourceTable->associations()->add($association->getName(), $association); |
||
169 | } |
||
170 | |||
171 | /** |
||
172 | * Build the query object. |
||
173 | * |
||
174 | * @param mixed $primaryKey Primary key |
||
175 | * @param array $data Data. |
||
176 | * @param \Cake\ORM\Association $inverseAssociation Inverse association. |
||
177 | * @return \Cake\ORM\Query |
||
178 | * @throws \LogicException Throws an exception if the result of the inner invoked action is not a Query object. |
||
179 | */ |
||
180 | protected function buildQuery($primaryKey, array $data, Association $inverseAssociation) |
||
181 | { |
||
182 | $joinData = !empty($data['joinData']); |
||
183 | $list = !empty($data['list']); |
||
184 | $only = (array)Hash::get($data, 'only', []); |
||
185 | unset($data['joinData'], $data['list'], $data['only']); |
||
186 | |||
187 | $table = $this->Association->getTarget(); |
||
188 | $query = $this->ListAction->execute($data); |
||
189 | if (!($query instanceof Query)) { |
||
190 | $type = is_object($query) ? get_class($query) : gettype($query); |
||
191 | |||
192 | throw new \LogicException(sprintf('Instance of "%s" expected, got "%s"', Query::class, $type)); |
||
193 | } |
||
194 | |||
195 | if ($list) { |
||
196 | $primaryKeyFields = array_map([$table, 'aliasField'], (array)$table->getPrimaryKey()); |
||
197 | $query = $query->select($primaryKeyFields); |
||
198 | } |
||
199 | if (!empty($only)) { |
||
200 | $query = $query->where(function (QueryExpression $exp) use ($table, $only) { |
||
201 | return $exp->in($table->aliasField($table->getPrimaryKey()), $only); |
||
202 | }); |
||
203 | } |
||
204 | if ($this->Association instanceof BelongsToMany && $joinData) { |
||
205 | $query = $query->select($this->Association->junction()); |
||
206 | } |
||
207 | if ($this->Association instanceof BelongsToMany || $this->Association instanceof HasMany) { |
||
208 | $sort = $this->sort($this->Association, $primaryKey); |
||
209 | $query = $query->order($sort); |
||
210 | } |
||
211 | |||
212 | $primaryKeyConditions = $this->primaryKeyConditions($inverseAssociation->getTarget(), $primaryKey); |
||
213 | |||
214 | return $query |
||
215 | ->enableAutoFields(!$list) |
||
216 | ->find($this->Association->getFinder()) |
||
217 | ->innerJoinWith($inverseAssociation->getName(), function (Query $query) use ($primaryKeyConditions) { |
||
218 | return $query->where($primaryKeyConditions); |
||
219 | }) |
||
220 | ->formatResults(function (CollectionInterface $results) use ($inverseAssociation) { |
||
221 | return $results->map(function (EntityInterface $entity) use ($inverseAssociation) { |
||
222 | if (!($this->Association instanceof BelongsToMany)) { |
||
223 | return $entity->setHidden([$inverseAssociation->getProperty()], true); |
||
224 | } |
||
225 | |||
226 | $joinData = Hash::get($entity, '_matchingData.' . $this->Association->junction()->getAlias()); |
||
227 | unset($entity['_matchingData']); |
||
228 | $entity->setHidden([$inverseAssociation->getProperty()], true); |
||
229 | |||
230 | if (!empty($joinData)) { |
||
231 | $this->prepareJoinEntity($joinData); |
||
232 | $entity->set('_joinData', $joinData); |
||
233 | } |
||
234 | |||
235 | return $entity; |
||
236 | }); |
||
237 | }); |
||
238 | } |
||
239 | |||
240 | /** |
||
241 | * Prepare `joinData` entity. |
||
242 | * |
||
243 | * @param \Cake\Datasource\EntityInterface $joinData Join data entity. |
||
244 | * @return void |
||
245 | * @codeCoverageIgnore |
||
246 | */ |
||
247 | protected function prepareJoinEntity(EntityInterface $joinData): void |
||
248 | { |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * Get association sort by association and primary key. |
||
253 | * When association name is "Children", use Folders.getSort($primaryKey). |
||
254 | * |
||
255 | * @param mixed $primaryKey Primary key |
||
256 | * @param \Cake\ORM\Association $association Association |
||
257 | * @return array |
||
258 | */ |
||
259 | protected function sort(Association $association, $primaryKey): array |
||
260 | { |
||
261 | if ($association->getName() === 'Children') { |
||
262 | return (array)TableRegistry::getTableLocator()->get('Folders')->getSort($primaryKey); |
||
263 | } |
||
264 | |||
265 | return (array)$association->getSort(); |
||
266 | } |
||
267 | |||
268 | /** |
||
269 | * {@inheritDoc} |
||
270 | * |
||
271 | * @return \Cake\ORM\Query|\Cake\Datasource\EntityInterface|null |
||
272 | */ |
||
273 | public function execute(array $data = []) |
||
274 | { |
||
275 | $this->checkEntityExists($data); |
||
276 | $primaryKey = $data['primaryKey']; |
||
277 | unset($data['primaryKey']); |
||
278 | |||
279 | $inverseAssociation = $this->buildInverseAssociation(); |
||
280 | $query = $this->buildQuery($primaryKey, $data, $inverseAssociation); |
||
281 | |||
282 | // remove temporary alias of inverse association from TableRegistry |
||
283 | TableRegistry::getTableLocator()->remove(static::INVERSE_ASSOCIATION_NAME); |
||
284 | |||
285 | if ($this->Association instanceof HasOne || $this->Association instanceof BelongsTo) { |
||
286 | return $query->first(); |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
287 | } |
||
288 | |||
289 | return $query; |
||
290 | } |
||
291 | } |
||
292 |