Passed
Push — master ( 343f3b...fd752b )
by Fabrice
02:25
created

UniqueKeyExtractorAbstract   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 382
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 4
dl 0
loc 382
rs 9
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getOnClause() 0 4 1
A setOnClause() 0 6 1
A registerJoinerOnClause() 0 6 1
A setJoinFrom() 0 20 3
A getRecordMap() 0 4 2
B extract() 0 37 6
A enforceBatchSize() 0 20 2
B exec() 0 28 4
B configureUniqueKey() 0 26 5
A cleanUpKeyName() 0 4 1
A setDefaultExtracted() 0 10 3
B genRecordMap() 0 33 5
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 Joinable we may be joining against
68
     *
69
     * @var JoinableInterface
70
     */
71
    protected $joinFrom;
72
73
    /**
74
     * generic extraction from tables with unique (composite) key
75
     *
76
     * @param string       $extractQuery
0 ignored issues
show
Documentation introduced by
Should the type for parameter $extractQuery not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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
        // at least make sure this joinable extends this very class
152
        // to enforce getRecordMap() type
153
        if (!is_a($joinFrom, self::class)) {
154
            throw new \Exception('[YaEtl] From extractor is not compatible, expected implementation of: ' . self::class . "\ngot: " . \get_class($joinFrom));
155
        }
156
157
        if (preg_match('`^(.+)(order\s+by.*)$`is', $this->extractQuery)) {
158
            throw new \Exception("[YaEtl] A Joiner must not order its query got: $this->extractQuery");
159
        }
160
161
        // since we are joining, we are not a traversable anymore
162
        $this->isATraversable = false;
163
        // and we return a value
164
        $this->isAReturningVal = true;
165
        $this->joinFrom        = $joinFrom;
166
167
        return $this;
168
    }
169
170
    /**
171
     * @param string $fromKeyAlias The from unique key to get the map against
0 ignored issues
show
Documentation introduced by
Should the type for parameter $fromKeyAlias not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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