• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

optimizely / php-sdk / 4536635044

pending completion
4536635044

Pull #265

github

GitHub
Merge 019774507 into 34bebf03a
Pull Request #265: [FSSDK-8940] Correct return type hints

2871 of 2957 relevant lines covered (97.09%)

63.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.08
/src/Optimizely/Bucketer.php
1
<?php
2
/**
3
 * Copyright 2016-2021, Optimizely
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 * http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
namespace Optimizely;
18

19
use Monolog\Logger;
20
use Optimizely\Config\ProjectConfigInterface;
21
use Optimizely\Entity\Experiment;
22
use Optimizely\Entity\Variation;
23
use Optimizely\Logger\LoggerInterface;
24

25
/**
26
 * Class Bucketer
27
 *
28
 * @package Optimizely
29
 */
30
class Bucketer
31
{
32
    /**
33
     * @var integer Seed to be used in bucketing hash.
34
     */
35
    private static $HASH_SEED = 1;
36

37
    /**
38
     * @var integer Maximum traffic allocation value.
39
     */
40
    private static $MAX_TRAFFIC_VALUE = 10000;
41

42
    /**
43
     * @var integer Maximum unsigned 32 bit value.
44
     */
45
    private static $UNSIGNED_MAX_32_BIT_VALUE = 0xFFFFFFFF;
46

47
    /**
48
     * @var integer Maximum possible hash value.
49
     */
50
    private static $MAX_HASH_VALUE = 0x100000000;
51

52
    /**
53
     * @var LoggerInterface Logger for logging messages.
54
     */
55
    private $_logger;
56

57
    /**
58
     * Bucketer constructor.
59
     *
60
     * @param LoggerInterface $logger
61
     */
62
    public function __construct(LoggerInterface $logger)
63
    {
64
        $this->_logger = $logger;
263✔
65
    }
263✔
66

67
    /**
68
     * Generate a hash value to be used in determining which variation the user will be put in.
69
     *
70
     * @param $bucketingKey string Value used for the key of the murmur hash.
71
     *
72
     * @return integer Unsigned value denoting the hash value for the user.
73
     */
74
    private function generateHashCode($bucketingKey)
75
    {
76
        return murmurhash3_int($bucketingKey, Bucketer::$HASH_SEED) & Bucketer::$UNSIGNED_MAX_32_BIT_VALUE;
46✔
77
    }
78

79
    /**
80
     * Generate an integer to be used in bucketing user to a particular variation.
81
     *
82
     * @param $bucketingKey string Value used for the key of the murmur hash.
83
     *
84
     * @return integer Value in the closed range [0, 9999] denoting the bucket the user belongs to.
85
     */
86
    protected function generateBucketValue($bucketingKey)
87
    {
88
        $hashCode = $this->generateHashCode($bucketingKey);
46✔
89
        $ratio = $hashCode / Bucketer::$MAX_HASH_VALUE;
46✔
90
        $bucketVal = intval(floor($ratio * Bucketer::$MAX_TRAFFIC_VALUE));
46✔
91

92
        /* murmurhash3_int returns both positive and negative integers for PHP x86 versions
93
        it returns negative integers when it tries to create 2^32 integers while PHP doesn't support
94
        unsigned integers and can store integers only upto 2^31.
95
        Observing generated hashcodes and their corresponding bucket values after normalization
96
        indicates that a negative bucket number on x86 is exactly 10,000 less than it's
97
        corresponding bucket number on x64. Hence we can safely add 10,000 to a negative number to
98
        make it consistent across both of the PHP variants. */
99
        
100
        if ($bucketVal < 0) {
46✔
101
            $bucketVal += 10000;
×
102
        }
×
103

104
        return $bucketVal;
46✔
105
    }
106

107
    /**
108
     * @param $bucketingId string A customer-assigned value used to create the key for the murmur hash.
109
     * @param $userId string ID for user.
110
     * @param $parentId mixed ID representing Experiment or Group.
111
     * @param $trafficAllocations array Traffic allocations for variation or experiment.
112
     *
113
     * @return [ string, array ]  ID representing experiment or variation and array of log messages representing decision making.
114
     */
115
    private function findBucket($bucketingId, $userId, $parentId, $trafficAllocations)
116
    {
117
        $decideReasons = [];
48✔
118
        // Generate the bucketing key based on combination of user ID and experiment ID or group ID.
119
        $bucketingKey = $bucketingId.$parentId;
48✔
120
        $bucketingNumber = $this->generateBucketValue($bucketingKey);
48✔
121
        $message = sprintf('Assigned bucket %s to user "%s" with bucketing ID "%s".', $bucketingNumber, $userId, $bucketingId);
48✔
122
        $this->_logger->log(Logger::DEBUG, $message);
48✔
123
        $decideReasons[] = $message;
48✔
124

125
        foreach ($trafficAllocations as $trafficAllocation) {
48✔
126
            $currentEnd = $trafficAllocation->getEndOfRange();
48✔
127
            if ($bucketingNumber < $currentEnd) {
48✔
128
                return [$trafficAllocation->getEntityId(), $decideReasons];
45✔
129
            }
130
        }
33✔
131

132
        return [null, $decideReasons];
6✔
133
    }
134

135
    /**
136
     * Determine variation the user should be put in.
137
     *
138
     * @param $config ProjectConfigInterface Configuration for the project.
139
     * @param $experiment Experiment Experiment or Rollout rule in which user is to be bucketed.
140
     * @param $bucketingId string A customer-assigned value used to create the key for the murmur hash.
141
     * @param $userId string User identifier.
142
     *
143
     * @return [ Variation, array ]  Variation which will be shown to the user and array of log messages representing decision making.
144
     */
145
    public function bucket(ProjectConfigInterface $config, Experiment $experiment, $bucketingId, $userId)
146
    {
147
        $decideReasons = [];
50✔
148

149
        if (is_null($experiment->getKey())) {
50✔
150
            return [ null, $decideReasons ];
2✔
151
        }
152

153
        // Determine if experiment is in a mutually exclusive group.
154
        // This will not affect evaluation of rollout rules.
155
        if ($experiment->isInMutexGroup()) {
48✔
156
            $group = $config->getGroup($experiment->getGroupId());
8✔
157

158
            if (is_null($group->getId())) {
8✔
159
                return [ null, $decideReasons ];
×
160
            }
161

162
            list($userExperimentId, $reasons) = $this->findBucket($bucketingId, $userId, $group->getId(), $group->getTrafficAllocation());
8✔
163
            $decideReasons = array_merge($decideReasons, $reasons);
8✔
164

165
            if (empty($userExperimentId)) {
8✔
166
                $message = sprintf('User "%s" is in no experiment.', $userId);
1✔
167
                $this->_logger->log(Logger::INFO, $message);
1✔
168
                $decideReasons[] = $message;
1✔
169
                return [ null, $decideReasons ];
1✔
170
            }
171

172
            if ($userExperimentId != $experiment->getId()) {
8✔
173
                $message = sprintf(
5✔
174
                    'User "%s" is not in experiment %s of group %s.',
5✔
175
                    $userId,
5✔
176
                    $experiment->getKey(),
5✔
177
                    $experiment->getGroupId()
5✔
178
                );
5✔
179

180
                $this->_logger->log(Logger::INFO, $message);
5✔
181
                $decideReasons[] = $message;
5✔
182
                return [ null, $decideReasons ];
5✔
183
            }
184

185
            $message = sprintf(
8✔
186
                'User "%s" is in experiment %s of group %s.',
8✔
187
                $userId,
8✔
188
                $experiment->getKey(),
8✔
189
                $experiment->getGroupId()
8✔
190
            );
8✔
191

192
            $this->_logger->log(Logger::INFO, $message);
8✔
193
            $decideReasons[] = $message;
8✔
194
        }
8✔
195

196
        // Bucket user if not in whitelist and in group (if any).
197
        list($variationId, $reasons) = $this->findBucket($bucketingId, $userId, $experiment->getId(), $experiment->getTrafficAllocation());
48✔
198
        $decideReasons = array_merge($decideReasons, $reasons);
48✔
199
        if (!empty($variationId)) {
48✔
200
            $variation = $config->getVariationFromIdByExperimentId($experiment->getId(), $variationId);
45✔
201

202
            return [ $variation, $decideReasons ];
45✔
203
        }
204
        
205
        return [ null, $decideReasons ];
5✔
206
    }
207
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc