Passed
Push — master ( f5f62a...783e97 )
by Christian
12:34 queued 10s
created

PromotionCodeService::splitPattern()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 7
rs 10
c 1
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Checkout\Promotion\Util;
4
5
use Shopware\Core\Checkout\Promotion\Exception\PatternAlreadyInUseException;
6
use Shopware\Core\Checkout\Promotion\Exception\PatternNotComplexEnoughException;
7
use Shopware\Core\Framework\Context;
8
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
9
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
10
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
11
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
12
13
class PromotionCodeService
14
{
15
    public const PROMOTION_PATTERN_REGEX = '/(?<prefix>[^%]*)(?<replacement>(%[sd])+)(?<suffix>.*)/';
16
    public const CODE_COMPLEXITY_FACTOR = 0.5;
17
18
    /**
19
     * @var EntityRepositoryInterface
20
     */
21
    private $individualCodesRepository;
22
23
    /**
24
     * @var EntityRepositoryInterface
25
     */
26
    private $promotionRepository;
27
28
    public function __construct(EntityRepositoryInterface $promotionRepository, EntityRepositoryInterface $individualCodesRepository)
29
    {
30
        $this->promotionRepository = $promotionRepository;
31
        $this->individualCodesRepository = $individualCodesRepository;
32
    }
33
34
    public function getFixedCode(): string
35
    {
36
        $pattern = implode('', array_fill(0, 4, '%s%d'));
37
38
        return $this->generateIndividualCodes($pattern, 1)[0];
39
    }
40
41
    public function getPreview(string $pattern): string
42
    {
43
        return $this->generateIndividualCodes($pattern, 1)[0];
44
    }
45
46
    public function replaceIndividualCodes(string $promotionId, Context $context, string $pattern, int $amount = 1): void
47
    {
48
        if ($this->isCodePatternAlreadyInUse($pattern, $promotionId, $context)) {
49
            throw new PatternAlreadyInUseException();
50
        }
51
52
        $codes = $this->generateIndividualCodes($pattern, $amount);
53
        $codeEntries = \array_values(\array_map(static function ($code) use ($promotionId) {
54
            return [
55
                'promotionId' => $promotionId,
56
                'code' => $code,
57
            ];
58
        }, $codes));
59
60
        $this->resetPromotionCodes($promotionId, $context);
61
        $this->individualCodesRepository->upsert($codeEntries, $context);
62
    }
63
64
    /**
65
     * @throws PatternNotComplexEnoughException
66
     *
67
     * @return array<string>
68
     */
69
    public function generateIndividualCodes(string $pattern, int $amount): array
70
    {
71
        if ($amount < 1) {
72
            return [];
73
        }
74
75
        $codePattern = $this->splitPattern($pattern);
76
77
        /*
78
         * This condition ensures a fundamental randomness to the generated codes in ratio to all possibilities, which
79
         * also minimizes the number of retries. Therefore, the CODE_COMPLEXITY_FACTOR is the worst-case-scenario
80
         * probability to find a new unique promotion code.
81
         */
82
        if ($this->calculatePossibilites($codePattern['replacementString']) * self::CODE_COMPLEXITY_FACTOR < $amount) {
83
            throw new PatternNotComplexEnoughException();
84
        }
85
86
        $codes = [];
87
        do {
88
            $codes[] = $this->generateCode($codePattern);
89
90
            if (\count($codes) >= $amount) {
91
                $codes = \array_unique($codes);
92
            }
93
        } while (\count($codes) < $amount);
94
95
        return $codes;
96
    }
97
98
    public function resetPromotionCodes(string $promotionId, Context $context): void
99
    {
100
        $criteria = (new Criteria())
101
            ->addFilter(new EqualsFilter('promotionId', $promotionId));
102
        $deleteCodes = \array_values($this->individualCodesRepository->searchIds($criteria, $context)->getData());
103
104
        $this->individualCodesRepository->delete($deleteCodes, $context);
105
    }
106
107
    public function splitPattern(string $pattern): array
108
    {
109
        \preg_match(self::PROMOTION_PATTERN_REGEX, $pattern, $codePattern);
110
        $codePattern['replacementString'] = \str_replace('%', '', $codePattern['replacement']);
111
        $codePattern['replacementArray'] = \str_split($codePattern['replacementString']);
112
113
        return $codePattern;
114
    }
115
116
    public function isCodePatternAlreadyInUse(string $pattern, string $promotionId, Context $context): bool
117
    {
118
        $criteria = (new Criteria())
119
            ->addFilter(new NotFilter('AND', [new EqualsFilter('id', $promotionId)]))
120
            ->addFilter(new EqualsFilter('individualCodePattern', $pattern));
121
122
        return $this->promotionRepository->search($criteria, $context)->getTotal() > 0;
123
    }
124
125
    private function generateCode(array $codePattern): string
126
    {
127
        $code = '';
128
        foreach ($codePattern['replacementArray'] as $letter) {
129
            $code .= $this->getRandomChar($letter);
130
        }
131
132
        return $codePattern['prefix'] . $code . $codePattern['suffix'];
133
    }
134
135
    private function calculatePossibilites(string $pattern): int
136
    {
137
        /*
138
         * These counts describe the amount of possibilities in a single digit, depending on variable type:
139
         * - d: digits (0-9)
140
         * - s: letters (A-Z)
141
         */
142
        $possibilityCounts = [
143
            'd' => 10,
144
            's' => 26,
145
        ];
146
        $counts = \count_chars($pattern, 1);
147
148
        $result = 1;
149
        foreach ($counts as $key => $count) {
150
            $result *= $possibilityCounts[\chr($key)] ** $count;
151
        }
152
153
        return $result;
154
    }
155
156
    private function getRandomChar(string $type): string
157
    {
158
        if ($type === 'd') {
159
            return (string) random_int(0, 9);
160
        }
161
162
        return \chr(random_int(65, 90));
163
    }
164
}
165