Completed
Pull Request — master (#177)
by
unknown
07:08 queued 03:59
created

SettingsManager::toggleExperiment()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
rs 8.5806
cc 4
eloc 16
nc 4
nop 1
1
<?php
2
3
/*
4
 * This file is part of the ONGR package.
5
 *
6
 * (c) NFQ Technologies UAB <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace ONGR\SettingsBundle\Service;
13
14
use Doctrine\Common\Cache\CacheProvider;
15
use ONGR\CookiesBundle\Cookie\Model\GenericCookie;
16
use ONGR\ElasticsearchBundle\Result\Aggregation\AggregationValue;
17
use ONGR\ElasticsearchBundle\Result\DocumentIterator;
18
use ONGR\ElasticsearchDSL\Aggregation\Bucketing\FilterAggregation;
19
use ONGR\ElasticsearchDSL\Aggregation\Bucketing\TermsAggregation;
20
use ONGR\ElasticsearchDSL\Aggregation\Metric\TopHitsAggregation;
21
use ONGR\ElasticsearchDSL\Query\BoolQuery;
22
use ONGR\ElasticsearchDSL\Query\TermQuery;
23
use ONGR\SettingsBundle\Event\Events;
24
use ONGR\SettingsBundle\Event\SettingActionEvent;
25
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
26
use ONGR\ElasticsearchBundle\Service\Repository;
27
use ONGR\ElasticsearchBundle\Service\Manager;
28
use ONGR\SettingsBundle\Document\Setting;
29
use Symfony\Component\Serializer\Exception\LogicException;
30
31
/**
32
 * Class SettingsManager responsible for managing settings actions.
33
 */
34
class SettingsManager
35
{
36
    /**
37
     * Symfony event dispatcher.
38
     *
39
     * @var EventDispatcherInterface
40
     */
41
    private $eventDispatcher;
42
43
    /**
44
     * Elasticsearch manager which handles setting repository.
45
     *
46
     * @var Manager
47
     */
48
    private $manager;
49
50
    /**
51
     * Settings repository.
52
     *
53
     * @var Repository
54
     */
55
    private $repo;
56
57
    /**
58
     * Cache pool container.
59
     *
60
     * @var CacheProvider
61
     */
62
    private $cache;
63
64
    /**
65
     * Cookie storage for active cookies.
66
     *
67
     * @var GenericCookie
68
     */
69
    private $activeProfilesCookie;
70
71
    /**
72
     * Cookie storage for active cookies.
73
     *
74
     * @var GenericCookie
75
     */
76
    private $activeExperimentProfilesCookie;
77
78
    /**
79
     * Active profiles setting name to store in the cache engine.
80
     *
81
     * @var string
82
     */
83
    private $activeProfilesSettingName;
84
85
    /**
86
     * Active profiles list collected from es, cache and cookie.
87
     *
88
     * @var array
89
     */
90
    private $activeProfilesList = [];
91
92
    /**
93
     * Active experiments setting name to store in the cache engine.
94
     *
95
     * @var string
96
     */
97
    private $activeExperimentsSettingName;
98
99
    /**
100
     * @param Repository               $repo
101
     * @param EventDispatcherInterface $eventDispatcher
102
     */
103
    public function __construct(
104
        $repo,
105
        EventDispatcherInterface $eventDispatcher
106
    ) {
107
        $this->repo = $repo;
108
        $this->manager = $repo->getManager();
109
        $this->eventDispatcher = $eventDispatcher;
110
    }
111
112
    /**
113
     * @return CacheProvider
114
     */
115
    public function getCache()
116
    {
117
        return $this->cache;
118
    }
119
120
    /**
121
     * @param CacheProvider $cache
122
     */
123
    public function setCache($cache)
124
    {
125
        $this->cache = $cache;
126
    }
127
128
    /**
129
     * @return GenericCookie
130
     */
131
    public function getActiveProfilesCookie()
132
    {
133
        return $this->activeProfilesCookie;
134
    }
135
136
    /**
137
     * @param GenericCookie $activeProfilesCookie
138
     */
139
    public function setActiveProfilesCookie($activeProfilesCookie)
140
    {
141
        $this->activeProfilesCookie = $activeProfilesCookie;
142
    }
143
144
    /**
145
     * @return GenericCookie
146
     */
147
    public function getActiveExperimentProfilesCookie()
148
    {
149
        return $this->activeExperimentProfilesCookie;
150
    }
151
152
    /**
153
     * @param GenericCookie $activeExperimentProfilesCookie
154
     */
155
    public function setActiveExperimentProfilesCookie($activeExperimentProfilesCookie)
156
    {
157
        $this->activeExperimentProfilesCookie = $activeExperimentProfilesCookie;
158
    }
159
160
    /**
161
     * @return string
162
     */
163
    public function getActiveProfilesSettingName()
164
    {
165
        return $this->activeProfilesSettingName;
166
    }
167
168
    /**
169
     * @param string $activeProfilesSettingName
170
     */
171
    public function setActiveProfilesSettingName($activeProfilesSettingName)
172
    {
173
        $this->activeProfilesSettingName = $activeProfilesSettingName;
174
    }
175
176
    /**
177
     * @return array
178
     */
179
    public function getActiveProfilesList()
180
    {
181
        return $this->activeProfilesList;
182
    }
183
184
    /**
185
     * @param array $activeProfilesList
186
     */
187
    public function setActiveProfilesList(array $activeProfilesList)
188
    {
189
        $this->activeProfilesList = $activeProfilesList;
190
    }
191
192
    /**
193
     * @param array $activeProfilesList
194
     */
195
    public function appendActiveProfilesList(array $activeProfilesList)
196
    {
197
        $this->activeProfilesList = array_merge($this->activeProfilesList, $activeProfilesList);
198
    }
199
200
    /**
201
     * @return string
202
     */
203
    public function getActiveExperimentsSettingName()
204
    {
205
        return $this->activeExperimentsSettingName;
206
    }
207
208
    /**
209
     * @param string $activeExperimentsSettingName
210
     */
211
    public function setActiveExperimentsSettingName($activeExperimentsSettingName)
212
    {
213
        $this->activeExperimentsSettingName = $activeExperimentsSettingName;
214
    }
215
216
    /**
217
     * Creates setting.
218
     *
219
     * @param array        $data
220
     *
221
     * @return Setting
222
     */
223
    public function create(array $data = [])
224
    {
225
        $data = array_filter($data);
226
        if (!isset($data['name']) || !isset($data['type'])) {
227
            throw new \LogicException('Missing one of the mandatory field!');
228
        }
229
230
        if (!isset($data['value'])) {
231
            $data['value'] = 0;
232
        }
233
234
        $name = $data['name'];
235
        $existingSetting = $this->get($name);
236
237
        if ($existingSetting) {
238
            throw new \LogicException(sprintf('Setting %s already exists.', $name));
239
        }
240
241
        $settingClass = $this->repo->getClassName();
242
        /** @var Setting $setting */
243
        $setting = new $settingClass();
244
245
        $this->eventDispatcher->dispatch(Events::PRE_CREATE, new SettingActionEvent($name, $data, $setting));
246
247
        #TODO Introduce array populate function in Setting document instead of this foreach.
248
        foreach ($data as $key => $value) {
249
            $setting->{'set'.ucfirst($key)}($value);
250
        }
251
252
        $this->manager->persist($setting);
253
        $this->manager->commit();
254
255
        $this->eventDispatcher->dispatch(Events::POST_CREATE, new SettingActionEvent($name, $data, $setting));
256
257
        return $setting;
258
    }
259
260
    /**
261
     * Overwrites setting parameters with given name.
262
     *
263
     * @param string      $name
264
     * @param array       $data
265
     *
266
     * @return Setting
267
     */
268
    public function update($name, $data = [])
269
    {
270
        $setting = $this->get($name);
271
272
        if (!$setting) {
273
            throw new \LogicException(sprintf('Setting %s not exist.', $name));
274
        }
275
276
        $this->eventDispatcher->dispatch(Events::PRE_UPDATE, new SettingActionEvent($name, $data, $setting));
277
278
        #TODO Add populate function to document class
279
        foreach ($data as $key => $value) {
280
            $setting->{'set'.ucfirst($key)}($value);
281
        }
282
283
        $this->manager->persist($setting);
284
        $this->manager->commit();
285
        $this->cache->delete($name);
286
287
        if ($setting->getType() == 'experiment') {
288
            $this->getActiveExperimentProfilesCookie()->setClear(true);
289
        }
290
291
        $this->eventDispatcher->dispatch(Events::PRE_UPDATE, new SettingActionEvent($name, $data, $setting));
292
293
        return $setting;
294
    }
295
296
    /**
297
     * Deletes a setting.
298
     *
299
     * @param string    $name
300
     *
301
     * @throws \LogicException
302
     * @return array
303
     */
304
    public function delete($name)
305
    {
306
        if ($this->has($name)) {
307
            $this->eventDispatcher->dispatch(Events::PRE_UPDATE, new SettingActionEvent($name, [], null));
308
309
            $setting = $this->get($name);
310
            $this->cache->delete($name);
311
            $response = $this->repo->remove($setting->getId());
312
313
            if ($setting->getType() == 'experiment') {
314
                $this->cache->delete($this->activeExperimentsSettingName);
315
                $this->getActiveExperimentProfilesCookie()->setClear(true);
316
            }
317
318
            $this->eventDispatcher->dispatch(Events::PRE_UPDATE, new SettingActionEvent($name, $response, $setting));
0 ignored issues
show
Documentation introduced by
$response is of type callable, but the function expects a null|array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
319
320
            return $response;
321
        }
322
323
        throw new \LogicException(sprintf('Setting with name %s doesn\'t exist.', $name));
324
    }
325
326
    /**
327
     * Returns setting object.
328
     *
329
     * @param string $name
330
     *
331
     * @return Setting
332
     */
333
    public function get($name)
334
    {
335
        $this->eventDispatcher->dispatch(Events::PRE_GET, new SettingActionEvent($name, [], null));
336
337
        /** @var Setting $setting */
338
        $setting = $this->repo->findOneBy(['name.name' => $name]);
339
340
        $this->eventDispatcher->dispatch(Events::PRE_GET, new SettingActionEvent($name, [], $setting));
341
342
        return $setting;
343
    }
344
345
    /**
346
     * Returns setting object.
347
     *
348
     * @param string $name
349
     *
350
     * @return bool
351
     */
352
    public function has($name)
353
    {
354
        /** @var Setting $setting */
355
        $setting = $this->repo->findOneBy(['name.name' => $name]);
356
357
        if ($setting) {
358
            return true;
359
        }
360
361
        return false;
362
    }
363
364
    /**
365
     * Get setting value by current active profiles setting.
366
     *
367
     * @param string $name
368
     * @param bool $default
369
     *
370
     * @return string|array|bool
371
     */
372
    public function getValue($name, $default = null)
373
    {
374
        $setting = $this->get($name);
375
376
        if ($setting) {
377
            return $setting->getValue();
378
        }
379
380
        return $default;
381
    }
382
383
    /**
384
     * Get setting value by checking also from cache engine.
385
     *
386
     * @param string $name
387
     * @param bool   $checkWithActiveProfiles Checks if setting is in active profile.
388
     *
389
     * @return mixed
390
     */
391
    public function getCachedValue($name, $checkWithActiveProfiles = true)
392
    {
393
        if ($this->cache->contains($name)) {
394
            $setting = $this->cache->fetch($name);
395
        } elseif ($this->has($name)) {
396
            $settingDocument = $this->get($name);
397
            $setting = [
398
                'value' => $settingDocument->getValue(),
399
                'profiles' => $settingDocument->getProfile(),
400
            ];
401
            $this->cache->save($name, $setting);
402
        } else {
403
            return null;
404
        }
405
406
        if ($checkWithActiveProfiles) {
407
            if (count(array_intersect($this->getActiveProfiles(), $setting['profiles']))) {
408
                return $setting['value'];
409
            }
410
411
            return null;
412
        }
413
414
        return $setting['value'];
415
    }
416
417
    /**
418
     * Get all full profile information.
419
     *
420
     * @return array
421
     */
422
    public function getAllProfiles()
423
    {
424
        $profiles = [];
425
426
        $search = $this->repo->createSearch();
427
        $filter = new BoolQuery();
428
        $filter->add(new TermQuery('type', 'experiment'), BoolQuery::MUST_NOT);
429
        $topHitsAgg = new TopHitsAggregation('documents', 20);
430
        $termAgg = new TermsAggregation('profiles', 'profile.profile');
431
        $filterAgg = new FilterAggregation('filter', $filter);
432
        $termAgg->addAggregation($topHitsAgg);
433
        $filterAgg->addAggregation($termAgg);
434
        $search->addAggregation($filterAgg);
435
436
        $result = $this->repo->findDocuments($search);
437
438
        /** @var Setting $activeProfiles */
439
        $activeProfiles = $this->getValue($this->activeProfilesSettingName, []);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a boolean|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
440
441
        /** @var AggregationValue $agg */
442
        foreach ($result->getAggregation('filter')->getAggregation('profiles') as $agg) {
443
            $settings = [];
444
            $docs = $agg->getAggregation('documents');
445
            foreach ($docs['hits']['hits'] as $doc) {
446
                $settings[] = $doc['_source']['name'];
447
            }
448
            $name = $agg->getValue('key');
449
            $profiles[] = [
450
                'active' => $activeProfiles ? in_array($agg->getValue('key'), (array)$activeProfiles) : false,
451
                'name' => $name,
452
                'settings' => implode(', ', $settings),
453
            ];
454
        }
455
456
        return $profiles;
457
    }
458
459
    /**
460
     * Returns profiles settings array
461
     *
462
     * @param string $profile
463
     *
464
     * @return array
465
     */
466
    public function getProfileSettings($profile)
467
    {
468
        $search = $this->repo->createSearch();
469
        $termQuery = new TermQuery('profile', $profile);
470
        $search->addQuery($termQuery);
471
        $search->setSize(1000);
472
473
        $settings = $this->repo->findArray($search);
474
475
        return $settings;
476
    }
477
478
    /**
479
     * Returns cached active profiles names list.
480
     *
481
     * @return array
482
     */
483
    public function getActiveProfiles()
484
    {
485
        if ($this->cache->contains($this->activeProfilesSettingName)) {
486
            $profiles = $this->cache->fetch($this->activeProfilesSettingName);
487
        } else {
488
            $profiles = [];
489
            $allProfiles = $this->getAllProfiles();
490
491
            foreach ($allProfiles as $profile) {
492
                if (!$profile['active']) {
493
                    continue;
494
                }
495
496
                $profiles[] = $profile['name'];
497
            }
498
499
            $this->cache->save($this->activeProfilesSettingName, $profiles);
500
        }
501
502
        $profiles = array_merge($profiles, $this->activeProfilesList);
503
504
        return $profiles;
505
    }
506
507
    /**
508
     * @return DocumentIterator
509
     */
510
    public function getAllExperiments()
511
    {
512
        $experiments = $this->repo->findBy(['type' => 'experiment']);
513
514
        return $experiments;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $experiments; (ONGR\ElasticsearchBundle...esult\RawIterator|array) is incompatible with the return type documented by ONGR\SettingsBundle\Serv...ager::getAllExperiments of type ONGR\ElasticsearchBundle\Result\DocumentIterator.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
515
    }
516
517
    /**
518
     * Returns active experiments either from cache or from es.
519
     * If none are found, the setting with no active experiments is
520
     * created.
521
     *
522
     * @return array
523
     */
524
    public function getActiveExperiments()
525
    {
526
        if ($this->cache->contains($this->activeExperimentsSettingName)) {
527
            return $this->cache->fetch($this->activeExperimentsSettingName)['value'];
528
        }
529
530
        if ($this->has($this->activeExperimentsSettingName)) {
531
            $experiments = $this->get($this->activeExperimentsSettingName)->getValue();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->get($this->active...ttingName)->getValue(); of type string adds the type string to the return on line 545 which is incompatible with the return type documented by ONGR\SettingsBundle\Serv...r::getActiveExperiments of type array.
Loading history...
532
        } else {
533
            $this->create(
534
                [
535
                    'name' => $this->activeExperimentsSettingName,
536
                    'value' => [],
537
                    'type' => 'hidden',
538
                ]
539
            );
540
            $experiments = [];
541
        }
542
543
        $this->cache->save($this->activeExperimentsSettingName, ['value' => $experiments]);
544
545
        return $experiments;
546
    }
547
548
    /**
549
     * @param string $name
550
     */
551
    public function toggleExperiment($name)
552
    {
553
        if (!$this->has($this->activeExperimentsSettingName)) {
554
            throw new LogicException(
555
                sprintf('The setting `%s` is not set', $this->activeExperimentsSettingName)
556
            );
557
        }
558
559
        $setting = $this->get($this->activeExperimentsSettingName);
560
        $experiments = $setting->getValue();
561
562
        if (is_array($experiments)) {
563
            if (($key = array_search($name, $experiments)) !== false) {
564
                unset($experiments[$key]);
565
                $experiments = array_values($experiments);
566
                $this->getActiveExperimentProfilesCookie()->setValue($experiments);
567
            } else {
568
                $experiments[] = $name;
569
            }
570
        } else {
571
            $experiments = [$name];
572
        }
573
574
        $this->update($setting->getName(), ['value' => $experiments]);
575
    }
576
577
    /**
578
     * Get full experiment by caching.
579
     *
580
     * @param string $name
581
     *
582
     * @return array|null
583
     *
584
     * @throws LogicException
585
     */
586
    public function getCachedExperiment($name)
587
    {
588
        if ($this->cache->contains($name)) {
589
            $experiment = $this->cache->fetch($name);
590
        } elseif ($this->has($name)) {
591
            $experiment = $this->get($name)->getSerializableData();
592
        } else {
593
            return null;
594
        }
595
596
        if (!isset($experiment['type']) || $experiment['type'] !== 'experiment') {
597
            throw new LogicException(
598
                sprintf('The setting `%s` was found but it is not an experiment', $name)
599
            );
600
        }
601
602
        $this->cache->save($name, $experiment);
603
604
        return $experiment;
605
    }
606
}
607