Completed
Push — 4-cactus ( 59c50f...21a27c )
by Alberto
02:40
created

Core/src/Model/Action/ListAssociatedAction.php (1 issue)

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
    /**
41
     * Name of inverse association.
42
     *
43
     * @var string
44
     */
45
    const INVERSE_ASSOCIATION_NAME = '_InverseAssociation';
46
47
    /**
48
     * Association.
49
     *
50
     * @var \Cake\ORM\Association
51
     */
52
    protected $Association;
53
54
    /**
55
     * Action used for listing entities.
56
     *
57
     * @var \BEdita\Core\Model\Action\BaseAction
58
     */
59
    protected $ListAction;
60
61
    /**
62
     * {@inheritDoc}
63
     */
64
    protected function initialize(array $config)
65
    {
66
        $this->Association = $this->getConfig('association');
67
68
        $table = $this->Association->getTarget();
69
        $this->ListAction = new ListEntitiesAction(compact('table'));
70
    }
71
72
    /**
73
     * Build conditions for primary key.
74
     *
75
     * @param \Cake\ORM\Table $table Table object.
76
     * @param mixed $primaryKey Primary key.
77
     * @return array
78
     * @throws \Cake\Datasource\Exception\InvalidPrimaryKeyException Throws an exception if primary key is invalid.
79
     */
80
    protected function primaryKeyConditions(Table $table, $primaryKey)
81
    {
82
        $primaryKeyFields = array_map([$table, 'aliasField'], (array)$table->getPrimaryKey());
83
84
        $primaryKey = (array)$primaryKey;
85
        if (count($primaryKeyFields) !== count($primaryKey)) {
86
            $primaryKey = $primaryKey ?: [null];
87
            $primaryKey = array_map(function ($key) {
88
                return var_export($key, true);
89
            }, $primaryKey);
90
91
            throw new InvalidPrimaryKeyException(__(
92
                'Record not found in table "{0}" with primary key [{1}]',
93
                $table->getTable(),
94
                implode($primaryKey, ', ')
0 ignored issues
show
The call to implode() has too many arguments starting with ', '. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

94
                /** @scrutinizer ignore-call */ 
95
                implode($primaryKey, ', ')

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
95
            ));
96
        }
97
98
        return array_combine($primaryKeyFields, $primaryKey);
99
    }
100
101
    /**
102
     * Check that the entity for which associated entities should be listed actually exists.
103
     *
104
     * @param array $data Data.
105
     * @return void
106
     * @throws \InvalidArgumentException Throws an exception if required option `primaryKey` is missing.
107
     * @throws \Cake\Datasource\Exception\RecordNotFoundException Throws an exception if the record could not be found.
108
     */
109
    protected function checkEntityExists(array $data)
110
    {
111
        if (empty($data['primaryKey'])) {
112
            throw new \InvalidArgumentException(__d('bedita', 'Missing required option "{0}"', 'primaryKey'));
113
        }
114
115
        $source = $this->Association->getSource();
116
        $conditions = $this->primaryKeyConditions($source, $data['primaryKey']);
117
118
        $existing = $source->find()
119
            ->where($conditions)
120
            ->count();
121
        if (!$existing) {
122
            throw new RecordNotFoundException(__('Record not found in table "{0}"', $source->getTable()));
123
        }
124
    }
125
126
    /**
127
     * Build inverse association for joining.
128
     *
129
     * @return \Cake\ORM\Association
130
     * @throws \LogicException Throws an exception if an Association of an unknown type is passed.
131
     */
132
    protected function buildInverseAssociation()
