Passed
Push — 4 ( d1e890...251093 )
by Damian
06:37 queued 10s
created

ManyManyThroughList::getJoinTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\ORM;
4
5
use BadMethodCallException;
6
use InvalidArgumentException;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Injector\Injector;
9
10
/**
11
 * ManyManyList backed by a dataobject join table
12
 */
13
class ManyManyThroughList extends RelationList
14
{
15
    /**
16
     * @var ManyManyThroughQueryManipulator
17
     */
18
    protected $manipulator;
19
20
    /**
21
     * Create a new ManyManyRelationList object. This relation will utilise an intermediary dataobject
22
     * as a join table, unlike ManyManyList which scaffolds a table automatically.
23
     *
24
     * @param string $dataClass The class of the DataObjects that this will list.
25
     * @param string $joinClass Class name of the joined dataobject record
26
     * @param string $localKey The key in the join table that maps to the dataClass' PK.
27
     * @param string $foreignKey The key in the join table that maps to joined class' PK.
28
     *
29
     * @param array $extraFields Ignored for ManyManyThroughList
30
     * @param string $foreignClass 'from' class
31
     * @param string $parentClass Parent class (should be subclass of 'from')
32
     * @example new ManyManyThroughList('Banner', 'PageBanner', 'BannerID', 'PageID');
33
     */
34
    public function __construct(
35
        $dataClass,
36
        $joinClass,
37
        $localKey,
38
        $foreignKey,
39
        $extraFields = [],
40
        $foreignClass = null,
41
        $parentClass = null
42
    ) {
43
        parent::__construct($dataClass);
44
45
        // Inject manipulator
46
        $this->manipulator = ManyManyThroughQueryManipulator::create(
47
            $joinClass,
48
            $localKey,
49
            $foreignKey,
50
            $foreignClass,
51
            $parentClass
52
        );
53
        $this->dataQuery->pushQueryManipulator($this->manipulator);
54
    }
55
56
    /**
57
     * Don't apply foreign ID filter until getFinalisedQuery()
58
     *
59
     * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current ids as
60
     * per getForeignID
61
     * @return array Condition In array(SQL => parameters format)
62
     */
63
    protected function foreignIDFilter($id = null)
64
    {
65
        // foreignIDFilter is applied to the HasManyList via ManyManyThroughQueryManipulator, not here
66
        return [];
67
    }
68
69
    public function createDataObject($row)
70
    {
71
        // Add joined record
72
        $joinRow = [];
73
        $joinAlias = $this->manipulator->getJoinAlias();
74
        $prefix = $joinAlias . '_';
75
        foreach ($row as $key => $value) {
76
            if (strpos($key, $prefix) === 0) {
77
                $joinKey = substr($key, strlen($prefix));
78
                $joinRow[$joinKey] = $value;
79
                unset($row[$key]);
80
            }
81
        }
82
83
        // Create parent record
84
        $record = parent::createDataObject($row);
85
86
        // Create joined record
87
        if ($joinRow) {
88
            $joinClass = $this->manipulator->getJoinClass();
89
            $joinQueryParams = $this->manipulator->extractInheritableQueryParameters($this->dataQuery);
90
            $joinRecord = Injector::inst()->create($joinClass, $joinRow, false, $joinQueryParams);
91
            $record->setJoin($joinRecord, $joinAlias);
92
        }
93
94
        return $record;
95
    }
96
97
    /**
98
     * Remove the given item from this list.
99
     *
100
     * Note that for a ManyManyList, the item is never actually deleted, only
101
     * the join table is affected.
102
     *
103
     * @param DataObject $item
104
     */
105
    public function remove($item)
106
    {
107
        if (!($item instanceof $this->dataClass)) {
108
            throw new InvalidArgumentException(
109
                "ManyManyThroughList::remove() expecting a {$this->dataClass} object"
110
            );
111
        }
112
113
        $this->removeByID($item->ID);
114
    }
115
116
    /**
117
     * Remove the given item from this list.
118
     *
119
     * Note that for a ManyManyList, the item is never actually deleted, only
120
     * the join table is affected
121
     *
122
     * @param int $itemID The item ID
123
     */
124
    public function removeByID($itemID)
125
    {
126
        if (!is_numeric($itemID)) {
0 ignored issues
show
introduced by
The condition is_numeric($itemID) is always true.
Loading history...
127
            throw new InvalidArgumentException("ManyManyThroughList::removeById() expecting an ID");
128
        }
129
130
        // Find has_many row with a local key matching the given id
131
        $hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
132
        $records = $hasManyList->filter($this->manipulator->getLocalKey(), $itemID);
133
134
        // Rather than simple un-associating the record (as in has_many list)
135
        // Delete the actual mapping row as many_many deletions behave.
136
        /** @var DataObject $record */
137
        foreach ($records as $record) {
138
            $record->delete();
139
        }
140
    }
141
142
    public function removeAll()
143
    {
144
        // Empty has_many table matching the current foreign key
145
        $hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
146
        $hasManyList->removeAll();
147
    }
148
149
    /**
150
     * @param mixed $item
151
     * @param array $extraFields
152
     */
153
    public function add($item, $extraFields = [])
154
    {
155
        // Ensure nulls or empty strings are correctly treated as empty arrays
156
        if (empty($extraFields)) {
157
            $extraFields = array();
158
        }
159
160
        // Determine ID of new record
161
        $itemID = null;
162
        if (is_numeric($item)) {
163
            $itemID = $item;
164
        } elseif ($item instanceof $this->dataClass) {
165
            /** @var DataObject $item */
166
            if (!$item->isInDB()) {
167
                $item->write();
168
            }
169
            $itemID = $item->ID;
170
        } else {
171
            throw new InvalidArgumentException(
172
                "ManyManyThroughList::add() expecting a $this->dataClass object, or ID value"
173
            );
174
        }
175
        if (empty($itemID)) {
176
            throw new InvalidArgumentException("ManyManyThroughList::add() could not add record without ID");
177
        }
178
179
        // Validate foreignID
180
        $foreignIDs = $this->getForeignID();
181
        if (empty($foreignIDs)) {
182
            throw new BadMethodCallException("ManyManyList::add() can't be called until a foreign ID is set");
183
        }
184
185
        // Apply this item to each given foreign ID record
186
        if (!is_array($foreignIDs)) {
0 ignored issues
show
introduced by
The condition is_array($foreignIDs) is always false.
Loading history...
187
            $foreignIDs = [$foreignIDs];
188
        }
189
        $foreignIDsToAdd = array_combine($foreignIDs, $foreignIDs);
190
191
        // Update existing records
192
        $localKey = $this->manipulator->getLocalKey();
193
        // Foreign key (or key for ID field if polymorphic)
194
        $foreignKey = $this->manipulator->getForeignIDKey();
195
        $hasManyList = $this->manipulator->getParentRelationship($this->dataQuery());
196
        $records = $hasManyList->filter($localKey, $itemID);
197
        /** @var DataObject $record */
198
        foreach ($records as $record) {
199
            if ($extraFields) {
200
                foreach ($extraFields as $field => $value) {
201
                    $record->$field = $value;
202
                }
203
                $record->write();
204
            }
205
            //
206
            $foreignID = $record->$foreignKey;
207
            unset($foreignIDsToAdd[$foreignID]);
208
        }
209
210
        // Check if any records remain to add
211
        if (empty($foreignIDsToAdd)) {
212
            return;
213
        }
214
215
        // Add item to relation
216
        $hasManyList = $hasManyList->forForeignID($foreignIDsToAdd);
217
        $record = $hasManyList->createDataObject($extraFields ?: []);
218
        $record->$localKey = $itemID;
219
        $hasManyList->add($record);
220
    }
221
222
    /**
223
     * Get extra fields used by this list
224
     *
225
     * @return array a map of field names to types
226
     */
227
    public function getExtraFields()
228
    {
229
        // Inherit config from join table
230
        $joinClass = $this->manipulator->getJoinClass();
231
        return Config::inst()->get($joinClass, 'db');
232
    }
233
234
    /**
235
     * @return string
236
     */
237
    public function getJoinTable()
238
    {
239
        $joinClass = $this->manipulator->getJoinClass();
240
        return DataObject::getSchema()->tableName($joinClass);
241
    }
242
}
243