Completed
Pull Request — master (#17)
by
unknown
10:41
created

AbTesting::getVisitor()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 3
nc 3
nop 1
crap 12
1
<?php
2
3
namespace Ben182\AbTesting;
4
5
use Illuminate\Support\Collection;
6
use Ben182\AbTesting\Models\Goal;
7
use Ben182\AbTesting\Models\Experiment;
8
use Ben182\AbTesting\Models\DatabaseVisitor;
9
use Ben182\AbTesting\Models\SessionVisitor;
10
use Ben182\AbTesting\Events\GoalCompleted;
11
use Ben182\AbTesting\Events\ExperimentNewVisitor;
12
use Ben182\AbTesting\Exceptions\InvalidConfiguration;
13
use Ben182\AbTesting\Contracts\VisitorInterface;
14
15
class AbTesting
16
{
17
    protected $experiments;
18
    protected $visitor;
19 48
20
    const SESSION_KEY_GOALS = 'ab_testing_goals';
21 48
22 48
    public function __construct()
23
    {
24
        $this->experiments = new Collection;
25
    }
26
27
    /**
28
     * Validates the config items and puts them into models.
29 48
     *
30
     * @return void
31 48
     */
32 48
    protected function start()
33
    {
34 48
        $configExperiments = config('ab-testing.experiments');
35 3
        $configGoals = config('ab-testing.goals');
36
37
        if (count($configExperiments) !== count(array_unique($configExperiments))) {
38 45
            throw InvalidConfiguration::experiment();
39 3
        }
40
41
        if (count($configGoals) !== count(array_unique($configGoals))) {
42 42
            throw InvalidConfiguration::goal();
43 42
        }
44 42
45
        foreach ($configExperiments as $configExperiment) {
46 42
            $this->experiments[] = $experiment = Experiment::firstOrCreate([
47
                'name' => $configExperiment,
48
            ], [
49 42
                'visitors' => 0,
50 42
            ]);
51 42
52
            foreach ($configGoals as $configGoal) {
53 42
                $experiment->goals()->firstOrCreate([
54
                    'name' => $configGoal,
55
                ], [
56
                    'hit' => 0,
57
                ]);
58 42
            }
59 42
        }
60
61 42
        session([
62
            self::SESSION_KEY_GOALS => new Collection,
63
        ]);
64
    }
65
66
   /**
67
     * Resets the visitor data.
68 48
     *
69
     * @return void
70 48
     */
71 48
    public function resetVisitor()
72 42
    {
73
        session()->flush();
74 42
        $this->visitor = null;
75
    }
76 42
77
    /**
78 9
     * Triggers a new visitor. Picks a new experiment and saves it to the Visitor.
79
     *
80
     * @param integer $visitor_id An optional visitor identifier
81
     *
82
     * @return \Ben182\AbTesting\Models\Experiment|void
83
     */
84
    public function pageView($visitor_id = null)
85 42
    {
86
        $visitor = $this->getVisitor($visitor_id);
87 42
88 42
        if (! session(self::SESSION_KEY_GOALS)) {
89
            $this->start();
90 42
            $this->setNextExperiment($visitor);
91 42
92
            event(new ExperimentNewVisitor($this->getExperiment(), $visitor));
93 42
94
            return $this->getExperiment();
95
        }
96
    }
97
98
    /**
99
     * Calculates a new experiment and sets it to the Visitor.
100 42
     *
101
     * @param VisitorInterface $visitor An object implementing VisitorInterface
102 42
     *
103
     * @return void
104 42
     */
105
    protected function setNextExperiment(VisitorInterface $visitor)
106
    {
107
        $next = $this->getNextExperiment();
108
        $next->incrementVisitor();
109
110
        $visitor->setExperiment($next);
0 ignored issues
show
Bug introduced by
It seems like $next defined by $this->getNextExperiment() on line 107 can be null; however, Ben182\AbTesting\Contrac...erface::setExperiment() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
111
    }
112
113
    /**
114 9
     * Calculates a new experiment.
115
     *
116 9
     * @return \Ben182\AbTesting\Models\Experiment|null
117
     */
118 9
    protected function getNextExperiment()
119
    {
120
        $sorted = $this->experiments->sortBy('visitors');
121
122
        return $sorted->first();
123
    }
124
125
    /**
126
     * Checks if the currently active experiment is the given one.
127
     *
128 15
     * @param string $name The experiments name
129
     *
130 15
     * @return bool
131 12
     */
132
    public function isExperiment(string $name)
133
    {
134 15
        $this->pageView();
135
136 15
        return $this->getExperiment()->name === $name;
137 3
    }
138
139
    /**
140 12
     * Completes a goal by incrementing the hit property of the model and setting its ID in the session.
141 3
     *
142
     * @param string $goal The goals name
143
     * @param integer $visitor_id An optional visitor identifier
144 12
     *
145
     * @return \Ben182\AbTesting\Models\Goal|false
146 12
     */
147 12
    public function completeGoal(string $goal, $visitor_id = null)
148
    {
149 12
        $this->pageView($visitor_id);
150
151
        $goal = $this->getExperiment($visitor_id)->goals->where('name', $goal)->first();
152
153
        if (! $goal) {
154
            return false;
155
        }
156
157 42
        if (session(self::SESSION_KEY_GOALS)->contains($goal->id)) {
158
            return false;
159 42
        }
160
161
        session(self::SESSION_KEY_GOALS)->push($goal->id);
162
163
        $goal->incrementHit();
164
        event(new GoalCompleted($goal));
165
166
        return $goal;
167 3
    }
168
169 3
    /**
170
     * Returns the currently active experiment.
171
     *
172
     * @param integer $visitor_id An optional visitor identifier
173
     *
174 3
     * @return \Ben182\AbTesting\Models\Experiment|null
175 3
     */
176
    public function getExperiment($visitor_id = null)
177
    {
178
        return $this->getVisitor($visitor_id)->getExperiment();
179
    }
180
181
    /**
182
     * Returns all the completed goals.
183
     *
184
     * @param integer $visitor_id An optional visitor identifier
0 ignored issues
show
Bug introduced by
There is no parameter named $visitor_id. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
185
     *
186
     * @return \Illuminate\Support\Collection|false
187
     */
188
    public function getCompletedGoals()
189
    {
190
        if (! session(self::SESSION_KEY_GOALS)) {
191
            return false;
192
        }
193
194
        return session(self::SESSION_KEY_GOALS)->map(function ($goalId) {
195
            return Goal::find($goalId);
196
        });
197
    }
198
199
    /**
200
     * Returns a visitor instance.
201
     *
202
     * @param integer $visitor_id An optional visitor identifier
203
     *
204
     * @return \Ben182\AbTesting\Models\SessionVisitor|\Ben182\AbTesting\Models\DatabaseVisitor
205
     */
206
    public function getVisitor($visitor_id = null)
207
    {
208
        if ( !is_null($this->visitor) ) {
209
            return $this->visitor;
210
        }
211
212
        if ($visitor_id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $visitor_id of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
213
            return $this->visitor = DatabaseVisitor::firstOrNew(['visitor_id' => $visitor_id]);
214
        } else {
215
            return $this->visitor = new SessionVisitor();
216
        }
217
    }
218
}
219