133
    {
134
        $sourceTable = $this->Association->getTarget();
135
        $targetTable = TableRegistry::get(static::INVERSE_ASSOCIATION_NAME, [
136
            'className' => $this->Association->getSource()->getRegistryAlias(),
137
        ]);
138
        $targetTable->setTable($this->Association->getSource()->getTable());
139
        $propertyName = Inflector::underscore(static::INVERSE_ASSOCIATION_NAME);
140
141
        $options = compact('propertyName', 'sourceTable', 'targetTable');
142
        if ($this->Association instanceof HasOne || $this->Association instanceof HasMany) {
143
            $options += [
144
                'foreignKey' => $this->Association->getForeignKey(),
145
                'bindingKey' => $this->Association->getBindingKey(),
146
            ];
147
148
            $association = new BelongsTo(static::INVERSE_ASSOCIATION_NAME, $options);
149
        } elseif ($this->Association instanceof BelongsTo) {
150
            $options += [
151
                'foreignKey' => $this->Association->getForeignKey(),
152
                'bindingKey' => $this->Association->getBindingKey(),
153
            ];
154
155
            $association = new HasMany(static::INVERSE_ASSOCIATION_NAME, $options);
156
        } elseif ($this->Association instanceof BelongsToMany) {
157
            $options += [
158
                'through' => $this->Association->junction()->getRegistryAlias(),
159
                'foreignKey' => $this->Association->getTargetForeignKey(),
160
                'targetForeignKey' => $this->Association->getForeignKey(),
161
                'conditions' => $this->Association->getConditions(),
162
            ];
163
164
            $association = new BelongsToMany(static::INVERSE_ASSOCIATION_NAME, $options);
165
        } else {
166
            throw new \LogicException(sprintf('Unknown association type "%s"', get_class($this->Association)));
167
        }
168
169
        return $sourceTable->associations()->add($association->getName(), $association);
170
    }
171
172
    /**
173
     * Build the query object.
174
     *
175
     * @param mixed $primaryKey Primary key
176
     * @param array $data Data.
177
     * @param \Cake\ORM\Association $inverseAssociation Inverse association.
178
     * @return \Cake\ORM\Query
179
     * @throws \LogicException Throws an exception if the result of the inner invoked action is not a Query object.
180
     */
181
    protected function buildQuery($primaryKey, array $data, Association $inverseAssociation)
182
    {
183
        $joinData = !empty($data['joinData']);
184
        $list = !empty($data['list']);
185
        $only = (array)Hash::get($data, 'only', []);
186
        unset($data['joinData'], $data['list'], $data['only']);
187
188
        $table = $this->Association->getTarget();
189
        $query = $this->ListAction->execute($data);
190
        if (!($query instanceof Query)) {
191
            $type = is_object($query) ? get_class($query) : gettype($query);
192
193
            throw new \LogicException(sprintf('Instance of "%s" expected, got "%s"', Query::class, $type));
194
        }
195
196
        if ($list) {
197
            $primaryKeyFields = array_map([$table, 'aliasField'], (array)$table->getPrimaryKey());
198
            $query = $query->select($primaryKeyFields);
199
        }
200
        if (!empty($only)) {
201
            $query = $query->where(function (QueryExpression $exp) use ($table, $only) {
202
                return $exp->in($table->aliasField($table->getPrimaryKey()), $only);
203
            });
204
        }
205
        if ($this->Association instanceof BelongsToMany && $joinData) {
206
            $query = $query->select($this->Association->junction());
207
        }
208
        if ($this->Association instanceof BelongsToMany || $this->Association instanceof HasMany) {
209
            $query = $query->order($this->Association->getSort());
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
                    $entity->unsetProperty('_matchingData');
228
                    $entity->setHidden([$inverseAssociation->getProperty()], true);
229
230
                    if (!empty($joinData)) {
231
                        $entity->set('_joinData', $joinData);
232
                    }
233
234
                    return $entity;
235
                });
236
            });
237
    }
238
239
    /**
240
     * {@inheritDoc}
241
     *
242
     * @return \Cake\ORM\Query|\Cake\Datasource\EntityInterface|null
243
     */
244
    public function execute(array $data = [])
245
    {
246
        $this->checkEntityExists($data);
247
        $primaryKey = $data['primaryKey'];
248
        unset($data['primaryKey']);
249
250
        $inverseAssociation = $this->buildInverseAssociation();
251
        $query = $this->buildQuery($primaryKey, $data, $inverseAssociation);
252
253
        // remove temporary alias of inverse association from TableRegistry
254
        TableRegistry::remove(static::INVERSE_ASSOCIATION_NAME);
255
256
        if ($this->Association instanceof HasOne || $this->Association instanceof BelongsTo) {
257
            return $query->first();
258
        }
259
260
        return $query;
261
    }
262
}
263