Completed
Push — master ( 608df1...440b57 )
by Thomas
02:22
created

Experiment::shouldTrigger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * InnoCraft Ltd - We are the makers of Piwik Analytics, the leading open source analytics platform.
4
 *
5
 * @link https://www.innocraft.com
6
 * @license https://www.gnu.org/licenses/lgpl-3.0.en.html LGPL v3.0
7
 */
8
9
namespace InnoCraft\Experiments;
10
11
use InnoCraft\Experiments\Filters\DefaultFilters;
12
use InnoCraft\Experiments\Filters\FilterInterface;
13
use InnoCraft\Experiments\Storage\Cookie;
14
use InnoCraft\Experiments\Storage\StorageInterface;
15
use InnoCraft\Experiments\Variations\VariationInterface;
16
use InvalidArgumentException;
17
use InnoCraft\Experiments\Variations\StandardVariation;
18
19
/**
20
 * Lets you create a new experiment to run an A/B test or a split test.
21
 */
22
class Experiment {
23
24
    /**
25
     * Defines the name of the original version.
26
     */
27
    const ORIGINAL_VARIATION_NAME = 'original';
28
29
    /**
30
     * Instead of the word 'original', one can also set '0' to mark a variation as the original version.
31
     */
32
    const ORIGINAL_VARIATION_ID = '0';
33
34
    /**
35
     * Is returned by {@link getActivatedVariation()} when no variation should be activated.
36
     */
37
    const DO_NOT_TRIGGER = null;
38
39
    /**
40
     * @var int|string
41
     */
42
    private $name;
43
44
    /**
45
     * @var Variations
46
     */
47
    private $variations;
48
49
    /**
50
     * @var FilterInterface
51
     */
52
    private $filter;
53
54
    /**
55
     * @var StorageInterface
56
     */
57
    private $storage;
58
59
    /**
60
     * Creates a new experiment
61
     *
62
     * @param string $experimentNameOrId Can be any experiment name or an id of the experiment (eg as given by A/B Testing for Piwik)
63
     * @param array|VariationInterface[] $variations
64
     * @param array $config
65
     */
66 23
    public function __construct($experimentNameOrId, $variations, $config = [])
67
    {
68 23
        if (!isset($experimentNameOrId) || $experimentNameOrId === false || $experimentNameOrId === '') {
69 3
            throw new InvalidArgumentException('no experimentNameOrId given');
70
        }
71
72 20
        $this->name = $experimentNameOrId;
73
74 20
        if ($variations instanceof Variations) {
75 2
            $this->variations = $variations;
76 2
        } else {
77 18
            $this->variations = new Variations($experimentNameOrId, $variations);
78
79
            // in Piwik A/B Testing there is always an original variation, we need to force the existence here.
80
            // if you do not want to have this behaviour, instead pass an instance of Variations.
81 18
            if (!$this->variations->exists(Experiment::ORIGINAL_VARIATION_NAME)
82 18
                && !$this->variations->exists(Experiment::ORIGINAL_VARIATION_ID)) {
83 16
                $this->variations->addVariation(new StandardVariation(['name' => Experiment::ORIGINAL_VARIATION_NAME]));
84 16
            }
85
        }
86
87 20 View Code Duplication
        if (isset($config['storage']) && $config['storage'] instanceof StorageInterface) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
88 19
            $this->storage = $config['storage'];
89 20
        } elseif (isset($config['storage'])) {
90 1
            throw new InvalidArgumentException('storage needs to be an instance of StorageInterface');
91
        } else {
92
            $this->storage = new Cookie();
93
        }
94
95 19 View Code Duplication
        if (isset($config['filter']) && $config['filter'] instanceof FilterInterface) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
96 15
            $this->filter = $config['filter'];
97 19
        } elseif (isset($config['filter'])) {
98 1
            throw new InvalidArgumentException('filter config needs to be an instance of FilterInterface');
99
        } else {
100 3
            $this->filter = new DefaultFilters($this->name, $this->storage, $config);
101
        }
102 18
    }
103
104
    /**
105
     * Get the name of this experiment.
106
     *
107
     * @return int|string
108
     */
109 7
    public function getExperimentName()
110
    {
111 7
        return $this->name;
112
    }
113
114
    /**
115
     * Forces the activation of the given variation name.
116
     *
117
     * @param string $variationName
118
     */
119 4
    public function forceVariationName($variationName)
