Test Failed
Push — master ( 6c6167...82c1a8 )
by Fabrice
02:45
created

UniqueKeyExtractorAbstract::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 2
1
<?php
2
3
/*
4
 * This file is part of YaEtl.
5
 *     (c) Fabrice de Stefanis / https://github.com/fab2s/YaEtl
6
 * This source file is licensed under the MIT license which you will
7
 * find in the LICENSE file or at https://opensource.org/licenses/MIT
8
 */
9
10
namespace fab2s\YaEtl\Extractors;
11
12
/**
13
 * Abstract Class UniqueKeyExtractorAbstract
14
 */
15
abstract class UniqueKeyExtractorAbstract extends DbExtractorAbstract implements JoinableInterface
16
{
17
    /**
18
     * the unique key name
19
     *
20
     * @var array|string
21
     */
22
    protected $compositeKey;
23
24
    /**
25
     * @var string
26
     */
27
    protected $uniqueKeyName;
28
29
    /**
30
     * @var string
31
     */
32
    protected $uniqueKeyAlias;
33
34
    /**
35
     * @var array
36
     */
37
    protected $uniqueKeyValues = [];
38
39
    /**
40
     * @var array
41
     */
42
    protected $uniqueKeyValueBuffer = [];
43
44
    /**
45
     * @var callable
46
     */
47
    protected $merger;
48
49
    /**
50
     * @var OnClauseInterface
51
     */
52
    protected $onClose;
53
54
    /**
55
     * @var array of OnClauseInterface
56
     */
57
    protected $joinerOnCloses = [];
58
59
    /**
60
     * the record map
61
     *
62
     * @var array
63
     */
64
    protected $recordMap;
65
66
    /**
67
     * The Joignable we may be joining against
68
     *
69
     * @var JoignableInterface
70
     */
71
    protected $joinFrom;
72
73
    /**
74
     * generic extraction from tables with unique (composite) key
75
     *
76
     * @param string       $extractQuery
77
     * @param array|string $uniqueKey    can be either a unique key name as
78
     *                                   string
79
     *                                   '(table.)compositeKeyName' // ('id' by default)
80
     *
81
     *                      or an array :
82
     *                      ['(table.)compositeKey1'] // single unique key
83
     *                      ['(table.)compositeKey1', '(table.)compositeKey2', ] // composite unique key
84
     *
85
     *                      or an associative array in case you are using aliases :
86
     *                      [
87
     *                          '(table.)compositeKey1' => 'aliasNameAsInRecord',
88
     *                      ]
89
     *
90
     *                      and :
91
     *                      [
92
     *                          '(table.)compositeKey1' => 'aliasNameAsInRecord1',
93
     *                          '(table.)compositeKey2' => 'aliasNameAsInRecord2',
94
     *                          // ...
95
     *                      ]
96
     */
97
    public function __construct($extractQuery = null, $uniqueKey = 'id')
98
    {
99
        $this->configureUniqueKey($uniqueKey);
100
101
        parent::__construct($extractQuery);
102
    }
103
104
    /**
105
     * get Joiner's ON clause. Only used in Join mode
106
     *
107
     * @return OnClauseInterface
108
     */
109
    public function getOnClause()
110
    {
111
        return $this->onClose;
112
    }
113
114
    /**
115
     * Set Joiner's ON clause. Only used in Join mode
116
     *
117
     * @param OnClauseInterface $onClause
118
     *
119
     * @return $this
120
     */
121
    public function setOnClause(OnClauseInterface $onClause)
122
    {
123
        $this->onClose = $onClause;
124
125
        return $this;
126
    }
127
128
    /**
129
     * register ON clause field mapping. Used by an eventual joiner to this
130
     *
131
     * @param OnClauseInterface $onClause
132
     *
133
     * @return $this
134
     */
135
    public function registerJoinerOnClause(OnClauseInterface $onClause)
136
    {
137
        $this->joinerOnCloses[] = $onClause;
138
139
        return $this;
140
    }
141
142
    /**
143
     * @param JoinableInterface $joinFrom
144
     *
145
     * @throws \Exception
146
     *
147
     * @return $this
148
     */
149
    public function setJoinFrom(JoinableInterface $joinFrom)
150
    {
151
        if (preg_match('`^(.+)(order\s+by.*)$`is', $this->extractQuery)) {
152
            throw new \Exception("[YaEtl] A Joiner must not order its query got: $this->extractQuery");
153
        }
154
155
        // since we are joining, we are not a traversable anymore
156
        $this->isATraversable = false;
157
        // and we return a value
158
        $this->isAReturningVal = true;
159
        $this->joinFrom        = $joinFrom;
0 ignored issues
show
Documentation Bug introduced by
It seems like $joinFrom of type object<fab2s\YaEtl\Extractors\JoinableInterface> is incompatible with the declared type object<fab2s\YaEtl\Extractors\JoignableInterface> of property $joinFrom.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
160
161
        return $this;
162
    }
163
164
    /**
165
     * @param string $fromKeyAlias The from unique key to get the map against
166
     *                             as exposed in the record
167
     *
168
     * @return array [keyValue1, keyValue2, ...]
169
     */
170
    public function getRecordMap($fromKeyAlias = null)
171
    {
172
        return $fromKeyAlias === null ? $this->recordMap : $this->recordMap[$fromKeyAlias];
173
    }
174
175
    /**
176
     * @param mixed $param
177
     *
178
     * @return bool
179
     */
180
    public function extract($param = null)
181
    {
182
        if (isset($this->joinFrom)) {
183
            // join mode, get record map
184
            $this->uniqueKeyValues = $this->joinFrom->getRecordMap($this->onClose->getFromKeyAlias());
185
            // limit does not apply in join mode
186
            $this->enforceBatchSize();
187
            if (empty($this->uniqueKeyValues)) {
188
                return false;
189
            }
190
191
            if ($this->fetchRecords()) {
192
                // gen record map before we set defaults
193
                $this->genRecordMap()
194
                    ->setDefaultExtracted();
195
196
                return true;
197
            }
198
199
            return false;
200
        }
201
202
        // enforce limit if any is set
203
        if ($this->isLimitReached()) {
204
            return false;
205
        }
206
207
        $this->enforceBatchSize();
208
        if ($this->fetchRecords()) {
209
            $this->incrementOffset()
210
                ->genRecordMap();
211
212
            return true;
213
        }
214
215
        return false;
216
    }
217
218
    /**
219
     * @return $this
220
     */
221
    public function enforceBatchSize()
222
    {
223
        if (isset($this->joinFrom)) {
224
            // obey batch size to allow fromer to fetch a huge amount of records
225
            // while keeping the where in query size under control by splitting
226
            // it into several chunks.
227
            if (!empty($this->uniqueKeyValueBuffer)) {
228
                // happend eventual new keys from upstream extractor
229
                \end($this->uniqueKeyValueBuffer);
230
                $lastKeyValue = \current($this->uniqueKeyValueBuffer);
231
                \reset($this->uniqueKeyValueBuffer);
232
                $set = false;
233
                foreach ($this->uniqueKeyValues as $uniqueKeyValue) {
234
                    // move forward in the array until we reach the
235
                    // last value we already had in the buffer and
236
                    // then happend the rest
237
                    if (!$set && $uniqueKeyValue === $lastKeyValue) {
238
                        $set = true;
239
                        continue;
240
                    }
241
242
                    $this->uniqueKeyValueBuffer[$uniqueKeyValue] = $uniqueKeyValue;
243
                }
244
            } else {
245
                // get everything
246
                $this->uniqueKeyValueBuffer = $this->uniqueKeyValues;
247
            }
248
249
            $this->uniqueKeyValues      = \array_slice($this->uniqueKeyValueBuffer, 0, $this->batchSize, true);
250
            $this->uniqueKeyValueBuffer = \array_slice($this->uniqueKeyValueBuffer, $this->batchSize, null, true);
251
252
            return $this;
253
        }
254
255
        parent::enforceBatchSize();
256
257
        return $this;
258
    }
259
260
    /**
261
     * @param mixed $record
262
     *
263
     * @return mixed The result of the join
264
     */
265
    public function exec($record)
266
    {
267
        $uniqueKeyValue = $record[$this->uniqueKeyAlias];
268
269
        if (isset($this->extracted[$uniqueKeyValue])) {
270
            $joinRecord = $this->extracted[$uniqueKeyValue];
271
            unset($this->extracted[$uniqueKeyValue]);
272
            if ($joinRecord === false) {
273
                // skip record
274
                $this->carrier->continueFlow();
275
276
                return $record;
277
            }
278
279
            ++$this->numRecords;
280
281
            return $this->onClose->merge($record, $joinRecord);
282
        } elseif (isset($this->uniqueKeyValueBuffer[$uniqueKeyValue])) {
283
            // uniqueKeyValueBuffer should never run out until
284
            // the fromer stop providing records which mean
285
            // we do not reach here. Case left is when this
286
            // batchSize < fromer batchSize.
287
            // trigger extract
288
            if ($this->extract()) {
289
                return $this->exec($record);
290
            }
291
        } elseif ($this->extract()) {
292
            return $this->exec($record);
293
        }
294
295
        // something is wrong
296
        throw new \Exception('[YaEtl] Record map missmatch betwen Joiner ' . \get_class($this) . ' and Fromer ' . \get_class($this->joinFrom));
297
    }
298
299
    /**
300
     * @param array|string $uniqueKey can be either a unique key name as
301
     *                                string
302
     *                                '(table.)compositeKeyName' // ('id' by default)
303
     *
304
     *                      or an array :
305
     *                      ['(table.)compositeKey1'] // single unique key
306
     *                      ['(table.)compositeKey1', '(table.)compositeKey2', ] // composite unique key
307
     *
308
     *                      or an associative array in case you are using aliases :
309
     *                      [
310
     *                          '(table.)compositeKey1' => 'aliasNameAsInRecord',
311
     *                      ]
312
     *
313
     *                      and :
314
     *                      [
315
     *                          '(table.)compositeKey1' => 'aliasNameAsInRecord1',
316
     *                          '(table.)compositeKey2' => 'aliasNameAsInRecord2',
317
     *                          // ...
318
     *                      ]
319
     *
320
     * @return $this
321
     */
322
    protected function configureUniqueKey($uniqueKey)
323
    {
324
        $uniqueKey            = \is_array($uniqueKey) ? $uniqueKey : [$uniqueKey];
325
        $this->compositeKey   = [];
326
        $this->uniqueKeyName  = null;
327
        $this->uniqueKeyAlias = null;
328
        foreach ($uniqueKey as $key => $value) {
329
            if (\is_numeric($key)) {
330
                $compositeKeyName  = $this->cleanUpKeyName($value);
331
                $compositeKeyParts = \explode('.', $compositeKeyName);
332
                $compositeKeyAlias = \end($compositeKeyParts);
333
            } else {
334
                $compositeKeyName  = $this->cleanUpKeyName($key);
335
                $compositeKeyAlias = $this->cleanUpKeyName($value);
336
            }
337
338
            $this->compositeKey[$compositeKeyName] = $compositeKeyAlias;
339
        }
340
341
        if (\count($this->compositeKey) === 1) {
342
            $this->uniqueKeyName  = \key($this->compositeKey);
343
            $this->uniqueKeyAlias = \current($this->compositeKey);
344
        }
345
346
        return $this;
347
    }
348
349
    /**
350
     * @param string $keyName
351
     *
352
     * @return string
353
     */
354
    protected function cleanUpKeyName($keyName)
355
    {
356
        return \trim($keyName, '` ');
357
    }
358
359
    /**
360
     * prepare record set to obey join mode eg return record = true
361
     * to break branch execution when no match are found in join more
362
     * or default to be later merged in left join mode
363
     *
364
     * @return $this
365
     */
366
    protected function setDefaultExtracted()
367
    {
368
        if ($this->joinFrom !== null) {
369
            $defaultExtracted = \array_fill_keys($this->uniqueKeyValues, $this->onClose->isLeftJoin() ? $this->onClose->getDefaultRecord() : false);
370
371
            foreach ($defaultExtracted as $keyValue => &$default) {
372
                // extracted is an array in join mode
373
                if (isset($this->extracted[$keyValue])) {
374
                    $default = $this->extracted[$keyValue];
375
                }
376
            }
377
378
            $this->extracted = $defaultExtracted;
379
        }
380
381
        return $this;
382
    }
383
384
    /**
385
     * @return $this
386
     */
387
    protected function genRecordMap()
388
    {
389
        // here we need to build record map ready for all joiners
390
        $this->recordMap = [];
391
        foreach ($this->joinerOnCloses as $onClose) {
392
            $fromKeyAlias = $onClose->getFromKeyAlias();
393
            if (isset($this->recordMap[$fromKeyAlias])) {
394
                // looks like there is more than
395
                // one joiner on this key
396
                continue;
397
            }
398
399
            // generate rercord map
400
            $this->recordMap[$fromKeyAlias] = [];
401
402
            $map = &$this->recordMap[$fromKeyAlias];
403
            // we do not want to map defaults here as we do not want joiners
404
            // to this to join on null
405
            // we could optimize a little bit for cases where
406
            // $this->extracted is an indexed array on the proper key but ...
407
            foreach ($this->extracted as $record) {
408
                if (!isset($record[$fromKeyAlias])) {
409
                    // Since we do not enforce key alias existance during init
410
                    // we have to do it here
411
                    throw new \Exception("[YaEtl] From Key Alias not found in record: $fromKeyAlias");
412
                }
413
414
                $fromKeyValue       = $record[$fromKeyAlias];
415
                $map[$fromKeyValue] = $fromKeyValue;
416
            }
417
        }
418
419
        return $this;
420
    }
421
}
422