120
    {
121 4
        $this->storage->set('experiment', $this->name, $variationName);
122 4
    }
123
124
    /**
125
     * Detect whether any variation, including the original version, should be activated.
126
     *
127
     * Returns true if a variation should and will be activated when calling eg {@link getActivatedVariation()}
128
     * or {@link run()}, false if no variation will be activated.
129
     *
130
     * @return bool
131
     */
132
    public function shouldTrigger()
133
    {
134
        return $this->getActivatedVariation() !== self::DO_NOT_TRIGGER;
135
    }
136
137
    /**
138
     * Get the activated variation for this experiment, or null if no variation was activated because of a set filter.
139
     * For example when the user does not take part in the experiment or when a scheduled date prevents the activation
140
     * of a variation. Returns the activated variation if no filter "blocked" it. On the first request, a variation
141
     * will be randomly chosen unless it was forced by {@link forceVariationName()}. On all subsequent requests
142
     * it will reuse the variation that was activated on the first request.
143
     *
144
     * @return VariationInterface|null
145
     */
146 7
    public function getActivatedVariation()
147
    {
148 7
        if (!$this->filter->shouldTrigger()) {
149 1
            return self::DO_NOT_TRIGGER;
150
        }
151
152 6
        $variationName = $this->storage->get('experiment', $this->name);
153
154 6
        if ($variationName && $this->variations->exists($variationName)) {
155 4
            return $this->variations->get($variationName);
156
        }
157
158 3
        $variation = $this->variations->selectRandomVariation();
159
160 3
        if ($variation) {
161 2
            $this->forceVariationName($variation->getName());
162
163 2
            return $variation;
164
        }
165
166
        // when no variation exists
167 1
        return self::DO_NOT_TRIGGER;
168
    }
169
170
    /**
171
     * Get the set filter.
172
     *
173
     * @return FilterInterface
174
     */
175 3
    public function getFilter()
176
    {
177 3
        return $this->filter;
178
    }
179
180
    /**
181
     * Get the set variations.
182
     *
183
     * @return Variations
184
     */
185 6
    public function getVariations()
186
    {
187 6
        return $this->variations;
188
    }
189
190
    /**
191
     * Get the set storage.
192
     * @return Cookie|StorageInterface
193
     */
194 4
    public function getStorage()
195
    {
196 4
        return $this->storage;
197
    }
198
199
    /**
200
     * Tracks the activation of a variation using for example the Piwik Tracker. This lets Piwik know which variation
201
     * was activated and should be used if you track your application using the Piwik Tracker server side. If you are
202
     * usually tracking using the JavaScript Tracker, have a look at {@link getTrackingScript()}.
203
     *
204
     * @param \stdClass|\PiwikTracker $tracker   The passed object needs to implement a `doTrackEvent` method accepting
205
     *                                           three parameters $category, $action, $name
206
     */
207 3
    public function trackVariationActivation($tracker)
208
    {
209
        // we do not use an interface here for simplicity so it is not needed to use an adapter or something
210
        // for Piwik tracker
211 3
        if ($tracker && method_exists($tracker, 'doTrackEvent')) {
212 1
            $variation = $this->getActivatedVariation();
213
214 1
            if ($variation === self::DO_NOT_TRIGGER) {
215
                return;
216
            }
217
218
            // eg PiwikTracker
219 1
            $tracker->doTrackEvent('abtesting', $this->getExperimentName(), $variation->getName());
220 1
        } else {
221 2
            throw new InvalidArgumentException('The given tracker does not implement the doTrackEvent method');
222
        }
223 1
    }
224
225
    /**
226
     * Returns the JavaScript tracking code that you can echo in your website to let Piwik know which variation was
227
     * activated server side.
228
     *
229
     * Do not pass variables from $_GET or $_POST etc. Make sure to escape the variables before passing them
230
     * to this method as you would otherwise risk an XSS.
231
     *
232
     * @param string $experimentName  ExperimentName and VariationName needs to be passed cause we do not yet have a way
233
     *                                here to properly escape it to prevent XSS.
234
     * @param string $variationName
235
     * @return string  The Piwik tracking code including the `<script>` elements and _paq.push().
236
     */
237 1
    public function getTrackingScript($experimentName, $variationName)
238
    {
239 1
        return sprintf('<script type="text/javascript">_paq.push(["AbTesting::enter", {experiment: "%s", variation: "%s"}]);</script>', $experimentName, $variationName);
240
    }
241
242
}